Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 20 additions & 9 deletions server/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,22 @@ const __dirname = path.dirname(__filename);
// release: <bundle>/server/dist/config/ → ../../.. → <bundle>/
const APP_ROOT = path.resolve(__dirname, '../../..');

// ── Path helpers ─────────────────────────────────────────────────────────────

/**
* Resolves a path relative to APP_ROOT when it is not already absolute.
* This prevents relative paths from .env (e.g. `./models`) being interpreted
* relative to the server/ working directory instead of the project root.
*/
function resolveFromRoot(p: string): string {
return path.isAbsolute(p) ? p : path.resolve(APP_ROOT, p);
}

// ── Binary resolution ───────────────────────────────────────────────────────

/** Resolves the ace-qwen3 LLM binary path (step 1 of the pipeline). */
function resolveLmBin(): string {
if (process.env.ACE_QWEN3_BIN) return process.env.ACE_QWEN3_BIN;
if (process.env.ACE_QWEN3_BIN) return resolveFromRoot(process.env.ACE_QWEN3_BIN);
for (const name of ['ace-qwen3', 'ace-qwen3.exe']) {
const p = path.join(APP_ROOT, 'bin', name);
if (existsSync(p)) return p;
Expand All @@ -27,7 +38,7 @@ function resolveLmBin(): string {

/** Resolves the dit-vae binary path (step 2 of the pipeline). */
function resolveDitVaeBin(): string {
if (process.env.DIT_VAE_BIN) return process.env.DIT_VAE_BIN;
if (process.env.DIT_VAE_BIN) return resolveFromRoot(process.env.DIT_VAE_BIN);
for (const name of ['dit-vae', 'dit-vae.exe']) {
const p = path.join(APP_ROOT, 'bin', name);
if (existsSync(p)) return p;
Expand All @@ -39,13 +50,13 @@ function resolveDitVaeBin(): string {

/** Resolves the models directory. */
function resolveModelsDir(): string {
if (process.env.MODELS_DIR) return process.env.MODELS_DIR;
if (process.env.MODELS_DIR) return resolveFromRoot(process.env.MODELS_DIR);
return path.join(APP_ROOT, 'models');
}

/** Resolves the DiT model (acestep-v15-turbo-*.gguf). */
function resolveDitModel(modelsDir: string): string {
if (process.env.ACESTEP_MODEL) return process.env.ACESTEP_MODEL;
if (process.env.ACESTEP_MODEL) return resolveFromRoot(process.env.ACESTEP_MODEL);
if (!existsSync(modelsDir)) return '';

const preference = [
Expand Down Expand Up @@ -73,7 +84,7 @@ function resolveDitModel(modelsDir: string): string {

/** Resolves the causal LM model (acestep-5Hz-lm-*.gguf). */
function resolveLmModel(modelsDir: string): string {
if (process.env.LM_MODEL) return process.env.LM_MODEL;
if (process.env.LM_MODEL) return resolveFromRoot(process.env.LM_MODEL);
if (!existsSync(modelsDir)) return '';

// Prefer 4B Q8_0, then smaller quantisations, then smaller LM sizes
Expand Down Expand Up @@ -103,7 +114,7 @@ function resolveLmModel(modelsDir: string): string {

/** Resolves the text-encoder model (Qwen3-Embedding-*.gguf). */
function resolveTextEncoderModel(modelsDir: string): string {
if (process.env.TEXT_ENCODER_MODEL) return process.env.TEXT_ENCODER_MODEL;
if (process.env.TEXT_ENCODER_MODEL) return resolveFromRoot(process.env.TEXT_ENCODER_MODEL);
if (!existsSync(modelsDir)) return '';

for (const name of [
Expand All @@ -125,7 +136,7 @@ function resolveTextEncoderModel(modelsDir: string): string {

/** Resolves the VAE model (vae-BF16.gguf). */
function resolveVaeModel(modelsDir: string): string {
if (process.env.VAE_MODEL) return process.env.VAE_MODEL;
if (process.env.VAE_MODEL) return resolveFromRoot(process.env.VAE_MODEL);
if (!existsSync(modelsDir)) return '';

for (const name of ['vae-BF16.gguf', 'vae-Q8_0.gguf']) {
Expand Down Expand Up @@ -174,7 +185,7 @@ export const config = {

// SQLite database
database: {
path: process.env.DATABASE_PATH || path.join(APP_ROOT, 'data', 'acestep.db'),
path: resolveFromRoot(process.env.DATABASE_PATH || path.join(APP_ROOT, 'data', 'acestep.db')),
},

// acestep-cpp — spawn mode uses ace-qwen3 + dit-vae directly.
Expand Down Expand Up @@ -204,7 +215,7 @@ export const config = {

storage: {
provider: 'local' as const,
audioDir: process.env.AUDIO_DIR || path.join(APP_ROOT, 'public', 'audio'),
audioDir: resolveFromRoot(process.env.AUDIO_DIR || path.join(APP_ROOT, 'public', 'audio')),
},

jwt: {
Expand Down
30 changes: 24 additions & 6 deletions server/src/services/acestep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,20 @@ let isProcessingQueue = false;
// Mode detection
// ---------------------------------------------------------------------------

function useSpawnMode(): boolean {
// Spawn mode requires both ace-qwen3 (LLM) and dit-vae (synthesis) binaries
return Boolean(config.acestep.lmBin && config.acestep.ditVaeBin);
/**
* Returns true when the spawn-mode binaries satisfy the requirements for
* the given generation parameters.
*
* - Cover/passthrough (sourceAudioUrl or audioCodes): only dit-vae is needed;
* ace-qwen3 is skipped because the audio codes come from the source audio.
* - Text-to-music: both ace-qwen3 (LLM) and dit-vae (synthesis) are required.
*/
function useSpawnMode(params?: Pick<GenerationParams, 'sourceAudioUrl' | 'audioCodes'>): boolean {
if (!config.acestep.ditVaeBin) return false;
// Cover / passthrough: only dit-vae is needed — no LLM step
if (params?.sourceAudioUrl || params?.audioCodes) return true;
// Text-to-music: need ace-qwen3 too
return Boolean(config.acestep.lmBin);
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -259,14 +270,20 @@ async function runViaSpawn(
if (params.timeSignature) requestJson.timesignature = params.timeSignature;
// Passthrough: skip the LLM when audio codes are already provided
if (params.audioCodes) requestJson.audio_codes = params.audioCodes;
// Cover/audio-to-audio: strength of the source audio influence on the output
if (params.audioCoverStrength !== undefined) requestJson.audio_cover_strength = params.audioCoverStrength;

const requestPath = path.join(tmpDir, 'request.json');
await writeFile(requestPath, JSON.stringify(requestJson, null, 2));

// ── Step 1: ace-qwen3 — LLM (lyrics + audio codes) ────────────────────
// Skipped when:
// • audio_codes are provided (passthrough) — codes are already known
// • sourceAudioUrl is provided (cover/audio-to-audio) — dit-vae derives
// codes directly from the source audio; running ace-qwen3 is not needed
let enrichedPaths: string[] = [];

if (!params.audioCodes) {
if (!params.audioCodes && !params.sourceAudioUrl) {
job.stage = 'LLM: generating lyrics and audio codes…';

const lmBin = config.acestep.lmBin!;
Expand Down Expand Up @@ -294,7 +311,8 @@ async function runViaSpawn(
throw new Error('ace-qwen3 produced no enriched request files');
}
} else {
// Passthrough: use the original request.json directly (audio_codes present)
// Passthrough: use the original request.json directly
// (audio codes provided, or source audio supplied for cover/audio-to-audio mode)
enrichedPaths = [requestPath];
}

Expand Down Expand Up @@ -619,7 +637,7 @@ async function processGeneration(

try {
job.stage = 'Generating music...';
if (useSpawnMode()) {
if (useSpawnMode(params)) {
await runViaSpawn(jobId, params, job);
} else {
await runViaHttp(jobId, params, job);
Expand Down
Loading