create-dwy 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/index.js +23 -19
  3. package/package.json +1 -1
  4. package/src/cli-help.js +44 -0
  5. package/src/sync-all.js +303 -59
  6. package/src/sync-global-skills.js +167 -0
  7. package/src/sync.js +21 -11
  8. package/templates/ai-tools/CHANGELOG.md +134 -0
  9. package/templates/ai-tools/VERSION +1 -1
  10. package/templates/ai-tools/rules/Android/dwy-android-core.md +65 -1
  11. package/templates/ai-tools/rules/Python/dwy-python-core.md +47 -0
  12. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +55 -0
  13. package/templates/ai-tools/rules/iOS/dwy-swift-core.md +46 -0
  14. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +128 -0
  15. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +65 -0
  16. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/assets/appstore-submission.yaml +43 -0
  17. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/api-workflow.md +17 -0
  18. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/in-app-purchase-review.md +16 -0
  19. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/screenshot-workflow.md +16 -0
  20. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/scripts/appstore_config.sh +140 -0
  21. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +11 -8
  22. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +2 -1
  23. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +2 -1
  24. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +24 -4
  25. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +2 -1
  26. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +2 -1
  27. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/git-tag.md +82 -0
  28. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -1
  29. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +3 -2
  30. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/monitor-notify.md +2 -2
  31. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +1 -1
  32. package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eapi/references/tasks-integration-guide.md +2 -2
  33. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/README.md +57 -0
  34. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +147 -0
  35. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +43 -0
  36. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/config.example.yaml +33 -0
  37. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/global_config.py +89 -0
  38. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/setup_config.py +175 -0
  39. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +339 -0
  40. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +127 -0
  41. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/assets/cover-style-master-v1.png +0 -0
  42. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +37 -0
  43. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-posting-times.md +193 -0
  44. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/SKILL.md +42 -0
  45. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates/topic-backlog.template.md +50 -0
  46. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates//351/200/211/351/242/230/346/261/240.template.md +16 -0
  47. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/SKILL.md +106 -0
  48. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates/channel-profile.template.md +52 -0
  49. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates//344/272/247/345/223/201/345/215/241.template.md +132 -0
  50. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/SKILL.md +60 -0
  51. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/build_facts_html.py +430 -0
  52. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_html.py +1287 -0
  53. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_report.py +189 -0
  54. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/templates//344/272/213/345/256/236/350/265/204/346/226/231/345/272/223.template.md +55 -0
  55. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/SKILL.md +36 -0
  56. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates/README.md +10 -0
  57. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//344/272/213/345/256/236.template.md +82 -0
  58. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//345/215/225/351/233/206/346/226/207/346/241/243.template.md +175 -0
  59. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//350/204/232/346/234/254.template.md +69 -0
  60. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//351/252/250/346/236/266-v1.1.md +174 -0
  61. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0050-design/SKILL.md +55 -0
  62. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0060-assets/SKILL.md +29 -0
  63. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/SKILL.md +42 -0
  64. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/scripts/validate_shots.py +207 -0
  65. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/shots.schema.json +186 -0
  66. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/345/214/205.template.md +1 -0
  67. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/350/257/264/346/230/216.md +15 -0
  68. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//346/210/220/347/211/207/345/214/205.template.md +49 -0
  69. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0080-factory/SKILL.md +58 -0
  70. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/SKILL.md +424 -0
  71. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/evals.md +41 -0
  72. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/CONTRACT.md +123 -0
  73. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/SKILL.md +81 -0
  74. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow//346/211/247/350/241/214/346/270/205/345/215/225.template.md +60 -0
  75. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/SKILL.md +78 -0
  76. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/agents/openai.yaml +4 -0
  77. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/knowledge-town-paper-film-v1.json +80 -0
  78. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/AudioTracks.tsx +20 -0
  79. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CaptionLayer.tsx +18 -0
  80. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CueTimeline.ts +24 -0
  81. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/KnowledgeTownComposition.tsx +21 -0
  82. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/cinematic-direction.md +73 -0
  83. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/sticker-asset-production.md +30 -0
  84. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/style-guide.md +43 -0
  85. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/build_cue_timeline.py +75 -0
  86. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_cue_timeline.py +68 -0
  87. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_style_preset.py +22 -0
  88. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/SKILL.md +0 -511
  89. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-data.md +0 -126
  90. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-network.md +0 -88
  91. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-runtime.md +0 -195
  92. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/security-doc-search.md +0 -179
  93. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_base.sh +0 -39
  94. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_capacity.sh +0 -278
  95. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_db.sh +0 -147
  96. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_docker.sh +0 -230
  97. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_env.sh +0 -64
  98. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_https.sh +0 -60
  99. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_logs.sh +0 -209
  100. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_nginx.sh +0 -100
  101. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_resilience.sh +0 -157
  102. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_secrets.sh +0 -265
  103. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_services.sh +0 -94
  104. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_ssh.sh +0 -56
  105. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/run_all.sh +0 -105
@@ -0,0 +1,60 @@
1
+ ---
2
+ version: 7
3
+ topic: "{主题}"
4
+ date: "{YYYY-MM-DD}"
5
+ episode_dir: "episodes/{YYYY-MM-DD}-{主题}"
6
+ work_dir: "media"
7
+ updated: "{YYYY-MM-DD}"
8
+ next_stage: ideation
9
+ # 因果:主题 → 0010(竞品+AI地图) → 0030事实 → 0040脚本
10
+ run_mode: ""
11
+ factory_choice: null
12
+ ---
13
+
14
+ # 执行清单 · {主题}
15
+
16
+ > 跨集:`.dwy/uc-media/channel-profile.md` · `topic-backlog.md`
17
+ > **0010** = 竞品 + **AI 推荐演讲地图**(已合并原 0020)
18
+
19
+ ## 进度
20
+
21
+ | # | stage | skill | status | 主文档 | summary | updated |
22
+ |---|-------|--------|--------|--------|---------|---------|
23
+ | — | channel | uc-media-0010-product | pending | `.dwy/uc-media/channel-profile.md` | 跨集 | |
24
+ | 0 | ideation | uc-media-0005-ideation | pending | `.dwy/uc-media/topic-backlog.md` | 可 skipped | |
25
+ | 1 | product | uc-media-0010-product | pending | `0010-product/产品卡-*.md` | 竞品+AI地图 | |
26
+ | 2 | facts | uc-media-0030-facts | pending | `0030-facts/事实-*.md` | 按地图扩展 | |
27
+ | 3 | script | uc-media-0040-script | pending | `0040-script/脚本-*.md` | | |
28
+ | 4 | design | uc-media-0050-design | pending | `0050-design/design-lock.md` | | |
29
+ | 5 | assets | uc-media-0060-assets | pending | `0060-assets/素材清单.md` | fast 连跑 | |
30
+ | 6 | package | uc-media-0070-package | pending | `0070-package/成片包.md` + shots | | |
31
+ | 7 | factory | uc-media-0080-factory | pending | `0080-factory/适配说明.md` | | |
32
+ | 8 | packaging | media-platform-packaging | pending | `0070-package/` 包装 | 可选 | |
33
+
34
+ status:`pending | in_progress | ready_for_confirm | done | stale | failed | skipped`
35
+
36
+ ## 工厂选型(0080)
37
+
38
+ | 项 | 值 |
39
+ |----|-----|
40
+ | 用户选择 | remotion / hyperframe / (未选) |
41
+ | 工程目录 | |
42
+ | 确认日期 | |
43
+
44
+ ## 硬停记录
45
+
46
+ | 节点 | 确认 | 日期 |
47
+ |------|------|------|
48
+ | 0005 人选 | | |
49
+ | 0010 产品卡(地图) | | |
50
+ | 0030 事实 | | |
51
+ | 0040 脚本 | | |
52
+ | 0050 design | | |
53
+ | 0070 成片包 | | |
54
+ | 0080 工厂 | | |
55
+ | render | | |
56
+ | 0090 包装 | | |
57
+
58
+ ## 备注
59
+
60
+ -
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: uc-media-knowledge-town
3
+ description: |
4
+ 将科普脚本制作成“知识小城”纸片电影:固定 LLM-S01-ChatDoor 的贴纸视觉系统,允许不同主题自由设计场景、道具和镜头,并生成可由 Remotion 渲染的素材、镜头与对轨数据。用于 0050 锁定视觉、0060 生成和验收角色/道具/档案贴纸、0070 组装 cue-timeline 或 0080 Remotion 制作;适用于二维纸片拼贴、边讲边展示、字幕/口播/BGM/音效同步的知识视频。
5
+ ---
6
+
7
+ # 知识小城纸片电影制作标准
8
+
9
+ 使用本 skill 时,保留现有流水线分工:0040 决定说什么,0050 选风格,0060 管素材,0070 生成机读对轨包,0080 生成 Remotion 工程。不要另建平行视频流程。
10
+
11
+ 本 skill 分成两层:**频道贴纸系统固定**,以 LLM `S01PaperFilmPreview` 的纸张、描边、阴影、强调色、名牌、档案框和底部对白框为母版;**主题叙事世界可变**,每期可自由选择空间、道具、构图与镜头。不要把 LLM 的聊天门、手机或 AI 主题当作跨集必需元素。
12
+
13
+ ## 风格锁定
14
+
15
+ 在 `0050-design/design-lock.md` 写入:
16
+
17
+ ```yaml
18
+ style_id: knowledge-town-paper-film-v1
19
+ production_stack: remotion
20
+ sync_source: media/0070-package/cue-timeline.json
21
+ ```
22
+
23
+ 读取 [`references/style-guide.md`](./references/style-guide.md)、[`references/sticker-asset-production.md`](./references/sticker-asset-production.md)、[`references/cinematic-direction.md`](./references/cinematic-direction.md) 和频道角色真源 `docs/知识小城频道角色规范.md`,再把 [`assets/knowledge-town-paper-film-v1.json`](./assets/knowledge-town-paper-film-v1.json) 复制到本集 `0060-assets/` 作为可改预设。品牌图标必须先从 `shared/icons/brands/` 取正式文件。
24
+
25
+ 0060 先按贴纸规范生成或采购素材,再在深海军蓝与暖米纸底色检查透明边缘。通过后才可登记 `shared/` 并复制进本集;真实截图保持原貌,必须放入统一档案贴纸框。不得为某一主题另起描边、阴影、卡片、箭头或字幕体系。
26
+
27
+ ## 对轨工作流
28
+
29
+ 1. 用 0040 的定稿口播产生配音和逐句字幕;配音真实时长是唯一时长基准。
30
+ 2. 为每句字幕指定一个 `cueId`、一个 `visualAction` 和零到多个 SFX。
31
+ 3. 运行 `scripts/build_cue_timeline.py` 合并字幕、视觉 beats 与预设,生成 `0070-package/cue-timeline.json`。
32
+ 4. 运行 `scripts/validate_cue_timeline.py`;失败时回到 0040/0060 修正,禁止在 0080 私自改时码。
33
+ 5. 在 Remotion 使用 `assets/remotion/` 的组件模板:一个 timeline 同时驱动旁白、字幕、动作和音效。
34
+
35
+ ```bash
36
+ python3 scripts/build_cue_timeline.py \
37
+ --captions media/0040-script/captions-remotion-strict.json \
38
+ --beats media/0070-package/caption-visual-beats.json \
39
+ --preset media/0060-assets/knowledge-town-paper-film-v1.json \
40
+ --voice audio/voice/s01-cn-v1.wav \
41
+ --output media/0070-package/cue-timeline.json
42
+ python3 scripts/validate_cue_timeline.py media/0070-package/cue-timeline.json
43
+ ```
44
+
45
+ ## 硬规则
46
+
47
+ - 一个 `cueId` 只讲一个新信息;视觉动作在字幕开始后 2–4 帧预备,关键词在本 cue 的 40%–55% 出现。
48
+ - 先为每个 cue 写 `事实来源 → 主问题 → 主动作 → 可见结果 → 术语揭示`;画面中出现的文字、年份、因果和示例必须可回溯至 0030 facts 或已批准口播。类比必须标明是类比,不能把道具名称当成技术事实。
49
+ - 一个 cue 最多 `1 主角 + 1 道具 + 1 信息对象`,最多一条从原因到结果的运动路径。下一 cue 改讲新事时,先将旧元素收拢、淡出或转成转场物,禁止叠加成信息板。
50
+ - **cue 场景图(硬门槛)**:实现前必须为每个 cue 列出 `elementId / 角色 / 文字来源 / 入场 / 落点 / 退场 / z-index 层`。当前 cue 只能保留本 cue 的对象,或最多一个已声明的转场物;上一 cue 的普通对象必须在新 cue 主动作开始前退场,禁止隐式继承造成叠字或残留道具。
51
+ - **层级与依赖(硬门槛)**:层级固定为 `背景 0–9 → 舞台 10–19 → 连线/光束 20–29 → 道具 30–39 → 可读文字卡 40–49 → 前景贴纸 50–59 → 字幕 60+`。连线必须低于它指向的卡片,且只能在两个对象的空隙中存在;箭头线、箭头尖、发光和说明标签必须继承同一可见性与退场状态,禁止出现孤立线条、孤立箭头或标签滞留。
52
+ - **文字白名单(硬门槛)**:屏幕上的每一段文字必须是“当前口播术语、事实例子、当前 cue 关键词或必要的操作标签”之一;不在 cue 场景图中的文字、数字、装饰性标签和重复术语一律不得出现。顶部关键词已表达的术语,内容区不得再重复。
53
+ - **视觉语义(硬门槛)**:每个圆圈、箭头、光晕、线条和图标都必须在场景图中写明“指向谁 / 表示什么 / 何时消失”。没有叙事对象的装饰图形不得出现;高亮必须贴合被强调对象的轮廓,不能用泛化圆圈圈住一片空白。箭头必须从可见的原因对象出发、停在结果对象外沿,路径本身要能读出方向与因果。
54
+ - **箭头原语(硬门槛)**:同一集只允许一套关系箭头原语:深蓝描边 + 珊瑚红纸带 + 固定比例的小箭头头部 + 绘制入场。关系默认使用对象之间的水平直连;不得用弧线、绕行线或流程图式折线增加装饰。不得混用 CSS 三角形、默认字符箭头、无描边线或不同颜色的箭头。空间不足时先重排对象,禁止把箭头压缩成只剩箭头头部。
55
+ - 术语只在观众看懂动作后的结果帧出现一次;顶部关键词、中心贴纸和字幕不可重复同一术语。术语贴纸出现时,顶部关键词必须让位。
56
+ - 每镜采用 `base_bg → content → text_sticker` 三层;不得用一张完成态大图替代渐进揭示。
57
+ - **文字与路径防碰撞(硬门槛)**:每张可读文字卡、术语贴纸、角色名牌的可见边界(含描边与阴影)之间至少留 `24px`;连线、箭头、光束只能停在对象外沿,不得压过文字、描边、阴影或进入文字卡的点击/阅读区。需要指向时,箭头尖端与对象外沿保留 `8–16px` 间隙。
58
+ - 禁止用不完整边框、孤立的半截箭头、无闭合折角来模拟纸片尾巴或连接关系;纸条拐角必须是完整闭合形状,连接线须在两个对象的空隙中可见且语义单一。
59
+ - 底部 180px 是字幕带;关键内容的包围盒不得进入。
60
+ - BGM、SFX、旁白分别建资产;旁白存在时 BGM 自动压低。只使用原创或许可明确可商用的音频和声音。
61
+ - `cue-timeline.json` 是 0070 对轨真源;Remotion 组件不得自行写死字幕或 SFX 帧。
62
+ - `LLM-S01-ChatDoor` 是频道贴纸系统的唯一质量母版,不是主题故事模板:先有角色要解决的事,再进入可感知的空间,由本主题英雄道具完成一次可见的变化,最后落在一个结果画面。
63
+ - 每个正式场景必须提交独立 Remotion composition;先做 8–20 秒动态样片并获确认,再推广到全场。不得只交付整片预览。
64
+ - 卡片、时间线节点、流程箭头只能作为道具的短暂状态,不能并排常驻承担讲解;禁止“背景 + 三张大卡 + 箭头 + 字幕”的成片结构。
65
+ - 每场至少包含:前/中/后景,角色或道具的因果动作,一次镜头推进/拉远/穿行/转场,以及动作落点音效。没有这些只可标为预演,不可标为正式镜头。
66
+ - 每个 cue 必须同时具备:`1 个叙事动作 + 1 个低频质感微动 + 1 个明确落点`。叙事动作推动因果(递交、选择、拆开、扫查、折叠等);质感微动只增强纸片空间(纸张轻呼吸、光圈扩散、景深视差、阴影微偏移等),不得替代叙事;落点必须在动作结束时清晰可见,并配对应 SFX 或静默停顿。
67
+ - 禁止无意义的漂浮、循环旋转、待机晃动和与画面无关的音效;角色只在入场、指向、递交、反应或收束等叙事动作时动画。
68
+ - 每场先输出 3–4 张关键帧(建立、推进、术语结果、转场),逐张检查:无遮挡、无重复术语、无并排卡片墙、无事实越界;并以文字卡的描边/阴影外扩边界复核最小间距、连线终点与箭头尖端。通过后才渲染独立预览。
69
+ - **动态碰撞验收(硬门槛)**:除建立/结果帧外,还必须抽检每段运动的 `25% / 50% / 75% / 90%` 帧。检查真实包围盒(文字、描边、阴影、缩放后的范围)是否相撞;尤其检查对象汇聚时不可落到同一坐标。任何重叠、遮字、无关残留或未绑定的线条,必须先修复场景图和生命周期,不能只靠微调坐标遮掩。
70
+
71
+ ## 资源
72
+
73
+ - 风格规则:[`references/style-guide.md`](./references/style-guide.md)
74
+ - 素材生成与透明验收:[`references/sticker-asset-production.md`](./references/sticker-asset-production.md)
75
+ - 电影化导演规则与镜头配方:[`references/cinematic-direction.md`](./references/cinematic-direction.md)
76
+ - 可复制预设:[`assets/knowledge-town-paper-film-v1.json`](./assets/knowledge-town-paper-film-v1.json)
77
+ - 对轨构建与校验:[`scripts/`](./scripts/)
78
+ - Remotion 组件模板:[`assets/remotion/`](./assets/remotion/)
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "知识小城统一视觉视频制作"
3
+ short_description: "生成统一贴纸风格、可对轨的科普视频资产。"
4
+ default_prompt: "使用 $uc-media-knowledge-town 为本期科普锁定知识小城贴纸视觉系统,并建立可由 Remotion 渲染的素材、字幕、音频与镜头动作数据。"
@@ -0,0 +1,80 @@
1
+ {
2
+ "version": 2,
3
+ "styleId": "knowledge-town-paper-film-v1",
4
+ "characterSystem": "xiaoqi-bubu-v1",
5
+ "characterRoles": {
6
+ "xiaoqi": "audience_question_and_reaction",
7
+ "bubu": "guide_prop_and_plain_language_takeaway"
8
+ },
9
+ "fps": 30,
10
+ "canvas": {"width": 1920, "height": 1080, "subtitleBandPx": 180},
11
+ "visualTokens": {
12
+ "paperBase": "#f4e8c6",
13
+ "paperCard": "#fffdf5",
14
+ "ink": "#14213d",
15
+ "accent": ["#ffd161", "#77dfd1", "#ff9cb9"],
16
+ "shadow": {"color": "#14213d", "offsetPx": [10, 12], "blurPx": 0},
17
+ "texture": "warm-rice-paper-visible"
18
+ },
19
+ "stickerSystem": {
20
+ "caption": {"required": true, "position": "bottom-dialogue-frame", "maxLines": 2},
21
+ "nameplate": {"use": "first-entry-or-identity-only", "border": "ink", "shadow": "hard-offset"},
22
+ "archiveFrame": {"use": "real-screenshot-or-source", "preserveOriginalImage": true},
23
+ "relationshipArrow": {"style": "ink-outline-coral-paper-ribbon", "motion": "draw-in"},
24
+ "forbidden": ["pure-black-shadow", "soft-blur-shadow", "glassmorphism", "gradient-sticker", "3d-render", "photorealistic-subject", "second-card-system"]
25
+ },
26
+ "assetAcceptance": {
27
+ "format": "png-alpha",
28
+ "alphaPreviewBackgrounds": ["#14213d", "#f4e8c6"],
29
+ "reject": ["magenta-fringe", "green-screen-fringe", "missing-alpha", "baked-text", "baked-ui", "baked-logo"]
30
+ },
31
+ "themeFreedom": {
32
+ "allowThemeSpecificSceneAndCamera": true,
33
+ "allowThemeSpecificHeroProps": true,
34
+ "mustKeep": ["visualTokens", "stickerSystem", "caption-system", "archive-frame"]
35
+ },
36
+ "layers": ["base_bg", "content", "text_sticker", "caption"],
37
+ "timing": {"prepareFrames": 3, "actionProgress": 0.2, "labelProgress": 0.48},
38
+ "audio": {
39
+ "bgmVolume": 0.18,
40
+ "bgmDuckVolume": 0.06,
41
+ "sfxMap": {
42
+ "chatBubblesPop": [{"file": "audio/sfx/paper-icon-pop-v1.wav", "offsetFrames": 24, "volume": 0.75}],
43
+ "llmLabelReveal": [{"file": "audio/sfx/paper-icon-pop-v1.wav", "offsetFrames": 9, "volume": 0.5}],
44
+ "comicBoxOpen": [{"file": "audio/sfx/town-arrival-chime-v1.wav", "offsetFrames": 0, "volume": 0.48}],
45
+ "mapPiecesGather": [{"file": "audio/sfx/town-arrival-chime-v1.wav", "offsetFrames": 0, "volume": 0.68}]
46
+ }
47
+ },
48
+ "qualityGate": {
49
+ "referenceComposition": "LLM-S01-ChatDoor",
50
+ "requiredBeats": ["problem", "action", "choice_or_turn", "result", "clear_or_transition"],
51
+ "requiredSpatialLayers": 3,
52
+ "requireStandalonePreview": true,
53
+ "forbidCardWallAsFinal": true,
54
+ "forbidIdleMotion": true,
55
+ "maxConcurrentNarrativeObjects": 3,
56
+ "requireFactTraceability": true,
57
+ "forbidDuplicateTermAcrossCaptionAndSticker": true,
58
+ "requireNarrativeActionPerCue": true,
59
+ "requireAmbientMicroMotionPerCue": true,
60
+ "requireReadableLandingPerCue": true,
61
+ "requireTextAndPathCollisionCheck": true,
62
+ "requireSceneGraphPerCue": true,
63
+ "requireExplicitExitForPriorCueElements": true,
64
+ "requireDependentConnectorLifecycle": true,
65
+ "forbidUndeclaredOnScreenText": true,
66
+ "requireZOrderBands": true,
67
+ "requireMidMotionCollisionSampling": true,
68
+ "collisionSampleFrames": [0.25, 0.5, 0.75, 0.9],
69
+ "maxCarryoverObjects": 1,
70
+ "minTextCardOuterGapPx": 24,
71
+ "arrowTipClearancePx": [8, 16],
72
+ "forbidIncompletePaperCorners": true
73
+ },
74
+ "recipes": {
75
+ "themeOpening": ["theme-space", "character-or-hero-prop", "paper-entrance", "result-reveal"],
76
+ "sourceCheck": ["claim-or-object", "archive-frame", "check-action", "verified-result"],
77
+ "mapFinale": ["topic-results", "paper-fold", "path-or-map"],
78
+ "cardWall": "preview_only_never_final"
79
+ }
80
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import {Sequence, staticFile, useCurrentFrame} from 'remotion';
3
+ import {Audio} from '@remotion/media';
4
+ import type {CueTimeline} from './CueTimeline';
5
+
6
+ export const AudioTracks: React.FC<{timeline: CueTimeline; bgmFile: string}> = ({timeline, bgmFile}) => {
7
+ const frame = useCurrentFrame();
8
+ const hasVoice = timeline.cues.some((cue) => frame >= cue.startFrame && frame < cue.endFrame);
9
+ return (
10
+ <>
11
+ <Audio src={staticFile(bgmFile)} volume={hasVoice ? 0.06 : 0.18} />
12
+ <Audio src={staticFile(timeline.voice.file)} volume={1} />
13
+ {timeline.cues.flatMap((cue) => cue.sfx).map((effect) => (
14
+ <Sequence key={`${effect.file}-${effect.frame}`} from={effect.frame} layout="none">
15
+ <Audio src={staticFile(effect.file)} volume={effect.volume ?? 0.7} />
16
+ </Sequence>
17
+ ))}
18
+ </>
19
+ );
20
+ };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import {interpolate, useCurrentFrame} from 'remotion';
3
+ import type {CueTimeline} from './CueTimeline';
4
+
5
+ export const CaptionLayer: React.FC<{timeline: CueTimeline}> = ({timeline}) => {
6
+ const frame = useCurrentFrame();
7
+ const cue = timeline.cues.find((item) => frame >= item.startFrame && frame < item.endFrame);
8
+ if (!cue) return null;
9
+ const opacity = interpolate(frame, [cue.startFrame, cue.startFrame + 4], [0, 1], {
10
+ extrapolateLeft: 'clamp',
11
+ extrapolateRight: 'clamp',
12
+ });
13
+ return (
14
+ <div style={{position: 'absolute', left: 220, right: 220, bottom: 42, minHeight: 112, zIndex: 50, opacity, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '18px 42px', color: '#14213d', background: '#fffdf5', border: '7px solid #14213d', borderRadius: 32, boxShadow: '0 12px 0 #14213d', fontFamily: 'Arial, PingFang SC, sans-serif', fontSize: 46, fontWeight: 900, lineHeight: 1.25, textAlign: 'center'}}>
15
+ {cue.caption.text}
16
+ </div>
17
+ );
18
+ };
@@ -0,0 +1,24 @@
1
+ export type CueSfx = {
2
+ file: string;
3
+ frame: number;
4
+ volume?: number;
5
+ };
6
+
7
+ export type Cue = {
8
+ cueId: string;
9
+ sceneId: string;
10
+ startFrame: number;
11
+ endFrame: number;
12
+ voText: string;
13
+ caption: {text: string; mode: 'bottomSpeechBubble'};
14
+ visual: {action: string; keyword: string};
15
+ sfx: CueSfx[];
16
+ };
17
+
18
+ export type CueTimeline = {
19
+ version: 1;
20
+ fps: number;
21
+ voice: {file: string; timingStatus: string};
22
+ canvas: {width: number; height: number; subtitleBandPx: number};
23
+ cues: Cue[];
24
+ };
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import {AbsoluteFill, useCurrentFrame} from 'remotion';
3
+ import {AudioTracks} from './AudioTracks';
4
+ import {CaptionLayer} from './CaptionLayer';
5
+ import type {CueTimeline} from './CueTimeline';
6
+
7
+ export const KnowledgeTownComposition: React.FC<{
8
+ timeline: CueTimeline;
9
+ bgmFile: string;
10
+ renderVisual: (action: string, frame: number) => React.ReactNode;
11
+ }> = ({timeline, bgmFile, renderVisual}) => {
12
+ const frame = useCurrentFrame();
13
+ const cue = timeline.cues.find((item) => frame >= item.startFrame && frame < item.endFrame);
14
+ return (
15
+ <AbsoluteFill style={{background: '#f4e8c6', overflow: 'hidden'}}>
16
+ {renderVisual(cue?.visual.action ?? 'hold', frame)}
17
+ <CaptionLayer timeline={timeline} />
18
+ <AudioTracks timeline={timeline} bgmFile={bgmFile} />
19
+ </AbsoluteFill>
20
+ );
21
+ };
@@ -0,0 +1,73 @@
1
+ # 电影化导演规则
2
+
3
+ ## Cue 模板
4
+
5
+ 每个 cue 先写五行,再画画面:
6
+
7
+ ```text
8
+ 事实来源:0030 facts / 已批准口播的句子
9
+ 主问题:观众此刻要解决的一个问题
10
+ 主动作:角色或英雄道具完成的一个动作
11
+ 可见结果:动作结束后唯一留下的新理解
12
+ 术语揭示:只在结果出现一次;没有必要则留空
13
+ ```
14
+
15
+ 禁止把“方便讲解的道具名”写成技术结论。比如 token 只能表现为“模型处理的文字小块,切分因模型而异”,不能用一个汉字断言一个 token。
16
+
17
+ ## 镜头语法
18
+
19
+ 1. **建立**:一名角色或一个道具进入可感知空间。
20
+ 2. **推进**:镜头推近唯一关键物;让它递交、拆开、选择、比较、盖章或折叠。
21
+ 3. **结果**:旧元素退场,只留一个新结果物;术语在这一拍出现。
22
+ 4. **转场**:结果物变成下一场的入口;不硬切到说明板。
23
+
24
+ ## 动效硬门槛
25
+
26
+ 每个 cue 都必须列出并实现以下三项:
27
+
28
+ 1. **叙事动作**:让信息发生变化,例如纸带送入、候选弹出、词卡回指、答案被选中、来源被扫查。
29
+ 2. **低频质感微动**:幅度小、速度慢,服务空间与材质,例如纸张 2–4px 呼吸、光圈扩散、前中后景视差、阴影微偏移。
30
+ 3. **明确落点**:动作结束后只留下一个可理解的结果,配一个准确 SFX 或 6–12 帧静默停顿。
31
+
32
+ 禁止用循环摇晃、无目的漂浮或持续旋转凑动效;它们不计入覆盖。
33
+
34
+ ## 场景图与叠放规范
35
+
36
+ 每个 cue 在编码前先给元素编号,并写清“何时出现、何时消失、处于哪层”。没有退场规则的元素不得进入下一 cue;例外只允许一个明确的转场物。
37
+
38
+ ```text
39
+ bg(0–9) → stage(10–19) → connector(20–29) → prop(30–39)
40
+ → readable-card(40–49) → foreground-sticker(50–59) → caption(60+)
41
+ ```
42
+
43
+ - 连线和箭头只在两个对象都可见时绘制,位于对象下层;箭头尖端停在目标外沿 `8–16px`,不穿入卡片、描边或阴影。
44
+ - 同一术语只能有一个屏幕承载点;优先顶部关键词,其余区域只呈现动作或事实例子。
45
+ - 对象做汇聚动画时,必须指定不同的最终落点;禁止多个标签落在同一坐标。
46
+ - 圆圈、光晕、箭头、连线和图标都要有明确的叙事对象与解释:高亮贴合目标形状;箭头从原因对象出发、停在结果对象外沿。不能说明“它在指谁、为何此刻出现”的装饰,一律删除。
47
+ - 关系箭头统一为深蓝描边、珊瑚红纸带、固定小箭头头部与绘制入场,并以对象之间的水平直连为默认。禁止弧线、绕行线、默认字符箭头、CSS 三角箭头和无描边线条混用。
48
+ - 每段运动抽检 `25% / 50% / 75% / 90%` 帧,按含阴影的真实包围盒查碰撞。发现遮挡、叠字、残留线条时,先修生命周期/层级,再调坐标。
49
+
50
+ ## 常用配方
51
+
52
+ | 讲解意图 | 电影动作 | 结果物 |
53
+ | --- | --- | --- |
54
+ | 预测候选 | 空位被推近,候选依次弹出,选中一张 | 被接回句尾的纸片 |
55
+ | 上下文关系 | 指代词孤立,手电只连到相关词 | 一条定格关系线 |
56
+ | 历史 | 引擎变车头,列车一次只停一站 | 下一站入口 |
57
+ | 训练步骤 | 黑暗中一次只开一扇门 | 当前步骤的门牌 |
58
+ | 可靠性 | 答案纸滑出,印章落下,具体处剥落 | 放大镜下的来源标签 |
59
+ | 收束 | 已验证概念按口播顺序折成路径 | 一个可提问的结尾 |
60
+
61
+ ## 验收
62
+
63
+ 逐帧检查建立、推进、结果、转场四帧:
64
+
65
+ - 是否只看得出一个主问题和一个动作?
66
+ - 是否所有文字和因果能回溯至 facts/口播?
67
+ - 是否术语只出现一次,且没有被字幕或顶部关键词重复?
68
+ - 是否旧画面已清场,没有遮挡、交叉线或站桩角色?
69
+ - 是否结果物能自然带入下一 cue?
70
+ - 是否每个 cue 都有叙事动作、低频质感微动和明确落点?
71
+ - 文字卡、术语贴纸、角色名牌是否包含描边与阴影后仍互不相撞(最小 `24px`)?
72
+ - 所有箭头、连线、光束是否只在对象间空隙中运动,尖端离对象外沿 `8–16px`,没有覆盖文字或边框?
73
+ - 纸条角、气泡尾巴、折页边是否为完整闭合形状,没有孤立折线或半截图形?
@@ -0,0 +1,30 @@
1
+ # 知识小城贴纸素材生产
2
+
3
+ 在 0060 生成或采购素材前,先读取 `knowledge-town-paper-film-v1.json` 的 `visualTokens` 与 `assetAcceptance`。
4
+
5
+ ## 通用提示词前缀
6
+
7
+ ```text
8
+ 2D paper-cut collage sticker, warm rice-paper texture, deep navy #14213d thick ink outline, crisp offset deep-navy paper shadow, warm cream paper base, small accents only in bright yellow #ffd161, teal #77dfd1 and coral pink #ff9cb9, tactile layered paper, clean readable silhouette, no text, no logo, no UI, no watermark, no photorealism, no 3D render
9
+ ```
10
+
11
+ ### 变量模板
12
+
13
+ | 素材 | 在前缀后补充 |
14
+ |---|---|
15
+ | 角色 | `full-body [role and action], transparent-background source, generous padding, no cast shadow` |
16
+ | 单道具 | `one oversized [topic prop] showing [single action], transparent-background source, generous padding` |
17
+ | 空景 | `[topic space] with foreground, midground and background layers, empty lower 180px subtitle area, no character, no text` |
18
+ | 档案框 | `empty paper archive frame with wide image window, no text, transparent-background source` |
19
+
20
+ ## 透明资产验收
21
+
22
+ 1. 生图阶段使用单一、远离主体配色的纯色键背景;不得有渐变、地面、投影或反射。
23
+ 2. 转换为 alpha PNG 后,在 `#14213d` 和 `#f4e8c6` 两种底色预览。
24
+ 3. 检查完整轮廓、头发和深色线稿;不能有洋红/绿色残边、黑洞、锯齿或被擦除的细节。
25
+ 4. 通过后,将正式 PNG、来源、版本、角色/道具用途与深浅底检查结果登记进共享资产表;色键源图只留作溯源。
26
+ 5. 未通过验收的文件不得复制到新集或标记为 `cross_episode`。
27
+
28
+ ## 不可变与可变
29
+
30
+ 不可变:纸张、深海军蓝描边与硬阴影、强调色、贴纸卡、档案框、箭头和对白框。可变:主题空间、主题道具、角色动作、镜头语言和故事节奏。
@@ -0,0 +1,43 @@
1
+ # 知识小城视觉规范
2
+
3
+ 以 LLM 集 `S01PaperFilmPreview` 为频道贴纸系统的唯一母版。它统一素材语言,不限制每期的主题空间、道具或镜头。
4
+
5
+ ## 频道贴纸系统(固定)
6
+
7
+ | 项目 | 标准 |
8
+ |---|---|
9
+ | 纸张 | 暖米纸底 `#f4e8c6`、可见米纸纹;卡片底 `#fffdf5` |
10
+ | 描边 | 深海军蓝 `#14213d` 粗描边 |
11
+ | 强调色 | 明黄 `#ffd161`、青绿 `#77dfd1`、珊瑚粉 `#ff9cb9`;只作小面积强调 |
12
+ | 阴影 | 深海军蓝硬边偏移阴影,默认 `10px 12px 0 #14213d`;不用纯黑或柔焦阴影 |
13
+ | 文字 | 文字由 Remotion 渲染;贴纸有粗描边、圆角与硬阴影,不烤进生图 |
14
+ | 字幕 | 底部对白框,最多两行;不与术语贴纸重复同一词 |
15
+
16
+ 所有自制角色、道具、术语卡、事实卡、关系箭头、档案框和地图收束件都必须使用这套 Token。不要引入玻璃拟态、渐变贴纸、3D、写实主体或第二套卡片/箭头体系。
17
+
18
+ ### 标准组件
19
+
20
+ - **角色名牌**:只在首次入场或身份需要识别时出现;明黄或青绿底、海军蓝描边与硬阴影。
21
+ - **术语贴纸**:在动作产生可见结果后出现一次,使用米白或强调色底;不得和字幕重复。
22
+ - **档案贴纸框**:真实截图、GitHub 页面、项目界面和资料图保持原貌,只放入统一海军蓝描边的框内。
23
+ - **关系箭头**:深海军蓝描边 + 珊瑚色纸带 + 绘制入场;只连接有因果关系的两个对象。
24
+ - **底部对白框**:所有主题共用,留出 `180px` 字幕带;不能被道具或角色侵入。
25
+
26
+ ## 主题叙事世界(可变)
27
+
28
+ 许可证闸机、聊天门、档案列车、实验室、地图等都只是主题实例。每期可以自由改变场景、道具、构图、景别、机位、镜头推进与转场动作,但不得改变频道贴纸系统。
29
+
30
+ 一场仍按 `问题 → 行动 → 意外或选择 → 结果` 展开。角色或英雄道具推进因果;卡片、箭头和时间线只能短暂作为道具状态,不可变成常驻讲解板。
31
+
32
+ ## 素材准入
33
+
34
+ - 角色和单道具使用透明 PNG;先检查 alpha 边缘,再登记共享资产库。
35
+ - 在深海军蓝 `#14213d` 和暖米纸 `#f4e8c6` 底色各预览一次;出现洋红、绿幕残边、缺口或暗部被抠空即拒绝。
36
+ - 生图只输出角色、场景或单道具:禁止烤入中文、品牌 Logo、UI、事实结论和字幕。
37
+ - 真实截图不得重绘成纸片画;使用档案贴纸框,并在本集素材表记录来源。
38
+
39
+ ## 叙事与对轨
40
+
41
+ 每个 cue 依次经历 `prepare → action → label → hold → clear/transition`。先让观众看到动作和结果,再出现术语贴纸。每个 cue 只保留一个主角/道具/信息对象组合;上一 cue 的元素必须退场或折成明确的转场物。
42
+
43
+ 详细镜头、层级、文字和碰撞验收规则见 [`cinematic-direction.md`](./cinematic-direction.md)。0060 的生成提示词与透明素材验收见 [`sticker-asset-production.md`](./sticker-asset-production.md)。
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """Build the single source of truth for narration, captions, visuals, and SFX."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+
9
+
10
+ def load_json(path: Path):
11
+ with path.open(encoding="utf-8") as handle:
12
+ return json.load(handle)
13
+
14
+
15
+ def main() -> None:
16
+ parser = argparse.ArgumentParser()
17
+ parser.add_argument("--captions", type=Path, required=True)
18
+ parser.add_argument("--beats", type=Path, required=True)
19
+ parser.add_argument("--preset", type=Path, required=True)
20
+ parser.add_argument("--voice", required=True)
21
+ parser.add_argument("--output", type=Path, required=True)
22
+ args = parser.parse_args()
23
+
24
+ captions = load_json(args.captions)
25
+ beat_document = load_json(args.beats)
26
+ preset = load_json(args.preset)
27
+ fps = beat_document.get("fps", preset.get("fps", 30))
28
+ beats = beat_document.get("beats", [])
29
+ caption_index = {(item["startMs"], item["endMs"]): item for item in captions}
30
+ sfx_map = preset.get("audio", {}).get("sfxMap", {})
31
+ cues = []
32
+
33
+ for beat in beats:
34
+ caption = caption_index.get((beat["startMs"], beat["endMs"]))
35
+ if caption is None:
36
+ raise ValueError(f"缺少与 {beat['cueId']} 对应的字幕时码")
37
+ if caption["text"].strip() != beat["captionText"].strip():
38
+ raise ValueError(f"{beat['cueId']} 字幕文本与视觉 beat 不一致")
39
+ sfx = []
40
+ for item in sfx_map.get(beat["visualAction"], []):
41
+ start_frame = beat["startFrame"] + item["offsetFrames"]
42
+ sfx.append({**item, "frame": start_frame})
43
+ cues.append({
44
+ "cueId": beat["cueId"],
45
+ "sceneId": beat["sceneId"],
46
+ "startMs": beat["startMs"],
47
+ "endMs": beat["endMs"],
48
+ "startFrame": beat["startFrame"],
49
+ "endFrame": beat["endFrame"],
50
+ "voText": caption["text"],
51
+ "caption": {"text": caption["text"], "mode": beat.get("captionMode", "bottomSpeechBubble")},
52
+ "visual": {
53
+ "action": beat["visualAction"],
54
+ "keyword": beat.get("onScreenKeyword", ""),
55
+ "timing": beat.get("progressiveTiming", {}),
56
+ },
57
+ "sfx": sfx,
58
+ })
59
+
60
+ output = {
61
+ "version": 1,
62
+ "styleId": preset["styleId"],
63
+ "fps": fps,
64
+ "voice": {"file": args.voice, "timingStatus": "estimated_until_forced_alignment"},
65
+ "canvas": preset["canvas"],
66
+ "audio": {"bgm": preset.get("audio", {}), "duckDuringVoice": True},
67
+ "cues": cues,
68
+ }
69
+ args.output.parent.mkdir(parents=True, exist_ok=True)
70
+ args.output.write_text(json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
71
+ print(args.output)
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ """Validate cue-timeline.json without third-party dependencies."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def validate(document: dict) -> list[str]:
11
+ errors: list[str] = []
12
+ if document.get("version") != 1:
13
+ errors.append("version must be 1")
14
+ fps = document.get("fps")
15
+ if not isinstance(fps, (int, float)) or fps <= 0:
16
+ errors.append("fps must be positive")
17
+ return errors
18
+ canvas = document.get("canvas", {})
19
+ if canvas.get("subtitleBandPx", 0) < 160:
20
+ errors.append("canvas.subtitleBandPx must be at least 160")
21
+ cues = document.get("cues")
22
+ if not isinstance(cues, list) or not cues:
23
+ return errors + ["cues must be a non-empty array"]
24
+ last_end_ms = last_end_frame = -1
25
+ ids: set[str] = set()
26
+ for cue in cues:
27
+ cue_id = cue.get("cueId")
28
+ if not cue_id or cue_id in ids:
29
+ errors.append(f"cueId must be unique: {cue_id!r}")
30
+ ids.add(cue_id)
31
+ start_ms, end_ms = cue.get("startMs"), cue.get("endMs")
32
+ start_frame, end_frame = cue.get("startFrame"), cue.get("endFrame")
33
+ if not isinstance(start_ms, int) or not isinstance(end_ms, int) or end_ms <= start_ms:
34
+ errors.append(f"{cue_id}: invalid millisecond range")
35
+ if not isinstance(start_frame, int) or not isinstance(end_frame, int) or end_frame <= start_frame:
36
+ errors.append(f"{cue_id}: invalid frame range")
37
+ if start_ms < last_end_ms or start_frame < last_end_frame:
38
+ errors.append(f"{cue_id}: overlaps previous cue")
39
+ if isinstance(start_ms, int) and isinstance(end_ms, int) and isinstance(start_frame, int) and isinstance(end_frame, int):
40
+ expected = round((end_ms - start_ms) * fps / 1000)
41
+ if abs((end_frame - start_frame) - expected) > 1:
42
+ errors.append(f"{cue_id}: frame duration disagrees with milliseconds")
43
+ if cue.get("voText") != cue.get("caption", {}).get("text"):
44
+ errors.append(f"{cue_id}: voiceover and caption text differ")
45
+ for effect in cue.get("sfx", []):
46
+ if effect.get("frame", -1) < start_frame or effect.get("frame", 10**9) >= end_frame:
47
+ errors.append(f"{cue_id}: SFX frame must fall inside the cue")
48
+ last_end_ms, last_end_frame = end_ms, end_frame
49
+ return errors
50
+
51
+
52
+ def main() -> int:
53
+ if len(sys.argv) != 2:
54
+ print("Usage: validate_cue_timeline.py cue-timeline.json", file=sys.stderr)
55
+ return 2
56
+ path = Path(sys.argv[1])
57
+ document = json.loads(path.read_text(encoding="utf-8"))
58
+ errors = validate(document)
59
+ if errors:
60
+ print(f"FAIL {path}")
61
+ print("\n".join(f" - {error}" for error in errors))
62
+ return 1
63
+ print(f"OK {path} cues={len(document['cues'])}")
64
+ return 0
65
+
66
+
67
+ if __name__ == "__main__":
68
+ raise SystemExit(main())
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env python3
2
+ """Validate the immutable visual tokens of the knowledge-town style preset."""
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+
8
+ PRESET_PATH = Path(__file__).resolve().parents[1] / "assets" / "knowledge-town-paper-film-v1.json"
9
+
10
+
11
+ def main() -> None:
12
+ preset = json.loads(PRESET_PATH.read_text(encoding="utf-8"))
13
+ assert preset["visualTokens"]["ink"] == "#14213d"
14
+ assert preset["visualTokens"]["accent"] == ["#ffd161", "#77dfd1", "#ff9cb9"]
15
+ assert preset["stickerSystem"]["caption"]["required"] is True
16
+ assert preset["assetAcceptance"]["alphaPreviewBackgrounds"] == ["#14213d", "#f4e8c6"]
17
+ assert preset["themeFreedom"]["allowThemeSpecificSceneAndCamera"] is True
18
+ print(f"Validated {preset['styleId']} visual system.")
19
+
20
+
21
+ if __name__ == "__main__":
22
+ main()