mirror of
https://github.com/Anil-matcha/Open-Generative-AI.git
synced 2026-05-07 01:17:18 +00:00
Fix LipSync uploads: use UploadPicker for image, VideoStudio pattern for video/audio buttons
This commit is contained in:
parent
89cb0e2321
commit
8bab51f10f
1 changed files with 176 additions and 238 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { muapi } from '../lib/muapi.js';
|
||||
import { lipsyncModels, imageLipSyncModels, videoLipSyncModels, getLipSyncModelById, getResolutionsForLipSyncModel } from '../lib/models.js';
|
||||
import { AuthModal } from './AuthModal.js';
|
||||
import { createUploadPicker } from './UploadPicker.js';
|
||||
import { savePendingJob, removePendingJob, getPendingJobs } from '../lib/pendingJobs.js';
|
||||
|
||||
export function LipSyncStudio() {
|
||||
|
|
@ -89,74 +90,161 @@ export function LipSyncStudio() {
|
|||
const uploadsRow = document.createElement('div');
|
||||
uploadsRow.className = 'flex items-start gap-3 px-2';
|
||||
|
||||
// ── Image Upload ──
|
||||
const imageFileInput = document.createElement('input');
|
||||
imageFileInput.type = 'file';
|
||||
imageFileInput.accept = 'image/*';
|
||||
imageFileInput.className = 'hidden';
|
||||
// ── Image Upload — uses createUploadPicker (same as VideoStudio) ──
|
||||
const imagePicker = createUploadPicker({
|
||||
anchorContainer: container,
|
||||
onSelect: ({ url }) => {
|
||||
uploadedImageUrl = url;
|
||||
imageStatusLabel.textContent = '✓ Image ready';
|
||||
imageStatusLabel.className = 'text-primary';
|
||||
},
|
||||
onClear: () => {
|
||||
uploadedImageUrl = null;
|
||||
imageStatusLabel.textContent = 'No image';
|
||||
imageStatusLabel.className = 'text-muted';
|
||||
}
|
||||
});
|
||||
// Size the trigger to match our other buttons
|
||||
imagePicker.trigger.className = imagePicker.trigger.className
|
||||
.replace('w-10 h-10', 'w-14 h-14')
|
||||
.replace('mt-1.5', '');
|
||||
container.appendChild(imagePicker.panel);
|
||||
|
||||
const imageUploadBtn = document.createElement('button');
|
||||
imageUploadBtn.type = 'button';
|
||||
imageUploadBtn.title = 'Upload portrait image';
|
||||
imageUploadBtn.className = 'flex-shrink-0 w-14 h-14 rounded-xl border transition-all flex flex-col items-center justify-center gap-1 bg-white/5 border-white/10 hover:bg-white/10 hover:border-primary/40 group relative overflow-hidden';
|
||||
imageUploadBtn.innerHTML = `
|
||||
<div class="image-icon flex flex-col items-center gap-1">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted group-hover:text-primary transition-colors"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
<span class="text-[9px] text-muted group-hover:text-primary font-bold">IMAGE</span>
|
||||
</div>
|
||||
<div class="image-spinner hidden items-center justify-center w-full h-full absolute inset-0"><span class="animate-spin text-primary text-sm">◌</span></div>
|
||||
<div class="image-ready hidden flex-col items-center gap-1 absolute inset-0 bg-primary/10 rounded-xl">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-primary mt-3"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/><polyline points="7 18 10 15 13 18" stroke="#d9ff00" stroke-width="2.5"/></svg>
|
||||
<span class="text-[9px] text-primary font-bold">READY</span>
|
||||
</div>
|
||||
`;
|
||||
imageUploadBtn.appendChild(imageFileInput);
|
||||
|
||||
// ── Video Upload ──
|
||||
// ── Video Upload Button (VideoStudio pattern — separate state divs, file input inside btn) ──
|
||||
const videoFileInput = document.createElement('input');
|
||||
videoFileInput.type = 'file';
|
||||
videoFileInput.accept = 'video/*';
|
||||
videoFileInput.className = 'hidden';
|
||||
|
||||
const videoUploadBtn = document.createElement('button');
|
||||
videoUploadBtn.type = 'button';
|
||||
videoUploadBtn.title = 'Upload source video';
|
||||
videoUploadBtn.className = 'flex-shrink-0 w-14 h-14 rounded-xl border transition-all flex flex-col items-center justify-center gap-1 bg-white/5 border-white/10 hover:bg-white/10 hover:border-primary/40 group relative overflow-hidden hidden';
|
||||
videoUploadBtn.innerHTML = `
|
||||
<div class="video-icon flex flex-col items-center gap-1">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted group-hover:text-primary transition-colors"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
||||
<span class="text-[9px] text-muted group-hover:text-primary font-bold">VIDEO</span>
|
||||
</div>
|
||||
<div class="video-spinner hidden items-center justify-center w-full h-full absolute inset-0"><span class="animate-spin text-primary text-sm">◌</span></div>
|
||||
<div class="video-ready hidden flex-col items-center gap-1 absolute inset-0 bg-primary/10 rounded-xl">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-primary mt-3"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/><polyline points="7 10 10 13 15 8" stroke="#d9ff00" stroke-width="2.5"/></svg>
|
||||
<span class="text-[9px] text-primary font-bold">READY</span>
|
||||
</div>
|
||||
`;
|
||||
videoUploadBtn.appendChild(videoFileInput);
|
||||
const videoPickerBtn = document.createElement('button');
|
||||
videoPickerBtn.type = 'button';
|
||||
videoPickerBtn.title = 'Upload source video';
|
||||
videoPickerBtn.className = 'flex-shrink-0 w-14 h-14 rounded-xl border transition-all flex items-center justify-center relative overflow-hidden hidden bg-white/5 border-white/10 hover:bg-white/10 hover:border-primary/40 group';
|
||||
|
||||
// ── Audio Upload ──
|
||||
const videoIconEl = document.createElement('div');
|
||||
videoIconEl.className = 'flex flex-col items-center justify-center gap-1 w-full h-full';
|
||||
videoIconEl.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted group-hover:text-primary transition-colors"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg><span class="text-[9px] text-muted group-hover:text-primary font-bold">VIDEO</span>`;
|
||||
|
||||
const videoSpinnerEl = document.createElement('div');
|
||||
videoSpinnerEl.className = 'hidden items-center justify-center w-full h-full';
|
||||
videoSpinnerEl.innerHTML = `<span class="animate-spin text-primary text-sm">◌</span>`;
|
||||
|
||||
const videoReadyEl = document.createElement('div');
|
||||
videoReadyEl.className = 'hidden flex-col items-center justify-center gap-1 w-full h-full absolute inset-0 bg-primary/10';
|
||||
videoReadyEl.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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><span class="text-[9px] text-primary font-bold">READY</span>`;
|
||||
|
||||
videoPickerBtn.appendChild(videoFileInput);
|
||||
videoPickerBtn.appendChild(videoIconEl);
|
||||
videoPickerBtn.appendChild(videoSpinnerEl);
|
||||
videoPickerBtn.appendChild(videoReadyEl);
|
||||
|
||||
const showVideoIcon = () => {
|
||||
videoIconEl.classList.replace('hidden', 'flex');
|
||||
videoSpinnerEl.classList.add('hidden'); videoSpinnerEl.classList.remove('flex');
|
||||
videoReadyEl.classList.add('hidden'); videoReadyEl.classList.remove('flex');
|
||||
videoPickerBtn.classList.remove('border-primary/60'); videoPickerBtn.classList.add('border-white/10');
|
||||
videoPickerBtn.title = 'Upload source video';
|
||||
mediaStatusLabel.textContent = 'No video'; mediaStatusLabel.className = 'text-muted';
|
||||
};
|
||||
const showVideoSpinner = () => {
|
||||
videoIconEl.classList.add('hidden'); videoIconEl.classList.remove('flex');
|
||||
videoSpinnerEl.classList.replace('hidden', 'flex');
|
||||
videoReadyEl.classList.add('hidden'); videoReadyEl.classList.remove('flex');
|
||||
};
|
||||
const showVideoReady = (name) => {
|
||||
videoIconEl.classList.add('hidden'); videoIconEl.classList.remove('flex');
|
||||
videoSpinnerEl.classList.add('hidden'); videoSpinnerEl.classList.remove('flex');
|
||||
videoReadyEl.classList.replace('hidden', 'flex');
|
||||
videoPickerBtn.classList.remove('border-white/10'); videoPickerBtn.classList.add('border-primary/60');
|
||||
videoPickerBtn.title = `${name} — click to clear`;
|
||||
mediaStatusLabel.textContent = `✓ ${name}`; mediaStatusLabel.className = 'text-primary';
|
||||
};
|
||||
|
||||
videoPickerBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (uploadedVideoUrl) { uploadedVideoUrl = null; showVideoIcon(); return; }
|
||||
videoFileInput.click();
|
||||
};
|
||||
videoFileInput.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) { AuthModal(() => videoFileInput.click()); return; }
|
||||
showVideoSpinner();
|
||||
try {
|
||||
uploadedVideoUrl = await muapi.uploadFile(file);
|
||||
showVideoReady(file.name);
|
||||
} catch (err) { showVideoIcon(); alert(`Video upload failed: ${err.message}`); }
|
||||
videoFileInput.value = '';
|
||||
};
|
||||
|
||||
// ── Audio Upload Button (same pattern as video) ──
|
||||
const audioFileInput = document.createElement('input');
|
||||
audioFileInput.type = 'file';
|
||||
audioFileInput.accept = 'audio/*';
|
||||
audioFileInput.className = 'hidden';
|
||||
|
||||
const audioUploadBtn = document.createElement('button');
|
||||
audioUploadBtn.type = 'button';
|
||||
audioUploadBtn.title = 'Upload audio file';
|
||||
audioUploadBtn.className = 'flex-shrink-0 w-14 h-14 rounded-xl border transition-all flex flex-col items-center justify-center gap-1 bg-white/5 border-white/10 hover:bg-white/10 hover:border-primary/40 group relative overflow-hidden';
|
||||
audioUploadBtn.innerHTML = `
|
||||
<div class="audio-icon flex flex-col items-center gap-1">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted group-hover:text-primary transition-colors"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></svg>
|
||||
<span class="text-[9px] text-muted group-hover:text-primary font-bold">AUDIO</span>
|
||||
</div>
|
||||
<div class="audio-spinner hidden items-center justify-center w-full h-full absolute inset-0"><span class="animate-spin text-primary text-sm">◌</span></div>
|
||||
<div class="audio-ready hidden flex-col items-center gap-1 absolute inset-0 bg-primary/10 rounded-xl">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-primary mt-3"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><polyline points="7 10 10 13 15 8" stroke="#d9ff00" stroke-width="2.5"/></svg>
|
||||
<span class="text-[9px] text-primary font-bold">READY</span>
|
||||
</div>
|
||||
`;
|
||||
audioUploadBtn.appendChild(audioFileInput);
|
||||
const audioPickerBtn = document.createElement('button');
|
||||
audioPickerBtn.type = 'button';
|
||||
audioPickerBtn.title = 'Upload audio file';
|
||||
audioPickerBtn.className = 'flex-shrink-0 w-14 h-14 rounded-xl border transition-all flex items-center justify-center relative overflow-hidden bg-white/5 border-white/10 hover:bg-white/10 hover:border-primary/40 group';
|
||||
|
||||
const audioIconEl = document.createElement('div');
|
||||
audioIconEl.className = 'flex flex-col items-center justify-center gap-1 w-full h-full';
|
||||
audioIconEl.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted group-hover:text-primary transition-colors"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></svg><span class="text-[9px] text-muted group-hover:text-primary font-bold">AUDIO</span>`;
|
||||
|
||||
const audioSpinnerEl = document.createElement('div');
|
||||
audioSpinnerEl.className = 'hidden items-center justify-center w-full h-full';
|
||||
audioSpinnerEl.innerHTML = `<span class="animate-spin text-primary text-sm">◌</span>`;
|
||||
|
||||
const audioReadyEl = document.createElement('div');
|
||||
audioReadyEl.className = 'hidden flex-col items-center justify-center gap-1 w-full h-full absolute inset-0 bg-primary/10';
|
||||
audioReadyEl.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-primary"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></svg><span class="text-[9px] text-primary font-bold">READY</span>`;
|
||||
|
||||
audioPickerBtn.appendChild(audioFileInput);
|
||||
audioPickerBtn.appendChild(audioIconEl);
|
||||
audioPickerBtn.appendChild(audioSpinnerEl);
|
||||
audioPickerBtn.appendChild(audioReadyEl);
|
||||
|
||||
const showAudioIcon = () => {
|
||||
audioIconEl.classList.replace('hidden', 'flex');
|
||||
audioSpinnerEl.classList.add('hidden'); audioSpinnerEl.classList.remove('flex');
|
||||
audioReadyEl.classList.add('hidden'); audioReadyEl.classList.remove('flex');
|
||||
audioPickerBtn.classList.remove('border-primary/60'); audioPickerBtn.classList.add('border-white/10');
|
||||
audioPickerBtn.title = 'Upload audio file';
|
||||
audioStatusLabel.textContent = 'No audio'; audioStatusLabel.className = 'text-muted';
|
||||
};
|
||||
const showAudioSpinner = () => {
|
||||
audioIconEl.classList.add('hidden'); audioIconEl.classList.remove('flex');
|
||||
audioSpinnerEl.classList.replace('hidden', 'flex');
|
||||
audioReadyEl.classList.add('hidden'); audioReadyEl.classList.remove('flex');
|
||||
};
|
||||
const showAudioReady = (name) => {
|
||||
audioIconEl.classList.add('hidden'); audioIconEl.classList.remove('flex');
|
||||
audioSpinnerEl.classList.add('hidden'); audioSpinnerEl.classList.remove('flex');
|
||||
audioReadyEl.classList.replace('hidden', 'flex');
|
||||
audioPickerBtn.classList.remove('border-white/10'); audioPickerBtn.classList.add('border-primary/60');
|
||||
audioPickerBtn.title = `${name} — click to clear`;
|
||||
audioStatusLabel.textContent = `✓ ${name}`; audioStatusLabel.className = 'text-primary';
|
||||
};
|
||||
|
||||
audioPickerBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (uploadedAudioUrl) { uploadedAudioUrl = null; showAudioIcon(); return; }
|
||||
audioFileInput.click();
|
||||
};
|
||||
audioFileInput.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) { AuthModal(() => audioFileInput.click()); return; }
|
||||
showAudioSpinner();
|
||||
try {
|
||||
uploadedAudioUrl = await muapi.uploadFile(file);
|
||||
showAudioReady(file.name);
|
||||
} catch (err) { showAudioIcon(); alert(`Audio upload failed: ${err.message}`); }
|
||||
audioFileInput.value = '';
|
||||
};
|
||||
|
||||
// ── Prompt Textarea ──
|
||||
const textarea = document.createElement('textarea');
|
||||
|
|
@ -164,9 +252,9 @@ export function LipSyncStudio() {
|
|||
textarea.className = 'flex-1 bg-transparent text-white placeholder-muted/50 text-sm resize-none outline-none min-h-[56px] leading-relaxed pt-1';
|
||||
textarea.rows = 2;
|
||||
|
||||
uploadsRow.appendChild(imageUploadBtn);
|
||||
uploadsRow.appendChild(videoUploadBtn);
|
||||
uploadsRow.appendChild(audioUploadBtn);
|
||||
uploadsRow.appendChild(imagePicker.trigger);
|
||||
uploadsRow.appendChild(videoPickerBtn);
|
||||
uploadsRow.appendChild(audioPickerBtn);
|
||||
uploadsRow.appendChild(textarea);
|
||||
bar.appendChild(uploadsRow);
|
||||
|
||||
|
|
@ -174,15 +262,18 @@ export function LipSyncStudio() {
|
|||
const statusRow = document.createElement('div');
|
||||
statusRow.className = 'flex items-center gap-3 px-2 text-xs text-muted';
|
||||
|
||||
const imageStatusLabel = document.createElement('span');
|
||||
imageStatusLabel.className = 'text-muted';
|
||||
imageStatusLabel.textContent = 'No image';
|
||||
// mediaStatusLabel: shows image or video status depending on mode
|
||||
const mediaStatusLabel = document.createElement('span');
|
||||
mediaStatusLabel.className = 'text-muted';
|
||||
mediaStatusLabel.textContent = 'No image';
|
||||
|
||||
const imageStatusLabel = mediaStatusLabel; // alias used in imagePicker callbacks
|
||||
|
||||
const audioStatusLabel = document.createElement('span');
|
||||
audioStatusLabel.className = 'text-muted';
|
||||
audioStatusLabel.textContent = 'No audio';
|
||||
|
||||
statusRow.appendChild(imageStatusLabel);
|
||||
statusRow.appendChild(mediaStatusLabel);
|
||||
statusRow.appendChild(document.createTextNode(' · '));
|
||||
statusRow.appendChild(audioStatusLabel);
|
||||
bar.appendChild(statusRow);
|
||||
|
|
@ -314,13 +405,17 @@ export function LipSyncStudio() {
|
|||
if (inputMode === 'image') {
|
||||
imageModeBtn.className = 'px-4 py-1.5 rounded-xl text-xs font-bold transition-all border border-primary bg-primary/10 text-primary';
|
||||
videoModeBtn.className = 'px-4 py-1.5 rounded-xl text-xs font-bold transition-all border border-white/10 text-muted hover:border-white/30 hover:text-white';
|
||||
imageUploadBtn.classList.remove('hidden');
|
||||
videoUploadBtn.classList.add('hidden');
|
||||
imagePicker.trigger.classList.remove('hidden');
|
||||
videoPickerBtn.classList.add('hidden');
|
||||
mediaStatusLabel.textContent = uploadedImageUrl ? '✓ Image ready' : 'No image';
|
||||
mediaStatusLabel.className = uploadedImageUrl ? 'text-primary' : 'text-muted';
|
||||
} else {
|
||||
videoModeBtn.className = 'px-4 py-1.5 rounded-xl text-xs font-bold transition-all border border-primary bg-primary/10 text-primary';
|
||||
imageModeBtn.className = 'px-4 py-1.5 rounded-xl text-xs font-bold transition-all border border-white/10 text-muted hover:border-white/30 hover:text-white';
|
||||
videoUploadBtn.classList.remove('hidden');
|
||||
imageUploadBtn.classList.add('hidden');
|
||||
videoPickerBtn.classList.remove('hidden');
|
||||
imagePicker.trigger.classList.add('hidden');
|
||||
mediaStatusLabel.textContent = uploadedVideoUrl ? '✓ Video ready' : 'No video';
|
||||
mediaStatusLabel.className = uploadedVideoUrl ? 'text-primary' : 'text-muted';
|
||||
}
|
||||
|
||||
// Switch to first model of new mode
|
||||
|
|
@ -346,7 +441,7 @@ export function LipSyncStudio() {
|
|||
if (inputMode === 'image') return;
|
||||
inputMode = 'image';
|
||||
uploadedVideoUrl = null;
|
||||
updateVideoUploadState('idle');
|
||||
showVideoIcon();
|
||||
updateUIForMode();
|
||||
};
|
||||
|
||||
|
|
@ -354,178 +449,10 @@ export function LipSyncStudio() {
|
|||
if (inputMode === 'video') return;
|
||||
inputMode = 'video';
|
||||
uploadedImageUrl = null;
|
||||
updateImageUploadState('idle');
|
||||
imagePicker.reset();
|
||||
updateUIForMode();
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// 5. UPLOAD HANDLERS
|
||||
// ==========================================
|
||||
const updateImageUploadState = (state, filename) => {
|
||||
const icon = imageUploadBtn.querySelector('.image-icon');
|
||||
const spinner = imageUploadBtn.querySelector('.image-spinner');
|
||||
const ready = imageUploadBtn.querySelector('.image-ready');
|
||||
if (state === 'idle') {
|
||||
icon.classList.remove('hidden'); icon.classList.add('flex');
|
||||
spinner.classList.add('hidden'); spinner.classList.remove('flex');
|
||||
ready.classList.add('hidden'); ready.classList.remove('flex');
|
||||
imageUploadBtn.classList.remove('border-primary/60');
|
||||
imageUploadBtn.classList.add('border-white/10');
|
||||
imageUploadBtn.title = 'Upload portrait image';
|
||||
imageStatusLabel.textContent = 'No image';
|
||||
imageStatusLabel.className = 'text-muted';
|
||||
} else if (state === 'loading') {
|
||||
icon.classList.add('hidden'); icon.classList.remove('flex');
|
||||
spinner.classList.remove('hidden'); spinner.classList.add('flex');
|
||||
ready.classList.add('hidden'); ready.classList.remove('flex');
|
||||
} else if (state === 'ready') {
|
||||
icon.classList.add('hidden'); icon.classList.remove('flex');
|
||||
spinner.classList.add('hidden'); spinner.classList.remove('flex');
|
||||
ready.classList.remove('hidden'); ready.classList.add('flex');
|
||||
imageUploadBtn.classList.remove('border-white/10');
|
||||
imageUploadBtn.classList.add('border-primary/60');
|
||||
imageUploadBtn.title = `${filename} — click to clear`;
|
||||
imageStatusLabel.textContent = `✓ ${filename}`;
|
||||
imageStatusLabel.className = 'text-primary';
|
||||
}
|
||||
};
|
||||
|
||||
const updateVideoUploadState = (state, filename) => {
|
||||
const icon = videoUploadBtn.querySelector('.video-icon');
|
||||
const spinner = videoUploadBtn.querySelector('.video-spinner');
|
||||
const ready = videoUploadBtn.querySelector('.video-ready');
|
||||
if (state === 'idle') {
|
||||
icon.classList.remove('hidden'); icon.classList.add('flex');
|
||||
spinner.classList.add('hidden'); spinner.classList.remove('flex');
|
||||
ready.classList.add('hidden'); ready.classList.remove('flex');
|
||||
videoUploadBtn.classList.remove('border-primary/60');
|
||||
videoUploadBtn.classList.add('border-white/10');
|
||||
videoUploadBtn.title = 'Upload source video';
|
||||
imageStatusLabel.textContent = 'No video';
|
||||
imageStatusLabel.className = 'text-muted';
|
||||
} else if (state === 'loading') {
|
||||
icon.classList.add('hidden'); icon.classList.remove('flex');
|
||||
spinner.classList.remove('hidden'); spinner.classList.add('flex');
|
||||
ready.classList.add('hidden'); ready.classList.remove('flex');
|
||||
} else if (state === 'ready') {
|
||||
icon.classList.add('hidden'); icon.classList.remove('flex');
|
||||
spinner.classList.add('hidden'); spinner.classList.remove('flex');
|
||||
ready.classList.remove('hidden'); ready.classList.add('flex');
|
||||
videoUploadBtn.classList.remove('border-white/10');
|
||||
videoUploadBtn.classList.add('border-primary/60');
|
||||
videoUploadBtn.title = `${filename} — click to clear`;
|
||||
imageStatusLabel.textContent = `✓ ${filename}`;
|
||||
imageStatusLabel.className = 'text-primary';
|
||||
}
|
||||
};
|
||||
|
||||
const updateAudioUploadState = (state, filename) => {
|
||||
const icon = audioUploadBtn.querySelector('.audio-icon');
|
||||
const spinner = audioUploadBtn.querySelector('.audio-spinner');
|
||||
const ready = audioUploadBtn.querySelector('.audio-ready');
|
||||
if (state === 'idle') {
|
||||
icon.classList.remove('hidden'); icon.classList.add('flex');
|
||||
spinner.classList.add('hidden'); spinner.classList.remove('flex');
|
||||
ready.classList.add('hidden'); ready.classList.remove('flex');
|
||||
audioUploadBtn.classList.remove('border-primary/60');
|
||||
audioUploadBtn.classList.add('border-white/10');
|
||||
audioUploadBtn.title = 'Upload audio file';
|
||||
audioStatusLabel.textContent = 'No audio';
|
||||
audioStatusLabel.className = 'text-muted';
|
||||
} else if (state === 'loading') {
|
||||
icon.classList.add('hidden'); icon.classList.remove('flex');
|
||||
spinner.classList.remove('hidden'); spinner.classList.add('flex');
|
||||
ready.classList.add('hidden'); ready.classList.remove('flex');
|
||||
} else if (state === 'ready') {
|
||||
icon.classList.add('hidden'); icon.classList.remove('flex');
|
||||
spinner.classList.add('hidden'); spinner.classList.remove('flex');
|
||||
ready.classList.remove('hidden'); ready.classList.add('flex');
|
||||
audioUploadBtn.classList.remove('border-white/10');
|
||||
audioUploadBtn.classList.add('border-primary/60');
|
||||
audioUploadBtn.title = `${filename} — click to clear`;
|
||||
audioStatusLabel.textContent = `✓ ${filename}`;
|
||||
audioStatusLabel.className = 'text-primary';
|
||||
}
|
||||
};
|
||||
|
||||
imageUploadBtn.onclick = async (e) => {
|
||||
e.stopPropagation();
|
||||
if (uploadedImageUrl) {
|
||||
uploadedImageUrl = null;
|
||||
updateImageUploadState('idle');
|
||||
return;
|
||||
}
|
||||
imageFileInput.click();
|
||||
};
|
||||
|
||||
imageFileInput.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) { AuthModal(() => imageFileInput.click()); return; }
|
||||
updateImageUploadState('loading');
|
||||
try {
|
||||
uploadedImageUrl = await muapi.uploadFile(file);
|
||||
updateImageUploadState('ready', file.name);
|
||||
} catch (err) {
|
||||
updateImageUploadState('idle');
|
||||
alert(`Image upload failed: ${err.message}`);
|
||||
}
|
||||
imageFileInput.value = '';
|
||||
};
|
||||
|
||||
videoUploadBtn.onclick = async (e) => {
|
||||
e.stopPropagation();
|
||||
if (uploadedVideoUrl) {
|
||||
uploadedVideoUrl = null;
|
||||
updateVideoUploadState('idle');
|
||||
return;
|
||||
}
|
||||
videoFileInput.click();
|
||||
};
|
||||
|
||||
videoFileInput.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) { AuthModal(() => videoFileInput.click()); return; }
|
||||
updateVideoUploadState('loading');
|
||||
try {
|
||||
uploadedVideoUrl = await muapi.uploadFile(file);
|
||||
updateVideoUploadState('ready', file.name);
|
||||
} catch (err) {
|
||||
updateVideoUploadState('idle');
|
||||
alert(`Video upload failed: ${err.message}`);
|
||||
}
|
||||
videoFileInput.value = '';
|
||||
};
|
||||
|
||||
audioUploadBtn.onclick = async (e) => {
|
||||
e.stopPropagation();
|
||||
if (uploadedAudioUrl) {
|
||||
uploadedAudioUrl = null;
|
||||
updateAudioUploadState('idle');
|
||||
return;
|
||||
}
|
||||
audioFileInput.click();
|
||||
};
|
||||
|
||||
audioFileInput.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) { AuthModal(() => audioFileInput.click()); return; }
|
||||
updateAudioUploadState('loading');
|
||||
try {
|
||||
uploadedAudioUrl = await muapi.uploadFile(file);
|
||||
updateAudioUploadState('ready', file.name);
|
||||
} catch (err) {
|
||||
updateAudioUploadState('idle');
|
||||
alert(`Audio upload failed: ${err.message}`);
|
||||
}
|
||||
audioFileInput.value = '';
|
||||
};
|
||||
|
||||
// Hide resolution if first model has none
|
||||
if (getResolutionsForLipSyncModel(selectedModel).length === 0) {
|
||||
resolutionBtn.classList.add('hidden');
|
||||
|
|
@ -705,6 +632,17 @@ export function LipSyncStudio() {
|
|||
hero.classList.remove('hidden', 'opacity-0', 'scale-95', '-translate-y-10', 'pointer-events-none');
|
||||
promptWrapper.classList.remove('hidden', 'opacity-40');
|
||||
textarea.value = '';
|
||||
// Reset uploads
|
||||
imagePicker.reset();
|
||||
uploadedImageUrl = null;
|
||||
uploadedVideoUrl = null;
|
||||
uploadedAudioUrl = null;
|
||||
showVideoIcon();
|
||||
showAudioIcon();
|
||||
mediaStatusLabel.textContent = inputMode === 'image' ? 'No image' : 'No video';
|
||||
mediaStatusLabel.className = 'text-muted';
|
||||
audioStatusLabel.textContent = 'No audio';
|
||||
audioStatusLabel.className = 'text-muted';
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue