@oh-story/dsh 0.1.13 → 0.1.15

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 (58) hide show
  1. package/README.md +7 -7
  2. package/lib/client.js +932 -631
  3. package/lib/client.js.map +4 -4
  4. package/lib/index.js +1 -0
  5. package/lib/index.js.map +2 -2
  6. package/lib/oh-story/manifest.json +72 -72
  7. package/lib/oh-story/skills/story/SKILL.md +1 -1
  8. package/lib/oh-story/skills/story/VERSION +1 -1
  9. package/lib/oh-story/skills/story/references/author-memory-maintenance.md +2 -2
  10. package/lib/oh-story/skills/story/references/author-memory.md +4 -4
  11. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +61 -58
  12. package/lib/oh-story/skills/story-deslop/SKILL.md +1 -1
  13. package/lib/oh-story/skills/story-deslop/references/agent-calls.md +1 -1
  14. package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +2 -2
  15. package/lib/oh-story/skills/story-deslop/references/author-memory.md +4 -4
  16. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +61 -58
  17. package/lib/oh-story/skills/story-long-scan/SKILL.md +2 -7
  18. package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +11 -1
  19. package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +12 -2
  20. package/lib/oh-story/skills/story-long-write/SKILL.md +1 -1
  21. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +2 -2
  22. package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +2 -2
  23. package/lib/oh-story/skills/story-long-write/references/author-memory.md +4 -4
  24. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +1 -0
  25. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +2 -1
  26. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +1 -1
  27. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +61 -58
  28. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +87 -20
  29. package/lib/oh-story/skills/story-review/SKILL.md +1 -1
  30. package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +2 -2
  31. package/lib/oh-story/skills/story-review/references/author-memory.md +4 -4
  32. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +61 -58
  33. package/lib/oh-story/skills/story-short-write/SKILL.md +1 -1
  34. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +2 -2
  35. package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +2 -2
  36. package/lib/oh-story/skills/story-short-write/references/author-memory.md +4 -4
  37. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +61 -58
  38. package/lib/types/client/canvas-viewport.d.ts +31 -0
  39. package/lib/types/client/canvas-viewport.d.ts.map +1 -0
  40. package/lib/types/client/drama-production-view.d.ts +3 -2
  41. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  42. package/lib/types/client/index.d.ts.map +1 -1
  43. package/lib/types/client/production-canvas.d.ts +24 -0
  44. package/lib/types/client/production-canvas.d.ts.map +1 -0
  45. package/lib/types/skill-provider.d.ts.map +1 -1
  46. package/lib/video-recap/CHANGELOG.md +16 -1
  47. package/lib/video-recap/README.en.md +2 -2
  48. package/lib/video-recap/README.md +2 -2
  49. package/lib/video-recap/manifest.json +25 -25
  50. package/lib/video-recap/skills/video-recap/SKILL.md +4 -2
  51. package/lib/video-recap/skills/video-recap/references/config-playbook.md +2 -2
  52. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +6 -0
  53. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +10 -1
  54. package/lib/video-recap/skills/video-script/SKILL.md +45 -18
  55. package/lib/video-recap/skills/video-script/scripts/review_runner.py +11 -2
  56. package/lib/video-recap/skills/video-voiceover/SKILL.md +34 -8
  57. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +21 -0
  58. package/package.json +1 -1
@@ -34,21 +34,22 @@ PENDING_MAX_BYTES = 12288
34
34
  JOURNAL_MAX_BYTES = 24576
35
35
  QUERY_MAX_BYTES = 2048
36
36
  ASSERTION_MAX_BYTES = 120 # 新建条目的断言限一句话;解释进 reason(不进 query 载荷)。
37
- OMITTED_IDS_MAX = 20 # omitted_ids 封顶,omitted 保留真实总数——漏项列表不许把载荷本身挤炸。
38
37
  LEGACY_ASSERTION_MAX_BYTES = 768 # 存量条目的读取上限;强化老条目不受新上限约束。
39
38
 
40
- # query 的输出是要原样贴进执行 agent prompt 的注入载荷,QUERY_MAX_BYTES
41
- # 是它在 prompt 里的注意力预算,不该放大。防「作者以为载入了、实际被静默
42
- # 截断挤掉」靠三层:①新建条目的断言限 ASSERTION_MAX_BYTES,从源头短(强化
43
- # 已有条目不受限,否则存量长断言再也无法被确认,只会派生重复条目);②写入
44
- # 端按下列任务组合(与 references/author-memory-maintenance.md 的映射表同包跟版)估算
45
- # 最坏查询情形——全局条目+各 scope 维度上最重的单一切片(一次查询只带一
46
- # 个 book/genre/workflow,不同书的条目不会同现;切片按 casefold 归并,与
47
- # same_scope_value 同一口径,轻重按 compact 字节+列表分隔符算,与真实载荷
48
- # 同一把尺),装不下时在返回的 warnings 里点名将被略过的条目、指向「整理作
49
- # 者记忆」,写入本身永不因注入预算失败;③查询按 重要度→本书例外→最近更新
50
- # 排序装填,被略过的恒是重要度较低的条目,漏下的 ID 按同一优先级顺序报进
51
- # omitted_ids。
39
+ # query 输出里的 lines(每条一行「- 断言(编号)」)是交给执行 agent 的注入
40
+ # 载荷,QUERY_MAX_BYTES 是它在 prompt 里的注意力预算,不该放大;量的是执行者
41
+ # 真正读到的这几行,不是 JSON 外壳(编号、kind、scope 只给主会话和脚本看)。
42
+ # 防「作者以为载入了、实际被静默挤掉」靠三层:①新建条目的断言限
43
+ # ASSERTION_MAX_BYTES,从源头短(强化已有条目不受限,否则存量长断言再也无法
44
+ # 被确认,只会派生重复条目);②写入端按下列任务组合(与
45
+ # references/author-memory-maintenance.md 的映射表同包跟版)估算最坏查询情形——
46
+ # 全局条目+各 scope 维度上最重的单一切片(一次查询只带一个 book/genre/workflow,
47
+ # 不同书的条目不会同现;切片按 casefold 归并,与 same_scope_value 同一口径,
48
+ # 轻重按渲染后的行字节算,与真实载荷同一把尺),装不下时在返回的 warnings 里
49
+ # 点名将被略过的条目、指向「整理作者记忆」,写入本身永不因注入预算失败;
50
+ # ③查询按 重要度→本书例外→最近更新 排序装填,被略过的恒是重要度较低的条目,
51
+ # 漏下的 ID 按同一优先级全数报进 omitted_ids,首句报进 omitted_summaries,
52
+ # 供主会话用原话告诉作者哪几条没带上。
52
53
  QUERY_COMBOS: dict[str, tuple[str, ...]] = {
53
54
  "正文初稿/续写": ("prose_style", "story_design"),
54
55
  "去AI味/改写": ("prose_style",),
@@ -1086,12 +1087,19 @@ def summarize_assertion(assertion: str, *, limit: int = 14) -> str:
1086
1087
 
1087
1088
 
1088
1089
  def compact_item(item: dict[str, Any]) -> dict[str, Any]:
1089
- """query 载荷只带这四个字段——估算与真实输出必须同一把尺。"""
1090
+ """query 的 items 只带这四个字段,给主会话和脚本按 kind/scope 判断用;
1091
+ reason、evidence 等一律不出 store。"""
1090
1092
  return {"id": item["id"], "kind": item["kind"], "scope": item["scope"], "assertion": item["assertion"]}
1091
1093
 
1092
1094
 
1093
- def compact_bytes(item: dict[str, Any]) -> int:
1094
- return len(json.dumps(compact_item(item), ensure_ascii=False).encode("utf-8"))
1095
+ def render_line(item: dict[str, Any]) -> str:
1096
+ """执行者读到的一行:断言原文+编号。注入预算量的就是这几行。"""
1097
+ return f"- {item['assertion']}({item['id']})"
1098
+
1099
+
1100
+ def line_bytes(item: dict[str, Any]) -> int:
1101
+ """一行在注入载荷里的占位,含行尾换行——估算与真实装填只用这一把尺。"""
1102
+ return len((render_line(item) + "\n").encode("utf-8"))
1095
1103
 
1096
1104
 
1097
1105
  SCOPE_RANK = {"book": 0, "genre": 1, "workflow": 2, "global": 3}
@@ -1121,48 +1129,38 @@ def fit_items(
1121
1129
  *,
1122
1130
  extra: dict[str, Any] | None = None,
1123
1131
  ) -> tuple[dict[str, Any], list[str]]:
1124
- """按 query 输出信封把条目装进 QUERY_MAX_BYTES:装不下的跳过而不中断
1125
- (一条长的不挡后面的短条),漏下的 ID 报进 omitted_ids(封顶
1126
- OMITTED_IDS_MAX 条,omitted 保留真实总数)。返回 (结果文档, 全部漏下 ID)。
1132
+ """按渲染后的行把条目装进 QUERY_MAX_BYTES:装不下的跳过而不中断(一条长的
1133
+ 不挡后面的短条)。返回 (结果文档, 全部漏下 ID)。
1127
1134
 
1128
- 漏项恒按候选优先级排序,不按被丢弃的先后:收尾回吐的条目优先级高于循环
1129
- 里跳过的,若按追加顺序排,omitted_ids 的封顶正好会把最该报的那条切掉。
1130
- `extra` 是同样计入信封的附加字段(book_revision 等)。
1135
+ 只有 lines 计入预算;items、漏项清单和 `extra`(book_revision 等)是给主会话
1136
+ 和脚本看的,不进执行者 prompt,所以漏项全数列出、不封顶,并按候选优先级
1137
+ 排序——排在前面的就是作者最该知道没带上的那几条。
1131
1138
  """
1139
+ kept: list[dict[str, Any]] = []
1140
+ dropped: list[dict[str, Any]] = []
1141
+ used = 0
1142
+ for item in sorted_items:
1143
+ size = line_bytes(item)
1144
+ if used + size > QUERY_MAX_BYTES:
1145
+ dropped.append(item)
1146
+ continue
1147
+ kept.append(item)
1148
+ used += size
1132
1149
  result: dict[str, Any] = {
1133
1150
  "ok": True,
1134
1151
  "command": "query",
1135
1152
  "initialized": True,
1136
1153
  "revision": revision,
1137
- "items": [],
1138
- "omitted": 0,
1139
- "omitted_ids": [],
1154
+ "items": [compact_item(item) for item in kept],
1155
+ "lines": [render_line(item) for item in kept],
1156
+ "omitted": len(dropped),
1157
+ "omitted_ids": [item["id"] for item in dropped],
1158
+ "omitted_summaries": {item["id"]: summarize_assertion(item["assertion"]) for item in dropped},
1159
+ # 组装脚本按题材×流程查好几次再合并,合并后只能按重要度重排漏项,所以把它带出来。
1160
+ "omitted_importance": {item["id"]: item["importance"] for item in dropped},
1140
1161
  }
1141
1162
  result.update(extra or {})
1142
- order = {item["id"]: index for index, item in enumerate(sorted_items)}
1143
- kept: list[dict[str, Any]] = []
1144
- dropped: set[str] = set()
1145
-
1146
- def ordered_omitted() -> list[str]:
1147
- return sorted(dropped, key=order.__getitem__)
1148
-
1149
- def envelope_bytes() -> int:
1150
- omitted = ordered_omitted()
1151
- result["items"] = [compact_item(item) for item in kept]
1152
- result["omitted"] = len(omitted)
1153
- result["omitted_ids"] = omitted[:OMITTED_IDS_MAX]
1154
- return len((json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8"))
1155
-
1156
- for item in sorted_items:
1157
- kept.append(item)
1158
- if envelope_bytes() > QUERY_MAX_BYTES:
1159
- kept.pop()
1160
- dropped.add(item["id"])
1161
- # omitted 计数落定后包可能恰好贴边超出一两个字节,回吐条目直到装下。
1162
- while kept and envelope_bytes() > QUERY_MAX_BYTES:
1163
- dropped.add(kept.pop()["id"])
1164
- envelope_bytes()
1165
- return result, ordered_omitted()
1163
+ return result, result["omitted_ids"]
1166
1164
 
1167
1165
 
1168
1166
  def build_query_result(
@@ -1170,8 +1168,8 @@ def build_query_result(
1170
1168
  project_state: dict[str, Any] | None,
1171
1169
  book_state: dict[str, Any] | None,
1172
1170
  ) -> tuple[dict[str, Any], list[str]]:
1173
- """真实查询与写入端估算共用的唯一信封构造:候选已按优先级排好,这里补上
1174
- book_revision 等附加字段再装填。两处信封差一个字段就是一次假阴性——回执
1171
+ """真实查询与写入端估算共用的唯一装填入口:候选已按优先级排好,这里补上
1172
+ book_revision 等附加字段再装填。两处装填口径一旦分叉就是假阴性——回执
1175
1173
  说没事、query 照样丢条。"""
1176
1174
  extra: dict[str, Any] = {}
1177
1175
  if book_state is not None:
@@ -1208,12 +1206,13 @@ def merged_query(
1208
1206
 
1209
1207
 
1210
1208
  def slice_weight(items: list[dict[str, Any]]) -> int:
1211
- """切片在真实载荷里的占位:compact 字节+每条在 JSON 数组里的分隔符。
1209
+ """切片在真实载荷里的占位:按渲染后的行(含换行)计。
1212
1210
 
1213
- 只比 compact 字节会挑错切片——条目多、单条短的切片字节和更小,实际占位
1214
- 却更大,于是估算判「装得下」而真实查询溢出(warnings 假阴性)。
1211
+ 必须与 fit_items 同一把尺。按 JSON 字节挑会挑错切片——scope 取值长、断言
1212
+ 短的切片 JSON 更重,渲染出的行却更轻,于是估算判「装得下」而真实查询溢出
1213
+ (warnings 假阴性)。
1215
1214
  """
1216
- return sum(compact_bytes(item) + 2 for item in items)
1215
+ return sum(line_bytes(item) for item in items)
1217
1216
 
1218
1217
 
1219
1218
  def active_of_kinds(state: dict[str, Any] | None, kinds: tuple[str, ...]) -> list[dict[str, Any]]:
@@ -1419,16 +1418,20 @@ def command_query(
1419
1418
  project_state = load_state(project_store(workspace))
1420
1419
  book_state = load_state(book_store(workspace, book_root, book)) if book_root is not None else None
1421
1420
  if project_state is None and book_state is None:
1422
- return {"ok": True, "command": "query", "initialized": False, "revision": 0, "items": [], "omitted": 0, "omitted_ids": []}
1421
+ return {"ok": True, "command": "query", "initialized": False, "revision": 0, "items": [], "lines": [],
1422
+ "omitted": 0, "omitted_ids": [], "omitted_summaries": {}, "omitted_importance": {}}
1423
1423
  requested_scopes = {
1424
1424
  "genre": optional_text(genre, "query.genre", max_bytes=180),
1425
1425
  "workflow": optional_text(workflow, "query.workflow", max_bytes=180),
1426
1426
  }
1427
1427
  # 装不下的**跳过而不中断**(一条长的不挡后面的短条),漏下的 ID 报进
1428
1428
  # omitted_ids:非空=记忆超编该整理了,不是「没有更多了」。写入端的
1429
- # warnings 与这里共用 build_query_result 同一个信封,估算即实况。
1429
+ # warnings 与这里共用 build_query_result 同一把尺,估算即实况。
1430
1430
  result, _ = merged_query(project_state, book_state, set(kinds), requested_scopes)
1431
- require(len((json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8")) <= QUERY_MAX_BYTES, "query result exceeds its fixed byte budget")
1431
+ require(
1432
+ sum(len((line + "\n").encode("utf-8")) for line in result["lines"]) <= QUERY_MAX_BYTES,
1433
+ "query lines exceed their fixed byte budget",
1434
+ )
1432
1435
  return result
1433
1436
 
1434
1437
 
@@ -0,0 +1,31 @@
1
+ import type { CanvasPoint } from "./production-runtime.js";
2
+ /**
3
+ * Screen = world * zoom + pan. The canvas has no edges: nodes and the pan offset take any
4
+ * finite coordinate, so nothing can be pushed out of reach.
5
+ */
6
+ export interface CanvasViewport {
7
+ readonly x: number;
8
+ readonly y: number;
9
+ readonly zoom: number;
10
+ }
11
+ export interface CanvasSize {
12
+ readonly width: number;
13
+ readonly height: number;
14
+ }
15
+ export declare const CANVAS_NODE_WIDTH = 180;
16
+ export declare const CANVAS_NODE_HEIGHT = 76;
17
+ export declare const MIN_CANVAS_ZOOM = 0.1;
18
+ export declare const MAX_CANVAS_ZOOM = 3;
19
+ export declare const DEFAULT_CANVAS_VIEWPORT: CanvasViewport;
20
+ export declare function clampZoom(zoom: number): number;
21
+ /** Scales by `factor` while the world point under `anchor` (viewport pixels) stays put. */
22
+ export declare function zoomAt(viewport: CanvasViewport, factor: number, anchor: CanvasPoint): CanvasViewport;
23
+ /** Wheel deltas arrive in pixels, lines or pages depending on device; one notch is ~10%. */
24
+ export declare function wheelZoomFactor(deltaY: number, deltaMode: number): number;
25
+ export declare function screenToWorld(viewport: CanvasViewport, point: CanvasPoint): CanvasPoint;
26
+ /** Pans so the node's centre sits in the middle of the viewport, keeping the zoom. */
27
+ export declare function centerOn(viewport: CanvasViewport, node: CanvasPoint, size: CanvasSize): CanvasViewport;
28
+ export declare function nodeVisible(viewport: CanvasViewport, node: CanvasPoint, size: CanvasSize): boolean;
29
+ /** Frames every node; a single tall column fits its width rather than shrinking to dots. */
30
+ export declare function fitNodes(nodes: readonly CanvasPoint[], size: CanvasSize, fallback?: CanvasViewport): CanvasViewport;
31
+ //# sourceMappingURL=canvas-viewport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-viewport.d.ts","sourceRoot":"","sources":["../../../src/client/canvas-viewport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,eAAe,MAAK,CAAC;AAClC,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,uBAAuB,EAAE,cAA0C,CAAC;AAGjF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,2FAA2F;AAC3F,wBAAgB,MAAM,CAAC,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,cAAc,CAIpG;AAED,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGzE;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,WAAW,GAAG,WAAW,CAEvF;AAED,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,cAAc,CAMtG;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAIlG;AAED,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,GAAE,cAAwC,GAAG,cAAc,CAe5I"}
@@ -1,4 +1,5 @@
1
1
  import { type DramaDocumentTarget, type DramaEpisodeProduction, type DramaProductionSection } from "./drama-production.js";
2
+ import type { CanvasViewport } from "./canvas-viewport.js";
2
3
  import { type CanvasPoint, type ProductionJob, type ProductionMediaVersion, type ProductionQueueEntry, type ProductionSequenceItem } from "./production-runtime.js";
3
4
  interface Props {
4
5
  readonly sessionId: string;
@@ -14,7 +15,7 @@ interface Props {
14
15
  readonly manualReferences: Readonly<Record<string, readonly string[]>>;
15
16
  readonly sequence: readonly ProductionSequenceItem[];
16
17
  readonly canvas: Readonly<Record<string, CanvasPoint>>;
17
- readonly zoom: number;
18
+ readonly viewport: CanvasViewport | undefined;
18
19
  readonly onSectionChange: (section: DramaProductionSection) => void;
19
20
  readonly onSelect: (id: string | undefined) => void;
20
21
  readonly onNavigate: (target: DramaDocumentTarget) => void;
@@ -24,7 +25,7 @@ interface Props {
24
25
  readonly onOpenMedia: (path: string) => void;
25
26
  readonly onSequenceChange: (sequence: ProductionSequenceItem[]) => void;
26
27
  readonly onCanvasChange: (canvas: Record<string, CanvasPoint>) => void;
27
- readonly onZoomChange: (zoom: number) => void;
28
+ readonly onViewportChange: (viewport: CanvasViewport) => void;
28
29
  readonly onDispatchPrompt: (prompt: string) => Promise<void>;
29
30
  readonly onCancelTurn: () => Promise<void>;
30
31
  readonly onRemoveQueued: (itemId: string) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"drama-production-view.d.ts","sourceRoot":"","sources":["../../../src/client/drama-production-view.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"drama-production-view.d.ts","sourceRoot":"","sources":["../../../src/client/drama-production-view.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAgEpE,eAAO,MAAM,IAAI,aAAa,CAAC;AAC/B,eAAO,MAAM,MAAM,UAAwC,CAAC;AAE5D,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,OAAO;QACf,oBAAoB,EAAE;YAAE,IAAI,EAAE,QAAQ,CAAC;YAAC,KAAK,EAAE,SAAS,CAAA;SAAE,CAAC;KAC5D;CACF;AAuzDD,iFAAiF;AACjF,wBAAgB,KAAK,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAsClD;;;;;;AAED,wBAAuC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAiEpE,eAAO,MAAM,IAAI,aAAa,CAAC;AAC/B,eAAO,MAAM,MAAM,UAAwC,CAAC;AAE5D,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,OAAO;QACf,oBAAoB,EAAE;YAAE,IAAI,EAAE,QAAQ,CAAC;YAAC,KAAK,EAAE,SAAS,CAAA;SAAE,CAAC;KAC5D;CACF;AAuzDD,iFAAiF;AACjF,wBAAgB,KAAK,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAsClD;;;;;;AAED,wBAAuC"}
@@ -0,0 +1,24 @@
1
+ import type { DramaDocumentTarget, DramaEpisodeProduction } from "./drama-production.js";
2
+ import type { CanvasPoint } from "./production-runtime.js";
3
+ import { type CanvasViewport } from "./canvas-viewport.js";
4
+ interface Props {
5
+ readonly production: DramaEpisodeProduction;
6
+ readonly selectedId: string | undefined;
7
+ readonly canvas: Readonly<Record<string, CanvasPoint>>;
8
+ /** Undefined until the creator pans or zooms this episode; the first view then frames the nodes. */
9
+ readonly viewport: CanvasViewport | undefined;
10
+ readonly onSelect: (id: string | undefined) => void;
11
+ readonly onNavigate: (target: DramaDocumentTarget) => void;
12
+ readonly onCanvasChange: (canvas: Record<string, CanvasPoint>) => void;
13
+ readonly onViewportChange: (viewport: CanvasViewport) => void;
14
+ }
15
+ interface CanvasNode {
16
+ readonly id: string;
17
+ readonly label: string;
18
+ readonly type: "asset" | "shot";
19
+ readonly initial: CanvasPoint;
20
+ }
21
+ export declare function canvasNodes(production: DramaEpisodeProduction): CanvasNode[];
22
+ export declare function ProductionCanvas(props: Props): import("react/jsx-runtime").JSX.Element;
23
+ export {};
24
+ //# sourceMappingURL=production-canvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"production-canvas.d.ts","sourceRoot":"","sources":["../../../src/client/production-canvas.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAUL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAE9B,UAAU,KAAK;IACb,QAAQ,CAAC,UAAU,EAAE,sBAAsB,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IACvD,oGAAoG;IACpG,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IACpD,QAAQ,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC3D,QAAQ,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,KAAK,IAAI,CAAC;IACvE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;CAC/D;AAED,UAAU,UAAU;IAClB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC/B;AAQD,wBAAgB,WAAW,CAAC,UAAU,EAAE,sBAAsB,GAAG,UAAU,EAAE,CAI5E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,2CA8Q5C"}
@@ -1 +1 @@
1
- {"version":3,"file":"skill-provider.d.ts","sourceRoot":"","sources":["../../src/skill-provider.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,wBAAwB,CAAC;AAwNhC,UAAU,WAAW;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAuBD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAa7D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAIrE;AAED,wBAAgB,uBAAuB,IAAI,MAAM,CAKhD;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAK9C;AAED,wBAAgB,2BAA2B,IAAI,MAAM,CAKpD;AAuDD,wBAAgB,0BAA0B,CAAC,SAAS,SAA4B,GAAG,aAAa,CAE/F;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,SAA0B,GAAG,MAAM,CAM/G;AAED,wBAAgB,wBAAwB,CAAC,SAAS,SAA0B,GAAG,aAAa,CAE3F;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED,wBAAgB,8BAA8B,CAAC,SAAS,SAAgC,GAAG,aAAa,CAEvG;AAED,wBAAgB,0BAA0B,IAAI,MAAM,CAKnD;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED,wBAAgB,6BAA6B,CAAC,SAAS,SAA+B,GAAG,aAAa,CAErG"}
1
+ {"version":3,"file":"skill-provider.d.ts","sourceRoot":"","sources":["../../src/skill-provider.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,wBAAwB,CAAC;AAyNhC,UAAU,WAAW;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAuBD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAa7D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAIrE;AAED,wBAAgB,uBAAuB,IAAI,MAAM,CAKhD;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAK9C;AAED,wBAAgB,2BAA2B,IAAI,MAAM,CAKpD;AAuDD,wBAAgB,0BAA0B,CAAC,SAAS,SAA4B,GAAG,aAAa,CAE/F;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,SAA0B,GAAG,MAAM,CAM/G;AAED,wBAAgB,wBAAwB,CAAC,SAAS,SAA0B,GAAG,aAAa,CAE3F;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED,wBAAgB,8BAA8B,CAAC,SAAS,SAAgC,GAAG,aAAa,CAEvG;AAED,wBAAgB,0BAA0B,IAAI,MAAM,CAKnD;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED,wBAAgB,6BAA6B,CAAC,SAAS,SAA+B,GAAG,aAAa,CAErG"}
@@ -10,6 +10,20 @@ All notable changes to this project are documented here.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.6.2] - 2026-10-03
14
+
15
+ 用 dub 模式的命令(包括旧 `work_dir` 打印的续跑命令)都要补上 `--confirm-voice-rights`,否则在外发任何音频之前报错退出;只在有权使用这段视频、且说话人同意被克隆声音时确认。
16
+ 补丁号不代表可以直接升级:本版收紧了 dub 的运行条件;video-script 与 video-voiceover 写明每条远程调用发什么、发给谁、怎样关闭;没有 key 时评审不再把稿件发出去。
17
+
18
+ ### Changed
19
+
20
+ - **dub 模式必须显式确认声音权利:`--edit-mode dub` 需要 `--confirm-voice-rights`。** dub 会把源视频音轨分窗发给 MiMo ASR,并把原说话人约 10 秒的声音当参考发给 MiMo voiceclone 克隆音色,以前只要选了 dub 就会运行,`dub.py` 被直接执行时也没有任何确认。现在 recap 在 dub 模式下缺少该参数时于建 `work_dir` 和探测视频之前报错,其他模式传入同样报错;`dub.py` 的 `--stage prepare|render` 也都必须带它,缺少时在抽取音频与任何请求之前退出并说明会外发什么。只在用户有权使用这段视频与音频、且说话人同意被克隆声音时确认。升级后在所有 dub 命令(包括旧 `work_dir` 打印的续跑命令)上补这个参数;没有环境变量形式。
21
+ - **video-script 与 video-voiceover 的 SKILL.md 写明每条远程调用发什么、发给谁、怎样关闭。** video-script 新增 §2:`review.py` 是唯一联网的脚本,只把旁白全文、选定时间段内的画面描述与对白转写文字、背景资料和策划文件摘录发到 MiMo chat 接口(不发视频、帧图片或音频),单独使用时只在显式执行时运行,编排时用 `--no-review-narration` / `REVIEW_NARRATION=0` 关闭;`validate.py` 与 lint 只在本地运行。video-voiceover 新增 §2 逐路径列出 MiMo TTS、`--voice-ref` 克隆、Fish Audio、自托管 IndexTTS 与 dub 各自收到的内容,新增 §8 说明 dub 是实验功能、如何触发与权利要求。两份文档其后的章节号各顺延一号。
22
+
23
+ ### Fixed
24
+
25
+ - **没有 `MIMO_API_KEY` 时 `review.py` 不再把稿件发出去。** 以前缺 key 也会把旁白与证据 POST 到 MiMo,再因 401 失败;现在在构造请求前退出,不写评审产物。编排入口的建议型评审照旧失败不阻断。
26
+
13
27
  ## [0.6.1] - 2026-10-03
14
28
 
15
29
  升级前先从脚本与环境里删掉本版移除的参数和环境变量(见 Removed,如 `--mimo-qc`、`validate.py --preserve-approved-text`、`cut.py --clip-padding`、`dashboard_server.py --host` 和 19 个调参变量),读 `golden_eval.json`、`mimo_qc.json`、`preflight_qc.json` 的脚本按 Removed 改读 `final_qc.json` 或对应产物。
@@ -463,7 +477,8 @@ recap feels like a recap, not captions over a clip.
463
477
  MiMo API key. Five independent skills (understanding, script, cut, voiceover, assemble)
464
478
  plus a thin orchestrator; optional 剪映 draft export.
465
479
 
466
- [Unreleased]: https://github.com/zenstory-ai/video-recap-skills/compare/v0.6.1...HEAD
480
+ [Unreleased]: https://github.com/zenstory-ai/video-recap-skills/compare/v0.6.2...HEAD
481
+ [0.6.2]: https://github.com/zenstory-ai/video-recap-skills/compare/v0.6.1...v0.6.2
467
482
  [0.6.1]: https://github.com/zenstory-ai/video-recap-skills/compare/v0.6.0...v0.6.1
468
483
  [0.6.0]: https://github.com/zenstory-ai/video-recap-skills/compare/v0.5.0...v0.6.0
469
484
  [0.5.0]: https://github.com/zenstory-ai/video-recap-skills/compare/v0.4.0...v0.5.0
@@ -396,10 +396,10 @@ The reference audio is sent to MiMo for synthesis; use it only with the voice ow
396
396
  **Dub an English video into Chinese while keeping the speaker's voice:**
397
397
 
398
398
  ```text
399
- Dub /path/to/english.mp4 into Chinese, keeping the original speaker's voice.
399
+ Dub /path/to/english.mp4 into Chinese, keeping the original speaker's voice. I have the rights to this video and the speaker consents to having their voice cloned.
400
400
  ```
401
401
 
402
- This replaces the original speech rather than overlaying commentary; the current version supports one speaker and full-track replacement without background-music separation.
402
+ This is experimental and replaces the original speech rather than overlaying commentary; the current version supports one speaker and full-track replacement without background-music separation. The source audio is sent to MiMo ASR for transcription and about 10 seconds of the original speaker's voice are sent to MiMo as the voice-clone reference, so the run requires `--confirm-voice-rights`: confirm only when you have the rights to the video and the speaker's consent to clone their voice.
403
403
 
404
404
  **Reuse subtitle style, voice, BGM and frame across a series:**
405
405
 
@@ -385,10 +385,10 @@ flowchart LR
385
385
  **英语视频译成中文并保留原说话人的声音:**
386
386
 
387
387
  ```text
388
- 把 /path/to/english.mp4 翻译成中文配音,保留原说话人的声音。
388
+ 把 /path/to/english.mp4 翻译成中文配音,保留原说话人的声音。我有权使用这段视频,说话人同意克隆其声音。
389
389
  ```
390
390
 
391
- 这是替换原始台词而不是叠加解说;当前支持单说话人整轨替换,不分离背景音乐。
391
+ 这是实验功能,替换原始台词而不是叠加解说;当前支持单说话人整轨替换,不分离背景音乐。源视频音频会发给 MiMo ASR 转写,原说话人约 10 秒的声音会作为参考发给 MiMo 克隆音色,所以运行需要 `--confirm-voice-rights`:只在你有权使用该视频、且说话人同意被克隆声音时确认。
392
392
 
393
393
  **同一个系列复用字幕样式、音色、BGM 与包框:**
394
394
 
@@ -2,10 +2,10 @@
2
2
  "schemaVersion": 1,
3
3
  "upstream": {
4
4
  "repository": "https://github.com/zenstory-ai/video-recap-skills.git",
5
- "commit": "88af4d4f37b09510a1169e945d43d0532770a8e4",
6
- "releaseVersion": "0.6.1"
5
+ "commit": "539168622918e058bd250c793b82f043eced31e0",
6
+ "releaseVersion": "0.6.2"
7
7
  },
8
- "generatedAt": "2026-10-03T14:32:42.171Z",
8
+ "generatedAt": "2026-10-08T11:36:39.521Z",
9
9
  "skills": [
10
10
  "video-assemble",
11
11
  "video-cut",
@@ -18,8 +18,8 @@
18
18
  "files": [
19
19
  {
20
20
  "path": "CHANGELOG.md",
21
- "sha256": "8b6c971f123fe170a97e7c517131fab120a2fcb5cb9bd06ebd2a795a7d26dfc3",
22
- "bytes": 104618
21
+ "sha256": "043143b851ca8357b371bb782c838a5e6593b6018b911edacab58fa862213786",
22
+ "bytes": 107052
23
23
  },
24
24
  {
25
25
  "path": "LICENSE",
@@ -28,13 +28,13 @@
28
28
  },
29
29
  {
30
30
  "path": "README.en.md",
31
- "sha256": "7557cd1dee6734697a26517d40a6337c2de5bcc470c17fd739c2a496fb018a24",
32
- "bytes": 30570
31
+ "sha256": "61ca8e1d4d2ff312fb15444a6dfe1044de77ce83f47063b16db3a245a2a41d02",
32
+ "bytes": 30977
33
33
  },
34
34
  {
35
35
  "path": "README.md",
36
- "sha256": "d70a2957ee9f1dcaa685fba9420ccd12506dc7509b2d96a647937ce535c465a4",
37
- "bytes": 26510
36
+ "sha256": "e431bb4c4fb45c21e8d9a9e4523bcaceb14cb5be545c55fbc7af3f379252f253",
37
+ "bytes": 26828
38
38
  },
39
39
  {
40
40
  "path": "skills/video-assemble/SKILL.md",
@@ -398,8 +398,8 @@
398
398
  },
399
399
  {
400
400
  "path": "skills/video-recap/SKILL.md",
401
- "sha256": "940ba0b6621d59f1fe30f7e598a80f96acfb700628bbdd86d852f98b32c5c38b",
402
- "bytes": 16498
401
+ "sha256": "dcee225f8a0e2f797ef03640bf5462c91b28efb34f4e9378ed7fcad373d5df2c",
402
+ "bytes": 17027
403
403
  },
404
404
  {
405
405
  "path": "skills/video-recap/references/audio-routing.md",
@@ -408,8 +408,8 @@
408
408
  },
409
409
  {
410
410
  "path": "skills/video-recap/references/config-playbook.md",
411
- "sha256": "45a1cbdff2c66fb0a083341355f3e051f8bb2a50a9943b1a29ba329421b1a68e",
412
- "bytes": 12839
411
+ "sha256": "0cfc3cf66d39a9faa56d1b61e1b1fba3fefb81a825a3352174a2d0f286cbd821",
412
+ "bytes": 13265
413
413
  },
414
414
  {
415
415
  "path": "skills/video-recap/references/data-schema.md",
@@ -453,8 +453,8 @@
453
453
  },
454
454
  {
455
455
  "path": "skills/video-recap/scripts/recap_cli.py",
456
- "sha256": "45058a6ea3cb147b79e3765d2314e184c0732acf9e08760d0bbac1aaa36c21d9",
457
- "bytes": 8819
456
+ "sha256": "02af53cc5a70229b8b4cafac470a650c996e6c6dc8403c96bb3c77f8a57bab4a",
457
+ "bytes": 9088
458
458
  },
459
459
  {
460
460
  "path": "skills/video-recap/scripts/recap_inspect.py",
@@ -468,8 +468,8 @@
468
468
  },
469
469
  {
470
470
  "path": "skills/video-recap/scripts/recap_runner.py",
471
- "sha256": "4643ddc62689940cf5ffff76f8f862bab10cb79a52ad392364d33172685f2ebc",
472
- "bytes": 28261
471
+ "sha256": "63eb03a6f383785cdbd1e1af736d97bc8f763045b63903a3cf1c5513485e07cf",
472
+ "bytes": 28906
473
473
  },
474
474
  {
475
475
  "path": "skills/video-recap/scripts/recap_runtime.py",
@@ -548,8 +548,8 @@
548
548
  },
549
549
  {
550
550
  "path": "skills/video-script/SKILL.md",
551
- "sha256": "387b8e7f57c11938c934d736ed5df564871b8f654a7d421dc2aeef73537dc3c9",
552
- "bytes": 15352
551
+ "sha256": "7ae12d12f77e300829650789452caf867f1cab6bd4f8e002fcb3fc4394b54369",
552
+ "bytes": 18296
553
553
  },
554
554
  {
555
555
  "path": "skills/video-script/references/creative-editing-playbook.md",
@@ -618,8 +618,8 @@
618
618
  },
619
619
  {
620
620
  "path": "skills/video-script/scripts/review_runner.py",
621
- "sha256": "da3c0b402bd11df72fad80a8a4f266e329a5b2c807f97ff53d92b67a7df4712b",
622
- "bytes": 6667
621
+ "sha256": "653c4b17ae6c6fc283934a82cd71801b00897bc9e0b7b862c64fdfe13985cecb",
622
+ "bytes": 7307
623
623
  },
624
624
  {
625
625
  "path": "skills/video-script/scripts/speech_ownership.py",
@@ -758,8 +758,8 @@
758
758
  },
759
759
  {
760
760
  "path": "skills/video-voiceover/SKILL.md",
761
- "sha256": "c6b407aec1310c2db4d7704bc522700461fae681cdfcafd99a533676db4192a1",
762
- "bytes": 7583
761
+ "sha256": "2ba0c0c0ded5d561f89ed80d42b7075edc469fb820224a547e97d122ff50fdf9",
762
+ "bytes": 11407
763
763
  },
764
764
  {
765
765
  "path": "skills/video-voiceover/references/index-tts.md",
@@ -773,8 +773,8 @@
773
773
  },
774
774
  {
775
775
  "path": "skills/video-voiceover/scripts/dub.py",
776
- "sha256": "78d494206648c6adb1637ec81a288dccc71a02beda53e646269b51a01a3ac7d3",
777
- "bytes": 25292
776
+ "sha256": "fd6eb27baf2c16f542e177d5ab99c55761c54da16efba71f1a4760f338851dd1",
777
+ "bytes": 26601
778
778
  },
779
779
  {
780
780
  "path": "skills/video-voiceover/scripts/lib.py",
@@ -210,10 +210,12 @@ python3 scripts/recap.py adopted.mp4 --work-dir packaging_work --audio-mode adop
210
210
 
211
211
  ## 5. 英译中原声复刻模式
212
212
 
213
- `--edit-mode dub` 把英文视频翻译为中文,并用原说话者的克隆音色替换人声;它不是在压低原声上叠加解说。
213
+ `--edit-mode dub` 是实验功能:把英文视频翻译为中文,并用原说话者的克隆音色替换人声;它不是在压低原声上叠加解说。只在用户明确要求英译中配音时使用。
214
+
215
+ dub 会把源视频音轨分窗发给 MiMo ASR 转写,并把截取的约 10 秒原说话人声音作为参考,连同每句译文发给 MiMo voiceclone。因此必须带 `--confirm-voice-rights`:先向用户确认他有权使用这段视频与音频、且说话人同意被克隆声音,确认后才加这个参数;缺少时启动即报错,不抽音频、不发请求。该参数只用于 dub,其他模式传入会报错。
214
216
 
215
217
  ```bash
216
- python3 scripts/recap.py <video> --edit-mode dub --work-dir <work_dir>
218
+ python3 scripts/recap.py <video> --edit-mode dub --confirm-voice-rights --work-dir <work_dir>
217
219
  ```
218
220
 
219
221
  准备阶段会转写英文、提取一段参考音频,并写出 `dub_brief.md` 与 `dub_transcript.json`。Agent 按 `dub_brief.md` 里的翻译要求(逐句忠实、时间窗、语速)写 `dub_script.json`:
@@ -41,7 +41,7 @@ Defaults below are bundle-level defaults unless a note scopes them to a specific
41
41
  | Final loudness | `FINAL_LOUDNORM` / `TARGET_LUFS` / `LOUDNESS_LIMITER_MAX_DB` | `true` / `-14` / `6` | end-of-pipeline two-pass normalize with one constant gain, never dynamically compressed; when the mix's true peak has no room for that gain an oversampled true-peak limiter takes up to `LOUDNESS_LIMITER_MAX_DB` of peak first, and only the gain beyond that lowers the target (`0` disables the limiter). The -1 dBTP ceiling holds for the delivered AAC file: the first render aims 0.5 dB under it, the encoded file is decoded and measured, and an overshoot re-encodes only the audio lower (at most twice; still over blocks with `delivered_true_peak_over_target`). `assembly_qc.json` records the mode ffmpeg actually ran (`loudness_mode`, `loudnorm_final_pass` incl. `peak_limiter` and the measured `delivered` file) |
42
42
  | Output compression | `OUTPUT_CRF` / `OUTPUT_PRESET` / `OUTPUT_MAX_HEIGHT` | `18` / `veryfast` / `0` | x264 re-encode controls, applied whenever the final mux re-encodes (burning subtitles / masking / scaling / `FORCE_VIDEO_REENCODE`, or a source picture that is not H.264 8-bit 4:2:0 with even dimensions; the delivery is always H.264 8-bit 4:2:0 with `+faststart`: `yuv420p`, or a full-range `yuvj420p` H.264 source copied unchanged; tagged BT.709 when the source is untagged or BT.709, keeping any other declared colour tags, and an RGB source (colour space `gbr` or an RGB/paletted pixel format) is converted to BT.709 limited range; multi-source cuts whose sources disagree convert every clip's matrix and range to BT.709 limited. Dub mode (`--edit-mode dub`) copies the source picture as is). Higher `OUTPUT_CRF` = smaller file/lower quality (18≈visually lossless, 23–26 much smaller); `slow`/`slower` preset shrinks more at the same CRF; `OUTPUT_MAX_HEIGHT>0` downscales the final height (keeps aspect, even width), e.g. `720` to halve 1080p pixels. Subtitles/mask render at native res then downscale, so they stay crisp |
43
43
  | Style | `--style` | `纪录片` | freeform verbatim guidance passed into the agent brief. The agent synthesizes voice/pacing from this exact text plus evidence; it is not a fixed option list, preset, switch, or finite style taxonomy |
44
- | Edit mode | `EDIT_MODE` / `--edit-mode` | `full` | `full`, `cut`, or `dub`; multi-source input supports `cut` only |
44
+ | Edit mode | `EDIT_MODE` / `--edit-mode` | `full` | `full`, `cut`, or `dub`; multi-source input supports `cut` only. Experimental `dub` also requires `--confirm-voice-rights` (the user may use the source audio and the speaker consents to voice cloning) and is rejected without it |
45
45
  | Cut target | `TARGET_DURATION` / `--target-duration` | — | e.g. `10m` (cut mode) |
46
46
  | Scene threshold | `--scene-threshold` | `0.1` | scene-cut sensitivity |
47
47
  | Shot-change-aware cut | `SCENE_CUT_SNAP` / `SCENE_CUT_SNAP_MARGIN` / `SCENE_CUT_DETECT_THRESHOLD` | on / `0.5` / `0.4` | cut mode: nudge each clip boundary off the original footage's hard cuts so the edit point doesn't flash a sliver of the adjacent shot (闪烁). source_start moves forward onto / source_end back onto any shot-change within the margin; boundaries already on a cut, or that would shrink a clip below ~0.5s, are left as-is. Set `SCENE_CUT_SNAP=0` to disable |
@@ -49,7 +49,7 @@ Defaults below are bundle-level defaults unless a note scopes them to a specific
49
49
  | Subtitle size | `SUBTITLE_FONT_SIZE` / `SUBTITLE_MARGIN_V` | `42` / `48` | look & placement |
50
50
  | Subtitle font | `SUBTITLE_FONT_NAME` / `SUBTITLE_FONT_FILE` | `Arial` / — | with a font file, burned ASS subtitles load fonts from its directory (`fontsdir`) and on-screen text overlays use it (`fontfile`); `SUBTITLE_FONT_NAME` must be that file's family name. A bound `subtitle_style` template sets both |
51
51
  | 整理 / index | `--no-consolidate` / `--consolidate-asr` | on | build the understanding index (and optionally clean ASR); use `--no-consolidate` to skip |
52
- | Advisory / strict narration review | `REVIEW_NARRATION` / `--review-narration` / `--no-review-narration`; strict: `REQUIRE_NARRATION_REVIEW` / `--require-narration-review` | advisory on, strict off | runs the narration review stage after validation and before TTS. Default advisory mode is fail-open; strict mode blocks TTS on review failure, parse error, or error-severity findings. In cut mode the reviewer uses `clip_plan_validated.json` to remap VLM/ASR grounding onto the output timeline |
52
+ | Advisory / strict narration review | `REVIEW_NARRATION` / `--review-narration` / `--no-review-narration`; strict: `REQUIRE_NARRATION_REVIEW` / `--require-narration-review` | advisory on, strict off | runs the narration review stage after validation and before TTS; it sends the narration text plus text excerpts of the understanding evidence and planning files (no video, frames or audio) to the MiMo chat endpoint, so `--no-review-narration` / `REVIEW_NARRATION=0` keeps the script on this machine unless strict review is on. Default advisory mode is fail-open; strict mode blocks TTS on review failure, parse error, or error-severity findings. In cut mode the reviewer uses `clip_plan_validated.json` to remap VLM/ASR grounding onto the output timeline |
53
53
  | 剪映 export (optional) | `--export-jianying` / `EXPORT_JIANYING` | off | after rendering, also write a 剪映/JianYing draft from `timeline.json`. Decoupled — the core render never needs it |
54
54
  | 剪映 draft dir | `JIANYING_DRAFT_DIR` | work_dir | parent folder for the exported draft (point it at 剪映's drafts root to open in-app) |
55
55
  | 剪映 bundle media | `JIANYING_BUNDLE_MEDIA` / `--jianying-no-bundle-media` | **on** | copies video/audio/photo into `Resources/local/*`, uses draft-placeholder paths, and writes the type-0 material index in `draft_meta_info.json`. This makes a cloned/moved draft self-contained and is required on sandboxed macOS. Use `--jianying-no-bundle-media` only if 剪映 can reach the original paths |
@@ -92,6 +92,12 @@ def parse_args(argv=None):
92
92
  default=os.environ.get("EDIT_MODE", "full"),
93
93
  choices=["full", "cut", "dub"],
94
94
  )
95
+ core.add_argument(
96
+ "--confirm-voice-rights",
97
+ action="store_true",
98
+ help="dub mode only, required there: the user confirms they may use this video's audio "
99
+ "and clone its speaker's voice (sent to MiMo ASR and MiMo voiceclone)",
100
+ )
95
101
  core.add_argument(
96
102
  "--target-duration", default=os.environ.get("TARGET_DURATION") or None
97
103
  )
@@ -434,6 +434,8 @@ def main():
434
434
  if (args.doctor or needs_voiceover(args)) and args.tts_provider not in TTS_PROVIDERS:
435
435
  ap.error("TTS_PROVIDER/--tts-provider must be one of: " + ", ".join(TTS_PROVIDERS))
436
436
 
437
+ if args.edit_mode != "dub" and args.confirm_voice_rights:
438
+ ap.error("--confirm-voice-rights only applies to --edit-mode dub")
437
439
  if args.doctor:
438
440
  if any(
439
441
  getattr(args, field) is not None
@@ -501,6 +503,12 @@ def main():
501
503
  )
502
504
  if args.edit_mode == "dub" and args.burn_subtitles:
503
505
  ap.error("--burn-subtitles is only supported in full/cut modes; dub never burns subtitles")
506
+ if args.edit_mode == "dub" and not args.confirm_voice_rights:
507
+ ap.error(
508
+ "--edit-mode dub requires --confirm-voice-rights: dub sends the source audio to "
509
+ "MiMo ASR and clones the original speaker's voice with MiMo voiceclone; confirm only "
510
+ "when the user has the rights to this video and the speaker's consent to clone the voice"
511
+ )
504
512
  if (args.subtitle_y_top is None) != (args.subtitle_y_bot is None):
505
513
  ap.error("--subtitle-y-top and --subtitle-y-bot must be provided together")
506
514
  if args.subtitle_y_top is not None:
@@ -572,7 +580,8 @@ def _run_dub(video, work_dir, args):
572
580
  One pause: prepare (ASR + sentence-seg + reference) -> agent writes the Chinese
573
581
  translation (dub_script.json) -> render (clone TTS + full-replace mux)."""
574
582
  dub_script = work_dir / "dub_script.json"
575
- dub_args = ["--video", str(video), "--work-dir", str(work_dir)]
583
+ # main() has already required --confirm-voice-rights for dub; forward it to both stages.
584
+ dub_args = ["--video", str(video), "--work-dir", str(work_dir), "--confirm-voice-rights"]
576
585
  if not dub_script.exists():
577
586
  _run("video-voiceover", "dub.py", "--stage", "prepare", *dub_args)
578
587
  _write_run_manifest(work_dir, video, args)