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 { useI18n } from '../context/I18nContext'; import { generateApi } from '../services/api'; import { MAIN_STYLES, SUB_STYLES } from '../data/genres'; import { EditableSlider } from './EditableSlider'; 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_LANGUAGE_KEYS = [ { value: 'unknown', key: 'autoInstrumental' as const }, { value: 'ar', key: 'vocalArabic' as const }, { value: 'az', key: 'vocalAzerbaijani' as const }, { value: 'bg', key: 'vocalBulgarian' as const }, { value: 'bn', key: 'vocalBengali' as const }, { value: 'ca', key: 'vocalCatalan' as const }, { value: 'cs', key: 'vocalCzech' as const }, { value: 'da', key: 'vocalDanish' as const }, { value: 'de', key: 'vocalGerman' as const }, { value: 'el', key: 'vocalGreek' as const }, { value: 'en', key: 'vocalEnglish' as const }, { value: 'es', key: 'vocalSpanish' as const }, { value: 'fa', key: 'vocalPersian' as const }, { value: 'fi', key: 'vocalFinnish' as const }, { value: 'fr', key: 'vocalFrench' as const }, { value: 'he', key: 'vocalHebrew' as const }, { value: 'hi', key: 'vocalHindi' as const }, { value: 'hr', key: 'vocalCroatian' as const }, { value: 'ht', key: 'vocalHaitianCreole' as const }, { value: 'hu', key: 'vocalHungarian' as const }, { value: 'id', key: 'vocalIndonesian' as const }, { value: 'is', key: 'vocalIcelandic' as const }, { value: 'it', key: 'vocalItalian' as const }, { value: 'ja', key: 'vocalJapanese' as const }, { value: 'ko', key: 'vocalKorean' as const }, { value: 'la', key: 'vocalLatin' as const }, { value: 'lt', key: 'vocalLithuanian' as const }, { value: 'ms', key: 'vocalMalay' as const }, { value: 'ne', key: 'vocalNepali' as const }, { value: 'nl', key: 'vocalDutch' as const }, { value: 'no', key: 'vocalNorwegian' as const }, { value: 'pa', key: 'vocalPunjabi' as const }, { value: 'pl', key: 'vocalPolish' as const }, { value: 'pt', key: 'vocalPortuguese' as const }, { value: 'ro', key: 'vocalRomanian' as const }, { value: 'ru', key: 'vocalRussian' as const }, { value: 'sa', key: 'vocalSanskrit' as const }, { value: 'sk', key: 'vocalSlovak' as const }, { value: 'sr', key: 'vocalSerbian' as const }, { value: 'sv', key: 'vocalSwedish' as const }, { value: 'sw', key: 'vocalSwahili' as const }, { value: 'ta', key: 'vocalTamil' as const }, { value: 'te', key: 'vocalTelugu' as const }, { value: 'th', key: 'vocalThai' as const }, { value: 'tl', key: 'vocalTagalog' as const }, { value: 'tr', key: 'vocalTurkish' as const }, { value: 'uk', key: 'vocalUkrainian' as const }, { value: 'ur', key: 'vocalUrdu' as const }, { value: 'vi', key: 'vocalVietnamese' as const }, { value: 'yue', key: 'vocalCantonese' as const }, { value: 'zh', key: 'vocalChineseMandarin' as const }, ]; export const CreatePanel: React.FC = ({ onGenerate, isGenerating, initialData, createdSongs = [], pendingAudioSelection, onAudioSelectionApplied, }) => { const { isAuthenticated, token, user } = useAuth(); const { t } = useI18n(); // Randomly select 6 music tags from MAIN_STYLES const [musicTags, setMusicTags] = useState(() => { const shuffled = [...MAIN_STYLES].sort(() => Math.random() - 0.5); return shuffled.slice(0, 6); }); // Function to refresh music tags const refreshMusicTags = useCallback(() => { const shuffled = [...MAIN_STYLES].sort(() => Math.random() - 0.5); setMusicTags(shuffled.slice(0, 6)); }, []); // 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(() => { const stored = localStorage.getItem('ace-batchSize'); return stored ? Number(stored) : 1; }); const [bulkCount, setBulkCount] = useState(() => { const stored = localStorage.getItem('ace-bulkCount'); return stored ? Number(stored) : 1; }); 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 [lmModel, setLmModel] = useState(() => { return localStorage.getItem('ace-lmModel') || 'acestep-5Hz-lm-0.6B'; }); 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); // LoRA Parameters const [showLoraPanel, setShowLoraPanel] = useState(false); const [loraPath, setLoraPath] = useState('./lora_output/final/adapter'); const [loraLoaded, setLoraLoaded] = useState(false); const [loraScale, setLoraScale] = useState(1.0); const [loraError, setLoraError] = useState(null); const [isLoraLoading, setIsLoraLoading] = useState(false); // Model selection const [selectedModel, setSelectedModel] = useState(() => { return localStorage.getItem('ace-model') || 'acestep-v15-turbo-shift3'; }); const [showModelMenu, setShowModelMenu] = useState(false); const modelMenuRef = useRef(null); const previousModelRef = useRef(selectedModel); // Available models fetched from backend const [fetchedModels, setFetchedModels] = useState<{ name: string; is_active: boolean; is_preloaded: boolean }[]>([]); // Fallback model list when backend is unavailable const availableModels = useMemo(() => { if (fetchedModels.length > 0) { return fetchedModels.map(m => ({ id: m.name, name: m.name })); } return [ { id: 'acestep-v15-base', name: 'acestep-v15-base' }, { id: 'acestep-v15-sft', name: 'acestep-v15-sft' }, { id: 'acestep-v15-turbo', name: 'acestep-v15-turbo' }, { id: 'acestep-v15-turbo-shift1', name: 'acestep-v15-turbo-shift1' }, { id: 'acestep-v15-turbo-shift3', name: 'acestep-v15-turbo-shift3' }, { id: 'acestep-v15-turbo-continuous', name: 'acestep-v15-turbo-continuous' }, ]; }, [fetchedModels]); // Map model ID to short display name const getModelDisplayName = (modelId: string): string => { const mapping: Record = { 'acestep-v15-base': '1.5B', 'acestep-v15-sft': '1.5S', 'acestep-v15-turbo-shift1': '1.5TS1', 'acestep-v15-turbo-shift3': '1.5TS3', 'acestep-v15-turbo-continuous': '1.5TC', 'acestep-v15-turbo': '1.5T', }; return mapping[modelId] || modelId; }; // Check if model is a turbo variant const isTurboModel = (modelId: string): boolean => { return modelId.includes('turbo'); }; // Genre selection state (cascading) const [selectedMainGenre, setSelectedMainGenre] = useState(''); const [selectedSubGenre, setSelectedSubGenre] = useState(''); // Filter sub-genres based on selected main genre const filteredSubGenres = useMemo(() => { if (!selectedMainGenre) return []; const mainLower = selectedMainGenre.toLowerCase().trim(); return SUB_STYLES.filter(style => style.toLowerCase().includes(mainLower) ); }, [selectedMainGenre]); 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); // Close model menu when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (modelMenuRef.current && !modelMenuRef.current.contains(event.target as Node)) { setShowModelMenu(false); } }; if (showModelMenu) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [showModelMenu]); // Auto-unload LoRA when model changes useEffect(() => { if (previousModelRef.current !== selectedModel && loraLoaded) { void handleLoraUnload(); } previousModelRef.current = selectedModel; }, [selectedModel, loraLoaded]); // Auto-disable thinking and ADG when LoRA is loaded useEffect(() => { if (loraLoaded) { if (thinking) setThinking(false); if (useAdg) setUseAdg(false); } }, [loraLoaded]); // LoRA API handlers const handleLoraToggle = async () => { if (!token) { setLoraError('Please sign in to use LoRA'); return; } if (!loraPath.trim()) { setLoraError('Please enter a LoRA path'); return; } setIsLoraLoading(true); setLoraError(null); try { if (loraLoaded) { await handleLoraUnload(); } else { const result = await generateApi.loadLora({ lora_path: loraPath }, token); setLoraLoaded(true); console.log('LoRA loaded:', result?.message); } } catch (err) { const message = err instanceof Error ? err.message : 'LoRA operation failed'; setLoraError(message); console.error('LoRA error:', err); } finally { setIsLoraLoading(false); } }; const handleLoraUnload = async () => { if (!token) return; setIsLoraLoading(true); setLoraError(null); try { const result = await generateApi.unloadLora(token); setLoraLoaded(false); console.log('LoRA unloaded:', result?.message); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to unload LoRA'; setLoraError(message); console.error('Unload error:', err); } finally { setIsLoraLoading(false); } }; const handleLoraScaleChange = async (newScale: number) => { setLoraScale(newScale); if (!token || !loraLoaded) return; try { await generateApi.setLoraScale({ scale: newScale }, token); } catch (err) { console.error('Failed to set LoRA scale:', err); } }; // 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]); const refreshModels = useCallback(async () => { try { const modelsRes = await fetch('/api/generate/models'); if (modelsRes.ok) { const data = await modelsRes.json(); const models = data.models || []; if (models.length > 0) { setFetchedModels(models); // Always sync to the backend's active model const active = models.find((m: any) => m.is_active); if (active) { setSelectedModel(active.name); localStorage.setItem('ace-model', active.name); } } } } catch { // ignore - will use fallback model list } }, []); useEffect(() => { const loadModelsAndLimits = async () => { await refreshModels(); // Fetch limits 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 } }; loadModelsAndLimits(); }, []); // Re-fetch models after generation completes to update active model const prevIsGeneratingRef = useRef(isGenerating); useEffect(() => { if (prevIsGeneratingRef.current && !isGenerating) { void refreshModels(); } prevIsGeneratingRef.current = isGenerating; }, [isGenerating, refreshModels]); 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, lmModel: lmModel || 'acestep-5Hz-lm-0.6B', lmBackend: lmBackend || 'pt', }, token); if (result.caption || result.lyrics || result.bpm || result.duration) { // 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) { const ts = String(result.time_signature); setTimeSignature(ts.includes('/') ? ts : `${ts}/4`); } if (result.vocal_language) setVocalLanguage(result.vocal_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, ditModel: selectedModel, 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, lmModel, 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, loraLoaded, }); } // Reset bulk count after generation if (bulkCount > 1) { setBulkCount(1); } }; return (
{isDraggingFile && (
{dragKind !== 'audio' && (
)}
{dragKind === 'audio' ? t('dropToUseAudio') : t('dropToUpload')}
{dragKind === 'audio' ? (audioTab === 'reference' ? t('usingAsReference') : t('usingAsCover')) : (audioTab === 'reference' ? t('uploadingAsReference') : t('uploadingAsCover'))}
)}
handleFileSelect(e, 'reference')} className="hidden" /> handleFileSelect(e, 'source')} className="hidden" />