| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- // scripts/music.mjs — AI 音乐生成(provider: music)
- //
- // ⚠️ 现状:平台 Suno 渠道未开通(实测 POST /suno/submit/music 返回 503 "Task request failed")。
- // 本脚本依然会「真试一次」——若平台后续开通,无需改代码即可跑通;
- // 失败则抛出带指引的 ProviderUnavailableError,绝不伪造音频文件。
- import { existsSync, writeFileSync } from 'node:fs';
- import { join } from 'node:path';
- import {
- parseArgs, readJSON, writeJSON, paths, ensureWork, log, invoke, download, paths as P,
- } from '../lib/index.mjs';
- import { getProvider } from '../lib/providers.mjs';
- import { run } from '../lib/ffmpeg.mjs';
- /** 从 provider 返回结果里尽力提取音频 URL */
- function extractAudioUrls(result) {
- const urls = [];
- const walk = (o, depth = 0) => {
- if (!o || depth > 5) return;
- if (typeof o === 'string' && /^https?:\/\/.+\.(mp3|wav|m4a|flac)(\?.*)?$/i.test(o)) urls.push(o);
- else if (Array.isArray(o)) o.forEach((x) => walk(x, depth + 1));
- else if (typeof o === 'object') {
- for (const [k, v] of Object.entries(o)) {
- if (/audio_url|stream_audio_url|source_audio_url|url|audio/i.test(k) && typeof v === 'string' && /^https?:/.test(v)) urls.push(v);
- else walk(v, depth + 1);
- }
- }
- };
- walk(result);
- return [...new Set(urls)];
- }
- export async function generateMusic({ prompt, lyrics, instrumental = false, title, outDir }) {
- const p = getProvider('music');
- ensureWork();
- const dir = outDir || P.audio;
- const { mkdirSync } = await import('node:fs');
- mkdirSync(dir, { recursive: true });
- log.info(`提交音乐生成(模型 ${p.defaultModel()})…`);
- const { taskId, result } = await p.call({ prompt, lyrics, instrumental, title });
- log.ok(`任务完成 taskId=${taskId}`);
- const urls = extractAudioUrls(result);
- if (!urls.length) {
- throw new Error(
- `音乐通道返回成功但未包含音频 URL。原始返回:\n${JSON.stringify(result).slice(0, 600)}`
- );
- }
- const files = [];
- for (let i = 0; i < urls.length; i++) {
- const buf = await download(urls[i]);
- const f = join(dir, urls.length > 1 ? `music-${i + 1}.mp3` : 'music.mp3');
- writeFileSync(f, buf);
- files.push({ path: f, url: urls[i], bytes: buf.length });
- log.ok(`已下载 ${f}(${Math.round(buf.length / 1024)}KB)`);
- }
- writeJSON(join(dir, 'music.json'), {
- taskId,
- title: title || '',
- model: p.defaultModel(),
- prompt,
- instrumental,
- files: files.map((f) => ({ path: f.path, bytes: f.bytes })),
- raw: result,
- _meta: { createdBy: 'fmode-mv/music', generatedAt: new Date().toISOString() },
- });
- return { taskId, files, raw: result };
- }
- export async function main(argv = process.argv.slice(2)) {
- const { _, flags } = parseArgs(argv);
- if (flags.help || (!_.length && !flags.from)) {
- console.log(`
- fmode-mv music — AI 音乐生成(music 通道)
- 用法:
- fmode-mv music --from work/song.json # 用作曲填词的产物直接生成
- fmode-mv music "dreamy city pop, 92bpm, female vocal" --title "夜行"
- fmode-mv music --from work/song.json --instrumental # 只要伴奏
- 选项:
- --from <file> 歌曲 JSON(读取 stylePrompt / lyrics / title)
- --title "歌名"
- --instrumental 纯伴奏,不生成人声
- --model <name> 音乐模型版本(默认 chirp-v3-5)
- --out <dir> 输出目录(默认 work/audio)
- ⚠️ 现状:平台 Suno 渠道未开通。本命令会真实尝试一次;
- 若失败会明确告知如何切换通道或对接外部服务(不会伪造产物)。
- 用 fmode-mv providers 查看通道状态。
- `);
- return 0;
- }
- ensureWork();
- let prompt = _.join(' ').trim();
- let lyrics = null;
- let title = flags.title ? String(flags.title) : '';
- if (flags.from) {
- const song = readJSON(String(flags.from));
- if (!song) throw new Error(`未找到文件:${flags.from}`);
- prompt = prompt || song.stylePrompt || song.genre || '';
- lyrics = song.lyrics || null;
- title = title || song.title || '';
- log.step(`AI 音乐生成:《${title}》`);
- log.key('风格', prompt.slice(0, 80) + (prompt.length > 80 ? '…' : ''));
- log.key('歌词', `${String(lyrics || '').length} 字`);
- } else {
- log.step('AI 音乐生成');
- log.key('风格', prompt);
- }
- if (!prompt) throw new Error('缺少风格 prompt。用 --from work/song.json 或直接给风格描述。');
- const provider = getProvider('music');
- const probe = await provider.probe({ deep: false });
- if (probe.available === false) {
- log.warn(`通道探活:${probe.detail}`);
- }
- const res = await generateMusic({
- prompt,
- lyrics,
- instrumental: !!flags.instrumental,
- title,
- outDir: typeof flags.out === 'string' ? flags.out : undefined,
- });
- log.key('产物', res.files.map((f) => f.path).join(', '));
- return 0;
- }
- if (import.meta.url === `file://${process.argv[1]}`) {
- main().then((c) => process.exit(Number(c) || 0)).catch(async (e) => {
- const { reportUnavailable, log: L } = await import('../lib/index.mjs');
- if (e?.name === 'ProviderUnavailableError') {
- reportUnavailable('music', e);
- process.exit(3);
- }
- L.err(e.message);
- process.exit(1);
- });
- }
|