diff --git a/.env.example b/.env.example index e76874a..f24a9ec 100644 --- a/.env.example +++ b/.env.example @@ -39,7 +39,11 @@ MODELS_DIR=./models # ACESTEP_CPP_BRANCH=main # ── Storage ─────────────────────────────────────────────────────────────────── -AUDIO_DIR=./public/audio +# Audio directory for generated songs and uploaded reference tracks. +# Relative paths are resolved from the project root (APP_ROOT). +# This is the single source of truth: LocalStorageProvider (writes), +# Express /audio/ endpoint (serves), and the spawn service (reads) all use it. +AUDIO_DIR=./server/public/audio # ── Auth ────────────────────────────────────────────────────────────────────── # Change this to a long random string in any multi-user or network-exposed setup. diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index cfd75ea..0000000 --- a/backend/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# backend/ — design note - -## Why there is no custom C++ HTTP server here - -An earlier design wrapped `acestep-generate` in a second C++ HTTP server process. -That was removed because it added unnecessary complexity: - -| Problem | Impact | -|---------|--------| -| Two processes to manage (Node.js + C++ server) | harder to deploy, restart, monitor | -| C++ server used `popen()` with shell-built strings | fragile, platform-specific, injection surface | -| LoRA state split across two processes | race conditions, stale cache | -| Extra HTTP hop for every generation request | added latency and error surface | -| Users need to build *two* C++ projects | poor DX | - -## Current architecture - -``` -Browser - │ - │ HTTP - ▼ -Node.js Express (port 3001) - │ handles: auth, songs DB, playlists, audio storage, job queue - │ - │ child_process.spawn(bin, args, { shell: false }) - ▼ -acestep-generate ←── GGUF model on GPU/CPU - │ - └─► writes audio files → ./public/audio/ -``` - -The Node.js server reads `ACESTEP_BIN` from `.env` and spawns `acestep-generate` -directly — the same pattern used by llama.cpp, whisper.cpp, and similar tools. -No shell is involved, so there is no injection risk. - -## When a separate HTTP server *would* make sense - -If `acestep.cpp` ever ships a **built-in** HTTP server mode (like `llama-server`), -you can point `ACESTEP_API_URL` at it and leave `ACESTEP_BIN` empty. -The Node.js service already has an HTTP-client fallback for exactly this case. - -See `server/src/services/acestep.ts` for the dual-mode implementation. diff --git a/server/src/config/index.ts b/server/src/config/index.ts index 14269e8..7abb41c 100644 --- a/server/src/config/index.ts +++ b/server/src/config/index.ts @@ -224,10 +224,11 @@ export const config = { storage: { provider: 'local' as const, - // Audio directory must match where LocalStorageProvider writes files and - // where Express serves /audio/ from (server/src/index.ts: '../public/audio'). - // Both resolve to /public/audio, so we use SERVER_ROOT here. - // AUDIO_DIR env override is still supported (resolved against APP_ROOT). + // Single source of truth for the audio directory. + // LocalStorageProvider, Express (/audio/), and the spawn service all read + // this value so they always point at the same filesystem location. + // Default: /public/audio (SERVER_ROOT = server/). + // Override via AUDIO_DIR in .env (relative paths are resolved from APP_ROOT). audioDir: resolveFromRoot(process.env.AUDIO_DIR || path.join(SERVER_ROOT, 'public', 'audio')), }, diff --git a/server/src/index.ts b/server/src/index.ts index f6bdbfb..e336f78 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -79,8 +79,9 @@ app.use(cors({ app.use(express.json()); -// Serve static audio files -app.use('/audio', express.static(path.join(__dirname, '../public/audio'))); +// Serve static audio files from the configured audio directory so that any +// AUDIO_DIR env override is honoured consistently across upload, spawn, and serving. +app.use('/audio', express.static(config.storage.audioDir)); // Audio Editor (AudioMass) - needs relaxed CSP for inline scripts and external images app.use('/editor', (req, res, next) => { diff --git a/server/src/services/acestep.ts b/server/src/services/acestep.ts index 53663a9..1e40d8e 100644 --- a/server/src/services/acestep.ts +++ b/server/src/services/acestep.ts @@ -640,7 +640,8 @@ async function runViaSpawn( const batchSize = Math.min(Math.max(params.batchSize ?? 1, 1), 8); if (batchSize > 1) ditArgs.push('--batch', String(batchSize)); - // Cover and repaint modes both require a source audio file + // Cover and repaint modes both require a source audio file. + // dit-vae reads WAV or MP3 natively (via dr_wav / dr_mp3 in audio.h). if (params.sourceAudioUrl) { const srcAudioPath = resolveAudioPath(params.sourceAudioUrl); ditArgs.push('--src-audio', srcAudioPath); diff --git a/server/src/services/storage/local.ts b/server/src/services/storage/local.ts index 903582c..94f5979 100644 --- a/server/src/services/storage/local.ts +++ b/server/src/services/storage/local.ts @@ -1,17 +1,16 @@ import { writeFile, unlink, stat, mkdir, copyFile } from 'fs/promises'; import path from 'path'; -import { fileURLToPath } from 'url'; import type { StorageProvider } from './index.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const AUDIO_DIR = path.join(__dirname, '../../../public/audio'); +import { config } from '../../config/index.js'; export class LocalStorageProvider implements StorageProvider { private audioDir: string; constructor() { - this.audioDir = AUDIO_DIR; + // Derive the audio directory from the central config so that the storage + // provider always writes to the same location the spawn service resolves + // paths from (config.storage.audioDir, which honours the AUDIO_DIR env var). + this.audioDir = config.storage.audioDir; } async upload(key: string, data: Buffer, _contentType: string): Promise {