diff --git a/README.md b/README.md
index d4af500..1883a94 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/components/VideoStudio.js b/src/components/VideoStudio.js
index 31136af..cac9574 100644
--- a/src/components/VideoStudio.js
+++ b/src/components/VideoStudio.js
@@ -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 = `
+
+ Extending previous Seedance 2.0 generation — add an optional prompt to guide the continuation
+ `;
+ 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() {
`, selectedResolution || '720p', 'v-resolution-btn');
+ const qualityBtn = createControlBtn(`
+
+ `, 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 = `
Quality
`;
+ 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 = `
+ ${q}
+ ${selectedQuality === q ? '' : ''}
+ `;
+ 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 = `Resolution
`;
@@ -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 = `◌ 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');
diff --git a/src/lib/models.js b/src/lib/models.js
index 22130a5..b8b4b41 100644
--- a/src/lib/models.js
+++ b/src/lib/models.js
@@ -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"
+ }
+ }
}
];
diff --git a/src/lib/muapi.js b/src/lib/muapi.js
index 36b1140..cb1e9bb 100644
--- a/src/lib/muapi.js
+++ b/src/lib/muapi.js
@@ -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;