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:
Anil Matcha 2026-03-04 15:11:09 +05:30
parent 492f8bf693
commit d707604afe
4 changed files with 209 additions and 21 deletions

View file

@ -80,13 +80,15 @@ The Video Studio follows the same pattern:
| Mode | Trigger | Models | Prompt |
| :--- | :--- | :--- | :--- |
| **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
| 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 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
@ -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 |
| **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 |
| **Image-to-Video** | 60+ | Kling v2.1 I2V, Veo3 I2V, Runway I2V, Midjourney v7 I2V, Hunyuan I2V, Wan2.2 I2V |
| **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, Seedance 2.0 I2V, Midjourney v7 I2V, Hunyuan I2V, Wan2.2 I2V |
## 🛠️ Tech Stack

View file

@ -14,6 +14,9 @@ export function VideoStudio() {
let selectedAr = defaultModel.inputs?.aspect_ratio?.default || '16:9';
let selectedDuration = defaultModel.inputs?.duration?.default || 5;
let selectedResolution = defaultModel.inputs?.resolution?.default || '';
let selectedQuality = defaultModel.inputs?.quality?.default || '';
let lastGenerationId = null;
let lastGenerationModel = null;
let dropdownOpen = null;
let uploadedImageUrl = null;
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 getCurrentDurations = (id) => imageMode ? getDurationsForI2VModel(id) : getDurationsForModel(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
@ -103,6 +111,15 @@ export function VideoStudio() {
topRow.appendChild(textarea);
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
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';
@ -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>
`, 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(arBtn);
controlsLeft.appendChild(durationBtn);
controlsLeft.appendChild(resolutionBtn);
controlsLeft.appendChild(qualityBtn);
// Initial visibility (t2v mode)
const initDurations = getDurationsForModel(defaultModel.id);
durationBtn.style.display = initDurations.length > 0 ? 'flex' : 'none';
const initResolutions = getResolutionsForVideoModel(defaultModel.id);
resolutionBtn.style.display = initResolutions.length > 0 ? 'flex' : 'none';
qualityBtn.style.display = 'none';
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';
@ -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';
const updateControlsForModel = (modelId) => {
const availableArs = getCurrentAspectRatios(modelId);
selectedAr = availableArs[0];
document.getElementById('v-ar-btn-label').textContent = selectedAr;
const model = getCurrentModels().find(m => m.id === modelId);
// 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);
if (durations.length > 0) {
selectedDuration = durations[0];
@ -181,6 +213,7 @@ export function VideoStudio() {
durationBtn.style.display = 'none';
}
// Resolution
const resolutions = getCurrentResolutions(modelId);
if (resolutions.length > 0) {
selectedResolution = resolutions[0];
@ -189,6 +222,26 @@ export function VideoStudio() {
} else {
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) => {
@ -296,6 +349,28 @@ export function VideoStudio() {
});
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') {
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>`;
@ -349,6 +424,7 @@ export function VideoStudio() {
arBtn.onclick = toggleDropdown('ar', arBtn);
durationBtn.onclick = toggleDropdown('duration', durationBtn);
resolutionBtn.onclick = toggleDropdown('resolution', resolutionBtn);
qualityBtn.onclick = toggleDropdown('quality', qualityBtn);
window.addEventListener('click', closeDropdown);
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.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');
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';
canvasControls.appendChild(regenerateBtn);
canvasControls.appendChild(extendBtn);
canvasControls.appendChild(downloadBtn);
canvasControls.appendChild(newPromptBtn);
@ -413,10 +495,14 @@ export function VideoStudio() {
container.appendChild(canvas);
// --- Helper: Show video in canvas ---
const showVideoInCanvas = (videoUrl) => {
const showVideoInCanvas = (videoUrl, genModel) => {
hero.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.onloadeddata = () => {
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`);
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 => {
t.classList.remove('border-primary', 'shadow-glow');
t.classList.add('border-white/10');
@ -508,17 +602,20 @@ export function VideoStudio() {
regenerateBtn.onclick = () => generateBtn.click();
newPromptBtn.onclick = () => {
const resetToPromptBar = () => {
canvas.classList.add('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
canvas.classList.remove('opacity-100', 'translate-y-0', 'scale-100');
canvasControls.classList.add('opacity-0');
canvasControls.classList.remove('opacity-100');
hero.classList.remove('hidden', 'opacity-0', 'scale-95', '-translate-y-10', 'pointer-events-none');
promptWrapper.classList.remove('hidden', 'opacity-40');
};
newPromptBtn.onclick = () => {
resetToPromptBar();
textarea.value = '';
picker.reset();
uploadedImageUrl = null;
// Reset to t2v mode
imageMode = false;
selectedModel = t2vModels[0].id;
selectedModelName = t2vModels[0].name;
@ -528,12 +625,35 @@ export function VideoStudio() {
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
// ==========================================
generateBtn.onclick = async () => {
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) {
alert('Please upload a start frame image first.');
return;
@ -556,13 +676,17 @@ export function VideoStudio() {
generateBtn.innerHTML = `<span class="animate-spin inline-block mr-2 text-black">◌</span> Generating...`;
try {
const params = {
model: selectedModel,
aspect_ratio: selectedAr,
};
const params = { model: selectedModel };
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);
if (durations.length > 0) params.duration = selectedDuration;
@ -570,16 +694,25 @@ export function VideoStudio() {
const resolutions = getCurrentResolutions(selectedModel);
if (resolutions.length > 0) params.resolution = selectedResolution;
const model = getCurrentModels().find(m => m.id === selectedModel);
if (model?.inputs?.quality) params.quality = model.inputs.quality.default;
if (selectedQuality) params.quality = selectedQuality;
const res = imageMode ? await muapi.generateI2V(params) : await muapi.generateVideo(params);
console.log('[VideoStudio] Full response:', res);
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({
id: res.id || Date.now().toString(),
id: genId,
url: res.url,
prompt,
model: selectedModel,
@ -587,7 +720,7 @@ export function VideoStudio() {
duration: selectedDuration,
timestamp: new Date().toISOString()
});
showVideoInCanvas(res.url);
showVideoInCanvas(res.url, selectedModel);
} else {
console.error('[VideoStudio] No video URL in response:', res);
throw new Error('No video URL returned by API');

View file

@ -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" }
}
},
{
"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",
"name": "Kling v2.1 Master",
@ -7865,6 +7876,46 @@ export const i2vModels = [
"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"
}
}
}
];

View file

@ -172,8 +172,10 @@ export class MuapiClient {
const endpoint = modelInfo?.endpoint || params.model;
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.duration) finalPayload.duration = params.duration;
if (params.resolution) finalPayload.resolution = params.resolution;