← Files 中文口播智能粗剪ARCHIVED FILE

skills/rough-cut/scripts/compile_postproduction_plan.py

9.27 KB · Oct 5, 2026 · 18:30 UTC

↓ Download file

#!/usr/bin/env python3
"""把模块化总计划拆成现有渲染脚本可消费的子计划。

输出:
- brand_plan.json:顶部主题、人物头衔
- effect_plan.json:章节卡、观点花字、累积花字
- emphasis_plan.json:普通字幕关键词着色
- visual_plan.json:故事入口暗角、引语/角色模拟滤镜
- audio_plan.json:已绑定文件的 BGM/SFX
- postproduction_manifest.json:已编译模块和 renderer-pending 模块

本脚本不渲染媒体,不会把尚未支持的模块伪装成已执行。
"""

import argparse
import json
from pathlib import Path


COMPILED_EVENT_MODULES = {
    "chapter-card",
    "cumulative-flower",
    "claim-flower",
    "keyword-emphasis",
    "sfx-cue",
}


def write_json(path, data):
    path.write_text(
        json.dumps(data, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def flower_event(ev, content, style, start, end, y, color=None):
    text = {
        "mode": "flower",
        "style": style,
        "content": content,
        "y": round(y, 4),
        "hide_captions": True,
    }
    if color:
        text["color"] = color
    return {
        "id": ev["id"],
        "type": ev.get("module"),
        "trigger_text": ev.get("trigger_text"),
        "start": round(float(start), 3),
        "end": round(float(end), 3),
        "text": text,
        "reason": ev.get("reason"),
    }


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("plan")
    ap.add_argument("--out", required=True)
    args = ap.parse_args()

    plan_path = Path(args.plan)
    plan = json.loads(plan_path.read_text(encoding="utf-8"))
    out = Path(args.out)
    out.mkdir(parents=True, exist_ok=True)

    foundation = (plan.get("modules") or {}).get("foundation") or {}
    topic = foundation.get("topic") or {}
    identity = foundation.get("identity") or {}
    brand = {
        "style": topic.get("style", "headline-yellow"),
        "topic_lines": topic.get("lines", []),
        "start": float(topic.get("start", 0)),
        "end": float(topic.get("end", plan.get("duration", 999))),
    }
    if topic.get("colors"):
        brand["topic_colors"] = topic["colors"]
    elif topic.get("style") == "topic-two-tone":
        brand["topic_colors"] = ["#FFFFFF", "#FFE100"]
    for key in ("title_font", "title_slant", "title_y"):
        if key in topic:
            brand[key] = topic[key]
    if identity.get("name"):
        brand["identity"] = {
            key: value for key, value in identity.items()
            if key not in {"status"}
        }

    effect_events = []
    emphasis = []
    audio_sfx = []
    visual_events = []
    compiled = []
    pending = []

    for ev in plan.get("events", []):
        module = ev.get("module")
        eid = ev.get("id", "unnamed")
        if module == "chapter-card":
            text = ev.get("text") or {}
            content = text.get("content")
            if content and "start" in ev and "end" in ev:
                effect_events.append(flower_event(
                    ev, content, "chapter", ev["start"], ev["end"],
                    float(text.get("y", 0.40)), text.get("color"),
                ))
                compiled.append(eid)
            else:
                pending.append({"id": eid, "module": module, "reason": "缺少文案或成片时间"})
        elif module == "claim-flower":
            text = ev.get("text") or {}
            raw = text.get("content", "")
            lines = [line.strip() for line in str(raw).splitlines() if line.strip()]
            colors = text.get("colors") or []
            if lines and "start" in ev and "end" in ev:
                base_y = float(text.get("y", 0.43))
                gap = float(text.get("line_gap", 0.08))
                for line_i, line in enumerate(lines):
                    item = flower_event(
                        ev, line, "emphasis", ev["start"], ev["end"],
                        base_y + line_i * gap,
                        colors[line_i] if line_i < len(colors) else text.get("color"),
                    )
                    item["id"] = f"{eid}-{line_i + 1}"
                    effect_events.append(item)
                compiled.append(eid)
            else:
                pending.append({"id": eid, "module": module, "reason": "缺少文案或成片时间"})
        elif module == "cumulative-flower":
            text = ev.get("text") or {}
            items = text.get("items") or []
            if items and "end" in ev:
                before = len(effect_events)
                base_y = float(text.get("y", 0.40))
                gap = float(text.get("line_gap", 0.075))
                for item_i, item in enumerate(items):
                    if not item.get("content") or "start" not in item:
                        continue
                    sub = flower_event(
                        ev, item["content"], "emphasis",
                        item["start"], ev["end"],
                        base_y + item_i * gap,
                        item.get("color") or text.get("color"),
                    )
                    sub["id"] = f"{eid}-{item_i + 1}"
                    effect_events.append(sub)
                if len(effect_events) > before:
                    compiled.append(eid)
            else:
                pending.append({"id": eid, "module": module, "reason": "缺少 items 或共同结束时间"})
        elif module == "keyword-emphasis":
            text = ev.get("text") or {}
            words = text.get("words") or (
                [{"word": text.get("word"), "color": text.get("color", "gold")}]
                if text.get("word") else [])
            for item in words:
                if item.get("word"):
                    emphasis.append({
                        "word": item["word"],
                        "color": item.get("color", "gold"),
                    })
            if words:
                compiled.append(eid)
        elif module in {"story-portal", "quote-simulation"}:
            if "start" in ev and "end" in ev:
                visual_events.append({
                    key: value for key, value in ev.items()
                    if key in {
                        "id", "module", "trigger_text", "end_text", "start", "end",
                        "confidence", "reason", "visual",
                    }
                })
                compiled.append(eid)
            else:
                pending.append({
                    "id": eid,
                    "module": module,
                    "reason": "缺少最终成片 start/end",
                })
        elif module == "sfx-cue":
            audio = ev.get("audio") or {}
            if audio.get("file") and ("at" in audio or "start" in ev):
                audio_sfx.append({
                    "name": audio.get("resource_name") or audio.get("category") or eid,
                    "file": audio["file"],
                    "at": float(audio.get("at", ev["start"])),
                    "volume": float(audio.get("volume", 0.16)),
                })
                compiled.append(eid)
            else:
                pending.append({
                    "id": eid,
                    "module": module,
                    "reason": "只确定音效类别,尚未绑定具体文件或时间",
                })
        else:
            status = (ev.get("visual") or {}).get("status") or ev.get("status")
            pending.append({
                "id": eid,
                "module": module,
                "reason": status or "当前渲染器尚未支持该模块",
            })

    audio_cfg = (plan.get("modules") or {}).get("audio") or {}
    bgm = audio_cfg.get("bgm") or {}
    audio_plan = {
        "voice_volume": float(audio_cfg.get("voice_volume", 1.0)),
        "sfx": audio_sfx,
    }
    if bgm.get("enabled") and bgm.get("file"):
        audio_plan["bgm"] = {
            key: value for key, value in bgm.items()
            if key in {"file", "volume", "start", "fade_in", "fade_out"}
        }

    write_json(out / "brand_plan.json", brand)
    write_json(out / "effect_plan.json", {"events": effect_events})
    write_json(out / "emphasis_plan.json", emphasis)
    write_json(out / "visual_plan.json", {"events": visual_events})
    write_json(out / "audio_plan.json", audio_plan)
    manifest = {
        "source_plan": str(plan_path.resolve()),
        "compiled_events": compiled,
        "renderer_pending": pending,
        "outputs": {
            "brand_plan": "brand_plan.json",
            "effect_plan": "effect_plan.json",
            "emphasis_plan": "emphasis_plan.json",
            "visual_plan": "visual_plan.json",
            "audio_plan": "audio_plan.json",
        },
        "note": "renderer_pending 中的模块尚未执行,必须补素材/渲染能力或人工处理。",
    }
    write_json(out / "postproduction_manifest.json", manifest)

    print(f"✓ 品牌计划:{out / 'brand_plan.json'}")
    print(f"✓ 花字计划:{out / 'effect_plan.json'}({len(effect_events)} 层)")
    print(f"✓ 关键词计划:{out / 'emphasis_plan.json'}({len(emphasis)} 词)")
    print(f"✓ 视觉滤镜计划:{out / 'visual_plan.json'}({len(visual_events)} 个事件)")
    print(f"✓ 音频计划:{out / 'audio_plan.json'}({len(audio_sfx)} 个已绑定音效)")
    print(f"✓ 待执行模块:{len(pending)}(见 postproduction_manifest.json)")


if __name__ == "__main__":
    main()

SHA-256: efe7e7a1eda4b2d42be1a9c1f406b8baa249e50a985c6b7e8be2f5b141a0ccd5