diff --git a/.env.example b/.env.example index 584f217..3a455ca 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,7 @@ MODELS_DIR=./models # Override individual binary paths only if they live outside ./bin/: # ACE_QWEN3_BIN=/path/to/ace-qwen3 # DIT_VAE_BIN=/path/to/dit-vae +# ACE_UNDERSTAND_BIN=/path/to/ace-understand # # Override the primary DiT GGUF only if you want a non-default model: # ACESTEP_MODEL=/path/to/models/acestep-v15-turbo-Q8_0.gguf diff --git a/build.bat b/build.bat index 9467983..9bf7cef 100644 --- a/build.bat +++ b/build.bat @@ -118,7 +118,7 @@ if %ERRORLEVEL% NEQ 0 ( echo cmake build failed & exit /b 1 ) :: Copy binaries if not exist "%BIN_DIR%" mkdir "%BIN_DIR%" set COPIED=0 -for %%N in (ace-qwen3 dit-vae neural-codec) do ( +for %%N in (ace-qwen3 dit-vae ace-understand neural-codec) do ( set FOUND= for /r "%BUILD_DIR%" %%F in (%%N.exe) do ( if exist "%%F" if "!FOUND!"=="" set FOUND=%%F diff --git a/build.sh b/build.sh index 6edd2b2..fb46499 100755 --- a/build.sh +++ b/build.sh @@ -137,7 +137,7 @@ cmake --build "$BUILD_DIR" --parallel # ── Copy binaries to bin/ ───────────────────────────────────────────────────── mkdir -p "$BIN_DIR" copied=0 -for name in ace-qwen3 dit-vae neural-codec; do +for name in ace-qwen3 dit-vae ace-understand neural-codec; do found=$(find "$BUILD_DIR" -name "$name" -type f 2>/dev/null | head -1) if [ -n "$found" ]; then cp "$found" "$BIN_DIR/$name" diff --git a/components/CreatePanel.tsx b/components/CreatePanel.tsx index 925c4ae..380aa13 100644 --- a/components/CreatePanel.tsx +++ b/components/CreatePanel.tsx @@ -205,7 +205,7 @@ export const CreatePanel: React.FC = ({ const [lmCfgScale, setLmCfgScale] = useState(savedSettings.lmCfgScale ?? 2.2); const [lmTopK, setLmTopK] = useState(savedSettings.lmTopK ?? 0); const [lmTopP, setLmTopP] = useState(savedSettings.lmTopP ?? 0.92); - const [lmNegativePrompt, setLmNegativePrompt] = useState(savedSettings.lmNegativePrompt ?? 'NO USER INPUT'); + const [lmNegativePrompt, setLmNegativePrompt] = useState(savedSettings.lmNegativePrompt ?? ''); // Expert Parameters (now in Advanced section) // Note: audio URLs are NOT persisted — they may point to deleted/temporary files @@ -1033,7 +1033,12 @@ export const CreatePanel: React.FC = ({ setUnderstandResult(prev => ({ ...prev, [target]: null })); setUnderstandError(prev => ({ ...prev, [target]: null })); try { - const result = await generateApi.understandAudioUrl(audioUrl, token); + // Use ID-based endpoint for uploaded reference tracks so the result is + // persisted to the database alongside the track record. + const matchingTrack = referenceTracks.find(t => t.audio_url === audioUrl); + const result = matchingTrack + ? await generateApi.understandReferenceTrack(matchingTrack.id, token) + : await generateApi.understandAudioUrl(audioUrl, token); setUnderstandResult(prev => ({ ...prev, [target]: result })); setUnderstandStatus(prev => ({ ...prev, [target]: 'done' })); } catch (err) { @@ -1727,6 +1732,23 @@ export const CreatePanel: React.FC = ({ )} + {/* Reference audio cover-strength slider (shown when a reference audio is loaded) */} + {audioTab === 'reference' && referenceAudioUrl && ( +
+ + setAudioCoverStrength(Number(e.target.value))} + className="flex-1 h-1.5 accent-emerald-500" + /> + {audioCoverStrength.toFixed(2)} +
+ )} + {/* Source/Cover Audio Player */} {audioTab === 'source' && sourceAudioUrl && ( <> @@ -2007,6 +2029,15 @@ export const CreatePanel: React.FC = ({ {formatTime(sourceDuration)} + + {/* Understand result panel (lego source) */} + {understandStatus.source !== 'idle' && ( +
+ {understandStatus.source === 'running' && {t('understandRunning')}} + {understandStatus.source === 'error' && {t('understandError')}: {understandError.source}} + {understandStatus.source === 'done' && understandResult.source && ( + <> +
{t('understandResult')}
+ {understandResult.source.caption &&
🎵 {String(understandResult.source.caption).slice(0, 80)}{String(understandResult.source.caption).length > 80 ? '…' : ''}
} +
+ {understandResult.source.bpm && BPM: {String(understandResult.source.bpm)}} + {understandResult.source.keyscale && Key: {String(understandResult.source.keyscale)}} + {understandResult.source.duration && Duration: {Math.round(Number(understandResult.source.duration))}s} +
+ + + )} +
+ )} )} diff --git a/server/src/db/migrate.ts b/server/src/db/migrate.ts index 385ef34..d750d06 100644 --- a/server/src/db/migrate.ts +++ b/server/src/db/migrate.ts @@ -158,6 +158,32 @@ function migrate(): void { throw error; } } + + // Incremental column additions — ALTER TABLE in SQLite does not support + // IF NOT EXISTS, so we catch the error when the column already exists. + const columnMigrations: Array<{ sql: string; description: string }> = [ + { + sql: 'ALTER TABLE reference_tracks ADD COLUMN understand_metadata TEXT', + description: 'Add understand_metadata column to reference_tracks', + }, + ]; + + for (const { sql, description } of columnMigrations) { + try { + db.exec(sql); + console.log(`Migration applied: ${description}`); + } catch (err) { + const msg = String(err); + // better-sqlite3 reports duplicate columns as: + // "table X already has column Y" + if (msg.includes('already has column') || msg.includes('duplicate column name') || msg.includes('already exists')) { + // Column already present — nothing to do + } else { + console.error(`Migration failed: ${description}`, err); + throw err; + } + } + } } // Run migrations diff --git a/server/src/routes/referenceTrack.ts b/server/src/routes/referenceTrack.ts index 542bf5c..2a5d065 100644 --- a/server/src/routes/referenceTrack.ts +++ b/server/src/routes/referenceTrack.ts @@ -351,6 +351,19 @@ router.post('/:id/understand', understandRateLimiter, authMiddleware, async (req const audioUrl = `/audio/${result.rows[0].storage_key}`; const understood = await runUnderstand(audioUrl); + + // Persist the understand result alongside the track record so it survives + // page reloads and can be reused without re-running the (expensive) analysis. + // This is best-effort — a save failure doesn't invalidate the result. + try { + await pool.query( + 'UPDATE reference_tracks SET understand_metadata = $1 WHERE id = $2', + [JSON.stringify(understood), req.params.id] + ); + } catch (saveErr) { + console.warn('Understand metadata save failed (non-fatal):', saveErr); + } + res.json(understood); } catch (error) { const msg = error instanceof Error ? error.message : 'Failed to understand audio'; diff --git a/server/src/services/acestep.ts b/server/src/services/acestep.ts index d62a2b9..1198865 100644 --- a/server/src/services/acestep.ts +++ b/server/src/services/acestep.ts @@ -617,6 +617,15 @@ async function runViaSpawn( requestJson.lm_top_k = params.lmTopK ?? 0; requestJson.lm_negative_prompt = params.lmNegativePrompt || ''; requestJson.use_cot_caption = params.useCotCaption ?? true; + + // Reference audio for style-guided text-to-music generation. + // When the user selects a reference track, pass it to dit-vae via the + // request JSON so the binary can condition the synthesis on that audio. + if (params.referenceAudioUrl) { + const refAudioPath = resolveAudioPath(params.referenceAudioUrl); + requestJson.reference_audio = refAudioPath; + requestJson.audio_cover_strength = params.audioCoverStrength ?? 1.0; + } } const requestPath = path.join(tmpDir, 'request.json');