| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- // scripts/separate.mjs — 人声 / 伴奏分离
- //
- // 两级策略:
- // 1) demucs(若已安装)—— 高质量,真·分离
- // 2) 纯 ffmpeg 兜底 —— 中置/侧信号相位处理,只能做「近似」,务必如实告知用户
- // · instrumental:L-R 侧信号,压低居中的人声
- // · vocals :中置信号 (L+R)/2,突出居中人声
- // 对立体声混音有效;单声道文件无法分离,会明确报错。
- import { existsSync, mkdirSync } from 'node:fs';
- import { join, basename } from 'node:path';
- import { parseArgs, paths, ensureWork, log } from '../lib/index.mjs';
- import {
- checkFfmpeg, checkDemucs, separateWithDemucs, separateByChannelPhase, probe, run, extractAudio, fileSize, humanSize,
- } from '../lib/ffmpeg.mjs';
- export async function separate(input, { outDir, method = 'auto', mode = 'both' } = {}) {
- checkFfmpeg();
- if (!existsSync(input)) throw new Error(`输入文件不存在:${input}`);
- const dir = outDir || paths.audio;
- mkdirSync(dir, { recursive: true });
- const info = await probe(input);
- const stem = basename(input).replace(/\.[^.]+$/, '');
- const demucs = checkDemucs();
- const useDemucs = method === 'demucs' || (method === 'auto' && demucs.available);
- if (method === 'demucs' && !demucs.available) {
- throw new Error('指定了 --method demucs,但未检测到 demucs。安装:pip install demucs(体积较大,非必需)');
- }
- if (useDemucs) {
- log.info(`使用 demucs 分离(高质量,可能耗时数分钟)…`);
- const out = await separateWithDemucs(input, dir);
- for (const [k, v] of Object.entries(out)) {
- if (existsSync(v)) log.ok(`${k}: ${v}(${humanSize(fileSize(v))})`);
- }
- return { method: 'demucs', ...out };
- }
- // ---- ffmpeg 兜底 ----
- log.warn('未检测到 demucs,使用 ffmpeg 相位兜底方案(近似分离,非真·分离)');
- if (!info.channels || info.channels < 2) {
- throw new Error(
- `输入是单声道(${info.channels || 1}ch),ffmpeg 相位法无法分离。\n` +
- ' 请改用立体声源,或安装 demucs:pip install demucs'
- );
- }
- const result = { method: 'ffmpeg-phase', input };
- let src = input;
- // 统一先转 wav,避免编码问题
- const wav = join(dir, `${stem}.src.wav`);
- await extractAudio(input, wav, { format: 'wav' });
- src = wav;
- if (mode === 'both' || mode === 'instrumental') {
- result.instrumental = await separateByChannelPhase(src, { outDir: dir, mode: 'instrumental' });
- log.ok(`伴奏(近似): ${result.instrumental}(${humanSize(fileSize(result.instrumental))})`);
- }
- if (mode === 'both' || mode === 'vocals') {
- result.vocals = await separateByChannelPhase(src, { outDir: dir, mode: 'vocals' });
- log.ok(`人声(近似): ${result.vocals}(${humanSize(fileSize(result.vocals))})`);
- }
- result.note = 'ffmpeg 兜底分离为相位近似,人声/伴奏均可能残留串扰;正式出版建议用 demucs 或平台分离通道。';
- return result;
- }
- export async function main(argv = process.argv.slice(2)) {
- const { _, flags } = parseArgs(argv);
- const input = flags.input || _[0];
- if (flags.help || !input) {
- console.log(`
- fmode-mv separate — 人声 / 伴奏分离(本地)
- 用法:
- fmode-mv separate work/audio/music.mp3
- fmode-mv separate work/audio/music.mp3 --mode instrumental
- fmode-mv separate work/audio/music.mp3 --method demucs # 高质量(需已安装 demucs)
- 选项:
- --input <file> | 位置参数 输入音频
- --out <dir> 输出目录(默认 work/audio)
- --mode both|vocals|instrumental 默认 both
- --method auto|demucs|ffmpeg 默认 auto(有 demucs 用 demucs,否则 ffmpeg 兜底)
- 现状说明:
- · demucs 未安装时走 ffmpeg 相位法,属「近似分离」,仅对立体声混音有效;
- · 需要高质量分离请安装 demucs(可选,非本技能依赖)。
- `);
- return 0;
- }
- ensureWork();
- log.step(`音轨分离:${input}`);
- const dm = checkDemucs();
- log.key('demucs', dm.available ? `可用(${dm.version})` : '未安装 → 将用 ffmpeg 兜底');
- const r = await separate(String(input), {
- outDir: typeof flags.out === 'string' ? flags.out : undefined,
- mode: flags.mode || 'both',
- method: flags.method || 'auto',
- });
- if (r.note) log.warn(r.note);
- log.key('方式', r.method);
- return 0;
- }
- if (import.meta.url === `file://${process.argv[1]}`) {
- main().then((c) => process.exit(Number(c) || 0)).catch((e) => {
- console.error(`✗ ${e.message}`);
- process.exit(1);
- });
- }
- export { run };
|