mirror of
https://github.com/Anil-matcha/Open-Generative-AI.git
synced 2026-05-07 01:17:18 +00:00
Added Video Studio
This commit is contained in:
parent
69f7dce699
commit
6eebebc9ec
4 changed files with 1015 additions and 2 deletions
554
src/components/VideoStudio.js
Normal file
554
src/components/VideoStudio.js
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
import { muapi } from '../lib/muapi.js';
|
||||
import { t2vModels, getAspectRatiosForVideoModel, getDurationsForModel, getResolutionsForVideoModel } from '../lib/models.js';
|
||||
import { AuthModal } from './AuthModal.js';
|
||||
|
||||
export function VideoStudio() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'w-full h-full flex flex-col items-center justify-center bg-app-bg relative p-4 md:p-6 overflow-y-auto custom-scrollbar overflow-x-hidden';
|
||||
|
||||
// --- State ---
|
||||
const defaultModel = t2vModels[0];
|
||||
let selectedModel = defaultModel.id;
|
||||
let selectedModelName = defaultModel.name;
|
||||
let selectedAr = defaultModel.inputs?.aspect_ratio?.default || '16:9';
|
||||
let selectedDuration = defaultModel.inputs?.duration?.default || 5;
|
||||
let selectedResolution = defaultModel.inputs?.resolution?.default || '';
|
||||
let dropdownOpen = null;
|
||||
|
||||
// ==========================================
|
||||
// 1. HERO SECTION
|
||||
// ==========================================
|
||||
const hero = document.createElement('div');
|
||||
hero.className = 'flex flex-col items-center mb-10 md:mb-20 animate-fade-in-up transition-all duration-700';
|
||||
hero.innerHTML = `
|
||||
<div class="mb-10 relative group">
|
||||
<div class="absolute inset-0 bg-primary/20 blur-[100px] rounded-full opacity-40 group-hover:opacity-70 transition-opacity duration-1000"></div>
|
||||
<div class="relative w-24 h-24 md:w-32 md:h-32 bg-teal-900/40 rounded-3xl flex items-center justify-center border border-white/5 overflow-hidden">
|
||||
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" class="text-primary opacity-20 absolute -right-4 -bottom-4">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
<div class="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center border border-primary/20 shadow-glow relative z-10">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="text-primary">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="absolute top-4 right-4 text-primary animate-pulse">✨</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-2xl sm:text-4xl md:text-7xl font-black text-white tracking-widest uppercase mb-4 selection:bg-primary selection:text-black text-center px-4">Video Studio</h1>
|
||||
<p class="text-secondary text-sm font-medium tracking-wide opacity-60">Create stunning AI videos from text in seconds</p>
|
||||
`;
|
||||
container.appendChild(hero);
|
||||
|
||||
// ==========================================
|
||||
// 2. PROMPT BAR
|
||||
// ==========================================
|
||||
const promptWrapper = document.createElement('div');
|
||||
promptWrapper.className = 'w-full max-w-4xl relative z-40 animate-fade-in-up';
|
||||
promptWrapper.style.animationDelay = '0.2s';
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'w-full bg-[#111]/90 backdrop-blur-xl border border-white/10 rounded-[1.5rem] md:rounded-[2.5rem] p-3 md:p-5 flex flex-col gap-3 md:gap-5 shadow-3xl';
|
||||
|
||||
const topRow = document.createElement('div');
|
||||
topRow.className = 'flex items-start gap-5 px-2';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.placeholder = 'Describe the video you imagine';
|
||||
textarea.className = 'flex-1 bg-transparent border-none text-white text-base md:text-xl placeholder:text-muted focus:outline-none resize-none pt-2.5 leading-relaxed min-h-[40px] max-h-[150px] md:max-h-[250px] overflow-y-auto custom-scrollbar';
|
||||
textarea.rows = 1;
|
||||
textarea.oninput = () => {
|
||||
textarea.style.height = 'auto';
|
||||
const maxHeight = window.innerWidth < 768 ? 150 : 250;
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, maxHeight) + 'px';
|
||||
};
|
||||
|
||||
topRow.appendChild(textarea);
|
||||
bar.appendChild(topRow);
|
||||
|
||||
// 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';
|
||||
|
||||
const controlsLeft = document.createElement('div');
|
||||
controlsLeft.className = 'flex items-center gap-1.5 md:gap-2.5 relative overflow-x-auto no-scrollbar pb-1 md:pb-0';
|
||||
|
||||
const createControlBtn = (icon, label, id) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.id = id;
|
||||
btn.className = 'flex items-center gap-1.5 md:gap-2.5 px-3 md:px-4 py-2 md:py-2.5 bg-white/5 hover:bg-white/10 rounded-xl md:rounded-2xl transition-all border border-white/5 group whitespace-nowrap';
|
||||
btn.innerHTML = `
|
||||
${icon}
|
||||
<span id="${id}-label" class="text-xs font-bold text-white group-hover:text-primary transition-colors">${label}</span>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" class="opacity-20 group-hover:opacity-100 transition-opacity"><path d="M6 9l6 6 6-6"/></svg>
|
||||
`;
|
||||
return btn;
|
||||
};
|
||||
|
||||
const modelBtn = createControlBtn(`
|
||||
<div class="w-5 h-5 bg-primary rounded-md flex items-center justify-center shadow-lg shadow-primary/20">
|
||||
<span class="text-[10px] font-black text-black">V</span>
|
||||
</div>
|
||||
`, selectedModelName, 'v-model-btn');
|
||||
|
||||
const arBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>
|
||||
`, selectedAr, 'v-ar-btn');
|
||||
|
||||
const durationBtn = createControlBtn(`
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="opacity-60 text-secondary"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
`, `${selectedDuration}s`, 'v-duration-btn');
|
||||
|
||||
const resolutionBtn = 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="M6 2L3 6v15a2 2 0 002 2h14a2 2 0 002-2V6l-3-4H6z"/></svg>
|
||||
`, selectedResolution || '720p', 'v-resolution-btn');
|
||||
|
||||
controlsLeft.appendChild(modelBtn);
|
||||
controlsLeft.appendChild(arBtn);
|
||||
controlsLeft.appendChild(durationBtn);
|
||||
controlsLeft.appendChild(resolutionBtn);
|
||||
|
||||
// Initial visibility
|
||||
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';
|
||||
|
||||
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.innerHTML = `Generate ✨`;
|
||||
|
||||
bottomRow.appendChild(controlsLeft);
|
||||
bottomRow.appendChild(generateBtn);
|
||||
bar.appendChild(bottomRow);
|
||||
promptWrapper.appendChild(bar);
|
||||
container.appendChild(promptWrapper);
|
||||
|
||||
// ==========================================
|
||||
// 3. DROPDOWNS
|
||||
// ==========================================
|
||||
const dropdown = document.createElement('div');
|
||||
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 = getAspectRatiosForVideoModel(modelId);
|
||||
selectedAr = availableArs[0];
|
||||
document.getElementById('v-ar-btn-label').textContent = selectedAr;
|
||||
|
||||
const durations = getDurationsForModel(modelId);
|
||||
if (durations.length > 0) {
|
||||
selectedDuration = durations[0];
|
||||
document.getElementById('v-duration-btn-label').textContent = `${selectedDuration}s`;
|
||||
durationBtn.style.display = 'flex';
|
||||
} else {
|
||||
durationBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
const resolutions = getResolutionsForVideoModel(modelId);
|
||||
if (resolutions.length > 0) {
|
||||
selectedResolution = resolutions[0];
|
||||
document.getElementById('v-resolution-btn-label').textContent = selectedResolution;
|
||||
resolutionBtn.style.display = 'flex';
|
||||
} else {
|
||||
resolutionBtn.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
const showDropdown = (type, anchorBtn) => {
|
||||
dropdown.innerHTML = '';
|
||||
dropdown.classList.remove('opacity-0', 'pointer-events-none');
|
||||
dropdown.classList.add('opacity-100', 'pointer-events-auto');
|
||||
|
||||
if (type === 'model') {
|
||||
dropdown.classList.add('w-[calc(100vw-3rem)]', 'max-w-xs');
|
||||
dropdown.classList.remove('max-w-[240px]', 'max-w-[200px]');
|
||||
dropdown.innerHTML = `
|
||||
<div class="flex flex-col h-full max-h-[70vh]">
|
||||
<div class="px-2 pb-3 mb-2 border-b border-white/5 shrink-0">
|
||||
<div class="flex items-center gap-3 bg-white/5 rounded-xl px-4 py-2.5 border border-white/5 focus-within:border-primary/50 transition-colors">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" class="text-muted"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||
<input type="text" id="v-model-search" placeholder="Search models..." class="bg-transparent border-none text-xs text-white focus:ring-0 w-full p-0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[10px] font-bold text-secondary uppercase tracking-widest px-3 py-2 shrink-0">Video models</div>
|
||||
<div id="v-model-list-container" class="flex flex-col gap-1.5 overflow-y-auto custom-scrollbar pr-1 pb-2"></div>
|
||||
</div>
|
||||
`;
|
||||
const list = dropdown.querySelector('#v-model-list-container');
|
||||
|
||||
const renderModels = (filter = '') => {
|
||||
list.innerHTML = '';
|
||||
const filtered = t2vModels.filter(m => m.name.toLowerCase().includes(filter.toLowerCase()) || m.id.toLowerCase().includes(filter.toLowerCase()));
|
||||
filtered.forEach(m => {
|
||||
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 border border-transparent hover:border-white/5 ${selectedModel === m.id ? 'bg-white/5 border-white/5' : ''}`;
|
||||
item.innerHTML = `
|
||||
<div class="flex items-center gap-3.5">
|
||||
<div class="w-10 h-10 ${m.id.includes('kling') ? 'bg-blue-500/10 text-blue-400' : m.id.includes('veo') ? 'bg-purple-500/10 text-purple-400' : m.id.includes('sora') ? 'bg-rose-500/10 text-rose-400' : 'bg-primary/10 text-primary'} border border-white/5 rounded-xl flex items-center justify-center font-black text-sm shadow-inner uppercase">${m.name.charAt(0)}</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs font-bold text-white tracking-tight">${m.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
${selectedModel === m.id ? '<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();
|
||||
selectedModel = m.id;
|
||||
selectedModelName = m.name;
|
||||
document.getElementById('v-model-btn-label').textContent = selectedModelName;
|
||||
updateControlsForModel(selectedModel);
|
||||
closeDropdown();
|
||||
};
|
||||
list.appendChild(item);
|
||||
});
|
||||
};
|
||||
|
||||
renderModels();
|
||||
const searchInput = dropdown.querySelector('#v-model-search');
|
||||
searchInput.onclick = (e) => e.stopPropagation();
|
||||
searchInput.oninput = (e) => renderModels(e.target.value);
|
||||
|
||||
} else if (type === 'ar') {
|
||||
dropdown.classList.add('max-w-[240px]');
|
||||
dropdown.innerHTML = `<div class="text-[10px] font-bold text-muted uppercase tracking-widest px-3 py-2 border-b border-white/5 mb-2">Aspect Ratio</div>`;
|
||||
const list = document.createElement('div');
|
||||
list.className = 'flex flex-col gap-1';
|
||||
const availableArs = getAspectRatiosForVideoModel(selectedModel);
|
||||
availableArs.forEach(r => {
|
||||
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 = `
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-6 h-6 border-2 border-white/20 rounded-md shadow-inner flex items-center justify-center group-hover:border-primary/50 transition-colors">
|
||||
<div class="w-3 h-3 bg-white/10 rounded-sm"></div>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-white opacity-80 group-hover:opacity-100 transition-opacity">${r}</span>
|
||||
</div>
|
||||
${selectedAr === r ? '<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();
|
||||
selectedAr = r;
|
||||
document.getElementById('v-ar-btn-label').textContent = r;
|
||||
closeDropdown();
|
||||
};
|
||||
list.appendChild(item);
|
||||
});
|
||||
dropdown.appendChild(list);
|
||||
|
||||
} else if (type === 'duration') {
|
||||
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">Duration</div>`;
|
||||
const list = document.createElement('div');
|
||||
list.className = 'flex flex-col gap-1';
|
||||
const durations = getDurationsForModel(selectedModel);
|
||||
durations.forEach(d => {
|
||||
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">${d}s</span>
|
||||
${selectedDuration === d ? '<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();
|
||||
selectedDuration = d;
|
||||
document.getElementById('v-duration-btn-label').textContent = `${d}s`;
|
||||
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>`;
|
||||
const list = document.createElement('div');
|
||||
list.className = 'flex flex-col gap-1';
|
||||
const resolutions = getResolutionsForVideoModel(selectedModel);
|
||||
resolutions.forEach(r => {
|
||||
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">${r}</span>
|
||||
${selectedResolution === r ? '<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();
|
||||
selectedResolution = r;
|
||||
document.getElementById('v-resolution-btn-label').textContent = r;
|
||||
closeDropdown();
|
||||
};
|
||||
list.appendChild(item);
|
||||
});
|
||||
dropdown.appendChild(list);
|
||||
}
|
||||
|
||||
// Position dropdown
|
||||
const btnRect = anchorBtn.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
if (window.innerWidth < 768) {
|
||||
dropdown.style.left = '50%';
|
||||
dropdown.style.transform = 'translateX(-50%) translate(0, 8px)';
|
||||
} else {
|
||||
dropdown.style.left = `${btnRect.left - containerRect.left}px`;
|
||||
dropdown.style.transform = 'translate(0, 8px)';
|
||||
}
|
||||
dropdown.style.bottom = `${containerRect.bottom - btnRect.top + 8}px`;
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
dropdown.classList.add('opacity-0', 'pointer-events-none');
|
||||
dropdown.classList.remove('opacity-100', 'pointer-events-auto');
|
||||
dropdownOpen = null;
|
||||
};
|
||||
|
||||
const toggleDropdown = (type, btn) => (e) => {
|
||||
e.stopPropagation();
|
||||
if (dropdownOpen === type) closeDropdown();
|
||||
else { dropdownOpen = type; showDropdown(type, btn); }
|
||||
};
|
||||
|
||||
modelBtn.onclick = toggleDropdown('model', modelBtn);
|
||||
arBtn.onclick = toggleDropdown('ar', arBtn);
|
||||
durationBtn.onclick = toggleDropdown('duration', durationBtn);
|
||||
resolutionBtn.onclick = toggleDropdown('resolution', resolutionBtn);
|
||||
|
||||
window.addEventListener('click', closeDropdown);
|
||||
container.appendChild(dropdown);
|
||||
|
||||
// ==========================================
|
||||
// 4. CANVAS AREA + HISTORY
|
||||
// ==========================================
|
||||
const generationHistory = [];
|
||||
|
||||
const historySidebar = document.createElement('div');
|
||||
historySidebar.className = 'fixed right-0 top-0 h-full w-20 md:w-24 bg-black/60 backdrop-blur-xl border-l border-white/5 z-50 flex flex-col items-center py-4 gap-3 overflow-y-auto transition-all duration-500 translate-x-full opacity-0';
|
||||
historySidebar.id = 'video-history-sidebar';
|
||||
|
||||
const historyLabel = document.createElement('div');
|
||||
historyLabel.className = 'text-[9px] font-bold text-muted uppercase tracking-widest mb-2';
|
||||
historyLabel.textContent = 'History';
|
||||
historySidebar.appendChild(historyLabel);
|
||||
|
||||
const historyList = document.createElement('div');
|
||||
historyList.className = 'flex flex-col gap-2 w-full px-2';
|
||||
historySidebar.appendChild(historyList);
|
||||
container.appendChild(historySidebar);
|
||||
|
||||
// Main canvas
|
||||
const canvas = document.createElement('div');
|
||||
canvas.className = 'absolute inset-0 flex flex-col items-center justify-center p-4 min-[800px]:p-16 z-10 opacity-0 pointer-events-none transition-all duration-1000 translate-y-10 scale-95';
|
||||
|
||||
const videoContainer = document.createElement('div');
|
||||
videoContainer.className = 'relative group';
|
||||
|
||||
const resultVideo = document.createElement('video');
|
||||
resultVideo.className = 'max-h-[60vh] max-w-[80vw] rounded-3xl shadow-3xl border border-white/10 interactive-glow object-contain';
|
||||
resultVideo.controls = true;
|
||||
resultVideo.loop = true;
|
||||
resultVideo.autoplay = true;
|
||||
resultVideo.muted = true;
|
||||
resultVideo.playsInline = true;
|
||||
videoContainer.appendChild(resultVideo);
|
||||
|
||||
// Canvas Controls
|
||||
const canvasControls = document.createElement('div');
|
||||
canvasControls.className = 'mt-6 flex gap-3 opacity-0 transition-opacity delay-500 duration-500 justify-center';
|
||||
|
||||
const regenerateBtn = document.createElement('button');
|
||||
regenerateBtn.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';
|
||||
regenerateBtn.textContent = '↻ Regenerate';
|
||||
|
||||
const downloadBtn = document.createElement('button');
|
||||
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 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(downloadBtn);
|
||||
canvasControls.appendChild(newPromptBtn);
|
||||
|
||||
canvas.appendChild(videoContainer);
|
||||
canvas.appendChild(canvasControls);
|
||||
container.appendChild(canvas);
|
||||
|
||||
// --- Helper: Show video in canvas ---
|
||||
const showVideoInCanvas = (videoUrl) => {
|
||||
hero.classList.add('hidden');
|
||||
promptWrapper.classList.add('hidden');
|
||||
|
||||
resultVideo.src = videoUrl;
|
||||
resultVideo.onloadeddata = () => {
|
||||
canvas.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-10', 'scale-95');
|
||||
canvas.classList.add('opacity-100', 'translate-y-0', 'scale-100');
|
||||
canvasControls.classList.remove('opacity-0');
|
||||
canvasControls.classList.add('opacity-100');
|
||||
};
|
||||
};
|
||||
|
||||
// --- Helper: Add to history ---
|
||||
const addToHistory = (entry) => {
|
||||
generationHistory.unshift(entry);
|
||||
localStorage.setItem('video_history', JSON.stringify(generationHistory.slice(0, 30)));
|
||||
historySidebar.classList.remove('translate-x-full', 'opacity-0');
|
||||
historySidebar.classList.add('translate-x-0', 'opacity-100');
|
||||
renderHistory();
|
||||
};
|
||||
|
||||
const renderHistory = () => {
|
||||
historyList.innerHTML = '';
|
||||
generationHistory.forEach((entry, idx) => {
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = `relative group/thumb cursor-pointer rounded-xl overflow-hidden border-2 transition-all duration-300 ${idx === 0 ? 'border-primary shadow-glow' : 'border-white/10 hover:border-white/30'}`;
|
||||
|
||||
thumb.innerHTML = `
|
||||
<video src="${entry.url}" preload="metadata" muted class="w-full aspect-square object-cover"></video>
|
||||
<div class="absolute inset-0 bg-black/60 opacity-0 group-hover/thumb:opacity-100 transition-opacity flex items-center justify-center gap-1">
|
||||
<button class="hist-download p-1.5 bg-primary rounded-lg text-black hover:scale-110 transition-transform" title="Download">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
thumb.onclick = (e) => {
|
||||
if (e.target.closest('.hist-download')) {
|
||||
downloadFile(entry.url, `video-${entry.id || idx}.mp4`);
|
||||
return;
|
||||
}
|
||||
showVideoInCanvas(entry.url);
|
||||
historyList.querySelectorAll('div').forEach(t => {
|
||||
t.classList.remove('border-primary', 'shadow-glow');
|
||||
t.classList.add('border-white/10');
|
||||
});
|
||||
thumb.classList.remove('border-white/10');
|
||||
thumb.classList.add('border-primary', 'shadow-glow');
|
||||
};
|
||||
|
||||
historyList.appendChild(thumb);
|
||||
});
|
||||
};
|
||||
|
||||
// --- Helper: Download file ---
|
||||
const downloadFile = async (url, filename) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
} catch (err) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
// --- Load history from localStorage ---
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem('video_history') || '[]');
|
||||
if (saved.length > 0) {
|
||||
saved.forEach(e => generationHistory.push(e));
|
||||
historySidebar.classList.remove('translate-x-full', 'opacity-0');
|
||||
historySidebar.classList.add('translate-x-0', 'opacity-100');
|
||||
renderHistory();
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// --- Button Handlers ---
|
||||
downloadBtn.onclick = () => {
|
||||
const current = resultVideo.src;
|
||||
if (current) {
|
||||
const entry = generationHistory.find(e => e.url === current);
|
||||
downloadFile(current, `video-${entry?.id || 'clip'}.mp4`);
|
||||
}
|
||||
};
|
||||
|
||||
regenerateBtn.onclick = () => generateBtn.click();
|
||||
|
||||
newPromptBtn.onclick = () => {
|
||||
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');
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// 5. GENERATION LOGIC
|
||||
// ==========================================
|
||||
generateBtn.onclick = async () => {
|
||||
const prompt = textarea.value.trim();
|
||||
if (!prompt) return;
|
||||
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) {
|
||||
AuthModal(() => generateBtn.click());
|
||||
return;
|
||||
}
|
||||
|
||||
hero.classList.add('opacity-0', 'scale-95', '-translate-y-10', 'pointer-events-none');
|
||||
|
||||
generateBtn.disabled = true;
|
||||
generateBtn.innerHTML = `<span class="animate-spin inline-block mr-2 text-black">◌</span> Generating...`;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
prompt,
|
||||
model: selectedModel,
|
||||
aspect_ratio: selectedAr,
|
||||
};
|
||||
|
||||
const durations = getDurationsForModel(selectedModel);
|
||||
if (durations.length > 0) params.duration = selectedDuration;
|
||||
|
||||
const resolutions = getResolutionsForVideoModel(selectedModel);
|
||||
if (resolutions.length > 0) params.resolution = selectedResolution;
|
||||
|
||||
const model = t2vModels.find(m => m.id === selectedModel);
|
||||
if (model?.inputs?.quality) params.quality = model.inputs.quality.default;
|
||||
|
||||
const res = await muapi.generateVideo(params);
|
||||
|
||||
console.log('[VideoStudio] Full response:', res);
|
||||
|
||||
if (res && res.url) {
|
||||
addToHistory({
|
||||
id: res.id || Date.now().toString(),
|
||||
url: res.url,
|
||||
prompt,
|
||||
model: selectedModel,
|
||||
aspect_ratio: selectedAr,
|
||||
duration: selectedDuration,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
showVideoInCanvas(res.url);
|
||||
} else {
|
||||
console.error('[VideoStudio] No video URL in response:', res);
|
||||
throw new Error('No video URL returned by API');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
generateBtn.innerHTML = `Error: ${e.message.slice(0, 40)}`;
|
||||
setTimeout(() => {
|
||||
generateBtn.innerHTML = `Generate ✨`;
|
||||
generateBtn.disabled = false;
|
||||
}, 3000);
|
||||
} finally {
|
||||
generateBtn.disabled = false;
|
||||
generateBtn.innerHTML = `Generate ✨`;
|
||||
}
|
||||
};
|
||||
|
||||
return container;
|
||||
}
|
||||
|
|
@ -2021,3 +2021,408 @@ export const getAspectRatiosForModel = (modelId) => {
|
|||
|
||||
return ['1:1', '16:9', '9:16', '4:3', '3:2', '21:9'];
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// Text-to-Video Models
|
||||
// ==========================================
|
||||
export const t2vModels = [
|
||||
{
|
||||
"id": "seedance-lite-t2v",
|
||||
"name": "Seedance Lite",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["480p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "480p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seedance-pro-t2v",
|
||||
"name": "Seedance Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["480p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "480p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seedance-pro-t2v-fast",
|
||||
"name": "Seedance Pro Fast",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["480p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "480p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seedance-v1.5-pro-t2v",
|
||||
"name": "Seedance v1.5 Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1", "3:4", "4:3", "21:9"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["480p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "720p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seedance-v1.5-pro-t2v-fast",
|
||||
"name": "Seedance v1.5 Pro Fast",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1", "3:4", "4:3", "21:9"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "720p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kling-v2.1-master-t2v",
|
||||
"name": "Kling v2.1 Master",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kling-v2.5-turbo-pro-t2v",
|
||||
"name": "Kling v2.5 Turbo Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "9:16" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kling-v2.6-pro-t2v",
|
||||
"name": "Kling v2.6 Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "enum": [5, 10], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds.", "default": 5 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kling-o1-text-to-video",
|
||||
"name": "Kling O1 Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "enum": [5, 10], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kling-v3.0-pro-text-to-video",
|
||||
"name": "Kling v3.0 Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "The aspect ratio of the generated video", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kling-v3.0-standard-text-to-video",
|
||||
"name": "Kling v3.0 Standard",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "The aspect ratio of the generated video", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "veo3-text-to-video",
|
||||
"name": "Veo 3",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the desired video content." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "veo3-fast-text-to-video",
|
||||
"name": "Veo 3 Fast",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the desired video content." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "veo3.1-text-to-video",
|
||||
"name": "Veo 3.1",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "enum": [8], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 8 },
|
||||
"resolution": { "enum": ["1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "1080p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "veo3.1-fast-text-to-video",
|
||||
"name": "Veo 3.1 Fast",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "enum": [8], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 8 },
|
||||
"resolution": { "enum": ["1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "1080p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "runway-text-to-video",
|
||||
"name": "Runway Gen-3",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to be used to generate a video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1", "4:3", "3:4"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "enum": [5, 8], "title": "Duration", "name": "duration", "type": "int", "description": "The duration in seconds. If 8-second video is selected, 1080p resolution cannot be used.", "default": 5 },
|
||||
"resolution": { "enum": ["720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video. If 1080p is selected, 8-second video cannot be generated.", "default": "720p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wan2.1-text-to-video",
|
||||
"name": "Wan 2.1",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["480p", "720p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "480p" },
|
||||
"quality": { "enum": ["medium", "high"], "title": "Quality", "name": "quality", "type": "string", "description": "The quality of the generated video.", "default": "medium" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wan2.2-text-to-video",
|
||||
"name": "Wan 2.2",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds.", "default": 5 },
|
||||
"resolution": { "enum": ["480p", "720p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "480p" },
|
||||
"quality": { "enum": ["medium", "high"], "title": "Quality", "name": "quality", "type": "string", "description": "The quality of the generated video.", "default": "medium" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wan2.2-5b-fast-t2v",
|
||||
"name": "Wan 2.2 Fast",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"resolution": { "enum": ["480p", "580p", "720p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "480p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wan2.5-text-to-video",
|
||||
"name": "Wan 2.5",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["480p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "480p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wan2.5-text-to-video-fast",
|
||||
"name": "Wan 2.5 Fast",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "720p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wan2.6-text-to-video",
|
||||
"name": "Wan 2.6",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "enum": [5, 10, 15], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "720p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hunyuan-text-to-video",
|
||||
"name": "Hunyuan",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hunyuan-fast-text-to-video",
|
||||
"name": "Hunyuan Fast",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pixverse-v4.5-t2v",
|
||||
"name": "Pixverse v4.5",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1", "4:3", "3:4"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds. 8s not supported for 1080p resolution.", "default": 5 },
|
||||
"resolution": { "enum": ["360p", "540p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "720p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pixverse-v5-t2v",
|
||||
"name": "Pixverse v5",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1", "4:3", "3:4"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["360p", "540p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "720p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pixverse-v5.5-t2v",
|
||||
"name": "Pixverse v5.5",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1", "4:3", "3:4"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "enum": [5, 8, 10], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds.", "default": 5 },
|
||||
"resolution": { "enum": ["360p", "540p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "360p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "minimax-hailuo-02-standard-t2v",
|
||||
"name": "Hailuo 02 Standard",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"duration": { "enum": [6, 10], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 6 },
|
||||
"resolution": { "enum": ["768P"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "768P" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "minimax-hailuo-02-pro-t2v",
|
||||
"name": "Hailuo 02 Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"duration": { "enum": [6], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 6 },
|
||||
"resolution": { "enum": ["1080P"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "1080P" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "minimax-hailuo-2.3-pro-t2v",
|
||||
"name": "Hailuo 2.3 Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"resolution": { "enum": ["1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "1080p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "minimax-hailuo-2.3-standard-t2v",
|
||||
"name": "Hailuo 2.3 Standard",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"duration": { "enum": [6, 10], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 6 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "openai-sora",
|
||||
"name": "Sora",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"resolution": { "enum": ["480p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "480p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "openai-sora-2-text-to-video",
|
||||
"name": "Sora 2",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "enum": [10, 15], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 10 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "openai-sora-2-pro-text-to-video",
|
||||
"name": "Sora 2 Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" },
|
||||
"duration": { "enum": [10, 15, 25], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds. Currently 25 seconds supports 720p only.", "default": 10 },
|
||||
"resolution": { "enum": ["720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "720p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vidu-v2.0-t2v",
|
||||
"name": "Vidu v2.0",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "The prompt to generate the video" },
|
||||
"aspect_ratio": { "enum": ["9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "9:16" },
|
||||
"duration": { "enum": [4], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds.", "default": 4 },
|
||||
"resolution": { "enum": ["1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "1080p" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ovi-text-to-video",
|
||||
"name": "OVI",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "16:9" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "grok-imagine-text-to-video",
|
||||
"name": "Grok Imagine",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["9:16", "16:9", "2:3", "3:2", "1:1"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "Aspect ratio of the output video.", "default": "1:1" },
|
||||
"duration": { "enum": [6, 10], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds.", "default": 6 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ltx-2-pro-text-to-video",
|
||||
"name": "LTX 2 Pro",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"duration": { "enum": [6, 8, 10], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 6 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ltx-2-fast-text-to-video",
|
||||
"name": "LTX 2 Fast",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"duration": { "enum": [6, 8, 10, 12, 14, 16, 18, 20], "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 6 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ltx-2-19b-text-to-video",
|
||||
"name": "LTX 2 19B",
|
||||
"inputs": {
|
||||
"prompt": { "type": "string", "title": "Prompt", "name": "prompt", "description": "Text prompt describing the video." },
|
||||
"aspect_ratio": { "enum": ["16:9", "9:16"], "title": "Aspect Ratio", "name": "aspect_ratio", "type": "string", "description": "The aspect ratio of the generated video", "default": "16:9" },
|
||||
"duration": { "title": "Duration", "name": "duration", "type": "int", "description": "The duration of the generated video in seconds", "default": 5 },
|
||||
"resolution": { "enum": ["480p", "720p", "1080p"], "title": "Resolution", "name": "resolution", "type": "string", "description": "The resolution of the generated video.", "default": "720p" }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const getVideoModelById = (id) => t2vModels.find(m => m.id === id);
|
||||
|
||||
export const getAspectRatiosForVideoModel = (modelId) => {
|
||||
const model = getVideoModelById(modelId);
|
||||
if (!model) return ['16:9'];
|
||||
const arInput = model.inputs?.aspect_ratio;
|
||||
if (arInput && arInput.enum) return arInput.enum;
|
||||
return ['16:9', '9:16', '1:1'];
|
||||
};
|
||||
|
||||
export const getDurationsForModel = (modelId) => {
|
||||
const model = getVideoModelById(modelId);
|
||||
if (!model) return [5];
|
||||
const durInput = model.inputs?.duration;
|
||||
if (durInput && durInput.enum) return durInput.enum;
|
||||
if (durInput) return [durInput.default || 5];
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getResolutionsForVideoModel = (modelId) => {
|
||||
const model = getVideoModelById(modelId);
|
||||
if (!model) return [];
|
||||
const resInput = model.inputs?.resolution;
|
||||
if (resInput && resInput.enum) return resInput.enum;
|
||||
return [];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getModelById } from './models.js';
|
||||
import { getModelById, getVideoModelById } from './models.js';
|
||||
|
||||
export class MuapiClient {
|
||||
constructor() {
|
||||
|
|
@ -160,6 +160,58 @@ export class MuapiClient {
|
|||
throw new Error('Generation timed out after polling.');
|
||||
}
|
||||
|
||||
async generateVideo(params) {
|
||||
const key = this.getKey();
|
||||
|
||||
const modelInfo = getVideoModelById(params.model);
|
||||
const endpoint = modelInfo?.endpoint || params.model;
|
||||
const url = `${this.baseUrl}/api/v1/${endpoint}`;
|
||||
|
||||
const finalPayload = { prompt: params.prompt };
|
||||
|
||||
if (params.aspect_ratio) finalPayload.aspect_ratio = params.aspect_ratio;
|
||||
if (params.duration) finalPayload.duration = params.duration;
|
||||
if (params.resolution) finalPayload.resolution = params.resolution;
|
||||
if (params.quality) finalPayload.quality = params.quality;
|
||||
|
||||
console.log('[Muapi] Video Request:', url);
|
||||
console.log('[Muapi] Video Payload:', finalPayload);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': key
|
||||
},
|
||||
body: JSON.stringify(finalPayload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
console.error('[Muapi] API Error Body:', errText);
|
||||
throw new Error(`API Request Failed: ${response.status} ${response.statusText} - ${errText.slice(0, 100)}`);
|
||||
}
|
||||
|
||||
const submitData = await response.json();
|
||||
console.log('[Muapi] Video Submit Response:', submitData);
|
||||
|
||||
const requestId = submitData.request_id || submitData.id;
|
||||
if (!requestId) return submitData;
|
||||
|
||||
console.log('[Muapi] Polling for video results, request_id:', requestId);
|
||||
const result = await this.pollForResult(requestId, key, 120, 2000);
|
||||
|
||||
const videoUrl = result.outputs?.[0] || result.url || result.output?.url;
|
||||
console.log('[Muapi] Video URL:', videoUrl);
|
||||
return { ...result, url: videoUrl };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Muapi Video Client Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getDimensionsFromAR(ar) {
|
||||
// Base unit 1024 (Flux standard)
|
||||
switch (ar) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ function navigate(page) {
|
|||
if (page === 'image') {
|
||||
contentArea.appendChild(ImageStudio());
|
||||
} else if (page === 'video') {
|
||||
contentArea.innerHTML = '<div class="flex items-center justify-center h-full text-secondary">Video Studio Coming Soon 🎬</div>';
|
||||
import('./components/VideoStudio.js').then(({ VideoStudio }) => {
|
||||
contentArea.appendChild(VideoStudio());
|
||||
});
|
||||
} else if (page === 'cinema') {
|
||||
import('./components/CinemaStudio.js').then(({ CinemaStudio }) => {
|
||||
contentArea.appendChild(CinemaStudio());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue