文件预览

generate_audition.py

查看 MIDI Music Composer 技能包中的文件内容。

文件内容

scripts/generate_audition.py

#!/usr/bin/env python3
"""Generate blind candidate sets for user rating."""

from __future__ import annotations

import argparse
import json
import string
import time
from pathlib import Path
from random import Random
from typing import Any

from generate_song import build_song, load_preferences, slugify, stable_seed


def public_candidate(label: str, manifest: dict[str, Any]) -> dict[str, Any]:
    midi_file = str(manifest.get("midi_file", ""))
    return {
        "label": label,
        "midi_file": midi_file,
        "audio_file": manifest.get("audio_file"),
        "manifest_file": midi_file.replace(".mid", ".json"),
        "composition_file": manifest.get("composition_file"),
    }


def main() -> None:
    parser = argparse.ArgumentParser(description="Generate a blind audition set for music-composer.")
    parser.add_argument("title")
    parser.add_argument("--out", default="out", help="Output directory.")
    parser.add_argument("--candidates", type=int, default=4, help="Number of blind candidates to generate.")
    parser.add_argument("--preferences", type=Path, default=None, help="Optional JSON preference memory file.")
    parser.add_argument("--render-audio", action="store_true", help="Try to render WAV previews when local tools exist.")
    parser.add_argument("--soundfont", type=Path, default=None, help="Optional soundfont path for fluidsynth.")
    args = parser.parse_args()

    count = max(2, min(args.candidates, len(string.ascii_uppercase)))
    labels = list(string.ascii_uppercase[:count])
    preferences = load_preferences(args.preferences)
    run_id = time.strftime("%Y%m%d-%H%M%S")
    audition_dir = Path(args.out) / f"{slugify(args.title)}-audition-{run_id}"
    audition_dir.mkdir(parents=True, exist_ok=True)

    variants = list(range(count))
    rng = Random(stable_seed(args.title, 30_000 + count))
    rng.shuffle(variants)

    candidates = []
    for label, variant in zip(labels, variants):
        manifest = build_song(
            args.title,
            audition_dir,
            variant=variant,
            suffix=f"-{label.lower()}",
            preferences=preferences,
            render_audio=args.render_audio,
            soundfont=args.soundfont,
        )
        candidates.append(public_candidate(label, manifest))

    audition = {
        "title": args.title,
        "created_at": run_id,
        "mode": "blind_audition",
        "candidate_count": count,
        "candidates": candidates,
        "instructions": "Ask the user to rate each label from 1-5 without showing score, genre, harmony, or critic metadata.",
    }
    audition_path = audition_dir / "audition.json"
    audition_path.write_text(json.dumps(audition, indent=2) + "\n", encoding="utf-8")

    print(json.dumps({
        "audition_file": str(audition_path),
        "title": args.title,
        "candidates": candidates,
    }, indent=2))


if __name__ == "__main__":
    main()