Various UI improvements
This commit is contained in:
@@ -565,6 +565,14 @@ export default function App() {
|
|||||||
viewCount: s.view_count || 0,
|
viewCount: s.view_count || 0,
|
||||||
userId: s.user_id,
|
userId: s.user_id,
|
||||||
creator: s.creator,
|
creator: s.creator,
|
||||||
|
generationParams: (() => {
|
||||||
|
try {
|
||||||
|
if (!s.generation_params) return undefined;
|
||||||
|
return typeof s.generation_params === 'string' ? JSON.parse(s.generation_params) : s.generation_params;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Preserve any generating songs that aren't in the loaded list
|
// Preserve any generating songs that aren't in the loaded list
|
||||||
@@ -579,6 +587,11 @@ export default function App() {
|
|||||||
// Sort by creation date, newest first
|
// Sort by creation date, newest first
|
||||||
return mergedSongs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
return mergedSongs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If the current selection was a temp/generating song, replace it with newest real song
|
||||||
|
if (selectedSong?.isGenerating || (selectedSong && !loadedSongs.some(s => s.id === selectedSong.id))) {
|
||||||
|
setSelectedSong(loadedSongs[0] ?? null);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh songs:', error);
|
console.error('Failed to refresh songs:', error);
|
||||||
}
|
}
|
||||||
@@ -623,7 +636,7 @@ export default function App() {
|
|||||||
title: params.title,
|
title: params.title,
|
||||||
instrumental: params.instrumental,
|
instrumental: params.instrumental,
|
||||||
vocalLanguage: params.vocalLanguage,
|
vocalLanguage: params.vocalLanguage,
|
||||||
duration: params.duration,
|
duration: params.duration && params.duration > 0 ? params.duration : undefined,
|
||||||
bpm: params.bpm,
|
bpm: params.bpm,
|
||||||
keyScale: params.keyScale,
|
keyScale: params.keyScale,
|
||||||
timeSignature: params.timeSignature,
|
timeSignature: params.timeSignature,
|
||||||
@@ -643,6 +656,8 @@ export default function App() {
|
|||||||
lmNegativePrompt: params.lmNegativePrompt,
|
lmNegativePrompt: params.lmNegativePrompt,
|
||||||
referenceAudioUrl: params.referenceAudioUrl,
|
referenceAudioUrl: params.referenceAudioUrl,
|
||||||
sourceAudioUrl: params.sourceAudioUrl,
|
sourceAudioUrl: params.sourceAudioUrl,
|
||||||
|
referenceAudioTitle: params.referenceAudioTitle,
|
||||||
|
sourceAudioTitle: params.sourceAudioTitle,
|
||||||
audioCodes: params.audioCodes,
|
audioCodes: params.audioCodes,
|
||||||
repaintingStart: params.repaintingStart,
|
repaintingStart: params.repaintingStart,
|
||||||
repaintingEnd: params.repaintingEnd,
|
repaintingEnd: params.repaintingEnd,
|
||||||
@@ -672,6 +687,9 @@ export default function App() {
|
|||||||
const pollInterval = setInterval(async () => {
|
const pollInterval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const status = await generateApi.getStatus(job.jobId, token);
|
const status = await generateApi.getStatus(job.jobId, token);
|
||||||
|
const normalizedProgress = Number.isFinite(Number(status.progress))
|
||||||
|
? (Number(status.progress) > 1 ? Number(status.progress) / 100 : Number(status.progress))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Update queue position on the temp song
|
// Update queue position on the temp song
|
||||||
setSongs(prev => prev.map(s => {
|
setSongs(prev => prev.map(s => {
|
||||||
@@ -679,8 +697,8 @@ export default function App() {
|
|||||||
return {
|
return {
|
||||||
...s,
|
...s,
|
||||||
queuePosition: status.status === 'queued' ? status.queuePosition : undefined,
|
queuePosition: status.status === 'queued' ? status.queuePosition : undefined,
|
||||||
progress: status.status === 'running' ? status.progress : undefined,
|
progress: normalizedProgress ?? s.progress,
|
||||||
stage: status.status === 'running' ? status.stage : undefined,
|
stage: status.stage ?? s.stage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
|
|||||||
+69
-21
@@ -146,6 +146,8 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
// Expert Parameters (now in Advanced section)
|
// Expert Parameters (now in Advanced section)
|
||||||
const [referenceAudioUrl, setReferenceAudioUrl] = useState('');
|
const [referenceAudioUrl, setReferenceAudioUrl] = useState('');
|
||||||
const [sourceAudioUrl, setSourceAudioUrl] = useState('');
|
const [sourceAudioUrl, setSourceAudioUrl] = useState('');
|
||||||
|
const [referenceAudioTitle, setReferenceAudioTitle] = useState('');
|
||||||
|
const [sourceAudioTitle, setSourceAudioTitle] = useState('');
|
||||||
const [audioCodes, setAudioCodes] = useState('');
|
const [audioCodes, setAudioCodes] = useState('');
|
||||||
const [repaintingStart, setRepaintingStart] = useState(0);
|
const [repaintingStart, setRepaintingStart] = useState(0);
|
||||||
const [repaintingEnd, setRepaintingEnd] = useState(-1);
|
const [repaintingEnd, setRepaintingEnd] = useState(-1);
|
||||||
@@ -169,11 +171,14 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
const [trackName, setTrackName] = useState('');
|
const [trackName, setTrackName] = useState('');
|
||||||
const [completeTrackClasses, setCompleteTrackClasses] = useState('');
|
const [completeTrackClasses, setCompleteTrackClasses] = useState('');
|
||||||
const [isFormatCaption, setIsFormatCaption] = useState(false);
|
const [isFormatCaption, setIsFormatCaption] = useState(false);
|
||||||
|
const [maxDurationWithLm, setMaxDurationWithLm] = useState(240);
|
||||||
|
const [maxDurationWithoutLm, setMaxDurationWithoutLm] = useState(240);
|
||||||
|
|
||||||
const [isUploadingReference, setIsUploadingReference] = useState(false);
|
const [isUploadingReference, setIsUploadingReference] = useState(false);
|
||||||
const [isUploadingSource, setIsUploadingSource] = useState(false);
|
const [isUploadingSource, setIsUploadingSource] = useState(false);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
const [isFormatting, setIsFormatting] = useState(false);
|
const [isFormattingStyle, setIsFormattingStyle] = useState(false);
|
||||||
|
const [isFormattingLyrics, setIsFormattingLyrics] = useState(false);
|
||||||
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
||||||
const referenceInputRef = useRef<HTMLInputElement>(null);
|
const referenceInputRef = useRef<HTMLInputElement>(null);
|
||||||
const sourceInputRef = useRef<HTMLInputElement>(null);
|
const sourceInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -202,10 +207,12 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
const getAudioLabel = (url: string) => {
|
const getAudioLabel = (url: string) => {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
return decodeURIComponent(parsed.pathname.split('/').pop() || parsed.hostname);
|
const name = decodeURIComponent(parsed.pathname.split('/').pop() || parsed.hostname);
|
||||||
|
return name.replace(/\.[^/.]+$/, '') || name;
|
||||||
} catch {
|
} catch {
|
||||||
const parts = url.split('/');
|
const parts = url.split('/');
|
||||||
return decodeURIComponent(parts[parts.length - 1] || url);
|
const name = decodeURIComponent(parts[parts.length - 1] || url);
|
||||||
|
return name.replace(/\.[^/.]+$/, '') || name;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,6 +282,34 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
};
|
};
|
||||||
}, [isResizing]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const isFileDrag = (e: DragEvent) =>
|
const isFileDrag = (e: DragEvent) =>
|
||||||
!!(e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files'));
|
!!(e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files'));
|
||||||
@@ -357,7 +392,11 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
// Format handler - uses LLM to enhance style/lyrics and auto-fill parameters
|
// Format handler - uses LLM to enhance style/lyrics and auto-fill parameters
|
||||||
const handleFormat = async (target: 'style' | 'lyrics') => {
|
const handleFormat = async (target: 'style' | 'lyrics') => {
|
||||||
if (!token || !style.trim()) return;
|
if (!token || !style.trim()) return;
|
||||||
setIsFormatting(true);
|
if (target === 'style') {
|
||||||
|
setIsFormattingStyle(true);
|
||||||
|
} else {
|
||||||
|
setIsFormattingLyrics(true);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const result = await generateApi.formatInput({
|
const result = await generateApi.formatInput({
|
||||||
caption: style,
|
caption: style,
|
||||||
@@ -389,7 +428,11 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
console.error('Format error:', err);
|
console.error('Format error:', err);
|
||||||
alert('Format failed. The LLM may not be available.');
|
alert('Format failed. The LLM may not be available.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsFormatting(false);
|
if (target === 'style') {
|
||||||
|
setIsFormattingStyle(false);
|
||||||
|
} else {
|
||||||
|
setIsFormattingLyrics(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -445,7 +488,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
|
|
||||||
// Also set as current reference/source
|
// Also set as current reference/source
|
||||||
const selectedTarget = target ?? audioModalTarget;
|
const selectedTarget = target ?? audioModalTarget;
|
||||||
applyAudioTargetUrl(selectedTarget, data.track.audio_url);
|
applyAudioTargetUrl(selectedTarget, data.track.audio_url, data.track.filename);
|
||||||
setShowAudioModal(false);
|
setShowAudioModal(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'Upload failed';
|
const message = err instanceof Error ? err.message : 'Upload failed';
|
||||||
@@ -477,7 +520,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useReferenceTrack = (track: ReferenceTrack) => {
|
const useReferenceTrack = (track: ReferenceTrack) => {
|
||||||
applyAudioTargetUrl(audioModalTarget, track.audio_url);
|
applyAudioTargetUrl(audioModalTarget, track.audio_url, track.filename);
|
||||||
setShowAudioModal(false);
|
setShowAudioModal(false);
|
||||||
setPlayingTrackId(null);
|
setPlayingTrackId(null);
|
||||||
};
|
};
|
||||||
@@ -504,13 +547,16 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
setTempAudioUrl('');
|
setTempAudioUrl('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyAudioTargetUrl = (target: 'reference' | 'source', url: string) => {
|
const applyAudioTargetUrl = (target: 'reference' | 'source', url: string, title?: string) => {
|
||||||
|
const derivedTitle = title ? title.replace(/\.[^/.]+$/, '') : getAudioLabel(url);
|
||||||
if (target === 'reference') {
|
if (target === 'reference') {
|
||||||
setReferenceAudioUrl(url);
|
setReferenceAudioUrl(url);
|
||||||
|
setReferenceAudioTitle(derivedTitle);
|
||||||
setReferenceTime(0);
|
setReferenceTime(0);
|
||||||
setReferenceDuration(0);
|
setReferenceDuration(0);
|
||||||
} else {
|
} else {
|
||||||
setSourceAudioUrl(url);
|
setSourceAudioUrl(url);
|
||||||
|
setSourceAudioTitle(derivedTitle);
|
||||||
setSourceTime(0);
|
setSourceTime(0);
|
||||||
setSourceDuration(0);
|
setSourceDuration(0);
|
||||||
if (taskType === 'text2music') {
|
if (taskType === 'text2music') {
|
||||||
@@ -601,6 +647,8 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
lmNegativePrompt,
|
lmNegativePrompt,
|
||||||
referenceAudioUrl: referenceAudioUrl.trim() || undefined,
|
referenceAudioUrl: referenceAudioUrl.trim() || undefined,
|
||||||
sourceAudioUrl: sourceAudioUrl.trim() || undefined,
|
sourceAudioUrl: sourceAudioUrl.trim() || undefined,
|
||||||
|
referenceAudioTitle: referenceAudioTitle.trim() || undefined,
|
||||||
|
sourceAudioTitle: sourceAudioTitle.trim() || undefined,
|
||||||
audioCodes: audioCodes.trim() || undefined,
|
audioCodes: audioCodes.trim() || undefined,
|
||||||
repaintingStart,
|
repaintingStart,
|
||||||
repaintingEnd,
|
repaintingEnd,
|
||||||
@@ -768,7 +816,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="-1"
|
min="-1"
|
||||||
max="240"
|
max={activeMaxDuration}
|
||||||
step="5"
|
step="5"
|
||||||
value={duration}
|
value={duration}
|
||||||
onChange={(e) => setDuration(Number(e.target.value))}
|
onChange={(e) => setDuration(Number(e.target.value))}
|
||||||
@@ -907,7 +955,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
</button>
|
</button>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-xs font-medium text-zinc-800 dark:text-zinc-200 truncate mb-1.5">
|
<div className="text-xs font-medium text-zinc-800 dark:text-zinc-200 truncate mb-1.5">
|
||||||
{getAudioLabel(referenceAudioUrl)}
|
{referenceAudioTitle || getAudioLabel(referenceAudioUrl)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[10px] text-zinc-400 tabular-nums">{formatTime(referenceTime)}</span>
|
<span className="text-[10px] text-zinc-400 tabular-nums">{formatTime(referenceTime)}</span>
|
||||||
@@ -933,7 +981,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setReferenceAudioUrl(''); setReferencePlaying(false); setReferenceTime(0); setReferenceDuration(0); }}
|
onClick={() => { setReferenceAudioUrl(''); setReferenceAudioTitle(''); setReferencePlaying(false); setReferenceTime(0); setReferenceDuration(0); }}
|
||||||
className="p-1.5 rounded-full hover:bg-zinc-200 dark:hover:bg-white/10 text-zinc-400 hover:text-zinc-600 dark:hover:text-white transition-colors"
|
className="p-1.5 rounded-full hover:bg-zinc-200 dark:hover:bg-white/10 text-zinc-400 hover:text-zinc-600 dark:hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/></svg>
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
@@ -960,7 +1008,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
</button>
|
</button>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-xs font-medium text-zinc-800 dark:text-zinc-200 truncate mb-1.5">
|
<div className="text-xs font-medium text-zinc-800 dark:text-zinc-200 truncate mb-1.5">
|
||||||
{getAudioLabel(sourceAudioUrl)}
|
{sourceAudioTitle || getAudioLabel(sourceAudioUrl)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[10px] text-zinc-400 tabular-nums">{formatTime(sourceTime)}</span>
|
<span className="text-[10px] text-zinc-400 tabular-nums">{formatTime(sourceTime)}</span>
|
||||||
@@ -986,7 +1034,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setSourceAudioUrl(''); setSourcePlaying(false); setSourceTime(0); setSourceDuration(0); }}
|
onClick={() => { setSourceAudioUrl(''); setSourceAudioTitle(''); setSourcePlaying(false); setSourceTime(0); setSourceDuration(0); }}
|
||||||
className="p-1.5 rounded-full hover:bg-zinc-200 dark:hover:bg-white/10 text-zinc-400 hover:text-zinc-600 dark:hover:text-white transition-colors"
|
className="p-1.5 rounded-full hover:bg-zinc-200 dark:hover:bg-white/10 text-zinc-400 hover:text-zinc-600 dark:hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/></svg>
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
@@ -1049,12 +1097,12 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
{instrumental ? 'Instrumental' : 'Vocal'}
|
{instrumental ? 'Instrumental' : 'Vocal'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors ${isFormatting ? 'text-pink-500' : 'text-zinc-500 hover:text-black dark:hover:text-white'}`}
|
className={`p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors ${isFormattingLyrics ? 'text-pink-500' : 'text-zinc-500 hover:text-black dark:hover:text-white'}`}
|
||||||
title="AI Format - Enhance style & auto-fill parameters"
|
title="AI Format - Enhance style & auto-fill parameters"
|
||||||
onClick={() => handleFormat('lyrics')}
|
onClick={() => handleFormat('lyrics')}
|
||||||
disabled={isFormatting || !style.trim()}
|
disabled={isFormattingLyrics || !style.trim()}
|
||||||
>
|
>
|
||||||
{isFormatting ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
|
{isFormattingLyrics ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded text-zinc-500 hover:text-black dark:hover:text-white transition-colors"
|
className="p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded text-zinc-500 hover:text-black dark:hover:text-white transition-colors"
|
||||||
@@ -1089,12 +1137,12 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
<p className="text-[11px] text-zinc-400 dark:text-zinc-500 mt-0.5">Genre, mood, instruments, vibe</p>
|
<p className="text-[11px] text-zinc-400 dark:text-zinc-500 mt-0.5">Genre, mood, instruments, vibe</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={`p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors ${isFormatting ? 'text-pink-500' : 'text-zinc-500 hover:text-black dark:hover:text-white'}`}
|
className={`p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors ${isFormattingStyle ? 'text-pink-500' : 'text-zinc-500 hover:text-black dark:hover:text-white'}`}
|
||||||
title="AI Format - Enhance style & auto-fill parameters"
|
title="AI Format - Enhance style & auto-fill parameters"
|
||||||
onClick={() => handleFormat('style')}
|
onClick={() => handleFormat('style')}
|
||||||
disabled={isFormatting || !style.trim()}
|
disabled={isFormattingStyle || !style.trim()}
|
||||||
>
|
>
|
||||||
{isFormatting ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
|
{isFormattingStyle ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -1258,7 +1306,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="-1"
|
min="-1"
|
||||||
max="240"
|
max={activeMaxDuration}
|
||||||
step="5"
|
step="5"
|
||||||
value={duration}
|
value={duration}
|
||||||
onChange={(e) => setDuration(Number(e.target.value))}
|
onChange={(e) => setDuration(Number(e.target.value))}
|
||||||
@@ -1266,7 +1314,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
|||||||
/>
|
/>
|
||||||
<div className="flex justify-between text-[10px] text-zinc-500">
|
<div className="flex justify-between text-[10px] text-zinc-500">
|
||||||
<span>Auto</span>
|
<span>Auto</span>
|
||||||
<span>4 min</span>
|
<span>{Math.round(activeMaxDuration / 60)} min</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+26
-25
@@ -43,11 +43,12 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ song, onClose, onOpe
|
|||||||
if (!url) return 'None';
|
if (!url) return 'None';
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url, window.location.origin);
|
const parsed = new URL(url, window.location.origin);
|
||||||
const name = parsed.pathname.split('/').pop();
|
const name = decodeURIComponent(parsed.pathname.split('/').pop() || url);
|
||||||
return decodeURIComponent(name || url);
|
return name.replace(/\.[^/.]+$/, '') || name;
|
||||||
} catch {
|
} catch {
|
||||||
const parts = url.split('/');
|
const parts = url.split('/');
|
||||||
return decodeURIComponent(parts[parts.length - 1] || url);
|
const name = decodeURIComponent(parts[parts.length - 1] || url);
|
||||||
|
return name.replace(/\.[^/.]+$/, '') || name;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -264,44 +265,44 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ song, onClose, onOpe
|
|||||||
Sources
|
Sources
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 dark:border-white/10 bg-white dark:bg-zinc-900/40 px-3 py-2">
|
{song.generationParams?.referenceAudioUrl && (
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 dark:border-white/10 bg-white dark:bg-zinc-900/40 px-3 py-2">
|
||||||
<Music size={14} className="text-zinc-400" />
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<div className="min-w-0">
|
<Music size={14} className="text-zinc-400" />
|
||||||
<div className="text-xs text-zinc-500">Reference</div>
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-medium text-zinc-900 dark:text-white truncate">
|
<div className="text-xs text-zinc-500">Reference</div>
|
||||||
{getSourceLabel(song.generationParams?.referenceAudioUrl)}
|
<div className="text-sm font-medium text-zinc-900 dark:text-white truncate">
|
||||||
|
{song.generationParams?.referenceAudioTitle || getSourceLabel(song.generationParams?.referenceAudioUrl)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{song.generationParams?.referenceAudioUrl && (
|
|
||||||
<button
|
<button
|
||||||
className="text-xs px-2 py-1 rounded-full border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors"
|
className="text-xs px-2 py-1 rounded-full border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors"
|
||||||
onClick={() => openSource(song.generationParams?.referenceAudioUrl)}
|
onClick={() => openSource(song.generationParams?.referenceAudioUrl)}
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 dark:border-white/10 bg-white dark:bg-zinc-900/40 px-3 py-2">
|
{song.generationParams?.sourceAudioUrl && (
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 dark:border-white/10 bg-white dark:bg-zinc-900/40 px-3 py-2">
|
||||||
<Layers size={14} className="text-zinc-400" />
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<div className="min-w-0">
|
<Layers size={14} className="text-zinc-400" />
|
||||||
<div className="text-xs text-zinc-500">Cover</div>
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-medium text-zinc-900 dark:text-white truncate">
|
<div className="text-xs text-zinc-500">Cover</div>
|
||||||
{getSourceLabel(song.generationParams?.sourceAudioUrl)}
|
<div className="text-sm font-medium text-zinc-900 dark:text-white truncate">
|
||||||
|
{song.generationParams?.sourceAudioTitle || getSourceLabel(song.generationParams?.sourceAudioUrl)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{song.generationParams?.sourceAudioUrl && (
|
|
||||||
<button
|
<button
|
||||||
className="text-xs px-2 py-1 rounded-full border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors"
|
className="text-xs px-2 py-1 rounded-full border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors"
|
||||||
onClick={() => openSource(song.generationParams?.sourceAudioUrl)}
|
onClick={() => openSource(song.generationParams?.sourceAudioUrl)}
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+11
-1
@@ -361,12 +361,22 @@ const SongItem: React.FC<SongItemProps> = ({
|
|||||||
{song.progress !== undefined ? (
|
{song.progress !== undefined ? (
|
||||||
<div
|
<div
|
||||||
className="h-full bg-gradient-to-r from-pink-500 to-purple-600 transition-all"
|
className="h-full bg-gradient-to-r from-pink-500 to-purple-600 transition-all"
|
||||||
style={{ width: `${Math.min(100, Math.max(0, song.progress * 100))}%` }}
|
style={{
|
||||||
|
width: `${Math.min(
|
||||||
|
100,
|
||||||
|
Math.max(0, (song.progress > 1 ? song.progress / 100 : song.progress) * 100)
|
||||||
|
)}%`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-1/3 bg-gradient-to-r from-pink-500 to-purple-600 animate-pulse" />
|
<div className="h-full w-1/3 bg-gradient-to-r from-pink-500 to-purple-600 animate-pulse" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{song.progress !== undefined && (
|
||||||
|
<div className="mt-1 text-[10px] text-zinc-500 dark:text-zinc-400">
|
||||||
|
{Math.round((song.progress > 1 ? song.progress / 100 : song.progress) * 100)}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import torch
|
||||||
|
|
||||||
# Get ACE-Step path from environment or use default
|
# Get ACE-Step path from environment or use default
|
||||||
ACESTEP_PATH = os.environ.get('ACESTEP_PATH', '/home/ambsd/Desktop/aceui/ACE-Step-1.5')
|
ACESTEP_PATH = os.environ.get('ACESTEP_PATH', '/home/ambsd/Desktop/aceui/ACE-Step-1.5')
|
||||||
@@ -27,11 +28,17 @@ _llm_handler = None
|
|||||||
def get_handlers():
|
def get_handlers():
|
||||||
global _handler, _llm_handler
|
global _handler, _llm_handler
|
||||||
if _handler is None:
|
if _handler is None:
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
device = "cuda"
|
||||||
|
elif torch.backends.mps.is_available():
|
||||||
|
device = "mps"
|
||||||
|
else:
|
||||||
|
device = "cpu"
|
||||||
_handler = AceStepHandler()
|
_handler = AceStepHandler()
|
||||||
_handler.initialize_service(
|
_handler.initialize_service(
|
||||||
project_root=ACESTEP_PATH,
|
project_root=ACESTEP_PATH,
|
||||||
config_path="acestep-v15-turbo",
|
config_path="acestep-v15-turbo",
|
||||||
device="cuda",
|
device=device,
|
||||||
offload_to_cpu=True, # For 12GB GPU
|
offload_to_cpu=True, # For 12GB GPU
|
||||||
)
|
)
|
||||||
_llm_handler = LLMHandler() # Create but don't initialize (not enough VRAM)
|
_llm_handler = LLMHandler() # Create but don't initialize (not enough VRAM)
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ interface GenerateBody {
|
|||||||
// Expert Parameters
|
// Expert Parameters
|
||||||
referenceAudioUrl?: string;
|
referenceAudioUrl?: string;
|
||||||
sourceAudioUrl?: string;
|
sourceAudioUrl?: string;
|
||||||
|
referenceAudioTitle?: string;
|
||||||
|
sourceAudioTitle?: string;
|
||||||
audioCodes?: string;
|
audioCodes?: string;
|
||||||
repaintingStart?: number;
|
repaintingStart?: number;
|
||||||
repaintingEnd?: number;
|
repaintingEnd?: number;
|
||||||
@@ -196,6 +198,8 @@ router.post('/', authMiddleware, async (req: AuthenticatedRequest, res: Response
|
|||||||
lmNegativePrompt,
|
lmNegativePrompt,
|
||||||
referenceAudioUrl,
|
referenceAudioUrl,
|
||||||
sourceAudioUrl,
|
sourceAudioUrl,
|
||||||
|
referenceAudioTitle,
|
||||||
|
sourceAudioTitle,
|
||||||
audioCodes,
|
audioCodes,
|
||||||
repaintingStart,
|
repaintingStart,
|
||||||
repaintingEnd,
|
repaintingEnd,
|
||||||
@@ -259,6 +263,8 @@ router.post('/', authMiddleware, async (req: AuthenticatedRequest, res: Response
|
|||||||
lmNegativePrompt,
|
lmNegativePrompt,
|
||||||
referenceAudioUrl,
|
referenceAudioUrl,
|
||||||
sourceAudioUrl,
|
sourceAudioUrl,
|
||||||
|
referenceAudioTitle,
|
||||||
|
sourceAudioTitle,
|
||||||
audioCodes,
|
audioCodes,
|
||||||
repaintingStart,
|
repaintingStart,
|
||||||
repaintingEnd,
|
repaintingEnd,
|
||||||
@@ -547,6 +553,60 @@ router.get('/health', async (_req, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/limits', async (_req, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const ACESTEP_DIR = process.env.ACESTEP_PATH || path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../ACE-Step-1.5');
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const SCRIPTS_DIR = path.join(__dirname, '../../scripts');
|
||||||
|
const LIMITS_SCRIPT = path.join(SCRIPTS_DIR, 'get_limits.py');
|
||||||
|
const pythonPath = resolvePythonPath(ACESTEP_DIR);
|
||||||
|
|
||||||
|
const result = await new Promise<{ success: boolean; data?: any; error?: string }>((resolve) => {
|
||||||
|
const proc = spawn(pythonPath, [LIMITS_SCRIPT], {
|
||||||
|
cwd: ACESTEP_DIR,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
ACESTEP_PATH: ACESTEP_DIR,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
proc.stdout.on('data', (data) => { stdout += data.toString(); });
|
||||||
|
proc.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||||
|
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
if (code === 0 && stdout) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stdout);
|
||||||
|
resolve({ success: true, data: parsed });
|
||||||
|
} catch {
|
||||||
|
resolve({ success: false, error: 'Failed to parse limits result' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: stderr || 'Failed to read limits' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', (err) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
res.json(result.data);
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: result.error || 'Failed to load limits' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Limits error:', error);
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/debug/:taskId', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
router.get('/debug/:taskId', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const rawResponse = getJobRawResponse(req.params.taskId);
|
const rawResponse = getJobRawResponse(req.params.taskId);
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ async function submitToApi(params: GenerationParams): Promise<{ taskId: string }
|
|||||||
const body: Record<string, unknown> = {
|
const body: Record<string, unknown> = {
|
||||||
prompt,
|
prompt,
|
||||||
lyrics,
|
lyrics,
|
||||||
audio_duration: params.duration ?? 60,
|
|
||||||
batch_size: params.batchSize ?? 1,
|
batch_size: params.batchSize ?? 1,
|
||||||
inference_steps: params.inferenceSteps ?? 8,
|
inference_steps: params.inferenceSteps ?? 8,
|
||||||
guidance_scale: params.guidanceScale ?? 10.0,
|
guidance_scale: params.guidanceScale ?? 10.0,
|
||||||
@@ -139,6 +138,7 @@ async function submitToApi(params: GenerationParams): Promise<{ taskId: string }
|
|||||||
use_cot_metas: false, // Explicitly disable CoT features that require LLM
|
use_cot_metas: false, // Explicitly disable CoT features that require LLM
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (params.duration && params.duration > 0) body.audio_duration = params.duration;
|
||||||
if (params.bpm && params.bpm > 0) body.bpm = params.bpm;
|
if (params.bpm && params.bpm > 0) body.bpm = params.bpm;
|
||||||
if (params.keyScale) body.key_scale = params.keyScale;
|
if (params.keyScale) body.key_scale = params.keyScale;
|
||||||
if (params.timeSignature) body.time_signature = params.timeSignature;
|
if (params.timeSignature) body.time_signature = params.timeSignature;
|
||||||
@@ -166,20 +166,42 @@ async function submitToApi(params: GenerationParams): Promise<{ taskId: string }
|
|||||||
if (params.cfgIntervalStart !== undefined && params.cfgIntervalStart > 0) body.cfg_interval_start = params.cfgIntervalStart;
|
if (params.cfgIntervalStart !== undefined && params.cfgIntervalStart > 0) body.cfg_interval_start = params.cfgIntervalStart;
|
||||||
if (params.cfgIntervalEnd !== undefined && params.cfgIntervalEnd < 1.0) body.cfg_interval_end = params.cfgIntervalEnd;
|
if (params.cfgIntervalEnd !== undefined && params.cfgIntervalEnd < 1.0) body.cfg_interval_end = params.cfgIntervalEnd;
|
||||||
|
|
||||||
|
const resolveAudioPath = (audioUrl: string): string => {
|
||||||
|
if (audioUrl.startsWith('/audio/')) {
|
||||||
|
return path.join(AUDIO_DIR, audioUrl.replace('/audio/', ''));
|
||||||
|
}
|
||||||
|
if (audioUrl.startsWith('http')) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(audioUrl);
|
||||||
|
if (parsed.pathname.startsWith('/audio/')) {
|
||||||
|
return path.join(AUDIO_DIR, parsed.pathname.replace('/audio/', ''));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return audioUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Guard: cover/audio2audio requires a source or audio codes
|
||||||
|
if ((params.taskType === 'cover' || params.taskType === 'audio2audio') && !params.sourceAudioUrl && !params.audioCodes) {
|
||||||
|
throw new Error(`task_type='${params.taskType}' requires a source audio or audio codes`);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle reference audio - need to pass file path
|
// Handle reference audio - need to pass file path
|
||||||
if (params.referenceAudioUrl) {
|
if (params.referenceAudioUrl) {
|
||||||
let refAudioPath = params.referenceAudioUrl;
|
body.reference_audio_path = resolveAudioPath(params.referenceAudioUrl);
|
||||||
if (refAudioPath.startsWith('/audio/')) {
|
|
||||||
refAudioPath = path.join(AUDIO_DIR, refAudioPath.replace('/audio/', ''));
|
|
||||||
}
|
|
||||||
body.reference_audio_path = refAudioPath;
|
|
||||||
}
|
}
|
||||||
if (params.sourceAudioUrl) {
|
if (params.sourceAudioUrl) {
|
||||||
let srcAudioPath = params.sourceAudioUrl;
|
body.src_audio_path = resolveAudioPath(params.sourceAudioUrl);
|
||||||
if (srcAudioPath.startsWith('/audio/')) {
|
}
|
||||||
srcAudioPath = path.join(AUDIO_DIR, srcAudioPath.replace('/audio/', ''));
|
|
||||||
}
|
if (params.taskType === 'cover' || params.taskType === 'audio2audio') {
|
||||||
body.src_audio_path = srcAudioPath;
|
console.log(`[ACE-Step] cover/audio2audio inputs`, {
|
||||||
|
reference_audio_path: body.reference_audio_path,
|
||||||
|
src_audio_path: body.src_audio_path,
|
||||||
|
has_audio_codes: Boolean(params.audioCodes),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${ACESTEP_API}/release_task`, {
|
const response = await fetch(`${ACESTEP_API}/release_task`, {
|
||||||
@@ -263,6 +285,20 @@ async function pollApiResult(taskId: string, maxWaitMs = 600000): Promise<ApiTas
|
|||||||
throw new Error(`Generation failed on API side: ${details}`);
|
throw new Error(`Generation failed on API side: ${details}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log progress while processing (if provided)
|
||||||
|
if (taskData.result) {
|
||||||
|
try {
|
||||||
|
const resultData = typeof taskData.result === 'string' ? JSON.parse(taskData.result) : taskData.result;
|
||||||
|
const item = Array.isArray(resultData) ? resultData[0] : resultData;
|
||||||
|
if (item && typeof item === 'object' && typeof (item as any).progress === 'number') {
|
||||||
|
const pct = Math.round((item as any).progress * 100);
|
||||||
|
console.log(`[ACE-Step] API task ${taskId} progress: ${pct}%`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore parse failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Still processing
|
// Still processing
|
||||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||||
}
|
}
|
||||||
@@ -349,6 +385,8 @@ export interface GenerationParams {
|
|||||||
// Expert Parameters
|
// Expert Parameters
|
||||||
referenceAudioUrl?: string;
|
referenceAudioUrl?: string;
|
||||||
sourceAudioUrl?: string;
|
sourceAudioUrl?: string;
|
||||||
|
referenceAudioTitle?: string;
|
||||||
|
sourceAudioTitle?: string;
|
||||||
audioCodes?: string;
|
audioCodes?: string;
|
||||||
repaintingStart?: number;
|
repaintingStart?: number;
|
||||||
repaintingEnd?: number;
|
repaintingEnd?: number;
|
||||||
@@ -403,6 +441,8 @@ interface ActiveJob {
|
|||||||
processPromise?: Promise<void>;
|
processPromise?: Promise<void>;
|
||||||
rawResponse?: unknown;
|
rawResponse?: unknown;
|
||||||
queuePosition?: number;
|
queuePosition?: number;
|
||||||
|
progress?: number;
|
||||||
|
stage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeJobs = new Map<string, ActiveJob>();
|
const activeJobs = new Map<string, ActiveJob>();
|
||||||
@@ -466,6 +506,8 @@ async function processQueue(): Promise<void> {
|
|||||||
|
|
||||||
// Submit generation job to queue
|
// Submit generation job to queue
|
||||||
export async function generateMusicViaAPI(params: GenerationParams): Promise<{ jobId: string }> {
|
export async function generateMusicViaAPI(params: GenerationParams): Promise<{ jobId: string }> {
|
||||||
|
// Force a fresh API availability check when starting a job
|
||||||
|
resetApiCache();
|
||||||
const jobId = `job_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
const jobId = `job_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||||
|
|
||||||
const job: ActiveJob = {
|
const job: ActiveJob = {
|
||||||
@@ -573,9 +615,10 @@ async function processGeneration(
|
|||||||
const jobOutputDir = path.join(ACESTEP_DIR, 'output', jobId);
|
const jobOutputDir = path.join(ACESTEP_DIR, 'output', jobId);
|
||||||
await mkdir(jobOutputDir, { recursive: true });
|
await mkdir(jobOutputDir, { recursive: true });
|
||||||
|
|
||||||
|
const durationToSend = params.duration && params.duration > 0 ? params.duration : 60;
|
||||||
const args = [
|
const args = [
|
||||||
'--prompt', prompt,
|
'--prompt', prompt,
|
||||||
'--duration', String(params.duration ?? 60),
|
'--duration', String(durationToSend),
|
||||||
'--batch-size', String(params.batchSize ?? 1),
|
'--batch-size', String(params.batchSize ?? 1),
|
||||||
'--infer-steps', String(params.inferenceSteps ?? 8),
|
'--infer-steps', String(params.inferenceSteps ?? 8),
|
||||||
'--guidance-scale', String(params.guidanceScale ?? 10.0),
|
'--guidance-scale', String(params.guidanceScale ?? 10.0),
|
||||||
@@ -701,7 +744,6 @@ function runPythonGeneration(scriptArgs: string[]): Promise<PythonResult> {
|
|||||||
cwd: ACESTEP_DIR,
|
cwd: ACESTEP_DIR,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
CUDA_VISIBLE_DEVICES: '0',
|
|
||||||
ACESTEP_PATH: ACESTEP_DIR,
|
ACESTEP_PATH: ACESTEP_DIR,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -863,13 +905,16 @@ export async function getJobStatus(jobId: string): Promise<JobStatus> {
|
|||||||
|
|
||||||
const item = Array.isArray(resultData) ? resultData[0] : resultData;
|
const item = Array.isArray(resultData) ? resultData[0] : resultData;
|
||||||
if (item && typeof item === 'object') {
|
if (item && typeof item === 'object') {
|
||||||
const progress = typeof (item as any).progress === 'number' ? (item as any).progress : undefined;
|
const rawProgress = (item as any).progress;
|
||||||
|
const progress = Number.isFinite(Number(rawProgress)) ? Number(rawProgress) : undefined;
|
||||||
const stage = typeof (item as any).stage === 'string' ? (item as any).stage : undefined;
|
const stage = typeof (item as any).stage === 'string' ? (item as any).stage : undefined;
|
||||||
|
if (progress !== undefined) job.progress = progress;
|
||||||
|
if (stage) job.stage = stage;
|
||||||
return {
|
return {
|
||||||
status: job.status,
|
status: job.status,
|
||||||
etaSeconds: Math.max(0, 180 - elapsed),
|
etaSeconds: Math.max(0, 180 - elapsed),
|
||||||
progress,
|
progress: progress ?? job.progress,
|
||||||
stage,
|
stage: stage ?? job.stage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -882,6 +927,8 @@ export async function getJobStatus(jobId: string): Promise<JobStatus> {
|
|||||||
return {
|
return {
|
||||||
status: job.status,
|
status: job.status,
|
||||||
etaSeconds: Math.max(0, 180 - elapsed), // 3 min estimate
|
etaSeconds: Math.max(0, 180 - elapsed), // 3 min estimate
|
||||||
|
progress: job.progress,
|
||||||
|
stage: job.stage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,8 @@ export interface GenerationParams {
|
|||||||
// Expert Parameters
|
// Expert Parameters
|
||||||
referenceAudioUrl?: string;
|
referenceAudioUrl?: string;
|
||||||
sourceAudioUrl?: string;
|
sourceAudioUrl?: string;
|
||||||
|
referenceAudioTitle?: string;
|
||||||
|
sourceAudioTitle?: string;
|
||||||
audioCodes?: string;
|
audioCodes?: string;
|
||||||
repaintingStart?: number;
|
repaintingStart?: number;
|
||||||
repaintingEnd?: number;
|
repaintingEnd?: number;
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export interface GenerationParams {
|
|||||||
// Expert Parameters
|
// Expert Parameters
|
||||||
referenceAudioUrl?: string;
|
referenceAudioUrl?: string;
|
||||||
sourceAudioUrl?: string;
|
sourceAudioUrl?: string;
|
||||||
|
referenceAudioTitle?: string;
|
||||||
|
sourceAudioTitle?: string;
|
||||||
audioCodes?: string;
|
audioCodes?: string;
|
||||||
repaintingStart?: number;
|
repaintingStart?: number;
|
||||||
repaintingEnd?: number;
|
repaintingEnd?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user