| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- // scripts/lyrics.mjs — 作曲填词
- //
- // 主题 → 歌词 + 曲风 prompt + 结构标记(intro/verse/chorus/bridge/outro)
- // 走 chat 通道(实测可用)。产物落 work/song.json。
- import {
- parseArgs, writeJSON, readJSON, paths, ensureWork, log, chatJSON,
- } from '../lib/index.mjs';
- import { buildLyricsPrompt } from '../lib/prompts.mjs';
- const SYSTEM = `你是资深华语流行音乐制作人兼作词人,为 AI 音乐生成模型(Suno 类)撰写可直接投喂的素材。
- 硬性要求:
- 1. 歌词必须自带结构标记,每段用方括号起头,例如 [Intro] [Verse 1] [Pre-Chorus] [Chorus] [Bridge] [Outro]。
- 2. Chorus 要朗朗上口、有记忆点,副歌句尾押韵(中文押韵即可,不必强求工整)。
- 3. 只输出 JSON,不要任何解释文字、不要 markdown 代码围栏。
- 4. stylePrompt 字段用英文逗号短语描述曲风/编曲/情绪,供音乐模型直接使用(例:"dreamy city pop, 88 bpm, warm analog synths, female vocal, lush reverb")。
- 5. 歌词内容必须为原创,不得抄袭任何已有歌曲的句子。`;
- const SCHEMA_HINT = `严格输出如下 JSON(字段名不可改):
- {
- "title": "歌名(4-10字,中文)",
- "genre": "曲风(中文,如:城市流行 City Pop)",
- "mood": "情绪关键词(中文,2-4个词)",
- "bpm": 92,
- "language": "zh",
- "structure": ["Intro","Verse 1","Pre-Chorus","Chorus","Verse 2","Chorus","Bridge","Chorus","Outro"],
- "lyrics": "[Intro]\\n(器乐)\\n\\n[Verse 1]\\n第一段歌词...\\n\\n[Chorus]\\n副歌...",
- "stylePrompt": "英文风格提示词,逗号分隔",
- "summary": "一句话说明这首歌讲什么(中文)"
- }`;
- export async function buildLyrics(theme, opts = {}) {
- const { genre = '', mood = '', style = '', language = '中文', extra = '', model } = opts;
- const userPrompt = buildLyricsPrompt({ theme, genre, mood, style, language, extra }) + '\n\n' + SCHEMA_HINT;
- const res = await chatJSON({
- model,
- system: SYSTEM,
- messages: [{ role: 'user', content: userPrompt }],
- temperature: 0.9,
- max_tokens: 4096,
- });
- const parsed = res.json;
- return {
- ...parsed,
- _meta: {
- model: res.model,
- createdBy: 'fmode-mv/lyrics',
- theme,
- maxTokensUsed: res.maxTokensUsed,
- truncated: !!res.repaired,
- },
- };
- }
- export async function main(argv = process.argv.slice(2)) {
- const { _, flags } = parseArgs(argv);
- const theme = _.join(' ').trim() || flags.theme;
- if (!theme || flags.help) {
- console.log(`
- fmode-mv lyrics — 作曲填词(chat 通道)
- 用法:
- fmode-mv lyrics "主题描述" [选项]
- 选项:
- --genre "曲风" 如 "city pop" / "国风民谣" / "电子舞曲"
- --mood "情绪" 如 "温暖治愈" / "孤独疏离"
- --style "补充要求" 如 "女声,钢琴为主"
- --lang zh|en 歌词语言(默认 zh)
- --model <name> 覆盖 LLM 模型(默认 deepseek-v4-flash)
- --out <file> 输出文件(默认 work/song.json)
- --json 只打印 JSON
- 示例:
- fmode-mv lyrics "深夜加班后独自回家的城市白领" --genre "city pop" --mood "孤独但温暖"
- `);
- return 0;
- }
- ensureWork();
- log.step(`作曲填词:${theme}`);
- if (flags.genre) log.key('曲风', flags.genre);
- if (flags.mood) log.key('情绪', flags.mood);
- const song = await buildLyrics(theme, {
- genre: flags.genre,
- mood: flags.mood,
- style: flags.style,
- language: flags.lang === 'en' ? 'English' : '中文',
- model: flags.model,
- });
- const outFile = typeof flags.out === 'string' ? flags.out : paths.song;
- writeJSON(outFile, song);
- if (flags.json) {
- console.log(JSON.stringify(song, null, 2));
- return 0;
- }
- log.ok(`歌名:《${song.title}》`);
- log.key('曲风', `${song.genre || '-'} / ${song.bpm || '?'} BPM / ${song.mood || '-'}`);
- log.key('结构', (song.structure || []).join(' → '));
- log.key('风格 prompt', song.stylePrompt || '-');
- console.log('');
- console.log(song.lyrics || '(空)');
- console.log('');
- log.ok(`已写入 ${outFile}`);
- // 自检:歌词结构标记 & 副歌存在
- const lyr = String(song.lyrics || '');
- const issues = [];
- if (!/\[Verse|\[verse|\[主歌/.test(lyr)) issues.push('缺少 Verse 段落标记');
- if (!/\[Chorus|\[chorus|\[副歌/.test(lyr)) issues.push('缺少 Chorus 段落标记');
- if (lyr.replace(/\[[^\]]*\]/g, '').trim().length < 60) issues.push('歌词正文过短');
- if (!song.stylePrompt) issues.push('缺少 stylePrompt(音乐模型需要)');
- if (issues.length) {
- log.warn(`歌词自检提示:${issues.join(';')}`);
- } else {
- log.ok('歌词自检通过(含主歌/副歌结构标记 + 风格 prompt)');
- }
- 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);
- });
- }
|