lyrics.mjs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. // scripts/lyrics.mjs — 作曲填词
  2. //
  3. // 主题 → 歌词 + 曲风 prompt + 结构标记(intro/verse/chorus/bridge/outro)
  4. // 走 chat 通道(实测可用)。产物落 work/song.json。
  5. import {
  6. parseArgs, writeJSON, readJSON, paths, ensureWork, log, chatJSON,
  7. } from '../lib/index.mjs';
  8. import { buildLyricsPrompt } from '../lib/prompts.mjs';
  9. const SYSTEM = `你是资深华语流行音乐制作人兼作词人,为 AI 音乐生成模型(Suno 类)撰写可直接投喂的素材。
  10. 硬性要求:
  11. 1. 歌词必须自带结构标记,每段用方括号起头,例如 [Intro] [Verse 1] [Pre-Chorus] [Chorus] [Bridge] [Outro]。
  12. 2. Chorus 要朗朗上口、有记忆点,副歌句尾押韵(中文押韵即可,不必强求工整)。
  13. 3. 只输出 JSON,不要任何解释文字、不要 markdown 代码围栏。
  14. 4. stylePrompt 字段用英文逗号短语描述曲风/编曲/情绪,供音乐模型直接使用(例:"dreamy city pop, 88 bpm, warm analog synths, female vocal, lush reverb")。
  15. 5. 歌词内容必须为原创,不得抄袭任何已有歌曲的句子。`;
  16. const SCHEMA_HINT = `严格输出如下 JSON(字段名不可改):
  17. {
  18. "title": "歌名(4-10字,中文)",
  19. "genre": "曲风(中文,如:城市流行 City Pop)",
  20. "mood": "情绪关键词(中文,2-4个词)",
  21. "bpm": 92,
  22. "language": "zh",
  23. "structure": ["Intro","Verse 1","Pre-Chorus","Chorus","Verse 2","Chorus","Bridge","Chorus","Outro"],
  24. "lyrics": "[Intro]\\n(器乐)\\n\\n[Verse 1]\\n第一段歌词...\\n\\n[Chorus]\\n副歌...",
  25. "stylePrompt": "英文风格提示词,逗号分隔",
  26. "summary": "一句话说明这首歌讲什么(中文)"
  27. }`;
  28. export async function buildLyrics(theme, opts = {}) {
  29. const { genre = '', mood = '', style = '', language = '中文', extra = '', model } = opts;
  30. const userPrompt = buildLyricsPrompt({ theme, genre, mood, style, language, extra }) + '\n\n' + SCHEMA_HINT;
  31. const res = await chatJSON({
  32. model,
  33. system: SYSTEM,
  34. messages: [{ role: 'user', content: userPrompt }],
  35. temperature: 0.9,
  36. max_tokens: 4096,
  37. });
  38. const parsed = res.json;
  39. return {
  40. ...parsed,
  41. _meta: {
  42. model: res.model,
  43. createdBy: 'fmode-mv/lyrics',
  44. theme,
  45. maxTokensUsed: res.maxTokensUsed,
  46. truncated: !!res.repaired,
  47. },
  48. };
  49. }
  50. export async function main(argv = process.argv.slice(2)) {
  51. const { _, flags } = parseArgs(argv);
  52. const theme = _.join(' ').trim() || flags.theme;
  53. if (!theme || flags.help) {
  54. console.log(`
  55. fmode-mv lyrics — 作曲填词(chat 通道)
  56. 用法:
  57. fmode-mv lyrics "主题描述" [选项]
  58. 选项:
  59. --genre "曲风" 如 "city pop" / "国风民谣" / "电子舞曲"
  60. --mood "情绪" 如 "温暖治愈" / "孤独疏离"
  61. --style "补充要求" 如 "女声,钢琴为主"
  62. --lang zh|en 歌词语言(默认 zh)
  63. --model <name> 覆盖 LLM 模型(默认 deepseek-v4-flash)
  64. --out <file> 输出文件(默认 work/song.json)
  65. --json 只打印 JSON
  66. 示例:
  67. fmode-mv lyrics "深夜加班后独自回家的城市白领" --genre "city pop" --mood "孤独但温暖"
  68. `);
  69. return 0;
  70. }
  71. ensureWork();
  72. log.step(`作曲填词:${theme}`);
  73. if (flags.genre) log.key('曲风', flags.genre);
  74. if (flags.mood) log.key('情绪', flags.mood);
  75. const song = await buildLyrics(theme, {
  76. genre: flags.genre,
  77. mood: flags.mood,
  78. style: flags.style,
  79. language: flags.lang === 'en' ? 'English' : '中文',
  80. model: flags.model,
  81. });
  82. const outFile = typeof flags.out === 'string' ? flags.out : paths.song;
  83. writeJSON(outFile, song);
  84. if (flags.json) {
  85. console.log(JSON.stringify(song, null, 2));
  86. return 0;
  87. }
  88. log.ok(`歌名:《${song.title}》`);
  89. log.key('曲风', `${song.genre || '-'} / ${song.bpm || '?'} BPM / ${song.mood || '-'}`);
  90. log.key('结构', (song.structure || []).join(' → '));
  91. log.key('风格 prompt', song.stylePrompt || '-');
  92. console.log('');
  93. console.log(song.lyrics || '(空)');
  94. console.log('');
  95. log.ok(`已写入 ${outFile}`);
  96. // 自检:歌词结构标记 & 副歌存在
  97. const lyr = String(song.lyrics || '');
  98. const issues = [];
  99. if (!/\[Verse|\[verse|\[主歌/.test(lyr)) issues.push('缺少 Verse 段落标记');
  100. if (!/\[Chorus|\[chorus|\[副歌/.test(lyr)) issues.push('缺少 Chorus 段落标记');
  101. if (lyr.replace(/\[[^\]]*\]/g, '').trim().length < 60) issues.push('歌词正文过短');
  102. if (!song.stylePrompt) issues.push('缺少 stylePrompt(音乐模型需要)');
  103. if (issues.length) {
  104. log.warn(`歌词自检提示:${issues.join(';')}`);
  105. } else {
  106. log.ok('歌词自检通过(含主歌/副歌结构标记 + 风格 prompt)');
  107. }
  108. return 0;
  109. }
  110. if (import.meta.url === `file://${process.argv[1]}`) {
  111. main().then((c) => process.exit(Number(c) || 0)).catch((e) => {
  112. console.error(`✗ ${e.message}`);
  113. process.exit(1);
  114. });
  115. }