Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
75311b5
Initial plan
Copilot Mar 12, 2026
05391fd
Changes before error encountered
Copilot Mar 12, 2026
d515840
Complete LEGO mode, Ace Understand, and security fixes
Copilot Mar 12, 2026
ccca16f
fix: add --wav flag to all dit-vae invocations
Copilot Mar 12, 2026
50657f0
fix: move LEGO controls out of Advanced Settings into inline Cover/Re…
Copilot Mar 12, 2026
40cd7ac
fix: unified single-mode panel, remove Simple/Custom toggle, fix LEGO…
Copilot Mar 12, 2026
9e6cc34
feat: restore LEGO as third audio tab with instrument naming and clea…
Copilot Mar 12, 2026
fcfba8d
feat: add Debug tab with live binary log console (auto-polling, color…
Copilot Mar 12, 2026
1eaf341
fix: use correct file extension (.wav/.flac/.mp3) when saving generat…
Copilot Mar 12, 2026
e084437
fix: move job audio files instead of copy to eliminate duplicates; ad…
Copilot Mar 12, 2026
24cb956
plan: remove songDescription box + persist all form settings to local…
Copilot Mar 12, 2026
cef1216
feat: remove DESCRIBE YOUR SONG card; persist all form settings to lo…
Copilot Mar 12, 2026
f12dc4d
fix: remove Complete Track Classes checkboxes from Lego tab, keep onl…
Copilot Mar 12, 2026
fe2098b
feat: redesign Advanced panel for acestep-cpp; wire use_cot_caption t…
Copilot Mar 12, 2026
a4427d2
fix: remove audioFormat from Advanced panel; fix model-switch presets…
Copilot Mar 12, 2026
7f2f28d
fix: expand lm_top_p and lm_top_k to full-width sliders in Advanced p…
Copilot Mar 12, 2026
1de1f6e
fix: show LM sampling section in Advanced panel for all modes, not ju…
Copilot Mar 13, 2026
a817fd7
Reorder top panel blocks: Title → Style → Lyrics → Audio Ref → LoRA
Copilot Mar 13, 2026
03a4884
feat: spacing between top blocks, advanced closed by default, URL wid…
Copilot Mar 13, 2026
ff1b5dc
feat: WAV/MP3/FLAC format toggle + rolling debug log + auto-open on e…
Copilot Mar 13, 2026
e6778c0
feat: remove FLAC, MP3 as default native output (no --wav), WAV adds …
Copilot Mar 13, 2026
d08ba7c
fix: remove 'flac' from GenerateBody.audioFormat in generate.ts (TS e…
Copilot Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ MODELS_DIR=./models
#
# Override the primary DiT GGUF only if you want a non-default model:
# ACESTEP_MODEL=/path/to/models/acestep-v15-turbo-Q8_0.gguf
#
# Override the base DiT GGUF used for lego mode (auto-detected from models/ by default):
# ACESTEP_BASE_MODEL=/path/to/models/acestep-v15-base-Q8_0.gguf

# Mode 2 (advanced): Connect to a separately running acestep-cpp HTTP server.
# Leave the above unset and configure ACESTEP_API_URL instead.
Expand Down
24 changes: 21 additions & 3 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { SearchPage } from './components/SearchPage';
import { NewsPage } from './components/NewsPage';
import { ModelManager } from './components/ModelManager';
import { ConfirmDialog } from './components/ConfirmDialog';
import { DebugPanel } from './components/DebugPanel';


function AppContent() {
Expand Down Expand Up @@ -215,7 +216,8 @@ function AppContent() {
const handleBackFromProfile = () => {
setViewingUsername(null);
setCurrentView('create');
window.history.pushState({}, '', '/');
const wid = new URLSearchParams(window.location.search).get('wid');
window.history.pushState({}, '', wid ? `/?wid=${wid}` : '/');
};

// Navigate to Song Handler
Expand All @@ -229,7 +231,8 @@ function AppContent() {
const handleBackFromSong = () => {
setViewingSongId(null);
setCurrentView('create');
window.history.pushState({}, '', '/');
const wid = new URLSearchParams(window.location.search).get('wid');
window.history.pushState({}, '', wid ? `/?wid=${wid}` : '/');
};

// Theme Effect
Expand Down Expand Up @@ -290,6 +293,8 @@ function AppContent() {
setCurrentView('news');
} else if (path === '/models') {
setCurrentView('models');
} else if (path === '/debug') {
setCurrentView('debug');
}
};

Expand Down Expand Up @@ -736,6 +741,9 @@ function AppContent() {
cleanupJob(jobId, tempId);
console.error(`Job ${jobId} failed:`, status.error);
showToast(`${t('generationFailed')}: ${status.error || 'Unknown error'}`, 'error');
// Auto-open debug view so the user can inspect the logs
setCurrentView('debug');
window.history.pushState({}, '', '/debug');
}
} catch (pollError) {
console.error(`Polling error for job ${jobId}:`, pollError);
Expand Down Expand Up @@ -1305,6 +1313,13 @@ function AppContent() {
case 'news':
return <NewsPage />;

case 'debug':
return (
<div className="flex-1 relative overflow-hidden">
<DebugPanel />
</div>
);

case 'create':
default:
return (
Expand Down Expand Up @@ -1402,7 +1417,8 @@ function AppContent() {
setCurrentView(v);
if (v === 'create') {
setMobileShowList(false);
window.history.pushState({}, '', '/');
const wid = new URLSearchParams(window.location.search).get('wid');
window.history.pushState({}, '', wid ? `/?wid=${wid}` : '/');
} else if (v === 'library') {
window.history.pushState({}, '', '/library');
} else if (v === 'models') {
Expand All @@ -1411,6 +1427,8 @@ function AppContent() {
window.history.pushState({}, '', '/search');
} else if (v === 'news') {
window.history.pushState({}, '', '/news');
} else if (v === 'debug') {
window.history.pushState({}, '', '/debug');
}
if (isMobile) setShowLeftSidebar(false);
}}
Expand Down
1,946 changes: 813 additions & 1,133 deletions components/CreatePanel.tsx

Large diffs are not rendered by default.

284 changes: 284 additions & 0 deletions components/DebugPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useAuth } from '../context/AuthContext';
import { Trash2, RefreshCw, ChevronDown } from 'lucide-react';

interface JobSummary {
jobId: string;
status: string;
startTime: number;
stage?: string;
logCount: number;
}

const POLL_INTERVAL_MS = 1500;

// Module-level rolling log that survives component remounts (persists for the tab session)
const rollingLog: { jobId: string; startTime: number; status: string; lines: string[] }[] = [];
const offsetByJob = new Map<string, number>();

export const DebugPanel: React.FC = () => {
const { token } = useAuth();

const [jobs, setJobs] = useState<JobSummary[]>([]);
const [selectedJobId, setSelectedJobId] = useState<string>('');
// Combined display lines (all jobs, newest at bottom)
const [displayLines, setDisplayLines] = useState<string[]>(() =>
rollingLog.flatMap(entry => [
`=== Job ${entry.jobId.slice(-8)} [${new Date(entry.startTime).toLocaleTimeString()}] — ${entry.status.toUpperCase()} ===`,
...entry.lines,
])
);
const [autoScroll, setAutoScroll] = useState(true);

const consoleRef = useRef<HTMLPreElement>(null);
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const selectedJobRef = useRef('');

// Keep ref in sync
useEffect(() => { selectedJobRef.current = selectedJobId; }, [selectedJobId]);

const appendToDisplay = useCallback((newLines: string[]) => {
setDisplayLines(prev => [...prev, ...newLines]);
}, []);

const fetchJobList = useCallback(async () => {
if (!token) return;
try {
const res = await fetch('/api/generate/logs', {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
const jobList: JobSummary[] = data.jobs || [];
setJobs(jobList);

// For any newly discovered job, ensure it has an entry in rollingLog
for (const j of jobList) {
if (!rollingLog.find(e => e.jobId === j.jobId)) {
rollingLog.push({ jobId: j.jobId, startTime: j.startTime, status: j.status, lines: [] });
offsetByJob.set(j.jobId, 0);
} else {
// Update status
const entry = rollingLog.find(e => e.jobId === j.jobId)!;
entry.status = j.status;
}
}

// Auto-select the most recent job if none is selected
if (!selectedJobRef.current && jobList.length > 0) {
setSelectedJobId(jobList[0].jobId);
}
}
} catch { /* ignore */ }
}, [token]);

const fetchLogsForAllJobs = useCallback(async () => {
if (!token) return;

for (const entry of rollingLog) {
const currentOffset = offsetByJob.get(entry.jobId) ?? 0;
try {
const res = await fetch(`/api/generate/logs/${entry.jobId}?after=${currentOffset}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
if (data.lines && data.lines.length > 0) {
const isNew = currentOffset === 0 && entry.lines.length === 0;
const newLines: string[] = isNew
? [`=== Job ${entry.jobId.slice(-8)} [${new Date(entry.startTime).toLocaleTimeString()}] — ${entry.status.toUpperCase()} ===`, ...data.lines]
: data.lines;
entry.lines.push(...data.lines);
offsetByJob.set(entry.jobId, currentOffset + data.lines.length);
appendToDisplay(newLines);
}
}
} catch { /* ignore */ }
}
}, [token, appendToDisplay]);

// Poll loop
const schedulePoll = useCallback(() => {
if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
pollTimerRef.current = setTimeout(async () => {
await fetchJobList();
await fetchLogsForAllJobs();
schedulePoll();
}, POLL_INTERVAL_MS);
}, [fetchJobList, fetchLogsForAllJobs]);

useEffect(() => {
void fetchJobList().then(() => fetchLogsForAllJobs());
schedulePoll();
return () => {
if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
};
}, [fetchJobList, fetchLogsForAllJobs, schedulePoll]);

// Auto-scroll to bottom when new lines arrive
useEffect(() => {
if (autoScroll && consoleRef.current) {
consoleRef.current.scrollTop = consoleRef.current.scrollHeight;
}
}, [displayLines, autoScroll]);

// Scroll to the section for the selected job
useEffect(() => {
if (!selectedJobId || !consoleRef.current) return;
const pre = consoleRef.current;
// Walk through child spans to find the matching line
const spans = pre.querySelectorAll('span[data-job]');
for (const span of Array.from(spans)) {
if ((span as HTMLElement).dataset.job === selectedJobId) {
(span as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'start' });
break;
}
}
}, [selectedJobId]);

const handleScroll = () => {
if (!consoleRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = consoleRef.current;
const atBottom = scrollHeight - scrollTop - clientHeight < 40;
setAutoScroll(atBottom);
};

const handleClear = () => {
setDisplayLines([]);
// Clear the module-level log too so it doesn't resurface on remount
rollingLog.length = 0;
offsetByJob.clear();
};

const handleRefresh = async () => {
await fetchJobList();
await fetchLogsForAllJobs();
};

const scrollToBottom = () => {
if (consoleRef.current) {
consoleRef.current.scrollTop = consoleRef.current.scrollHeight;
}
setAutoScroll(true);
};

const formatTime = (ts: number) => new Date(ts).toLocaleTimeString();

const statusColor = (s: string) => {
if (s === 'succeeded') return 'text-green-400';
if (s === 'failed') return 'text-red-400';
if (s === 'running') return 'text-amber-400';
return 'text-zinc-400';
};

const colorize = (line: string) => {
if (/^=== .* ===$/.test(line)) return 'text-cyan-300 font-bold';
if (/^--- Running /.test(line) || /^\$ /.test(line)) return 'text-emerald-400 font-semibold';
if (/error|Error|failed|Failed|FAILED/i.test(line)) return 'text-red-400';
if (/warning|Warning/i.test(line)) return 'text-amber-400';
if (/^\[DiT\]/.test(line)) return 'text-sky-300';
if (/^\[VAE\]/.test(line)) return 'text-violet-300';
if (/^\[Phase1\]|\[Phase2\]|\[Decode\]/.test(line)) return 'text-pink-300';
if (/^\[stdout\]/.test(line)) return 'text-zinc-400';
return 'text-green-300';
};

// Determine which job each display line belongs to (for scroll-to-job)
const getJobIdForLine = (line: string): string | null => {
const m = line.match(/^=== Job ([0-9a-f]{8}) \[/);
if (!m) return null;
const suffix = m[1];
return jobs.find(j => j.jobId.endsWith(suffix))?.jobId ?? null;
};

return (
<div className="flex flex-col h-full bg-zinc-950 text-green-300 font-mono">
{/* Toolbar */}
<div className="flex items-center gap-3 px-4 py-2.5 bg-zinc-900 border-b border-zinc-800 flex-shrink-0">
<span className="text-xs font-bold text-zinc-300 uppercase tracking-widest">Debug Console</span>
<span className="text-[9px] text-zinc-500 italic">rolling log — all jobs</span>

{/* Job jump selector */}
<div className="relative flex-1 max-w-xs">
<select
value={selectedJobId}
onChange={(e) => setSelectedJobId(e.target.value)}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-emerald-500 appearance-none pr-6 cursor-pointer"
>
{jobs.length === 0 && <option value="">No jobs yet</option>}
{jobs.map(j => (
<option key={j.jobId} value={j.jobId}>
[{j.status.toUpperCase()}] {formatTime(j.startTime)} — {j.jobId.slice(-8)} ({j.logCount} lines)
</option>
))}
</select>
<ChevronDown size={12} className="absolute right-2 top-1/2 -translate-y-1/2 text-zinc-500 pointer-events-none" />
</div>

{/* Job status badge */}
{selectedJobId && jobs.find(j => j.jobId === selectedJobId) && (
<span className={`text-[10px] font-bold uppercase ${statusColor(jobs.find(j => j.jobId === selectedJobId)!.status)}`}>
● {jobs.find(j => j.jobId === selectedJobId)!.status}
</span>
)}

<div className="ml-auto flex items-center gap-2">
<span className="text-[9px] text-emerald-500 animate-pulse">● LIVE</span>
<button
onClick={handleRefresh}
title="Refresh now"
className="p-1.5 rounded hover:bg-zinc-700 text-zinc-400 hover:text-zinc-200 transition-colors"
>
<RefreshCw size={13} />
</button>
<button
onClick={handleClear}
title="Clear all logs"
className="p-1.5 rounded hover:bg-zinc-700 text-zinc-400 hover:text-zinc-200 transition-colors"
>
<Trash2 size={13} />
</button>
</div>
</div>

{/* Log output */}
<pre
ref={consoleRef}
onScroll={handleScroll}
className="flex-1 overflow-y-auto px-4 py-3 text-[11px] leading-[1.6] whitespace-pre-wrap break-all custom-scrollbar"
style={{ fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace" }}
>
{displayLines.length === 0 ? (
<span className="text-zinc-600">
{jobs.length === 0
? 'No generation jobs found. Start a generation to see debug output here.'
: 'Waiting for output…'}
</span>
) : (
displayLines.map((line, i) => {
const jobId = getJobIdForLine(line);
return (
<span
key={i}
data-job={jobId ?? undefined}
className={`block ${colorize(line)}`}
>
{line}
</span>
);
})
)}
</pre>

{/* Footer: scroll-to-bottom hint */}
{!autoScroll && displayLines.length > 0 && (
<button
onClick={scrollToBottom}
className="absolute bottom-16 right-6 flex items-center gap-1.5 bg-emerald-700 hover:bg-emerald-600 text-white text-[10px] font-medium px-3 py-1.5 rounded-full shadow-lg transition-colors"
>
<ChevronDown size={12} /> Scroll to bottom
</button>
)}
</div>
);
};
9 changes: 8 additions & 1 deletion components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Library, Disc, Search, LogIn, LogOut, Sun, Moon, Newspaper, Box } from 'lucide-react';
import { Library, Disc, Search, LogIn, LogOut, Sun, Moon, Newspaper, Box, Terminal } from 'lucide-react';
import { View } from '../types';
import { useI18n } from '../context/I18nContext';

Expand Down Expand Up @@ -119,6 +119,13 @@ export const Sidebar: React.FC<SidebarProps> = ({
onClick={() => onNavigate('models')}
isExpanded={isOpen}
/>
<NavItem
icon={<Terminal size={20} />}
label={t('debug')}
active={currentView === 'debug'}
onClick={() => onNavigate('debug')}
isExpanded={isOpen}
/>

<div className="mt-auto flex flex-col gap-2">
{/* Theme Toggle */}
Expand Down
Loading
Loading