import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Sparkles, ChevronDown, Settings2, Trash2, Music2, Sliders, Dices, Hash, RefreshCw, Plus, Upload, Play, Pause } 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; } 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 }) => { const { isAuthenticated, token } = 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'); // 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(7.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(8); const [inferMethod, setInferMethod] = useState<'ode' | 'sde'>('ode'); const [shift, setShift] = useState(3.0); // LM Parameters (under Expert) const [showLmParams, setShowLmParams] = useState(false); const [lmTemperature, setLmTemperature] = useState(0.85); const [lmCfgScale, setLmCfgScale] = useState(2.0); const [lmTopK, setLmTopK] = useState(0); const [lmTopP, setLmTopP] = useState(0.9); const [lmNegativePrompt, setLmNegativePrompt] = useState('NO USER INPUT'); // Expert Parameters (now in Advanced section) const [referenceAudioUrl, setReferenceAudioUrl] = useState(''); const [sourceAudioUrl, setSourceAudioUrl] = 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 [isUploadingReference, setIsUploadingReference] = useState(false); const [isUploadingSource, setIsUploadingSource] = useState(false); const [uploadError, setUploadError] = useState(null); const [isFormatting, setIsFormatting] = useState(false); const referenceInputRef = useRef(null); const sourceInputRef = useRef(null); 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 modalAudioRef = useRef(null); const [modalTrackTime, setModalTrackTime] = useState(0); const [modalTrackDuration, setModalTrackDuration] = useState(0); const getAudioLabel = (url: string) => { try { const parsed = new URL(url); return decodeURIComponent(parsed.pathname.split('/').pop() || parsed.hostname); } catch { const parts = url.split('/'); return decodeURIComponent(parts[parts.length - 1] || url); } }; // 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(() => { 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 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 uploadAudio(file, target); } e.target.value = ''; }; // Format handler - uses LLM to enhance style and auto-fill parameters const handleFormat = async () => { if (!token || !style.trim()) return; setIsFormatting(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 (result.caption) setStyle(result.caption); if (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); 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 { setIsFormatting(false); } }; const openAudioModal = (target: 'reference' | 'source') => { setAudioModalTarget(target); setTempAudioUrl(''); 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) => { 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 if (audioModalTarget === 'reference') { setReferenceAudioUrl(data.track.audio_url); } else { setSourceAudioUrl(data.track.audio_url); } setShowAudioModal(false); } catch (err) { const message = err instanceof Error ? err.message : 'Upload failed'; setUploadError(message); } finally { setIsUploadingReference(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) { setPlayingTrackId(null); if (modalAudioRef.current) { modalAudioRef.current.pause(); } } } } catch (err) { console.error('Failed to delete track:', err); } }; const useReferenceTrack = (track: ReferenceTrack) => { if (audioModalTarget === 'reference') { setReferenceAudioUrl(track.audio_url); } else { setSourceAudioUrl(track.audio_url); } setShowAudioModal(false); setPlayingTrackId(null); }; const toggleModalTrack = (track: ReferenceTrack) => { if (playingTrackId === track.id) { if (modalAudioRef.current) { modalAudioRef.current.pause(); } setPlayingTrackId(null); } else { setPlayingTrackId(track.id); if (modalAudioRef.current) { modalAudioRef.current.src = track.audio_url; modalAudioRef.current.play().catch(() => undefined); } } }; const applyAudioUrl = () => { if (!tempAudioUrl.trim()) return; if (audioModalTarget === 'reference') { setReferenceAudioUrl(tempAudioUrl.trim()); setReferenceTime(0); setReferenceDuration(0); } else { setSourceAudioUrl(tempAudioUrl.trim()); setSourceTime(0); setSourceDuration(0); } setShowAudioModal(false); setTempAudioUrl(''); }; 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 uploadAudio(file, target); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); }; const handleGenerate = () => { // 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, 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, shift, lmTemperature, lmCfgScale, lmTopK, lmTopP, lmNegativePrompt, referenceAudioUrl: referenceAudioUrl.trim() || undefined, sourceAudioUrl: sourceAudioUrl.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 (
handleFileSelect(e, 'reference')} className="hidden" /> handleFileSelect(e, 'source')} className="hidden" />