import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { Sparkles, ChevronDown, Settings2, Trash2, Music2, Sliders, Dices, Hash, RefreshCw, Plus, Upload, Play, Pause, Loader2 } from 'lucide-react'; import { GenerationParams, Song } from '../types'; import { useAuth } from '../context/AuthContext'; import { generateApi } from '../services/api'; interface ReferenceTrack { id: string; filename: string; storage_key: string; duration: number | null; file_size_bytes: number | null; tags: string[] | null; created_at: string; audio_url: string; } interface CreatePanelProps { onGenerate: (params: GenerationParams) => void; isGenerating: boolean; initialData?: { song: Song, timestamp: number } | null; createdSongs?: Song[]; pendingAudioSelection?: { target: 'reference' | 'source'; url: string; title?: string } | null; onAudioSelectionApplied?: () => void; } const KEY_SIGNATURES = [ '', 'C major', 'C minor', 'C# major', 'C# minor', 'Db major', 'Db minor', 'D major', 'D minor', 'D# major', 'D# minor', 'Eb major', 'Eb minor', 'E major', 'E minor', 'F major', 'F minor', 'F# major', 'F# minor', 'Gb major', 'Gb minor', 'G major', 'G minor', 'G# major', 'G# minor', 'Ab major', 'Ab minor', 'A major', 'A minor', 'A# major', 'A# minor', 'Bb major', 'Bb minor', 'B major', 'B minor' ]; const TIME_SIGNATURES = ['', '2/4', '3/4', '4/4', '6/8']; const VOCAL_LANGUAGES = [ { value: 'unknown', label: 'Auto / Instrumental' }, { value: 'ar', label: 'Arabic' }, { value: 'az', label: 'Azerbaijani' }, { value: 'bg', label: 'Bulgarian' }, { value: 'bn', label: 'Bengali' }, { value: 'ca', label: 'Catalan' }, { value: 'cs', label: 'Czech' }, { value: 'da', label: 'Danish' }, { value: 'de', label: 'German' }, { value: 'el', label: 'Greek' }, { value: 'en', label: 'English' }, { value: 'es', label: 'Spanish' }, { value: 'fa', label: 'Persian' }, { value: 'fi', label: 'Finnish' }, { value: 'fr', label: 'French' }, { value: 'he', label: 'Hebrew' }, { value: 'hi', label: 'Hindi' }, { value: 'hr', label: 'Croatian' }, { value: 'ht', label: 'Haitian Creole' }, { value: 'hu', label: 'Hungarian' }, { value: 'id', label: 'Indonesian' }, { value: 'is', label: 'Icelandic' }, { value: 'it', label: 'Italian' }, { value: 'ja', label: 'Japanese' }, { value: 'ko', label: 'Korean' }, { value: 'la', label: 'Latin' }, { value: 'lt', label: 'Lithuanian' }, { value: 'ms', label: 'Malay' }, { value: 'ne', label: 'Nepali' }, { value: 'nl', label: 'Dutch' }, { value: 'no', label: 'Norwegian' }, { value: 'pa', label: 'Punjabi' }, { value: 'pl', label: 'Polish' }, { value: 'pt', label: 'Portuguese' }, { value: 'ro', label: 'Romanian' }, { value: 'ru', label: 'Russian' }, { value: 'sa', label: 'Sanskrit' }, { value: 'sk', label: 'Slovak' }, { value: 'sr', label: 'Serbian' }, { value: 'sv', label: 'Swedish' }, { value: 'sw', label: 'Swahili' }, { value: 'ta', label: 'Tamil' }, { value: 'te', label: 'Telugu' }, { value: 'th', label: 'Thai' }, { value: 'tl', label: 'Tagalog' }, { value: 'tr', label: 'Turkish' }, { value: 'uk', label: 'Ukrainian' }, { value: 'ur', label: 'Urdu' }, { value: 'vi', label: 'Vietnamese' }, { value: 'yue', label: 'Cantonese' }, { value: 'zh', label: 'Chinese (Mandarin)' }, ]; export const CreatePanel: React.FC = ({ onGenerate, isGenerating, initialData, createdSongs = [], pendingAudioSelection, onAudioSelectionApplied, }) => { const { isAuthenticated, token, user } = useAuth(); // Mode const [customMode, setCustomMode] = useState(true); // Simple Mode const [songDescription, setSongDescription] = useState(''); // Custom Mode const [lyrics, setLyrics] = useState(''); const [style, setStyle] = useState(''); const [title, setTitle] = useState(''); // Common const [instrumental, setInstrumental] = useState(false); const [vocalLanguage, setVocalLanguage] = useState('en'); const [vocalGender, setVocalGender] = useState<'male' | 'female' | ''>(''); // Music Parameters const [bpm, setBpm] = useState(0); const [keyScale, setKeyScale] = useState(''); const [timeSignature, setTimeSignature] = useState(''); // Advanced Settings const [showAdvanced, setShowAdvanced] = useState(false); const [duration, setDuration] = useState(-1); const [batchSize, setBatchSize] = useState(2); const [bulkCount, setBulkCount] = useState(1); // Number of independent generation jobs to queue const [guidanceScale, setGuidanceScale] = useState(9.0); const [randomSeed, setRandomSeed] = useState(true); const [seed, setSeed] = useState(-1); const [thinking, setThinking] = useState(false); // Default false for GPU compatibility const [audioFormat, setAudioFormat] = useState<'mp3' | 'flac'>('mp3'); const [inferenceSteps, setInferenceSteps] = useState(12); const [inferMethod, setInferMethod] = useState<'ode' | 'sde'>('ode'); const [lmBackend, setLmBackend] = useState<'pt' | 'vllm'>('pt'); const [shift, setShift] = useState(3.0); // LM Parameters (under Expert) const [showLmParams, setShowLmParams] = useState(false); const [lmTemperature, setLmTemperature] = useState(0.8); const [lmCfgScale, setLmCfgScale] = useState(2.2); const [lmTopK, setLmTopK] = useState(0); const [lmTopP, setLmTopP] = useState(0.92); const [lmNegativePrompt, setLmNegativePrompt] = useState('NO USER INPUT'); // Expert Parameters (now in Advanced section) const [referenceAudioUrl, setReferenceAudioUrl] = useState(''); const [sourceAudioUrl, setSourceAudioUrl] = useState(''); const [referenceAudioTitle, setReferenceAudioTitle] = useState(''); const [sourceAudioTitle, setSourceAudioTitle] = useState(''); const [audioCodes, setAudioCodes] = useState(''); const [repaintingStart, setRepaintingStart] = useState(0); const [repaintingEnd, setRepaintingEnd] = useState(-1); const [instruction, setInstruction] = useState('Fill the audio semantic mask based on the given conditions:'); const [audioCoverStrength, setAudioCoverStrength] = useState(1.0); const [taskType, setTaskType] = useState('text2music'); const [useAdg, setUseAdg] = useState(false); const [cfgIntervalStart, setCfgIntervalStart] = useState(0.0); const [cfgIntervalEnd, setCfgIntervalEnd] = useState(1.0); const [customTimesteps, setCustomTimesteps] = useState(''); const [useCotMetas, setUseCotMetas] = useState(true); const [useCotCaption, setUseCotCaption] = useState(true); const [useCotLanguage, setUseCotLanguage] = useState(true); const [autogen, setAutogen] = useState(false); const [constrainedDecodingDebug, setConstrainedDecodingDebug] = useState(false); const [allowLmBatch, setAllowLmBatch] = useState(true); const [getScores, setGetScores] = useState(false); const [getLrc, setGetLrc] = useState(false); const [scoreScale, setScoreScale] = useState(0.5); const [lmBatchChunkSize, setLmBatchChunkSize] = useState(8); const [trackName, setTrackName] = useState(''); const [completeTrackClasses, setCompleteTrackClasses] = useState(''); const [isFormatCaption, setIsFormatCaption] = useState(false); const [maxDurationWithLm, setMaxDurationWithLm] = useState(240); const [maxDurationWithoutLm, setMaxDurationWithoutLm] = useState(240); const [isUploadingReference, setIsUploadingReference] = useState(false); const [isUploadingSource, setIsUploadingSource] = useState(false); const [isTranscribingReference, setIsTranscribingReference] = useState(false); const transcribeAbortRef = useRef(null); const [uploadError, setUploadError] = useState(null); const [isFormattingStyle, setIsFormattingStyle] = useState(false); const [isFormattingLyrics, setIsFormattingLyrics] = useState(false); const [isDraggingFile, setIsDraggingFile] = useState(false); const [dragKind, setDragKind] = useState<'file' | 'audio' | null>(null); const referenceInputRef = useRef(null); const sourceInputRef = useRef(null); const dragDepthRef = useRef(0); const [showAudioModal, setShowAudioModal] = useState(false); const [audioModalTarget, setAudioModalTarget] = useState<'reference' | 'source'>('reference'); const [tempAudioUrl, setTempAudioUrl] = useState(''); const [audioTab, setAudioTab] = useState<'reference' | 'source'>('reference'); const referenceAudioRef = useRef(null); const sourceAudioRef = useRef(null); const [referencePlaying, setReferencePlaying] = useState(false); const [sourcePlaying, setSourcePlaying] = useState(false); const [referenceTime, setReferenceTime] = useState(0); const [sourceTime, setSourceTime] = useState(0); const [referenceDuration, setReferenceDuration] = useState(0); const [sourceDuration, setSourceDuration] = useState(0); // Reference tracks modal state const [referenceTracks, setReferenceTracks] = useState([]); const [isLoadingTracks, setIsLoadingTracks] = useState(false); const [playingTrackId, setPlayingTrackId] = useState(null); const [playingTrackSource, setPlayingTrackSource] = useState<'uploads' | 'created' | null>(null); const modalAudioRef = useRef(null); const [modalTrackTime, setModalTrackTime] = useState(0); const [modalTrackDuration, setModalTrackDuration] = useState(0); const [libraryTab, setLibraryTab] = useState<'uploads' | 'created'>('uploads'); const createdTrackOptions = useMemo(() => { return createdSongs .filter(song => !song.isGenerating) .filter(song => (user ? song.userId === user.id : true)) .filter(song => Boolean(song.audioUrl)) .map(song => ({ id: song.id, title: song.title || 'Untitled', audio_url: song.audioUrl!, duration: song.duration, })); }, [createdSongs, user]); const getAudioLabel = (url: string) => { try { const parsed = new URL(url); const name = decodeURIComponent(parsed.pathname.split('/').pop() || parsed.hostname); return name.replace(/\.[^/.]+$/, '') || name; } catch { const parts = url.split('/'); const name = decodeURIComponent(parts[parts.length - 1] || url); return name.replace(/\.[^/.]+$/, '') || name; } }; // Resize Logic const [lyricsHeight, setLyricsHeight] = useState(() => { const saved = localStorage.getItem('acestep_lyrics_height'); return saved ? parseInt(saved, 10) : 144; // Default h-36 is 144px (9rem * 16) }); const [isResizing, setIsResizing] = useState(false); const lyricsRef = useRef(null); // Reuse Effect - must be after all state declarations useEffect(() => { if (initialData) { setCustomMode(true); setLyrics(initialData.song.lyrics); setStyle(initialData.song.style); setTitle(initialData.song.title); setInstrumental(initialData.song.lyrics.length === 0); } }, [initialData]); useEffect(() => { if (!pendingAudioSelection) return; applyAudioTargetUrl( pendingAudioSelection.target, pendingAudioSelection.url, pendingAudioSelection.title ); onAudioSelectionApplied?.(); }, [pendingAudioSelection, onAudioSelectionApplied]); useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isResizing) return; // Calculate new height based on mouse position relative to the lyrics container top // We can't easily get the container top here without a ref to it, // but we can use dy (delta y) from the previous position if we tracked it, // OR simpler: just update based on movement if we track the start. // // Better approach for absolute sizing: // 1. Get the bounding rect of the textarea wrapper on mount/resize start? // We can just rely on the fact that we are dragging the bottom. // So new height = currentMouseY - topOfElement. if (lyricsRef.current) { const rect = lyricsRef.current.getBoundingClientRect(); const newHeight = e.clientY - rect.top; // detailed limits: min 96px (h-24), max 600px if (newHeight > 96 && newHeight < 600) { setLyricsHeight(newHeight); } } }; const handleMouseUp = () => { setIsResizing(false); document.body.style.cursor = 'default'; document.body.style.userSelect = 'auto'; // Save height to localStorage localStorage.setItem('acestep_lyrics_height', String(lyricsHeight)); }; if (isResizing) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); document.body.style.cursor = 'ns-resize'; document.body.style.userSelect = 'none'; // Prevent text selection while dragging } return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); document.body.style.cursor = 'default'; document.body.style.userSelect = 'auto'; }; }, [isResizing]); useEffect(() => { const loadLimits = async () => { try { const response = await fetch('/api/generate/limits'); if (!response.ok) return; const data = await response.json(); if (typeof data.max_duration_with_lm === 'number') { setMaxDurationWithLm(data.max_duration_with_lm); } if (typeof data.max_duration_without_lm === 'number') { setMaxDurationWithoutLm(data.max_duration_without_lm); } } catch { // ignore limits fetch failures } }; loadLimits(); }, []); const activeMaxDuration = thinking ? maxDurationWithLm : maxDurationWithoutLm; useEffect(() => { if (duration > activeMaxDuration) { setDuration(activeMaxDuration); } }, [duration, activeMaxDuration]); useEffect(() => { const getDragKind = (e: DragEvent): 'file' | 'audio' | null => { if (!e.dataTransfer) return null; const types = Array.from(e.dataTransfer.types); if (types.includes('Files')) return 'file'; if (types.includes('application/x-ace-audio')) return 'audio'; return null; }; const handleDragEnter = (e: DragEvent) => { const kind = getDragKind(e); if (!kind) return; dragDepthRef.current += 1; setIsDraggingFile(true); setDragKind(kind); e.preventDefault(); }; const handleDragOver = (e: DragEvent) => { const kind = getDragKind(e); if (!kind) return; setDragKind(kind); e.preventDefault(); }; const handleDragLeave = (e: DragEvent) => { const kind = getDragKind(e); if (!kind) return; dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); if (dragDepthRef.current === 0) { setIsDraggingFile(false); setDragKind(null); } }; const handleDrop = (e: DragEvent) => { const kind = getDragKind(e); if (!kind) return; e.preventDefault(); dragDepthRef.current = 0; setIsDraggingFile(false); setDragKind(null); }; window.addEventListener('dragenter', handleDragEnter); window.addEventListener('dragover', handleDragOver); window.addEventListener('dragleave', handleDragLeave); window.addEventListener('drop', handleDrop); return () => { window.removeEventListener('dragenter', handleDragEnter); window.removeEventListener('dragover', handleDragOver); window.removeEventListener('dragleave', handleDragLeave); window.removeEventListener('drop', handleDrop); }; }, []); const startResizing = (e: React.MouseEvent) => { e.preventDefault(); setIsResizing(true); }; const uploadAudio = async (file: File, target: 'reference' | 'source') => { if (!token) { setUploadError('Please sign in to upload audio.'); return; } setUploadError(null); const setUploading = target === 'reference' ? setIsUploadingReference : setIsUploadingSource; const setUrl = target === 'reference' ? setReferenceAudioUrl : setSourceAudioUrl; setUploading(true); try { const result = await generateApi.uploadAudio(file, token); setUrl(result.url); setShowAudioModal(false); setTempAudioUrl(''); } catch (err) { const message = err instanceof Error ? err.message : 'Upload failed'; setUploadError(message); } finally { setUploading(false); } }; const handleFileSelect = (e: React.ChangeEvent, target: 'reference' | 'source') => { const file = e.target.files?.[0]; if (file) { void uploadReferenceTrack(file, target); } e.target.value = ''; }; // Format handler - uses LLM to enhance style/lyrics and auto-fill parameters const handleFormat = async (target: 'style' | 'lyrics') => { if (!token || !style.trim()) return; if (target === 'style') { setIsFormattingStyle(true); } else { setIsFormattingLyrics(true); } try { const result = await generateApi.formatInput({ caption: style, lyrics: lyrics, bpm: bpm > 0 ? bpm : undefined, duration: duration > 0 ? duration : undefined, keyScale: keyScale || undefined, timeSignature: timeSignature || undefined, temperature: lmTemperature, topK: lmTopK > 0 ? lmTopK : undefined, topP: lmTopP, }, token); if (result.success) { // Update fields with LLM-generated values if (target === 'style' && result.caption) setStyle(result.caption); if (target === 'lyrics' && result.lyrics) setLyrics(result.lyrics); if (result.bpm && result.bpm > 0) setBpm(result.bpm); if (result.duration && result.duration > 0) setDuration(result.duration); if (result.key_scale) setKeyScale(result.key_scale); if (result.time_signature) setTimeSignature(result.time_signature); if (result.language) setVocalLanguage(result.language); if (target === 'style') setIsFormatCaption(true); } else { console.error('Format failed:', result.error || result.status_message); alert(result.error || result.status_message || 'Format failed. Make sure the LLM is initialized.'); } } catch (err) { console.error('Format error:', err); alert('Format failed. The LLM may not be available.'); } finally { if (target === 'style') { setIsFormattingStyle(false); } else { setIsFormattingLyrics(false); } } }; const openAudioModal = (target: 'reference' | 'source', tab: 'uploads' | 'created' = 'uploads') => { setAudioModalTarget(target); setTempAudioUrl(''); setLibraryTab(tab); setShowAudioModal(true); void fetchReferenceTracks(); }; const fetchReferenceTracks = useCallback(async () => { if (!token) return; setIsLoadingTracks(true); try { const response = await fetch('/api/reference-tracks', { headers: { Authorization: `Bearer ${token}` } }); if (response.ok) { const data = await response.json(); setReferenceTracks(data.tracks || []); } } catch (err) { console.error('Failed to fetch reference tracks:', err); } finally { setIsLoadingTracks(false); } }, [token]); const uploadReferenceTrack = async (file: File, target?: 'reference' | 'source') => { if (!token) { setUploadError('Please sign in to upload audio.'); return; } setUploadError(null); setIsUploadingReference(true); try { const formData = new FormData(); formData.append('audio', file); const response = await fetch('/api/reference-tracks', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData }); if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Upload failed'); } const data = await response.json(); setReferenceTracks(prev => [data.track, ...prev]); // Also set as current reference/source const selectedTarget = target ?? audioModalTarget; applyAudioTargetUrl(selectedTarget, data.track.audio_url, data.track.filename); if (data.whisper_available && data.track?.id) { void transcribeReferenceTrack(data.track.id).then(() => undefined); } else { setShowAudioModal(false); } } catch (err) { const message = err instanceof Error ? err.message : 'Upload failed'; setUploadError(message); } finally { setIsUploadingReference(false); } }; const transcribeReferenceTrack = async (trackId: string) => { if (!token) return; setIsTranscribingReference(true); const controller = new AbortController(); transcribeAbortRef.current = controller; try { const response = await fetch(`/api/reference-tracks/${trackId}/transcribe`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, signal: controller.signal, }); if (!response.ok) { throw new Error('Failed to transcribe'); } const data = await response.json(); if (data.lyrics) { setLyrics(prev => prev || data.lyrics); } } catch (err) { if (controller.signal.aborted) return; console.error('Transcription failed:', err); } finally { if (transcribeAbortRef.current === controller) { transcribeAbortRef.current = null; } setIsTranscribingReference(false); } }; const cancelTranscription = () => { if (transcribeAbortRef.current) { transcribeAbortRef.current.abort(); transcribeAbortRef.current = null; } setIsTranscribingReference(false); }; const deleteReferenceTrack = async (trackId: string) => { if (!token) return; try { const response = await fetch(`/api/reference-tracks/${trackId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } }); if (response.ok) { setReferenceTracks(prev => prev.filter(t => t.id !== trackId)); if (playingTrackId === trackId && playingTrackSource === 'uploads') { setPlayingTrackId(null); setPlayingTrackSource(null); if (modalAudioRef.current) { modalAudioRef.current.pause(); } } } } catch (err) { console.error('Failed to delete track:', err); } }; const useReferenceTrack = (track: { audio_url: string; title?: string }) => { applyAudioTargetUrl(audioModalTarget, track.audio_url, track.title); setShowAudioModal(false); setPlayingTrackId(null); setPlayingTrackSource(null); }; const toggleModalTrack = (track: { id: string; audio_url: string; source: 'uploads' | 'created' }) => { if (playingTrackId === track.id) { if (modalAudioRef.current) { modalAudioRef.current.pause(); } setPlayingTrackId(null); setPlayingTrackSource(null); } else { setPlayingTrackId(track.id); setPlayingTrackSource(track.source); if (modalAudioRef.current) { modalAudioRef.current.src = track.audio_url; modalAudioRef.current.play().catch(() => undefined); } } }; const applyAudioUrl = () => { if (!tempAudioUrl.trim()) return; applyAudioTargetUrl(audioModalTarget, tempAudioUrl.trim()); setShowAudioModal(false); setTempAudioUrl(''); }; const applyAudioTargetUrl = (target: 'reference' | 'source', url: string, title?: string) => { const derivedTitle = title ? title.replace(/\.[^/.]+$/, '') : getAudioLabel(url); if (target === 'reference') { setReferenceAudioUrl(url); setReferenceAudioTitle(derivedTitle); setReferenceTime(0); setReferenceDuration(0); } else { setSourceAudioUrl(url); setSourceAudioTitle(derivedTitle); setSourceTime(0); setSourceDuration(0); if (taskType === 'text2music') { setTaskType('cover'); } } }; const formatTime = (time: number) => { if (!Number.isFinite(time) || time <= 0) return '0:00'; const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return `${minutes}:${String(seconds).padStart(2, '0')}`; }; const toggleAudio = (target: 'reference' | 'source') => { const audio = target === 'reference' ? referenceAudioRef.current : sourceAudioRef.current; if (!audio) return; if (audio.paused) { audio.play().catch(() => undefined); } else { audio.pause(); } }; const handleDrop = (e: React.DragEvent, target: 'reference' | 'source') => { e.preventDefault(); const file = e.dataTransfer.files?.[0]; if (file) { void uploadReferenceTrack(file, target); return; } const payload = e.dataTransfer.getData('application/x-ace-audio'); if (payload) { try { const data = JSON.parse(payload); if (data?.url) { applyAudioTargetUrl(target, data.url, data.title); } } catch { // ignore } } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); }; const handleWorkspaceDrop = (e: React.DragEvent) => { if (e.dataTransfer.files?.length || e.dataTransfer.types.includes('application/x-ace-audio')) { handleDrop(e, audioTab); } }; const handleWorkspaceDragOver = (e: React.DragEvent) => { if (e.dataTransfer.types.includes('Files') || e.dataTransfer.types.includes('application/x-ace-audio')) { e.preventDefault(); } }; const handleGenerate = () => { const styleWithGender = (() => { if (!vocalGender) return style; const genderHint = vocalGender === 'male' ? 'Male vocals' : 'Female vocals'; const trimmed = style.trim(); return trimmed ? `${trimmed}\n${genderHint}` : genderHint; })(); // Bulk generation: loop bulkCount times for (let i = 0; i < bulkCount; i++) { // Seed handling: first job uses user's seed, rest get random seeds let jobSeed = -1; if (!randomSeed && i === 0) { jobSeed = seed; } else if (!randomSeed && i > 0) { // Subsequent jobs get random seeds for variety jobSeed = Math.floor(Math.random() * 4294967295); } onGenerate({ customMode, songDescription: customMode ? undefined : songDescription, prompt: lyrics, lyrics, style: styleWithGender, title: bulkCount > 1 ? `${title} (${i + 1})` : title, instrumental, vocalLanguage, bpm, keyScale, timeSignature, duration, inferenceSteps, guidanceScale, batchSize, randomSeed: randomSeed || i > 0, // Force random for subsequent bulk jobs seed: jobSeed, thinking, audioFormat, inferMethod, lmBackend, shift, lmTemperature, lmCfgScale, lmTopK, lmTopP, lmNegativePrompt, referenceAudioUrl: referenceAudioUrl.trim() || undefined, sourceAudioUrl: sourceAudioUrl.trim() || undefined, referenceAudioTitle: referenceAudioTitle.trim() || undefined, sourceAudioTitle: sourceAudioTitle.trim() || undefined, audioCodes: audioCodes.trim() || undefined, repaintingStart, repaintingEnd, instruction, audioCoverStrength, taskType, useAdg, cfgIntervalStart, cfgIntervalEnd, customTimesteps: customTimesteps.trim() || undefined, useCotMetas, useCotCaption, useCotLanguage, autogen, constrainedDecodingDebug, allowLmBatch, getScores, getLrc, scoreScale, lmBatchChunkSize, trackName: trackName.trim() || undefined, completeTrackClasses: (() => { const parsed = completeTrackClasses .split(',') .map((item) => item.trim()) .filter(Boolean); return parsed.length ? parsed : undefined; })(), isFormatCaption, }); } // Reset bulk count after generation if (bulkCount > 1) { setBulkCount(1); } }; return (
{isDraggingFile && (
{dragKind !== 'audio' && (
)}
{dragKind === 'audio' ? 'Drop to use audio' : 'Drop to upload'}
{dragKind === 'audio' ? `Using as ${audioTab === 'reference' ? 'Reference' : 'Cover'}` : `Uploading as ${audioTab === 'reference' ? 'Reference' : 'Cover'}`}
)}
handleFileSelect(e, 'reference')} className="hidden" /> handleFileSelect(e, 'source')} className="hidden" />