Add Seedance 2.0 i2v and video extend support
- Add seedance-v2.0-i2v to i2vModels with images_list field, aspect ratio, duration (5/10/15s), and quality controls - Add seedance-v2.0-extend to t2vModels for seamlessly continuing Seedance 2.0 generations - Auto-store request_id after each Seedance 2.0 t2v/i2v generation - Show Extend button on canvas after Seedance 2.0 generation; clicking it pre-selects the extend model and auto-passes stored request_id - Add quality dropdown control to VideoStudio for models that support it - Show extend banner when extend model is active; optional prompt guides the continuation - Restore extend context when browsing Seedance 2.0 history entries - Make prompt optional in muapi.generateVideo and forward request_id for extend endpoint - Update README with new models and features
This commit is contained in:
parent
492f8bf693
commit
d707604afe
4 changed files with 209 additions and 21 deletions
|
|
@ -80,13 +80,15 @@ The Video Studio follows the same pattern:
|
||||||
| Mode | Trigger | Models | Prompt |
|
| Mode | Trigger | Models | Prompt |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- |
|
||||||
| **Text-to-Video** | Default (no image) | 40+ t2v models (Kling, Sora, Veo, Wan, Seedance 2.0, Hailuo, Runway…) | Required |
|
| **Text-to-Video** | Default (no image) | 40+ t2v models (Kling, Sora, Veo, Wan, Seedance 2.0, Hailuo, Runway…) | Required |
|
||||||
| **Image-to-Video** | Start frame uploaded | 60+ i2v models (Kling I2V, Veo3 I2V, Runway I2V, Wan I2V, Midjourney I2V…) | Optional |
|
| **Image-to-Video** | Start frame uploaded | 60+ i2v models (Kling I2V, Veo3 I2V, Runway I2V, Wan I2V, Seedance 2.0 I2V, Midjourney I2V…) | Optional |
|
||||||
|
|
||||||
#### Newly Added Models
|
#### Newly Added Models
|
||||||
|
|
||||||
| Model | Type | Key Features |
|
| Model | Type | Key Features |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **Seedance 2.0** | Text-to-Video | ByteDance · Aspect ratios 16:9 / 9:16 / 4:3 / 3:4 · Duration 5 / 10 / 15s · Quality basic/high |
|
| **Seedance 2.0** | Text-to-Video | ByteDance · Aspect ratios 16:9 / 9:16 / 4:3 / 3:4 · Duration 5 / 10 / 15s · Quality basic/high |
|
||||||
|
| **Seedance 2.0 I2V** | Image-to-Video | ByteDance · Animate images into video · Up to 9 reference images · Aspect ratios 16:9 / 9:16 / 4:3 / 3:4 · Duration 5 / 10 / 15s · Quality basic/high |
|
||||||
|
| **Seedance 2.0 Extend** | Video Extension | ByteDance · Seamlessly continue any Seedance 2.0 generation · Preserves style, motion & audio · Optional continuation prompt · Duration 5 / 10 / 15s · Quality basic/high |
|
||||||
|
|
||||||
### 🎥 Cinema Studio Controls
|
### 🎥 Cinema Studio Controls
|
||||||
|
|
||||||
|
|
@ -184,8 +186,8 @@ File uploads use `POST /api/v1/upload_file` (multipart/form-data) and return a h
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Text-to-Image** | 50+ | Flux Dev, Nano Banana 2, Seedream 5.0, Ideogram v3, Midjourney v7, GPT-4o, SDXL |
|
| **Text-to-Image** | 50+ | Flux Dev, Nano Banana 2, Seedream 5.0, Ideogram v3, Midjourney v7, GPT-4o, SDXL |
|
||||||
| **Image-to-Image** | 55+ | Nano Banana 2 Edit (×14), Flux Kontext Pro, GPT-4o Edit, Seededit v3, Upscaler, Background Remover |
|
| **Image-to-Image** | 55+ | Nano Banana 2 Edit (×14), Flux Kontext Pro, GPT-4o Edit, Seededit v3, Upscaler, Background Remover |
|
||||||
| **Text-to-Video** | 40+ | Kling v3, Sora 2, Veo 3, Wan 2.6, Seedance 2.0, Seedance Pro, Hailuo 2.3, Runway Gen-3 |
|
| **Text-to-Video** | 40+ | Kling v3, Sora 2, Veo 3, Wan 2.6, Seedance 2.0, Seedance 2.0 Extend, Seedance Pro, Hailuo 2.3, Runway Gen-3 |
|
||||||
| **Image-to-Video** | 60+ | Kling v2.1 I2V, Veo3 I2V, Runway I2V, Midjourney v7 I2V, Hunyuan I2V, Wan2.2 I2V |
|
| **Image-to-Video** | 60+ | Kling v2.1 I2V, Veo3 I2V, Runway I2V, Seedance 2.0 I2V, Midjourney v7 I2V, Hunyuan I2V, Wan2.2 I2V |
|
||||||
|
|
||||||
## 🛠️ Tech Stack
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ export function VideoStudio() {
|
||||||
let selectedAr = defaultModel.inputs?.aspect_ratio?.default || '16:9';
|
let selectedAr = defaultModel.inputs?.aspect_ratio?.default || '16:9';
|
||||||
let selectedDuration = defaultModel.inputs?.duration?.default || 5;
|
let selectedDuration = defaultModel.inputs?.duration?.default || 5;
|
||||||
let selectedResolution = defaultModel.inputs?.resolution?.default || '';
|
let selectedResolution = defaultModel.inputs?.resolution?.default || '';
|
||||||
|
let selectedQuality = defaultModel.inputs?.quality?.default || '';
|
||||||
|
let lastGenerationId = null;
|
||||||
|
let lastGenerationModel = null;
|
||||||
let dropdownOpen = null;
|
let dropdownOpen = null;
|
||||||
let uploadedImageUrl = null;
|
let uploadedImageUrl = null;
|
||||||
let imageMode = false; // false = t2v models, true = i2v models
|
let imageMode = false; // false = t2v models, true = i2v models
|
||||||
|
|
@ -22,6 +25,11 @@ export function VideoStudio() {
|
||||||
const getCurrentAspectRatios = (id) => imageMode ? getAspectRatiosForI2VModel(id) : getAspectRatiosForVideoModel(id);
|
const getCurrentAspectRatios = (id) => imageMode ? getAspectRatiosForI2VModel(id) : getAspectRatiosForVideoModel(id);
|
||||||
const getCurrentDurations = (id) => imageMode ? getDurationsForI2VModel(id) : getDurationsForModel(id);
|
const getCurrentDurations = (id) => imageMode ? getDurationsForI2VModel(id) : getDurationsForModel(id);
|
||||||
const getCurrentResolutions = (id) => imageMode ? getResolutionsForI2VModel(id) : getResolutionsForVideoModel(id);
|
const getCurrentResolutions = (id) => imageMode ? getResolutionsForI2VModel(id) : getResolutionsForVideoModel(id);
|
||||||
|
const getCurrentModel = () => getCurrentModels().find(m => m.id === selectedModel);
|
||||||
|
const getQualitiesForModel = (id) => {
|
||||||
|
const model = getCurrentModels().find(m => m.id === id);
|
||||||
|
return model?.inputs?.quality?.enum || [];
|
||||||
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 1. HERO SECTION
|
// 1. HERO SECTION
|
||||||
|
|
@ -103,6 +111,15 @@ export function VideoStudio() {
|
||||||
topRow.appendChild(textarea);
|
topRow.appendChild(textarea);
|
||||||
bar.appendChild(topRow);
|
bar.appendChild(topRow);
|
||||||
|
|
||||||
|
// Extend mode banner (shown when extend model is active, not editable by user)
|
||||||
|
const extendBanner = document.createElement('div');
|
||||||
|
extendBanner.className = 'hidden items-center gap-2 px-4 py-2 mx-2 mt-2 bg-primary/10 border border-primary/20 rounded-xl text-xs text-primary';
|
||||||
|
extendBanner.innerHTML = `
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
|
<span>Extending previous Seedance 2.0 generation — add an optional prompt to guide the continuation</span>
|
||||||
|
`;
|
||||||
|
bar.appendChild(extendBanner);
|
||||||
|
|
||||||
// Bottom Row: Controls
|
// Bottom Row: Controls
|
||||||
const bottomRow = document.createElement('div');
|
const bottomRow = document.createElement('div');
|
||||||
bottomRow.className = 'flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 px-2 pt-4 border-t border-white/5';
|
bottomRow.className = 'flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 px-2 pt-4 border-t border-white/5';
|
||||||
|
|
@ -140,16 +157,22 @@ export function VideoStudio() {
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><path d="M6 2L3 6v15a2 2 0 002 2h14a2 2 0 002-2V6l-3-4H6z"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><path d="M6 2L3 6v15a2 2 0 002 2h14a2 2 0 002-2V6l-3-4H6z"/></svg>
|
||||||
`, selectedResolution || '720p', 'v-resolution-btn');
|
`, selectedResolution || '720p', 'v-resolution-btn');
|
||||||
|
|
||||||
|
const qualityBtn = createControlBtn(`
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||||
|
`, selectedQuality || 'basic', 'v-quality-btn');
|
||||||
|
|
||||||
controlsLeft.appendChild(modelBtn);
|
controlsLeft.appendChild(modelBtn);
|
||||||
controlsLeft.appendChild(arBtn);
|
controlsLeft.appendChild(arBtn);
|
||||||
controlsLeft.appendChild(durationBtn);
|
controlsLeft.appendChild(durationBtn);
|
||||||
controlsLeft.appendChild(resolutionBtn);
|
controlsLeft.appendChild(resolutionBtn);
|
||||||
|
controlsLeft.appendChild(qualityBtn);
|
||||||
|
|
||||||
// Initial visibility (t2v mode)
|
// Initial visibility (t2v mode)
|
||||||
const initDurations = getDurationsForModel(defaultModel.id);
|
const initDurations = getDurationsForModel(defaultModel.id);
|
||||||
durationBtn.style.display = initDurations.length > 0 ? 'flex' : 'none';
|
durationBtn.style.display = initDurations.length > 0 ? 'flex' : 'none';
|
||||||
const initResolutions = getResolutionsForVideoModel(defaultModel.id);
|
const initResolutions = getResolutionsForVideoModel(defaultModel.id);
|
||||||
resolutionBtn.style.display = initResolutions.length > 0 ? 'flex' : 'none';
|
resolutionBtn.style.display = initResolutions.length > 0 ? 'flex' : 'none';
|
||||||
|
qualityBtn.style.display = 'none';
|
||||||
|
|
||||||
const generateBtn = document.createElement('button');
|
const generateBtn = document.createElement('button');
|
||||||
generateBtn.className = 'bg-primary text-black px-6 md:px-8 py-3 md:py-3.5 rounded-xl md:rounded-[1.5rem] font-black text-sm md:text-base hover:shadow-glow hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2.5 w-full sm:w-auto shadow-lg';
|
generateBtn.className = 'bg-primary text-black px-6 md:px-8 py-3 md:py-3.5 rounded-xl md:rounded-[1.5rem] font-black text-sm md:text-base hover:shadow-glow hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2.5 w-full sm:w-auto shadow-lg';
|
||||||
|
|
@ -168,10 +191,19 @@ export function VideoStudio() {
|
||||||
dropdown.className = 'absolute bottom-[102%] left-2 z-50 transition-all opacity-0 pointer-events-none scale-95 origin-bottom-left glass rounded-3xl p-3 translate-y-2 w-[calc(100vw-3rem)] max-w-xs shadow-4xl border border-white/10 flex flex-col';
|
dropdown.className = 'absolute bottom-[102%] left-2 z-50 transition-all opacity-0 pointer-events-none scale-95 origin-bottom-left glass rounded-3xl p-3 translate-y-2 w-[calc(100vw-3rem)] max-w-xs shadow-4xl border border-white/10 flex flex-col';
|
||||||
|
|
||||||
const updateControlsForModel = (modelId) => {
|
const updateControlsForModel = (modelId) => {
|
||||||
const availableArs = getCurrentAspectRatios(modelId);
|
const model = getCurrentModels().find(m => m.id === modelId);
|
||||||
selectedAr = availableArs[0];
|
|
||||||
document.getElementById('v-ar-btn-label').textContent = selectedAr;
|
|
||||||
|
|
||||||
|
// Aspect ratio
|
||||||
|
const availableArs = getCurrentAspectRatios(modelId);
|
||||||
|
if (availableArs.length > 0) {
|
||||||
|
selectedAr = availableArs[0];
|
||||||
|
document.getElementById('v-ar-btn-label').textContent = selectedAr;
|
||||||
|
arBtn.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
arBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration
|
||||||
const durations = getCurrentDurations(modelId);
|
const durations = getCurrentDurations(modelId);
|
||||||
if (durations.length > 0) {
|
if (durations.length > 0) {
|
||||||
selectedDuration = durations[0];
|
selectedDuration = durations[0];
|
||||||
|
|
@ -181,6 +213,7 @@ export function VideoStudio() {
|
||||||
durationBtn.style.display = 'none';
|
durationBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolution
|
||||||
const resolutions = getCurrentResolutions(modelId);
|
const resolutions = getCurrentResolutions(modelId);
|
||||||
if (resolutions.length > 0) {
|
if (resolutions.length > 0) {
|
||||||
selectedResolution = resolutions[0];
|
selectedResolution = resolutions[0];
|
||||||
|
|
@ -189,6 +222,26 @@ export function VideoStudio() {
|
||||||
} else {
|
} else {
|
||||||
resolutionBtn.style.display = 'none';
|
resolutionBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quality
|
||||||
|
const qualities = getQualitiesForModel(modelId);
|
||||||
|
if (qualities.length > 0) {
|
||||||
|
selectedQuality = model?.inputs?.quality?.default || qualities[0];
|
||||||
|
document.getElementById('v-quality-btn-label').textContent = selectedQuality;
|
||||||
|
qualityBtn.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
selectedQuality = '';
|
||||||
|
qualityBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend banner (extend model only)
|
||||||
|
if (model?.requiresRequestId) {
|
||||||
|
extendBanner.classList.remove('hidden');
|
||||||
|
extendBanner.classList.add('flex');
|
||||||
|
} else {
|
||||||
|
extendBanner.classList.add('hidden');
|
||||||
|
extendBanner.classList.remove('flex');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showDropdown = (type, anchorBtn) => {
|
const showDropdown = (type, anchorBtn) => {
|
||||||
|
|
@ -296,6 +349,28 @@ export function VideoStudio() {
|
||||||
});
|
});
|
||||||
dropdown.appendChild(list);
|
dropdown.appendChild(list);
|
||||||
|
|
||||||
|
} else if (type === 'quality') {
|
||||||
|
dropdown.classList.add('max-w-[200px]');
|
||||||
|
dropdown.innerHTML = `<div class="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Quality</div>`;
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'flex flex-col gap-1';
|
||||||
|
getQualitiesForModel(selectedModel).forEach(q => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all group';
|
||||||
|
item.innerHTML = `
|
||||||
|
<span class="text-xs font-bold text-white opacity-80 group-hover:opacity-100 capitalize">${q}</span>
|
||||||
|
${selectedQuality === q ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#d9ff00" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg>' : ''}
|
||||||
|
`;
|
||||||
|
item.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
selectedQuality = q;
|
||||||
|
document.getElementById('v-quality-btn-label').textContent = q;
|
||||||
|
closeDropdown();
|
||||||
|
};
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
dropdown.appendChild(list);
|
||||||
|
|
||||||
} else if (type === 'resolution') {
|
} else if (type === 'resolution') {
|
||||||
dropdown.classList.add('max-w-[200px]');
|
dropdown.classList.add('max-w-[200px]');
|
||||||
dropdown.innerHTML = `<div class="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Resolution</div>`;
|
dropdown.innerHTML = `<div class="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Resolution</div>`;
|
||||||
|
|
@ -349,6 +424,7 @@ export function VideoStudio() {
|
||||||
arBtn.onclick = toggleDropdown('ar', arBtn);
|
arBtn.onclick = toggleDropdown('ar', arBtn);
|
||||||
durationBtn.onclick = toggleDropdown('duration', durationBtn);
|
durationBtn.onclick = toggleDropdown('duration', durationBtn);
|
||||||
resolutionBtn.onclick = toggleDropdown('resolution', resolutionBtn);
|
resolutionBtn.onclick = toggleDropdown('resolution', resolutionBtn);
|
||||||
|
qualityBtn.onclick = toggleDropdown('quality', qualityBtn);
|
||||||
|
|
||||||
window.addEventListener('click', closeDropdown);
|
window.addEventListener('click', closeDropdown);
|
||||||
container.appendChild(dropdown);
|
container.appendChild(dropdown);
|
||||||
|
|
@ -400,11 +476,17 @@ export function VideoStudio() {
|
||||||
downloadBtn.className = 'bg-primary text-black px-6 py-2.5 rounded-2xl text-xs font-bold transition-all shadow-glow active:scale-95';
|
downloadBtn.className = 'bg-primary text-black px-6 py-2.5 rounded-2xl text-xs font-bold transition-all shadow-glow active:scale-95';
|
||||||
downloadBtn.textContent = '↓ Download';
|
downloadBtn.textContent = '↓ Download';
|
||||||
|
|
||||||
|
const extendBtn = document.createElement('button');
|
||||||
|
extendBtn.className = 'hidden bg-white/10 hover:bg-white/20 px-6 py-2.5 rounded-2xl text-xs font-bold transition-all border border-primary/30 text-primary backdrop-blur-lg';
|
||||||
|
extendBtn.textContent = '↗ Extend';
|
||||||
|
extendBtn.title = 'Extend this video using Seedance 2.0 Extend';
|
||||||
|
|
||||||
const newPromptBtn = document.createElement('button');
|
const newPromptBtn = document.createElement('button');
|
||||||
newPromptBtn.className = 'bg-white/10 hover:bg-white/20 px-6 py-2.5 rounded-2xl text-xs font-bold transition-all border border-white/5 backdrop-blur-lg text-white';
|
newPromptBtn.className = 'bg-white/10 hover:bg-white/20 px-6 py-2.5 rounded-2xl text-xs font-bold transition-all border border-white/5 backdrop-blur-lg text-white';
|
||||||
newPromptBtn.textContent = '+ New';
|
newPromptBtn.textContent = '+ New';
|
||||||
|
|
||||||
canvasControls.appendChild(regenerateBtn);
|
canvasControls.appendChild(regenerateBtn);
|
||||||
|
canvasControls.appendChild(extendBtn);
|
||||||
canvasControls.appendChild(downloadBtn);
|
canvasControls.appendChild(downloadBtn);
|
||||||
canvasControls.appendChild(newPromptBtn);
|
canvasControls.appendChild(newPromptBtn);
|
||||||
|
|
||||||
|
|
@ -413,10 +495,14 @@ export function VideoStudio() {
|
||||||
container.appendChild(canvas);
|
container.appendChild(canvas);
|
||||||
|
|
||||||
// --- Helper: Show video in canvas ---
|
// --- Helper: Show video in canvas ---
|
||||||
const showVideoInCanvas = (videoUrl) => {
|
const showVideoInCanvas = (videoUrl, genModel) => {
|
||||||
hero.classList.add('hidden');
|
hero.classList.add('hidden');
|
||||||
promptWrapper.classList.add('hidden');
|
promptWrapper.classList.add('hidden');
|
||||||
|
|
||||||
|
// Show extend button only for seedance-v2.0-t2v and i2v (not extend itself)
|
||||||
|
const isSeedance2 = genModel && (genModel === 'seedance-v2.0-t2v' || genModel === 'seedance-v2.0-i2v');
|
||||||
|
extendBtn.classList.toggle('hidden', !isSeedance2);
|
||||||
|
|
||||||
resultVideo.src = videoUrl;
|
resultVideo.src = videoUrl;
|
||||||
resultVideo.onloadeddata = () => {
|
resultVideo.onloadeddata = () => {
|
||||||
canvas.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
|
canvas.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
|
||||||
|
|
@ -455,7 +541,15 @@ export function VideoStudio() {
|
||||||
downloadFile(entry.url, `video-${entry.id || idx}.mp4`);
|
downloadFile(entry.url, `video-${entry.id || idx}.mp4`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showVideoInCanvas(entry.url);
|
// Restore extend context when viewing a seedance-v2.0 generation
|
||||||
|
if (entry.model === 'seedance-v2.0-t2v' || entry.model === 'seedance-v2.0-i2v') {
|
||||||
|
lastGenerationId = entry.id;
|
||||||
|
lastGenerationModel = entry.model;
|
||||||
|
} else {
|
||||||
|
lastGenerationId = null;
|
||||||
|
lastGenerationModel = null;
|
||||||
|
}
|
||||||
|
showVideoInCanvas(entry.url, entry.model);
|
||||||
historyList.querySelectorAll('div').forEach(t => {
|
historyList.querySelectorAll('div').forEach(t => {
|
||||||
t.classList.remove('border-primary', 'shadow-glow');
|
t.classList.remove('border-primary', 'shadow-glow');
|
||||||
t.classList.add('border-white/10');
|
t.classList.add('border-white/10');
|
||||||
|
|
@ -508,17 +602,20 @@ export function VideoStudio() {
|
||||||
|
|
||||||
regenerateBtn.onclick = () => generateBtn.click();
|
regenerateBtn.onclick = () => generateBtn.click();
|
||||||
|
|
||||||
newPromptBtn.onclick = () => {
|
const resetToPromptBar = () => {
|
||||||
canvas.classList.add('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
|
canvas.classList.add('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
|
||||||
canvas.classList.remove('opacity-100', 'translate-y-0', 'scale-100');
|
canvas.classList.remove('opacity-100', 'translate-y-0', 'scale-100');
|
||||||
canvasControls.classList.add('opacity-0');
|
canvasControls.classList.add('opacity-0');
|
||||||
canvasControls.classList.remove('opacity-100');
|
canvasControls.classList.remove('opacity-100');
|
||||||
hero.classList.remove('hidden', 'opacity-0', 'scale-95', '-translate-y-10', 'pointer-events-none');
|
hero.classList.remove('hidden', 'opacity-0', 'scale-95', '-translate-y-10', 'pointer-events-none');
|
||||||
promptWrapper.classList.remove('hidden', 'opacity-40');
|
promptWrapper.classList.remove('hidden', 'opacity-40');
|
||||||
|
};
|
||||||
|
|
||||||
|
newPromptBtn.onclick = () => {
|
||||||
|
resetToPromptBar();
|
||||||
textarea.value = '';
|
textarea.value = '';
|
||||||
picker.reset();
|
picker.reset();
|
||||||
uploadedImageUrl = null;
|
uploadedImageUrl = null;
|
||||||
// Reset to t2v mode
|
|
||||||
imageMode = false;
|
imageMode = false;
|
||||||
selectedModel = t2vModels[0].id;
|
selectedModel = t2vModels[0].id;
|
||||||
selectedModelName = t2vModels[0].name;
|
selectedModelName = t2vModels[0].name;
|
||||||
|
|
@ -528,12 +625,35 @@ export function VideoStudio() {
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extendBtn.onclick = () => {
|
||||||
|
if (!lastGenerationId) return;
|
||||||
|
resetToPromptBar();
|
||||||
|
textarea.value = '';
|
||||||
|
picker.reset();
|
||||||
|
uploadedImageUrl = null;
|
||||||
|
imageMode = false;
|
||||||
|
selectedModel = 'seedance-v2.0-extend';
|
||||||
|
selectedModelName = 'Seedance 2.0 Extend';
|
||||||
|
document.getElementById('v-model-btn-label').textContent = selectedModelName;
|
||||||
|
updateControlsForModel(selectedModel);
|
||||||
|
textarea.placeholder = 'Optional: describe how to continue the video...';
|
||||||
|
textarea.focus();
|
||||||
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 5. GENERATION LOGIC
|
// 5. GENERATION LOGIC
|
||||||
// ==========================================
|
// ==========================================
|
||||||
generateBtn.onclick = async () => {
|
generateBtn.onclick = async () => {
|
||||||
const prompt = textarea.value.trim();
|
const prompt = textarea.value.trim();
|
||||||
if (imageMode) {
|
const model = getCurrentModel();
|
||||||
|
const isExtendMode = model?.requiresRequestId;
|
||||||
|
|
||||||
|
if (isExtendMode) {
|
||||||
|
if (!lastGenerationId) {
|
||||||
|
alert('No Seedance 2.0 generation found to extend. Generate a video first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (imageMode) {
|
||||||
if (!uploadedImageUrl) {
|
if (!uploadedImageUrl) {
|
||||||
alert('Please upload a start frame image first.');
|
alert('Please upload a start frame image first.');
|
||||||
return;
|
return;
|
||||||
|
|
@ -556,13 +676,17 @@ export function VideoStudio() {
|
||||||
generateBtn.innerHTML = `<span class="animate-spin inline-block mr-2 text-black">◌</span> Generating...`;
|
generateBtn.innerHTML = `<span class="animate-spin inline-block mr-2 text-black">◌</span> Generating...`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = { model: selectedModel };
|
||||||
model: selectedModel,
|
|
||||||
aspect_ratio: selectedAr,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (prompt) params.prompt = prompt;
|
if (prompt) params.prompt = prompt;
|
||||||
if (imageMode && uploadedImageUrl) params.image_url = uploadedImageUrl;
|
|
||||||
|
// Extend mode: pass stored request_id, skip aspect_ratio
|
||||||
|
if (isExtendMode) {
|
||||||
|
params.request_id = lastGenerationId;
|
||||||
|
} else {
|
||||||
|
params.aspect_ratio = selectedAr;
|
||||||
|
if (imageMode && uploadedImageUrl) params.image_url = uploadedImageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
const durations = getCurrentDurations(selectedModel);
|
const durations = getCurrentDurations(selectedModel);
|
||||||
if (durations.length > 0) params.duration = selectedDuration;
|
if (durations.length > 0) params.duration = selectedDuration;
|
||||||
|
|
@ -570,16 +694,25 @@ export function VideoStudio() {
|
||||||
const resolutions = getCurrentResolutions(selectedModel);
|
const resolutions = getCurrentResolutions(selectedModel);
|
||||||
if (resolutions.length > 0) params.resolution = selectedResolution;
|
if (resolutions.length > 0) params.resolution = selectedResolution;
|
||||||
|
|
||||||
const model = getCurrentModels().find(m => m.id === selectedModel);
|
if (selectedQuality) params.quality = selectedQuality;
|
||||||
if (model?.inputs?.quality) params.quality = model.inputs.quality.default;
|
|
||||||
|
|
||||||
const res = imageMode ? await muapi.generateI2V(params) : await muapi.generateVideo(params);
|
const res = imageMode ? await muapi.generateI2V(params) : await muapi.generateVideo(params);
|
||||||
|
|
||||||
console.log('[VideoStudio] Full response:', res);
|
console.log('[VideoStudio] Full response:', res);
|
||||||
|
|
||||||
if (res && res.url) {
|
if (res && res.url) {
|
||||||
|
const genId = res.id || res.request_id || Date.now().toString();
|
||||||
|
// Store request_id for seedance-v2.0 models (enables Extend button)
|
||||||
|
if (selectedModel === 'seedance-v2.0-t2v' || selectedModel === 'seedance-v2.0-i2v') {
|
||||||
|
lastGenerationId = genId;
|
||||||
|
lastGenerationModel = selectedModel;
|
||||||
|
} else {
|
||||||
|
lastGenerationId = null;
|
||||||
|
lastGenerationModel = null;
|
||||||
|
}
|
||||||
|
|
||||||
addToHistory({
|
addToHistory({
|
||||||
id: res.id || Date.now().toString(),
|
id: genId,
|
||||||
url: res.url,
|
url: res.url,
|
||||||
prompt,
|
prompt,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
|
|
@ -587,7 +720,7 @@ export function VideoStudio() {
|
||||||
duration: selectedDuration,
|
duration: selectedDuration,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
showVideoInCanvas(res.url);
|
showVideoInCanvas(res.url, selectedModel);
|
||||||
} else {
|
} else {
|
||||||
console.error('[VideoStudio] No video URL in response:', res);
|
console.error('[VideoStudio] No video URL in response:', res);
|
||||||
throw new Error('No video URL returned by API');
|
throw new Error('No video URL returned by API');
|
||||||
|
|
|
||||||
|
|
@ -2170,6 +2170,17 @@ export const t2vModels = [
|
||||||
"quality": { "enum": ["high", "basic"], "title": "Quality", "name": "quality", "type": "string", "description": "Quality of the generated video.", "default": "basic" }
|
"quality": { "enum": ["high", "basic"], "title": "Quality", "name": "quality", "type": "string", "description": "Quality of the generated video.", "default": "basic" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "seedance-v2.0-extend",
|
||||||
|
"name": "Seedance 2.0 Extend",
|
||||||
|
"requiresRequestId": true,
|
||||||
|
"inputs": {
|
||||||
|
"request_id": { "type": "string", "title": "Request ID", "name": "request_id", "description": "Request ID of the original Seedance 2.0 video generation.", "placeholder": "abcdefg-123-456-789-a1b2c3d4e5f6" },
|
||||||
|
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Optional prompt to guide the extension. If omitted, the model continues with the original scene." },
|
||||||
|
"duration": { "enum": [5, 10, 15], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video extension in seconds", "default": 5 },
|
||||||
|
"quality": { "enum": ["high", "basic"], "title": "Quality", "name": "quality", "type": "string", "description": "Quality of the generated video.", "default": "basic" }
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "kling-v2.1-master-t2v",
|
"id": "kling-v2.1-master-t2v",
|
||||||
"name": "Kling v2.1 Master",
|
"name": "Kling v2.1 Master",
|
||||||
|
|
@ -7865,6 +7876,46 @@ export const i2vModels = [
|
||||||
"default": true
|
"default": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "seedance-v2.0-i2v",
|
||||||
|
"name": "Seedance 2.0 I2V",
|
||||||
|
"endpoint": "seedance-v2.0-i2v",
|
||||||
|
"family": "seedance-v2.0",
|
||||||
|
"imageField": "images_list",
|
||||||
|
"hasPrompt": true,
|
||||||
|
"inputs": {
|
||||||
|
"prompt": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Prompt",
|
||||||
|
"name": "prompt",
|
||||||
|
"description": "The prompt to guide video generation from the image."
|
||||||
|
},
|
||||||
|
"aspect_ratio": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Aspect Ratio",
|
||||||
|
"name": "aspect_ratio",
|
||||||
|
"description": "Aspect ratio of the output video.",
|
||||||
|
"enum": ["16:9", "9:16", "4:3", "3:4"],
|
||||||
|
"default": "16:9"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"type": "int",
|
||||||
|
"title": "Duration",
|
||||||
|
"name": "duration",
|
||||||
|
"description": "The duration of the generated video in seconds",
|
||||||
|
"enum": [5, 10, 15],
|
||||||
|
"default": 5
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Quality",
|
||||||
|
"name": "quality",
|
||||||
|
"description": "Quality of the generated video.",
|
||||||
|
"enum": ["high", "basic"],
|
||||||
|
"default": "basic"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,10 @@ export class MuapiClient {
|
||||||
const endpoint = modelInfo?.endpoint || params.model;
|
const endpoint = modelInfo?.endpoint || params.model;
|
||||||
const url = `${this.baseUrl}/api/v1/${endpoint}`;
|
const url = `${this.baseUrl}/api/v1/${endpoint}`;
|
||||||
|
|
||||||
const finalPayload = { prompt: params.prompt };
|
const finalPayload = {};
|
||||||
|
|
||||||
|
if (params.prompt) finalPayload.prompt = params.prompt;
|
||||||
|
if (params.request_id) finalPayload.request_id = params.request_id;
|
||||||
if (params.aspect_ratio) finalPayload.aspect_ratio = params.aspect_ratio;
|
if (params.aspect_ratio) finalPayload.aspect_ratio = params.aspect_ratio;
|
||||||
if (params.duration) finalPayload.duration = params.duration;
|
if (params.duration) finalPayload.duration = params.duration;
|
||||||
if (params.resolution) finalPayload.resolution = params.resolution;
|
if (params.resolution) finalPayload.resolution = params.resolution;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue