feat: add full 4-stem demucs mode with MIDI instrument replacement
- Support mode='full' in handle_demucs_separating config - Full mode: 4-stem separation → basic-pitch MIDI → fluidsynth render → mix - Each instrument gets correct GM instrument (drums=channel10, bass=34, other=1) - 2-stem mode preserved as default for backward compatibility - Uses run_full.py standalone script on GPU server via SSH
This commit is contained in:
parent
0bc74fdd1a
commit
ffdba55fd7
@ -156,9 +156,15 @@ async def handle_video_preparing(tenant_id, task_id, step_name, input_data, conf
|
|||||||
# ─── Demucs Separation ───────────────────────────────────────────────
|
# ─── Demucs Separation ───────────────────────────────────────────────
|
||||||
|
|
||||||
async def handle_demucs_separating(tenant_id, task_id, step_name, input_data, config):
|
async def handle_demucs_separating(tenant_id, task_id, step_name, input_data, config):
|
||||||
"""Run Demucs on GPU server to separate vocals and accompaniment."""
|
"""Run Demucs on GPU server to separate vocals and accompaniment.
|
||||||
|
|
||||||
|
Supports two modes via config.mode:
|
||||||
|
- "2stem" (default): classic vocals + no_vocals
|
||||||
|
- "full": 4-stem → MIDI replace instruments → mix accompaniment
|
||||||
|
"""
|
||||||
work_dir = _task_dir(task_id)
|
work_dir = _task_dir(task_id)
|
||||||
gpu_dir = _gpu_task_dir(task_id)
|
gpu_dir = _gpu_task_dir(task_id)
|
||||||
|
mode = config.get("mode", "2stem")
|
||||||
|
|
||||||
# Find audio path from deps
|
# Find audio path from deps
|
||||||
audio_path = None
|
audio_path = None
|
||||||
@ -178,7 +184,18 @@ async def handle_demucs_separating(tenant_id, task_id, step_name, input_data, co
|
|||||||
remote_audio = f"{gpu_dir}/audio.mp3"
|
remote_audio = f"{gpu_dir}/audio.mp3"
|
||||||
await _copy_to_gpu(audio_path, remote_audio)
|
await _copy_to_gpu(audio_path, remote_audio)
|
||||||
|
|
||||||
# Run Demucs on GPU
|
if mode == "full":
|
||||||
|
# ── 4-stem + MIDI replace pipeline ──────────────────────────
|
||||||
|
result = await _demucs_full_pipeline(task_id, remote_audio, gpu_dir, work_dir)
|
||||||
|
else:
|
||||||
|
# ── Classic 2-stem pipeline ─────────────────────────────────
|
||||||
|
result = await _demucs_2stem_pipeline(remote_audio, gpu_dir, work_dir)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def _demucs_2stem_pipeline(remote_audio, gpu_dir, work_dir):
|
||||||
|
"""Classic 2-stem vocal separation."""
|
||||||
demucs_cmd = (
|
demucs_cmd = (
|
||||||
f"cd {gpu_dir} && "
|
f"cd {gpu_dir} && "
|
||||||
f"source {GPU_DEMUCS_VENV}/bin/activate && "
|
f"source {GPU_DEMUCS_VENV}/bin/activate && "
|
||||||
@ -189,7 +206,6 @@ async def handle_demucs_separating(tenant_id, task_id, step_name, input_data, co
|
|||||||
if rc != 0:
|
if rc != 0:
|
||||||
raise ValueError(f"Demucs 分离失败: {stderr}")
|
raise ValueError(f"Demucs 分离失败: {stderr}")
|
||||||
|
|
||||||
# Copy results back
|
|
||||||
vocals_local = os.path.join(work_dir, "vocals.wav")
|
vocals_local = os.path.join(work_dir, "vocals.wav")
|
||||||
no_vocals_local = os.path.join(work_dir, "no_vocals.wav")
|
no_vocals_local = os.path.join(work_dir, "no_vocals.wav")
|
||||||
base = os.path.splitext(os.path.basename(remote_audio))[0]
|
base = os.path.splitext(os.path.basename(remote_audio))[0]
|
||||||
@ -199,6 +215,60 @@ async def handle_demucs_separating(tenant_id, task_id, step_name, input_data, co
|
|||||||
return {
|
return {
|
||||||
"vocals_path": vocals_local,
|
"vocals_path": vocals_local,
|
||||||
"no_vocals_path": no_vocals_local,
|
"no_vocals_path": no_vocals_local,
|
||||||
|
"mode": "2stem",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _demucs_full_pipeline(task_id, remote_audio, gpu_dir, work_dir):
|
||||||
|
"""4-stem separation → MIDI instrument replacement → accompaniment mix.
|
||||||
|
|
||||||
|
Runs the standalone run_full.py on GPU server via SSH.
|
||||||
|
"""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
output_dir = f"{gpu_dir}/full_output"
|
||||||
|
|
||||||
|
cmd = (
|
||||||
|
f"cd /data/ymq/demucs-service && "
|
||||||
|
f"DEMUCS_GPU_ID=5 "
|
||||||
|
f"{GPU_DEMUCS_VENV}/bin/python run_full.py "
|
||||||
|
f"'{remote_audio}' '{output_dir}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"[demucs-full] Starting full pipeline for task {task_id}")
|
||||||
|
stdout, stderr, rc = await _run_gpu(cmd, timeout=900)
|
||||||
|
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError(f"Demucs full separation failed: {stderr[-500:]}")
|
||||||
|
|
||||||
|
# Parse JSON result from stdout
|
||||||
|
try:
|
||||||
|
gpu_result = _json.loads(stdout)
|
||||||
|
except _json.JSONDecodeError:
|
||||||
|
# Try to extract JSON from mixed output
|
||||||
|
lines = stdout.strip().split("\n")
|
||||||
|
for line in reversed(lines):
|
||||||
|
try:
|
||||||
|
gpu_result = _json.loads(line)
|
||||||
|
break
|
||||||
|
except _json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Failed to parse demucs output: {stdout[-500:]}")
|
||||||
|
|
||||||
|
# Copy results back to local
|
||||||
|
vocals_local = os.path.join(work_dir, "vocals.wav")
|
||||||
|
accompaniment_local = os.path.join(work_dir, "accompaniment.wav")
|
||||||
|
|
||||||
|
await _copy_from_gpu(gpu_result["vocals_path"], vocals_local)
|
||||||
|
await _copy_from_gpu(gpu_result["accompaniment_path"], accompaniment_local)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"vocals_path": vocals_local,
|
||||||
|
"no_vocals_path": accompaniment_local, # backward compat
|
||||||
|
"accompaniment_path": accompaniment_local, # new name
|
||||||
|
"mode": "full",
|
||||||
|
"duration": gpu_result.get("duration", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user