Merge PR #24: Various UI improvements from riversedge
Includes progress bar, drag and drop, gender buttons, upload improvements, and dynamic duration limits.
This commit is contained in:
@@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import torch
|
||||
|
||||
# Get ACE-Step path from environment or use default
|
||||
ACESTEP_PATH = os.environ.get('ACESTEP_PATH', '/home/ambsd/Desktop/aceui/ACE-Step-1.5')
|
||||
@@ -27,12 +28,18 @@ def get_llm_handler():
|
||||
# Initialize the LLM with the 0.6B model (lighter on VRAM)
|
||||
checkpoint_dir = os.path.join(ACESTEP_PATH, "checkpoints")
|
||||
lm_model_path = "acestep-5Hz-lm-0.6B" # Use the smaller 0.6B model
|
||||
if torch.cuda.is_available():
|
||||
device = "cuda"
|
||||
elif torch.backends.mps.is_available():
|
||||
device = "mps"
|
||||
else:
|
||||
device = "cpu"
|
||||
|
||||
status, success = _llm_handler.initialize(
|
||||
checkpoint_dir=checkpoint_dir,
|
||||
lm_model_path=lm_model_path,
|
||||
backend="pt", # Use PyTorch backend
|
||||
device="cuda",
|
||||
device=device,
|
||||
offload_to_cpu=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
ACESTEP_PATH = os.environ.get('ACESTEP_PATH', '/home/ambsd/Desktop/aceui/ACE-Step-1.5')
|
||||
sys.path.insert(0, ACESTEP_PATH)
|
||||
|
||||
from acestep.gpu_config import get_gpu_config
|
||||
|
||||
|
||||
def main():
|
||||
cfg = get_gpu_config()
|
||||
print(json.dumps({
|
||||
"tier": cfg.tier,
|
||||
"gpu_memory_gb": cfg.gpu_memory_gb,
|
||||
"max_duration_with_lm": cfg.max_duration_with_lm,
|
||||
"max_duration_without_lm": cfg.max_duration_without_lm,
|
||||
"max_batch_size_with_lm": cfg.max_batch_size_with_lm,
|
||||
"max_batch_size_without_lm": cfg.max_batch_size_without_lm,
|
||||
}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import torch
|
||||
|
||||
# Get ACE-Step path from environment or use default
|
||||
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():
|
||||
global _handler, _llm_handler
|
||||
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.initialize_service(
|
||||
project_root=ACESTEP_PATH,
|
||||
config_path="acestep-v15-turbo",
|
||||
device="cuda",
|
||||
device=device,
|
||||
offload_to_cpu=True, # For 12GB GPU
|
||||
)
|
||||
_llm_handler = LLMHandler() # Create but don't initialize (not enough VRAM)
|
||||
|
||||
@@ -34,13 +34,15 @@ const audioUpload = multer({
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
'audio/mp4',
|
||||
'audio/x-m4a',
|
||||
'audio/aac',
|
||||
'audio/ogg',
|
||||
'audio/webm',
|
||||
'video/mp4',
|
||||
];
|
||||
|
||||
// Also check file extension as fallback
|
||||
const allowedExtensions = ['.mp3', '.wav', '.flac', '.m4a', '.aac', '.ogg', '.webm', '.opus'];
|
||||
const allowedExtensions = ['.mp3', '.wav', '.flac', '.m4a', '.mp4', '.aac', '.ogg', '.webm', '.opus'];
|
||||
const fileExt = file.originalname.toLowerCase().match(/\.[^.]+$/)?.[0];
|
||||
|
||||
if (allowedTypes.includes(file.mimetype) || (fileExt && allowedExtensions.includes(fileExt))) {
|
||||
@@ -95,6 +97,8 @@ interface GenerateBody {
|
||||
// Expert Parameters
|
||||
referenceAudioUrl?: string;
|
||||
sourceAudioUrl?: string;
|
||||
referenceAudioTitle?: string;
|
||||
sourceAudioTitle?: string;
|
||||
audioCodes?: string;
|
||||
repaintingStart?: number;
|
||||
repaintingEnd?: number;
|
||||
@@ -142,10 +146,13 @@ router.post('/upload-audio', authMiddleware, audioUpload.single('audio'), async
|
||||
case 'audio/ogg':
|
||||
return '.ogg';
|
||||
case 'audio/mp4':
|
||||
case 'audio/x-m4a':
|
||||
case 'audio/aac':
|
||||
return '.m4a';
|
||||
case 'audio/webm':
|
||||
return '.webm';
|
||||
case 'video/mp4':
|
||||
return '.mp4';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -193,6 +200,8 @@ router.post('/', authMiddleware, async (req: AuthenticatedRequest, res: Response
|
||||
lmBackend,
|
||||
referenceAudioUrl,
|
||||
sourceAudioUrl,
|
||||
referenceAudioTitle,
|
||||
sourceAudioTitle,
|
||||
audioCodes,
|
||||
repaintingStart,
|
||||
repaintingEnd,
|
||||
@@ -257,6 +266,8 @@ router.post('/', authMiddleware, async (req: AuthenticatedRequest, res: Response
|
||||
lmBackend,
|
||||
referenceAudioUrl,
|
||||
sourceAudioUrl,
|
||||
referenceAudioTitle,
|
||||
sourceAudioTitle,
|
||||
audioCodes,
|
||||
repaintingStart,
|
||||
repaintingEnd,
|
||||
@@ -373,7 +384,8 @@ router.get('/status/:jobId', authMiddleware, async (req: AuthenticatedRequest, r
|
||||
const { buffer } = await downloadAudioToBuffer(audioUrl);
|
||||
const ext = audioUrl.includes('.flac') ? '.flac' : '.mp3';
|
||||
const storageKey = `${req.user!.id}/${songId}${ext}`;
|
||||
const storedPath = await storage.upload(storageKey, buffer, `audio/${ext.slice(1)}`);
|
||||
await storage.upload(storageKey, buffer, `audio/${ext.slice(1)}`);
|
||||
const storedPath = storage.getPublicUrl(storageKey);
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO songs (id, user_id, title, lyrics, style, caption, audio_url,
|
||||
@@ -436,6 +448,8 @@ router.get('/status/:jobId', authMiddleware, async (req: AuthenticatedRequest, r
|
||||
status: aceStatus.status,
|
||||
queuePosition: aceStatus.queuePosition,
|
||||
etaSeconds: aceStatus.etaSeconds,
|
||||
progress: aceStatus.progress,
|
||||
stage: aceStatus.stage,
|
||||
result: aceStatus.result,
|
||||
error: aceStatus.error,
|
||||
});
|
||||
@@ -449,6 +463,8 @@ router.get('/status/:jobId', authMiddleware, async (req: AuthenticatedRequest, r
|
||||
res.json({
|
||||
jobId: req.params.jobId,
|
||||
status: job.status,
|
||||
progress: undefined,
|
||||
stage: undefined,
|
||||
result: job.result && typeof job.result === 'string' ? JSON.parse(job.result) : job.result,
|
||||
error: job.error,
|
||||
});
|
||||
@@ -544,6 +560,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) => {
|
||||
try {
|
||||
const rawResponse = getJobRawResponse(req.params.taskId);
|
||||
@@ -596,7 +666,6 @@ router.post('/format', authMiddleware, async (req: AuthenticatedRequest, res: Re
|
||||
cwd: ACESTEP_DIR,
|
||||
env: {
|
||||
...process.env,
|
||||
CUDA_VISIBLE_DEVICES: '0',
|
||||
ACESTEP_PATH: ACESTEP_DIR,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,25 +1,130 @@
|
||||
import { Router, Response } from 'express';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { promises as fs } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { pool } from '../db/pool.js';
|
||||
import { authMiddleware, AuthenticatedRequest } from '../middleware/auth.js';
|
||||
import { getStorageProvider } from '../services/storage/factory.js';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const router = Router();
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const AUDIO_DIR = path.join(__dirname, '../../public/audio');
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB max
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/flac', 'audio/mp3', 'audio/x-wav', 'audio/x-flac'];
|
||||
if (allowedTypes.includes(file.mimetype) || file.originalname.match(/\.(mp3|wav|flac)$/i)) {
|
||||
const allowedTypes = [
|
||||
'audio/mpeg',
|
||||
'audio/wav',
|
||||
'audio/flac',
|
||||
'audio/mp3',
|
||||
'audio/x-wav',
|
||||
'audio/x-flac',
|
||||
'audio/mp4',
|
||||
'audio/x-m4a',
|
||||
'audio/aac',
|
||||
'video/mp4',
|
||||
];
|
||||
if (allowedTypes.includes(file.mimetype) || file.originalname.match(/\.(mp3|wav|flac|m4a|mp4)$/i)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type. Only MP3, WAV, and FLAC are allowed.'));
|
||||
cb(new Error('Invalid file type. Only MP3, WAV, FLAC, M4A, and MP4 are allowed.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const findWhisperExecutable = async (): Promise<string | null> => {
|
||||
if (process.env.WHISPER_CMD) return process.env.WHISPER_CMD;
|
||||
const customPath = process.env.WHISPER_PATH;
|
||||
if (customPath) {
|
||||
const candidate = path.join(customPath, 'whisper');
|
||||
try {
|
||||
await fs.access(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const pathEntries = (process.env.PATH || '').split(path.delimiter);
|
||||
for (const entry of pathEntries) {
|
||||
const candidate = path.join(entry, 'whisper');
|
||||
try {
|
||||
await fs.access(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const transcribeWithWhisper = async (buffer: Buffer, originalFilename: string, signal?: AbortSignal): Promise<string | null> => {
|
||||
const whisperCmd = await findWhisperExecutable();
|
||||
if (!whisperCmd) return null;
|
||||
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'whisper-'));
|
||||
const ext = path.extname(originalFilename) || '.mp3';
|
||||
const inputPath = path.join(tempDir, `input${ext}`);
|
||||
const outputDir = path.join(tempDir, 'out');
|
||||
|
||||
try {
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
await fs.writeFile(inputPath, buffer);
|
||||
|
||||
const args = [
|
||||
inputPath,
|
||||
'--model', 'base',
|
||||
'--output_format', 'txt',
|
||||
'--output_dir', outputDir,
|
||||
'--fp16', 'False'
|
||||
];
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(whisperCmd, args, { stdio: 'ignore' });
|
||||
const handleAbort = () => {
|
||||
proc.kill('SIGTERM');
|
||||
reject(new Error('Transcription cancelled'));
|
||||
};
|
||||
if (signal) {
|
||||
if (signal.aborted) {
|
||||
handleAbort();
|
||||
return;
|
||||
}
|
||||
signal.addEventListener('abort', handleAbort, { once: true });
|
||||
}
|
||||
proc.on('error', reject);
|
||||
proc.on('close', (code) => {
|
||||
if (signal) {
|
||||
signal.removeEventListener('abort', handleAbort);
|
||||
}
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`Whisper exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
|
||||
const files = await fs.readdir(outputDir);
|
||||
const txtFile = files.find((file) => file.endsWith('.txt'));
|
||||
if (!txtFile) return null;
|
||||
const text = await fs.readFile(path.join(outputDir, txtFile), 'utf8');
|
||||
return text.trim() || null;
|
||||
} catch (error) {
|
||||
console.warn('Whisper transcription failed:', error);
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get user's reference tracks
|
||||
router.get('/', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
@@ -61,6 +166,7 @@ router.post('/', authMiddleware, upload.single('audio'), async (req: Authenticat
|
||||
const storage = getStorageProvider();
|
||||
await storage.upload(key, req.file.buffer, req.file.mimetype);
|
||||
const audioUrl = storage.getPublicUrl(key);
|
||||
const whisperAvailable = Boolean(await findWhisperExecutable());
|
||||
|
||||
// Parse tags from request body if provided
|
||||
const tags = req.body.tags ? JSON.parse(req.body.tags) : null;
|
||||
@@ -76,7 +182,8 @@ router.post('/', authMiddleware, upload.single('audio'), async (req: Authenticat
|
||||
track: {
|
||||
...result.rows[0],
|
||||
audio_url: audioUrl
|
||||
}
|
||||
},
|
||||
whisper_available: whisperAvailable
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload reference track error:', error);
|
||||
@@ -142,6 +249,44 @@ router.patch('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Resp
|
||||
}
|
||||
});
|
||||
|
||||
// Transcribe a reference track with whisper (if available)
|
||||
router.post('/:id/transcribe', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const whisperCmd = await findWhisperExecutable();
|
||||
if (!whisperCmd) {
|
||||
res.status(404).json({ error: 'Whisper not available' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT user_id, filename, storage_key FROM reference_tracks WHERE id = $1',
|
||||
[req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
res.status(404).json({ error: 'Track not found' });
|
||||
return;
|
||||
}
|
||||
if (result.rows[0].user_id !== req.user!.id) {
|
||||
res.status(403).json({ error: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
const audioPath = path.join(AUDIO_DIR, result.rows[0].storage_key);
|
||||
const buffer = await fs.readFile(audioPath);
|
||||
const controller = new AbortController();
|
||||
|
||||
req.on('close', () => controller.abort());
|
||||
|
||||
const lyrics = await transcribeWithWhisper(buffer, result.rows[0].filename, controller.signal);
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
res.json({ lyrics: lyrics || '' });
|
||||
} catch (error) {
|
||||
console.error('Transcribe reference track error:', error);
|
||||
res.status(500).json({ error: 'Failed to transcribe' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a reference track
|
||||
router.delete('/:id', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
|
||||
@@ -107,7 +107,7 @@ router.get('/', authMiddleware, async (req: AuthenticatedRequest, res: Response)
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public,
|
||||
s.like_count, s.view_count, s.user_id, s.created_at,
|
||||
s.like_count, s.view_count, s.user_id, s.created_at, s.generation_params,
|
||||
COALESCE(u.username, 'Anonymous') as creator
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
@@ -137,7 +137,7 @@ router.get('/public/featured', optionalAuthMiddleware, async (_req: Authenticate
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.like_count, s.view_count, s.created_at, s.user_id,
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar, s.generation_params
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
ORDER BY RANDOM()
|
||||
@@ -184,7 +184,7 @@ router.get('/public', optionalAuthMiddleware, async (req: AuthenticatedRequest,
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.like_count, s.created_at,
|
||||
COALESCE(u.username, 'Anonymous') as creator
|
||||
COALESCE(u.username, 'Anonymous') as creator, s.generation_params
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
WHERE s.is_public = true
|
||||
@@ -213,7 +213,7 @@ router.get('/:id', optionalAuthMiddleware, async (req: AuthenticatedRequest, res
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.user_id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public, s.like_count, s.view_count, s.created_at,
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar, s.generation_params
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
WHERE s.id = $1`,
|
||||
@@ -252,7 +252,7 @@ router.get('/:id/full', optionalAuthMiddleware, async (req: AuthenticatedRequest
|
||||
pool.query(
|
||||
`SELECT s.id, s.user_id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public,
|
||||
s.like_count, s.view_count, s.created_at,
|
||||
s.like_count, s.view_count, s.created_at, s.generation_params,
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
@@ -496,7 +496,7 @@ router.get('/liked/list', authMiddleware, async (req: AuthenticatedRequest, res:
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.title, s.lyrics, s.style, s.cover_url, s.audio_url,
|
||||
s.duration, s.tags, s.like_count, s.created_at, s.is_public,
|
||||
COALESCE(u.username, 'Anonymous') as creator
|
||||
COALESCE(u.username, 'Anonymous') as creator, s.generation_params
|
||||
FROM liked_songs ls
|
||||
JOIN songs s ON ls.song_id = s.id
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
|
||||
+108
-14
@@ -125,7 +125,6 @@ async function submitToApi(params: GenerationParams): Promise<{ taskId: string }
|
||||
const body: Record<string, unknown> = {
|
||||
prompt,
|
||||
lyrics,
|
||||
audio_duration: params.duration ?? 60,
|
||||
batch_size: params.batchSize ?? 1,
|
||||
inference_steps: params.inferenceSteps ?? 8,
|
||||
guidance_scale: params.guidanceScale ?? 10.0,
|
||||
@@ -140,6 +139,7 @@ async function submitToApi(params: GenerationParams): Promise<{ taskId: string }
|
||||
lm_backend: params.lmBackend || 'pt',
|
||||
};
|
||||
|
||||
if (params.duration && params.duration > 0) body.audio_duration = params.duration;
|
||||
if (params.bpm && params.bpm > 0) body.bpm = params.bpm;
|
||||
if (params.keyScale) body.key_scale = params.keyScale;
|
||||
if (params.timeSignature) body.time_signature = params.timeSignature;
|
||||
@@ -172,20 +172,42 @@ async function submitToApi(params: GenerationParams): Promise<{ taskId: string }
|
||||
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;
|
||||
|
||||
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
|
||||
if (params.referenceAudioUrl) {
|
||||
let refAudioPath = params.referenceAudioUrl;
|
||||
if (refAudioPath.startsWith('/audio/')) {
|
||||
refAudioPath = path.join(AUDIO_DIR, refAudioPath.replace('/audio/', ''));
|
||||
}
|
||||
body.reference_audio_path = refAudioPath;
|
||||
body.reference_audio_path = resolveAudioPath(params.referenceAudioUrl);
|
||||
}
|
||||
if (params.sourceAudioUrl) {
|
||||
let srcAudioPath = params.sourceAudioUrl;
|
||||
if (srcAudioPath.startsWith('/audio/')) {
|
||||
srcAudioPath = path.join(AUDIO_DIR, srcAudioPath.replace('/audio/', ''));
|
||||
}
|
||||
body.src_audio_path = srcAudioPath;
|
||||
body.src_audio_path = resolveAudioPath(params.sourceAudioUrl);
|
||||
}
|
||||
|
||||
if (params.taskType === 'cover' || params.taskType === 'audio2audio') {
|
||||
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`, {
|
||||
@@ -261,7 +283,26 @@ async function pollApiResult(taskId: string, maxWaitMs = 600000): Promise<ApiTas
|
||||
|
||||
return { status: 1, audioPaths, metas };
|
||||
} else if (taskData.status === 2) {
|
||||
throw new Error('Generation failed on API side');
|
||||
const details = taskData.error
|
||||
|| taskData.message
|
||||
|| taskData.status_message
|
||||
|| taskData.result
|
||||
|| JSON.stringify(taskData);
|
||||
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
|
||||
@@ -351,6 +392,8 @@ export interface GenerationParams {
|
||||
// Expert Parameters
|
||||
referenceAudioUrl?: string;
|
||||
sourceAudioUrl?: string;
|
||||
referenceAudioTitle?: string;
|
||||
sourceAudioTitle?: string;
|
||||
audioCodes?: string;
|
||||
repaintingStart?: number;
|
||||
repaintingEnd?: number;
|
||||
@@ -389,6 +432,8 @@ interface JobStatus {
|
||||
status: 'queued' | 'running' | 'succeeded' | 'failed';
|
||||
queuePosition?: number;
|
||||
etaSeconds?: number;
|
||||
progress?: number;
|
||||
stage?: string;
|
||||
result?: GenerationResult;
|
||||
error?: string;
|
||||
}
|
||||
@@ -403,6 +448,8 @@ interface ActiveJob {
|
||||
processPromise?: Promise<void>;
|
||||
rawResponse?: unknown;
|
||||
queuePosition?: number;
|
||||
progress?: number;
|
||||
stage?: string;
|
||||
}
|
||||
|
||||
const activeJobs = new Map<string, ActiveJob>();
|
||||
@@ -466,6 +513,8 @@ async function processQueue(): Promise<void> {
|
||||
|
||||
// Submit generation job to queue
|
||||
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 job: ActiveJob = {
|
||||
@@ -509,6 +558,7 @@ async function processGeneration(
|
||||
try {
|
||||
// Submit to API
|
||||
const { taskId } = await submitToApi(params);
|
||||
job.taskId = taskId;
|
||||
console.log(`Job ${jobId}: Submitted to API as task ${taskId}`);
|
||||
|
||||
// Poll for result
|
||||
@@ -572,9 +622,10 @@ async function processGeneration(
|
||||
const jobOutputDir = path.join(ACESTEP_DIR, 'output', jobId);
|
||||
await mkdir(jobOutputDir, { recursive: true });
|
||||
|
||||
const durationToSend = params.duration && params.duration > 0 ? params.duration : 60;
|
||||
const args = [
|
||||
'--prompt', prompt,
|
||||
'--duration', String(params.duration ?? 60),
|
||||
'--duration', String(durationToSend),
|
||||
'--batch-size', String(params.batchSize ?? 1),
|
||||
'--infer-steps', String(params.inferenceSteps ?? 8),
|
||||
'--guidance-scale', String(params.guidanceScale ?? 10.0),
|
||||
@@ -706,7 +757,6 @@ function runPythonGeneration(scriptArgs: string[]): Promise<PythonResult> {
|
||||
cwd: ACESTEP_DIR,
|
||||
env: {
|
||||
...process.env,
|
||||
CUDA_VISIBLE_DEVICES: '0',
|
||||
ACESTEP_PATH: ACESTEP_DIR,
|
||||
},
|
||||
});
|
||||
@@ -845,9 +895,53 @@ export async function getJobStatus(jobId: string): Promise<JobStatus> {
|
||||
};
|
||||
}
|
||||
|
||||
if (job.status === 'running' && job.taskId) {
|
||||
try {
|
||||
const response = await fetch(`${ACESTEP_API}/query_result`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ task_id_list: [job.taskId] }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const taskData = result.data?.[0];
|
||||
if (taskData?.result) {
|
||||
let resultData: unknown = taskData.result;
|
||||
if (typeof resultData === 'string') {
|
||||
try {
|
||||
resultData = JSON.parse(resultData);
|
||||
} catch {
|
||||
resultData = null;
|
||||
}
|
||||
}
|
||||
|
||||
const item = Array.isArray(resultData) ? resultData[0] : resultData;
|
||||
if (item && typeof item === 'object') {
|
||||
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;
|
||||
if (progress !== undefined) job.progress = progress;
|
||||
if (stage) job.stage = stage;
|
||||
return {
|
||||
status: job.status,
|
||||
etaSeconds: Math.max(0, 180 - elapsed),
|
||||
progress: progress ?? job.progress,
|
||||
stage: stage ?? job.stage,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore progress fetch failures, fall back to ETA only
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: job.status,
|
||||
etaSeconds: Math.max(0, 180 - elapsed), // 3 min estimate
|
||||
progress: job.progress,
|
||||
stage: job.stage,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export class LocalStorageProvider implements StorageProvider {
|
||||
const filepath = path.join(this.audioDir, key);
|
||||
await mkdir(path.dirname(filepath), { recursive: true });
|
||||
await writeFile(filepath, data);
|
||||
return `/audio/${key}`;
|
||||
return key;
|
||||
}
|
||||
|
||||
async getUrl(key: string, _expiresIn?: number): Promise<string> {
|
||||
@@ -26,6 +26,9 @@ export class LocalStorageProvider implements StorageProvider {
|
||||
}
|
||||
|
||||
getPublicUrl(key: string): string {
|
||||
if (key.startsWith('/audio/')) {
|
||||
return key;
|
||||
}
|
||||
return `/audio/${key}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user