diff --git a/src/components/VideoStudio.js b/src/components/VideoStudio.js index 2c4e632..769cd86 100644 --- a/src/components/VideoStudio.js +++ b/src/components/VideoStudio.js @@ -17,6 +17,7 @@ export function VideoStudio() { let selectedResolution = defaultModel.inputs?.resolution?.default || ''; let selectedQuality = defaultModel.inputs?.quality?.default || ''; let selectedMode = ''; + let selectedEffectName = ''; let lastGenerationId = null; let lastGenerationModel = null; let dropdownOpen = null; @@ -35,6 +36,10 @@ export function VideoStudio() { const model = getCurrentModels().find(m => m.id === id); return model?.inputs?.quality?.enum || []; }; + const getEffectNamesForModel = (id) => { + const model = getCurrentModels().find(m => m.id === id); + return model?.inputs?.name?.enum || []; + }; // ========================================== // 1. HERO SECTION @@ -291,12 +296,17 @@ export function VideoStudio() { `, selectedMode || 'normal', 'v-mode-btn'); + const effectNameBtn = createControlBtn(` + + `, 'Effect', 'v-effect-btn', 'Select effect type'); + controlsLeft.appendChild(modelBtn); controlsLeft.appendChild(arBtn); controlsLeft.appendChild(durationBtn); controlsLeft.appendChild(resolutionBtn); controlsLeft.appendChild(qualityBtn); - + controlsLeft.appendChild(effectNameBtn); + // Advanced options toggle button const advancedBtn = createControlBtn(` @@ -310,6 +320,7 @@ export function VideoStudio() { resolutionBtn.style.display = initResolutions.length > 0 ? 'flex' : 'none'; qualityBtn.style.display = 'none'; modeBtn.style.display = getModesForModel(defaultModel.id).length > 0 ? 'flex' : 'none'; + effectNameBtn.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'; @@ -338,6 +349,7 @@ export function VideoStudio() { resolutionBtn.style.display = 'none'; qualityBtn.style.display = 'none'; modeBtn.style.display = 'none'; + effectNameBtn.style.display = 'none'; extendBanner.classList.add('hidden'); extendBanner.classList.remove('flex'); return; @@ -395,6 +407,17 @@ export function VideoStudio() { modeBtn.style.display = 'none'; } + // Effect name (ai-video-effects / motion-controls) + const effectNames = getEffectNamesForModel(modelId); + if (effectNames.length > 0) { + selectedEffectName = model?.inputs?.name?.default || effectNames[0]; + document.getElementById('v-effect-btn-label').textContent = selectedEffectName; + effectNameBtn.style.display = 'flex'; + } else { + selectedEffectName = ''; + effectNameBtn.style.display = 'none'; + } + // Extend banner (extend model only) if (model?.requiresRequestId) { extendBanner.classList.remove('hidden'); @@ -617,6 +640,29 @@ export function VideoStudio() { list.appendChild(item); }); dropdown.appendChild(list); + + } else if (type === 'effect') { + dropdown.classList.add('max-w-[240px]'); + dropdown.classList.remove('max-w-[200px]'); + dropdown.innerHTML = `
Effect Type
`; + const list = document.createElement('div'); + list.className = 'flex flex-col gap-1 max-h-[50vh] overflow-y-auto custom-scrollbar'; + getEffectNamesForModel(selectedModel).forEach(e => { + const item = document.createElement('div'); + item.className = 'flex items-center justify-between p-3 hover:bg-white/5 rounded-2xl cursor-pointer transition-all group'; + item.innerHTML = ` + ${e} + ${selectedEffectName === e ? '' : ''} + `; + item.onclick = (ev) => { + ev.stopPropagation(); + selectedEffectName = e; + document.getElementById('v-effect-btn-label').textContent = e; + closeDropdown(); + }; + list.appendChild(item); + }); + dropdown.appendChild(list); } // Position dropdown @@ -650,6 +696,7 @@ export function VideoStudio() { resolutionBtn.onclick = toggleDropdown('resolution', resolutionBtn); qualityBtn.onclick = toggleDropdown('quality', qualityBtn); modeBtn.onclick = toggleDropdown('mode', modeBtn); + effectNameBtn.onclick = toggleDropdown('effect', effectNameBtn); window.addEventListener('click', closeDropdown); container.appendChild(dropdown); @@ -977,7 +1024,7 @@ export function VideoStudio() { image_url: uploadedImageUrl, onRequestId, }; - if (prompt) i2vParams.prompt = prompt; + i2vParams.prompt = prompt || ''; i2vParams.aspect_ratio = selectedAr; const durations = getCurrentDurations(selectedModel); if (durations.length > 0) i2vParams.duration = selectedDuration; @@ -985,6 +1032,7 @@ export function VideoStudio() { if (resolutions.length > 0) i2vParams.resolution = selectedResolution; if (selectedQuality) i2vParams.quality = selectedQuality; if (selectedMode) i2vParams.mode = selectedMode; + if (selectedEffectName) i2vParams.name = selectedEffectName; const res = await muapi.generateI2V(i2vParams); console.log('[VideoStudio] I2V response:', res); diff --git a/src/lib/muapi.js b/src/lib/muapi.js index 75c432a..4ca587a 100644 --- a/src/lib/muapi.js +++ b/src/lib/muapi.js @@ -245,7 +245,7 @@ export class MuapiClient { const finalPayload = {}; // Only include prompt if the model supports it and one was provided - if (params.prompt) finalPayload.prompt = params.prompt; + finalPayload.prompt = params.prompt || ''; // Place the uploaded image(s) in the correct field for this model const imageField = modelInfo?.imageField || 'image_url'; @@ -331,6 +331,7 @@ export class MuapiClient { if (params.resolution) finalPayload.resolution = params.resolution; if (params.quality) finalPayload.quality = params.quality; if (params.mode) finalPayload.mode = params.mode; + if (params.name) finalPayload.name = params.name; console.log('[Muapi] I2V Request:', url); console.log('[Muapi] I2V Payload:', finalPayload);