File size: 5,398 Bytes
dd63733
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""cli.py — 项目入口 (PM v0.2 §10)。

命令:
  python cli.py baseline --seconds 5      # Milestone 1: 跑通并测量
  python cli.py generate "描述" --seconds 30  # 直接生成 (阶段驻留管线)
  python cli.py idea "想法" --seconds 30  # 完整 Idea→Music (重写 caption + 生成)
  python cli.py webui                     # 启动 WebUI (浏览器测试入口)
"""
from __future__ import annotations

import sys
from pathlib import Path

import click

ROOT = Path(__file__).parent


@click.group()
def main() -> None:
    """Music3-Mac16: MiniMax Music3 on 16GB Apple Silicon Mac."""


@main.command()
@click.option("--host", default="127.0.0.1", help="监听地址")
@click.option("--port", default=8642, type=int, help="端口")
def webui(host: str, port: int) -> None:
    """启动 WebUI (浏览器测试入口)。

    用法: music-minimax webui  → 打开 http://127.0.0.1:8642
    """
    import webbrowser

    from webui.app import start

    url = f"http://{host}:{port}"
    click.echo(f"启动 WebUI: {url}")
    webbrowser.open(url)
    start(host=host, port=port)


@main.command()
@click.option("--seconds", default=5, type=click.IntRange(1, 60), help="目标生成秒数")
@click.option("--label", default="baseline", help="本次运行的标签(写进 profile json)")
def baseline(seconds: int, label: str) -> None:
    """Milestone 1: Baseline — 跑通现有 MLX Music3 并记录峰值内存/速度/正确性。"""
    from generation.pipeline import BaselineRunner

    runner = BaselineRunner(label=label)
    runner.run(target_seconds=seconds)


@main.command()
@click.argument("prompt")
@click.option("--seconds", default=15, type=click.IntRange(1, 240), help="目标生成秒数")
@click.option("--lyrics", default=None, help="结构化歌词 (可选, 缺省为 [instrumental])")
@click.option("--steps", default=30, type=click.IntRange(1, 50), help="Flow-matching 步数")
@click.option("--seed", default=0, type=int, help="随机种子")
@click.option("--label", default="idea", help="输出目录标签")
@click.option("--model", default="models/runtime/MiniMax-Music3-4bit",
              help="模型路径 (4bit / mxfp8 / 8bit 等 mlx-community 变体)")
@click.option("--experimental-long-form", is_flag=True,
              help="显式开启 120/180/240 秒实验路径,固定使用 flush=32")
def generate(prompt: str, seconds: int, lyrics: str | None, steps: int, seed: int,
             label: str, model: str, experimental_long_form: bool) -> None:
    """Idea→Music: 从自然语言描述生成 WAV(阶段驻留管线)。

    用法: music-minimax generate "dark melodic techno, 128 BPM" --seconds 30
          music-minimax generate "pop ballad" --model models/runtime/MiniMax-Music3-mxfp8
    """
    from generation.phased_pipeline import PhasedPipeline

    pipeline = PhasedPipeline(label=label, model_path=model)
    output = None
    if experimental_long_form:
        long_form_dir = ROOT / "outputs" / "long_form_test" / f"{seconds}s"
        output = str(long_form_dir / "output.wav")
    report = pipeline.generate(
        caption=prompt,
        lyrics=lyrics or "[instrumental]",
        duration=seconds,
        steps=steps,
        seed=seed,
        output=output,
        experimental_long_form=experimental_long_form,
    )
    click.echo(f"✓ 生成完成: {report['output_wav']}")
    click.echo(f"  峰值内存: {report['max_stage_peak_gb']} GB (预算 11.5GB)")
    click.echo(f"  AR 阶段  : {report['ar_peak_gb']} GB | Flow 阶段: {report['flow_peak_gb']} GB")


@main.command()
@click.argument("idea")
@click.option("--lyrics", default=None, help="可选歌词 (带段落标签)")
@click.option("--seconds", default=30, type=click.IntRange(1, 60), help="目标生成秒数")
@click.option("--rewriter-model", default=None,
              help="本地重写模型 (默认 mlx-community/Qwen3-8B-4bit, 首次自动下载)")
@click.option("--label", default="idea_to_music", help="输出目录标签")
@click.option("--dry-run", is_flag=True, help="只重写 caption 不生成音乐")
def idea(idea: str, lyrics: str | None, seconds: int, rewriter_model: str | None,
         label: str, dry_run: bool) -> None:
    """Idea→Music 完整工作流: 想法 → caption 重写 → 本地生成 WAV。

    用法: music-minimax idea "dark melodic techno with female airy vocal, lonely"
    """
    from caption.rewriter import rewrite_caption
    from generation.phased_pipeline import PhasedPipeline

    click.echo("① 重写想法 → 结构化 caption (本地模型)...")
    caption = rewrite_caption(
        idea=idea,
        lyrics=lyrics or "",
        model=rewriter_model or "mlx-community/Qwen3-8B-4bit",
    )
    click.echo(f"\n── 生成的 caption ──\n{caption}\n─────────────────────")

    if dry_run:
        click.echo("(--dry-run: 只输出 caption, 不生成音乐)")
        return

    click.echo(f"② 生成 {seconds}s 音乐 (阶段驻留管线)...")
    pipeline = PhasedPipeline(label=label)
    report = pipeline.generate(
        caption=caption,
        lyrics=lyrics or "[instrumental]",
        duration=seconds,
    )
    click.echo(f"✓ 完整工作流完成: {report['output_wav']}")
    click.echo(f"  峰值内存: {report['max_stage_peak_gb']} GB (预算 11.5GB)")


if __name__ == "__main__":
    main()