@remixmate/cli 0.9.72 → 0.9.73

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 (179) hide show
  1. package/LICENSE +45 -21
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/dist/argv.js +1 -51
  5. package/dist/auth/auth-lock.js +1 -100
  6. package/dist/auth/auto-login.js +7 -75
  7. package/dist/auth/commands.js +40 -290
  8. package/dist/auth/credential-store.js +1 -143
  9. package/dist/auth/device-flow-runner.js +13 -110
  10. package/dist/auth/device-flow.js +1 -132
  11. package/dist/auth/ensure.js +1 -62
  12. package/dist/auth/environment.js +1 -69
  13. package/dist/auth/pending-store.js +1 -45
  14. package/dist/auth/resolve.js +1 -44
  15. package/dist/billing.js +3 -76
  16. package/dist/capabilities.js +1 -75
  17. package/dist/cli.js +39 -211
  18. package/dist/diagnostics.js +2 -70
  19. package/dist/doctor.js +9 -188
  20. package/dist/errors.js +1 -33
  21. package/dist/exec.js +4 -47
  22. package/dist/handlers/gen-digital-human.js +33 -264
  23. package/dist/handlers/gen-image.js +9 -113
  24. package/dist/handlers/gen-post.js +15 -192
  25. package/dist/handlers/gen-video.js +7 -185
  26. package/dist/handlers/gen-voice.js +19 -133
  27. package/dist/handlers/index.js +1 -20
  28. package/dist/handlers/shared.js +1 -66
  29. package/dist/handlers/template-init.js +1 -21
  30. package/dist/http.js +3 -155
  31. package/dist/index.js +1 -8
  32. package/dist/install.js +40 -448
  33. package/dist/manifest.json +44 -3
  34. package/dist/progress.js +2 -15
  35. package/dist/project/commands.js +34 -156
  36. package/dist/project/host.js +1 -151
  37. package/dist/project/resolve.js +1 -65
  38. package/dist/project/store.js +2 -97
  39. package/dist/project/take.js +1 -106
  40. package/dist/project/trace.js +5 -278
  41. package/dist/registry.js +3 -70
  42. package/dist/runner.js +7 -225
  43. package/dist/skill-schema.js +1 -194
  44. package/dist/template/args.js +1 -69
  45. package/dist/template/check.js +2 -30
  46. package/dist/template/commands.js +34 -163
  47. package/dist/template/constants.js +1 -46
  48. package/dist/template/init.js +5 -133
  49. package/dist/template/list.js +1 -46
  50. package/dist/template/preview.js +7 -318
  51. package/dist/template/proc.js +1 -43
  52. package/dist/template/project.js +6 -161
  53. package/dist/template/publish.js +5 -185
  54. package/dist/template/render-client.js +2 -79
  55. package/dist/template/upgrade.js +4 -123
  56. package/dist/text.js +1 -15
  57. package/package.json +2 -35
  58. package/skills/doc-parse/scripts/extract_via_render.py +32 -32
  59. package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
  60. package/skills/gen-script/SKILL.md +23 -1
  61. package/skills/gen-script/scripts/gen_script.py +644 -551
  62. package/skills/gen-script/skill.json +12 -0
  63. package/skills/gen-script/version.json +1 -1
  64. package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
  65. package/skills/render-video/scripts/_chrome_vendor.py +17 -17
  66. package/skills/render-video/scripts/_digital_human_segments.py +34 -34
  67. package/skills/render-video/scripts/_video_probe.py +33 -33
  68. package/skills/render-video/scripts/_vod_polling.py +13 -13
  69. package/skills/render-video/scripts/remote_renderer_client.py +42 -42
  70. package/skills/render-video/scripts/render_video.py +639 -639
  71. package/skills/render-video/scripts/required_props.py +37 -37
  72. package/skills/render-video/scripts/upload_video.py +35 -35
  73. package/skills/render-video/version.json +1 -1
  74. package/skills/template-registry/SKILL.md +16 -0
  75. package/skills/template-registry/scripts/countdown_spec.py +92 -92
  76. package/skills/template-registry/scripts/doc_extract.py +40 -40
  77. package/skills/template-registry/scripts/list_templates.py +303 -191
  78. package/skills/template-registry/scripts/log_diagnostics.py +1 -1
  79. package/skills/template-registry/scripts/match_template.py +87 -87
  80. package/skills/template-registry/scripts/payload_hydrator.py +89 -89
  81. package/skills/template-registry/scripts/registry_loader.py +159 -159
  82. package/skills/template-registry/scripts/render_job_client.py +75 -75
  83. package/skills/template-registry/scripts/showcase_loader.py +44 -44
  84. package/skills/template-registry/skill.json +4 -1
  85. package/skills/template-registry/version.json +1 -1
  86. package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
  87. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
  88. package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
  89. package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
  90. package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
  91. package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
  92. package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
  93. package/skills/video-parser/scripts/analyze_video.py +173 -173
  94. package/skills/video-parser/scripts/deconstruct_video.py +91 -91
  95. package/skills/video-parser/scripts/parse_via_render.py +21 -21
  96. package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
  97. package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
  98. package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
  99. package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
  100. package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
  101. package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
  102. package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
  103. package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
  104. package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
  105. package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
  106. package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
  107. package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
  108. package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
  109. package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
  110. package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
  111. package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
  112. package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
  113. package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
  114. package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
  115. package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
  116. package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
  117. package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
  118. package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
  119. package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
  120. package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
  121. package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
  122. package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
  123. package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
  124. package/skills/web-screenshot/scripts/_media_upload.py +33 -33
  125. package/skills/web-screenshot/scripts/read_page.py +15 -15
  126. package/skills/web-screenshot/scripts/record.py +26 -26
  127. package/skills/web-screenshot/scripts/screenshot.py +5 -5
  128. package/dist/argv.d.ts +0 -15
  129. package/dist/auth/auth-lock.d.ts +0 -26
  130. package/dist/auth/auto-login.d.ts +0 -20
  131. package/dist/auth/commands.d.ts +0 -8
  132. package/dist/auth/credential-store.d.ts +0 -60
  133. package/dist/auth/device-flow-runner.d.ts +0 -58
  134. package/dist/auth/device-flow.d.ts +0 -52
  135. package/dist/auth/ensure.d.ts +0 -55
  136. package/dist/auth/environment.d.ts +0 -38
  137. package/dist/auth/pending-store.d.ts +0 -33
  138. package/dist/auth/resolve.d.ts +0 -30
  139. package/dist/billing.d.ts +0 -44
  140. package/dist/capabilities.d.ts +0 -63
  141. package/dist/cli.d.ts +0 -13
  142. package/dist/diagnostics.d.ts +0 -24
  143. package/dist/doctor.d.ts +0 -22
  144. package/dist/errors.d.ts +0 -29
  145. package/dist/exec.d.ts +0 -18
  146. package/dist/handlers/gen-digital-human.d.ts +0 -17
  147. package/dist/handlers/gen-image.d.ts +0 -20
  148. package/dist/handlers/gen-post.d.ts +0 -22
  149. package/dist/handlers/gen-video.d.ts +0 -19
  150. package/dist/handlers/gen-voice.d.ts +0 -26
  151. package/dist/handlers/index.d.ts +0 -20
  152. package/dist/handlers/shared.d.ts +0 -26
  153. package/dist/handlers/template-init.d.ts +0 -10
  154. package/dist/http.d.ts +0 -72
  155. package/dist/index.d.ts +0 -9
  156. package/dist/install.d.ts +0 -71
  157. package/dist/progress.d.ts +0 -23
  158. package/dist/project/commands.d.ts +0 -19
  159. package/dist/project/host.d.ts +0 -91
  160. package/dist/project/resolve.d.ts +0 -43
  161. package/dist/project/store.d.ts +0 -31
  162. package/dist/project/take.d.ts +0 -50
  163. package/dist/project/trace.d.ts +0 -98
  164. package/dist/registry.d.ts +0 -30
  165. package/dist/runner.d.ts +0 -39
  166. package/dist/skill-schema.d.ts +0 -138
  167. package/dist/template/args.d.ts +0 -25
  168. package/dist/template/check.d.ts +0 -23
  169. package/dist/template/commands.d.ts +0 -25
  170. package/dist/template/constants.d.ts +0 -39
  171. package/dist/template/init.d.ts +0 -28
  172. package/dist/template/list.d.ts +0 -23
  173. package/dist/template/preview.d.ts +0 -60
  174. package/dist/template/proc.d.ts +0 -26
  175. package/dist/template/project.d.ts +0 -47
  176. package/dist/template/publish.d.ts +0 -43
  177. package/dist/template/render-client.d.ts +0 -39
  178. package/dist/template/upgrade.d.ts +0 -44
  179. package/dist/text.d.ts +0 -8
@@ -1,25 +1,25 @@
1
- """
2
- 旁白时长预算 —— 规划期(写脚本时)估算「这段旁白念完、这一场实际会有多长」。
3
1
 
4
- 为什么单独成模块:以前这个数有三份且互不一致 —— gen_script 的 ``CHARS_PER_SECOND = 5.0``
5
- (从没参与计算,只出现在注释里)、gen-script SKILL.md 的「3–4 字/秒」、timeline_compiler
6
- 的 5.0(那份是没有 TTS 时间戳时估字幕时长的兜底,语义不同,不归这里管)。没有任何一环
7
- 真的拿旁白字数去核对场景时长,于是 2026-09-27 线上一条会话:确认稿写 75s、每场 8–9s,
8
- 每场却写了 45–58 字,成片 116.9s(+56%),用户确认的时长从一开始就不可能兑现。
9
2
 
10
- 模型 = 语速 + 行间停顿 + 场景尾巴,三项都按那条会话的 9 条 TTS 实测标定:
11
3
 
12
- * 语速:纯整段旁白的场景 1.1× 下 ≈ 4.8–5.2 计数字/秒(汉字 + 英文词×2)→ 1.0× 取
13
- ``PLAN_CHARS_PER_SEC_1X = 4.5``(略偏保守);
14
- * 行间停顿:分行旁白(``narration.items`` / intro / outro)每多一行,TTS 按段合成,
15
- 段间约 0.9s 的静音 —— 逐项高亮版式同样字数要慢出 30–60%,主要就是它;
16
- * 场景尾巴:``render_video.adjust_timeline_to_audio`` 在每场音频后补的留白
17
- (模板 ``capabilities.defaultTailPadSec``,缺省 1.5s)。9 场就是 13.5s,写脚本时
18
- 从没人算进去。
19
4
 
20
- 样本只有一条会话,所以这些是**规划用**的估计,不是承诺;估不准的方向应当是偏保守
21
- (宁可让模型少写几个字)。英文语速未标定,先按常见 TTS 语速给。
22
- """
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
23
 
24
24
  from __future__ import annotations
25
25
 
@@ -27,27 +27,27 @@ import math
27
27
  import re
28
28
  from typing import Optional
29
29
 
30
- # 语速(1.0× 下)。中文按「计数字」(见 count_speech_units),英文按词。
30
+
31
31
  PLAN_CHARS_PER_SEC_1X = 4.5
32
- PLAN_WORDS_PER_SEC_1X = 2.5 # 未标定
32
+ PLAN_WORDS_PER_SEC_1X = 2.5
33
+
33
34
 
34
- # 分行旁白每一处换行带来的静音。
35
35
  LINE_PAUSE_SEC = 0.9
36
36
 
37
- # 必须与 render_video.adjust_timeline_to_audio 的缺省尾巴一致(int(fps * 1.5))。
37
+
38
38
  DEFAULT_TAIL_PAD_SEC = 1.5
39
39
 
40
- # 估算总时长超出目标多少才告警。低于这个幅度属于估算误差本身的量级。
40
+
41
41
  OVERRUN_WARN_RATIO = 0.3
42
42
 
43
43
  _CJK = re.compile(r"[\u3400-\u9fff\uf900-\ufaff]")
44
- # 中文旁白里夹的英文词 / 数字组("AI Agent"、"2026")。一个英文词 TTS 念出来大约
45
- # 占两个汉字的时长。
44
+
45
+
46
46
  _LATIN_WORD = re.compile(r"[A-Za-z0-9]+(?:['’.-][A-Za-z0-9]+)*")
47
47
 
48
48
 
49
49
  def count_speech_units(text: str) -> tuple[float, str]:
50
- """返回 (计数, 语言)。有汉字按中文计(汉字 + 英文词×2),否则按英文词计。"""
50
+
51
51
  if not isinstance(text, str) or not text.strip():
52
52
  return 0.0, "zh"
53
53
  cjk = len(_CJK.findall(text))
@@ -58,10 +58,10 @@ def count_speech_units(text: str) -> tuple[float, str]:
58
58
 
59
59
 
60
60
  def narration_lines(narration: object) -> list[str]:
61
- """旁白按 TTS 实际合成的段拆开:分行形态 = intro + items + outro,否则整段 text。
62
61
 
63
- 与 render_video.extract_narration_lines 同口径:有 items 时 text 被整个忽略。
64
- """
62
+
63
+
64
+
65
65
  if not isinstance(narration, dict):
66
66
  return []
67
67
  items = narration.get("items")
@@ -90,7 +90,7 @@ def speech_rate(language: str, speed: float) -> float:
90
90
 
91
91
 
92
92
  def estimate_speech_sec(narration: object, speed: float = 1.0) -> float:
93
- """念完这段旁白要多少秒(含分行停顿,不含场景尾巴)。"""
93
+
94
94
  lines = narration_lines(narration)
95
95
  if not lines:
96
96
  return 0.0
@@ -99,7 +99,7 @@ def estimate_speech_sec(narration: object, speed: float = 1.0) -> float:
99
99
 
100
100
 
101
101
  def template_tail_pad_sec(template_config: Optional[dict]) -> float:
102
- """场景尾巴:模板声明的 capabilities.defaultTailPadSec,缺省 1.5s。"""
102
+
103
103
  caps = (template_config or {}).get("capabilities") or {}
104
104
  v = caps.get("defaultTailPadSec") if isinstance(caps, dict) else None
105
105
  if isinstance(v, bool) or not isinstance(v, (int, float)) or v < 0:
@@ -108,7 +108,7 @@ def template_tail_pad_sec(template_config: Optional[dict]) -> float:
108
108
 
109
109
 
110
110
  def estimate_scene_sec(narration: object, speed: float = 1.0, tail_pad_sec: float = DEFAULT_TAIL_PAD_SEC) -> float:
111
- """这一场按旁白估出来的时长。没有旁白返回 0(由调用方退回场景自己的 duration)。"""
111
+
112
112
  speech = estimate_speech_sec(narration, speed)
113
113
  return speech + tail_pad_sec if speech > 0 else 0.0
114
114
 
@@ -120,10 +120,10 @@ def speech_budget(
120
120
  lines: int = 1,
121
121
  tail_pad_sec: float = DEFAULT_TAIL_PAD_SEC,
122
122
  ) -> int:
123
- """一场 ``duration_sec`` 秒的场景,旁白最多写多少字(中文)/ 词(英文)。
124
123
 
125
- ``lines`` > 1 时扣掉分行停顿 —— 逐项高亮的版式同样秒数能写的字明显更少。
126
- 至少返回 1:预算为 0 的场景说明秒数本身就不够,调用方应当提示加时长而不是写空旁白。
127
- """
124
+
125
+
126
+
127
+
128
128
  available = duration_sec - tail_pad_sec - LINE_PAUSE_SEC * max(lines - 1, 0)
129
129
  return max(1, int(math.floor(available * speech_rate(language, speed))))
@@ -1,12 +1,12 @@
1
- """Template-declared content requirements and recursive asset references.
2
1
 
3
- Shared by DSL preparation and saved RenderPlan validation. Requirements use a
4
- small JSON Schema subset; values never appear in diagnostic messages.
5
- """
2
+
3
+
4
+
5
+
6
6
 
7
7
 
8
8
  def payload_values(props: dict) -> dict:
9
- """Flatten templateData wrappers, with non-null outer props taking precedence."""
9
+
10
10
  layers, seen = [], set()
11
11
  while isinstance(props, dict) and id(props) not in seen and len(layers) < 10:
12
12
  layers.append(props)
@@ -19,7 +19,7 @@ def payload_values(props: dict) -> dict:
19
19
 
20
20
 
21
21
  def requirement_errors(value, schema: dict, path: str = "customPayload") -> list[str]:
22
- """Enforce type/const/enum/required/properties/items/minItems/anyOf."""
22
+
23
23
  errors = []
24
24
  kind = schema.get("type")
25
25
  matches = {"object": isinstance(value, dict), "array": isinstance(value, list),
@@ -58,7 +58,7 @@ def required_payload_errors(template: dict, props: dict) -> list[str]:
58
58
 
59
59
 
60
60
  def asset_references(value, path: str = "", *, bound_props: bool = False):
61
- """Walk nested assetRef fields; bound props also use *AssetId names."""
61
+
62
62
  if isinstance(value, list):
63
63
  for index, item in enumerate(value):
64
64
  yield from asset_references(item, f"{path}[{index}]", bound_props=bound_props)
@@ -1,32 +1,32 @@
1
- """
2
- 图片 Prompt 增强模块 — 利用旁白语义 + 模版视觉指南生成高质量图片提示词。
3
1
 
4
- 纯函数模块,无外部依赖。主要入口:
5
- - build_enhanced_prompt() : 根据 purpose/topic/style/narration/styleGuide 构建增强 prompt
6
- - enrich_asset_payload() : 一站式入口,自动判断并增强 asset payload
7
- - compose_image_payload() : imageStyleGuide.mode == "compose" 的模板专用 —— agent 只写
8
- 画面主体,这里在旁白定稿后确定地、只一次地套上模板画风
9
- (见 docs/picture-book-image-prompt-design.md)
10
- """
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
11
 
12
12
  import re
13
13
  from typing import Optional
14
14
 
15
- # ---------------------------------------------------------------------------
16
- # 常量
17
- # ---------------------------------------------------------------------------
18
15
 
19
- # 口语化填充词 / 无视觉意义的片段
16
+
17
+
18
+
19
+
20
20
  _FILLER_PATTERNS = re.compile(
21
21
  r"(那么|其实|所以说|然后呢|接下来|大家好|你好|对吧|对不对|是不是|"
22
22
  r"嗯|啊|呢|吧|哦|哈|了|的话|来说|而言|就是说|换句话说|"
23
23
  r"如果觉得有用就|点个关注|下期见|别忘了)"
24
24
  )
25
25
 
26
- # 标点符号归一化
26
+
27
27
  _PUNCT = re.compile("[,。!?、;:\u201c\u201d\u2018\u2019《》【】()\\s]+")
28
28
 
29
- # 默认按 purpose 的 prompt 模板(无模版 styleGuide 时的兜底)
29
+
30
30
  DEFAULT_PURPOSE_PATTERNS: dict[str, str] = {
31
31
  "opening": "{narration_summary},{topic}主题概念图,视觉冲击力强,{style_suffix},高清大图",
32
32
  "intro": "{narration_summary},{topic}主题引入场景,{style_suffix},清晰直观",
@@ -39,30 +39,30 @@ DEFAULT_PURPOSE_PATTERNS: dict[str, str] = {
39
39
  "ending": "感谢观看结尾画面,{style_suffix},温馨大气",
40
40
  }
41
41
 
42
- # prompt 长度阈值:超过此字符数的 prompt 视为已精心编写
42
+
43
43
  WELL_CRAFTED_THRESHOLD = 80
44
44
 
45
- # gen_script 骨架占位的前缀。旁白与 compose 模式的图片主体共用这一组:以它开头说明
46
- # agent 还没把内容填进来。gen_script 从这里导入,两边不各写一份。
45
+
46
+
47
47
  SKELETON_PREFIXES = ("[骨架待填充]", "[skeleton placeholder]")
48
48
 
49
- # compose 模式下 gen_script 写进 payload.prompt 的占位:告诉 agent 这里只填「画什么」。
49
+
50
50
  COMPOSE_SUBJECT_PLACEHOLDER = (
51
51
  f"{SKELETON_PREFIXES[0]} 本页画面主体:谁、在哪、在做什么、什么表情、什么光线"
52
52
  )
53
53
 
54
- # compose 模式下,模板没给本 purpose 写 promptTemplate 时的兜底
54
+
55
55
  COMPOSE_DEFAULT_TEMPLATE = "{subject}。{character_sheet}"
56
56
 
57
57
  _CJK_CHAR = re.compile(r"[\u4e00-\u9fff\u3400-\u4dbf]")
58
58
 
59
- # ---------------------------------------------------------------------------
60
- # 工具函数
61
- # ---------------------------------------------------------------------------
59
+
60
+
61
+
62
62
 
63
63
 
64
64
  def _is_mostly_latin(text: str) -> bool:
65
- """CJK 字符占非空白字符不足 10% —— 按英文等拼音文字处理。"""
65
+
66
66
  chars = [c for c in text if not c.isspace()]
67
67
  if not chars:
68
68
  return False
@@ -70,16 +70,16 @@ def _is_mostly_latin(text: str) -> bool:
70
70
 
71
71
 
72
72
  def summarize_narration(text: str, max_chars: int = 60) -> str:
73
- """从旁白文本提取视觉关键信息,去除口语化填充词。
74
73
 
75
- 英文旁白不套中文填充词正则("了""的话"之类对英文无意义,反而可能误删),
76
- 按单词边界截断,上限放宽到 200 字符 —— 60 字符的英文常常只剩半句。
77
74
 
78
- >>> summarize_narration("那么接下来我们来看一下AI如何帮你高效背单词")
79
- 'AI如何帮你高效背单词'
80
- >>> summarize_narration("A tiny seed slept in the warm, dark soil.")
81
- 'A tiny seed slept in the warm, dark soil'
82
- """
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
83
  if not text:
84
84
  return ""
85
85
  if _is_mostly_latin(text):
@@ -90,25 +90,25 @@ def summarize_narration(text: str, max_chars: int = 60) -> str:
90
90
  space = cut.rfind(" ")
91
91
  cleaned = (cut[:space] if space > 0 else cut).rstrip(".!?;:,")
92
92
  return cleaned or text[:limit]
93
- # 去除填充词
93
+
94
94
  cleaned = _FILLER_PATTERNS.sub("", text)
95
- # 归一化标点 → 空格
95
+
96
96
  cleaned = _PUNCT.sub(" ", cleaned).strip()
97
- # 去除开头尾部空白
97
+
98
98
  if not cleaned:
99
99
  return text[:max_chars]
100
- # 截断到 max_chars
100
+
101
101
  if len(cleaned) > max_chars:
102
102
  cleaned = cleaned[:max_chars]
103
103
  return cleaned
104
104
 
105
105
 
106
106
  def extract_scene_context(scene: dict) -> dict:
107
- """从 DSL scene 提取视觉相关上下文。
108
107
 
109
- Returns:
110
- dict with keys: narration_text, title, badge, headline, subheadline, purpose
111
- """
108
+
109
+
110
+
111
+
112
112
  purpose = scene.get("purpose", "default")
113
113
  narration_text = ""
114
114
  audio = scene.get("audio", {})
@@ -134,10 +134,10 @@ def extract_scene_context(scene: dict) -> dict:
134
134
 
135
135
 
136
136
  def resolve_style_guide(template: Optional[dict], variant_id: str = "") -> dict:
137
- """合并模版根级 + 变体级的 imageStyleGuide。
138
137
 
139
- 变体级字段覆盖根级同名字段,purposeOverrides 做 merge。
140
- """
138
+
139
+
140
+
141
141
  if not template:
142
142
  return {}
143
143
 
@@ -150,7 +150,7 @@ def resolve_style_guide(template: Optional[dict], variant_id: str = "") -> dict:
150
150
  if not variant_guide:
151
151
  return base
152
152
 
153
- # 合并 purposeOverrides
153
+
154
154
  base_overrides = dict(base.get("purposeOverrides", {}) or {})
155
155
  variant_overrides = dict(variant_guide.get("purposeOverrides", {}) or {})
156
156
 
@@ -162,9 +162,9 @@ def resolve_style_guide(template: Optional[dict], variant_id: str = "") -> dict:
162
162
  return merged
163
163
 
164
164
 
165
- # ---------------------------------------------------------------------------
166
- # 核心构建函数
167
- # ---------------------------------------------------------------------------
165
+
166
+
167
+
168
168
 
169
169
 
170
170
  def build_enhanced_prompt(
@@ -175,21 +175,21 @@ def build_enhanced_prompt(
175
175
  scene_title: str = "",
176
176
  style_guide: Optional[dict] = None,
177
177
  ) -> dict:
178
- """构建增强 prompt,返回 {prompt, negative_prompt, guidance_scale}。
179
178
 
180
- 优先使用 style_guide 中的模板和修饰符;无 style_guide 时使用内置默认模板。
181
- """
179
+
180
+
181
+
182
182
  style_guide = style_guide or {}
183
183
  style_suffix = f"{style}风格" if style else ""
184
184
 
185
- # 提取旁白摘要
185
+
186
186
  narration_summary = summarize_narration(narration_text)
187
187
  if not narration_summary and scene_title:
188
188
  narration_summary = scene_title
189
189
  if not narration_summary:
190
190
  narration_summary = topic or ""
191
191
 
192
- # 基础变量
192
+
193
193
  vars_ = {
194
194
  "narration_summary": narration_summary,
195
195
  "topic": topic or "",
@@ -197,7 +197,7 @@ def build_enhanced_prompt(
197
197
  "scene_title": scene_title or "",
198
198
  }
199
199
 
200
- # 决定 prompt 模板
200
+
201
201
  purpose_overrides = style_guide.get("purposeOverrides", {}) or {}
202
202
  override = purpose_overrides.get(purpose, {})
203
203
 
@@ -207,28 +207,28 @@ def build_enhanced_prompt(
207
207
  purpose, DEFAULT_PURPOSE_PATTERNS.get("point", "")
208
208
  )
209
209
 
210
- # 格式化模板(安全替换)
210
+
211
211
  prompt = _tidy_punctuation(_safe_format(prompt_template, vars_))
212
212
 
213
- # 追加 styleModifier
213
+
214
214
  style_modifier = override.get("styleModifier", "")
215
215
  if style_modifier:
216
216
  prompt = f"{prompt},{style_modifier}"
217
217
 
218
- # 追加 baseStyle
218
+
219
219
  base_style = style_guide.get("baseStyle", "")
220
220
  if base_style:
221
221
  prompt = f"{prompt},{base_style}"
222
222
 
223
- # 追加 colorDirective
223
+
224
224
  color_directive = style_guide.get("colorDirective", "")
225
225
  if color_directive:
226
226
  prompt = f"{prompt},{color_directive}"
227
227
 
228
- # negative prompt
228
+
229
229
  negative_prompt = override.get("negativePrompt", "") or style_guide.get("negativePrompt", "")
230
230
 
231
- # guidance scale
231
+
232
232
  guidance_scale = style_guide.get("guidanceScale")
233
233
 
234
234
  result = {"prompt": prompt}
@@ -241,7 +241,7 @@ def build_enhanced_prompt(
241
241
 
242
242
 
243
243
  def _safe_format(template: str, vars_: dict) -> str:
244
- """安全的字符串格式化,缺失变量保留占位符原文。"""
244
+
245
245
  try:
246
246
  return template.format(**vars_)
247
247
  except (KeyError, IndexError):
@@ -253,7 +253,7 @@ def _safe_format(template: str, vars_: dict) -> str:
253
253
 
254
254
  _EMPTY_BRACKETS = re.compile(r"[((]\s*[))]")
255
255
  _PUNCT_BEFORE_CLOSE = re.compile(r"[,。、;,;.\s]+(?=[))])")
256
- # 两个及以上相邻的分隔标点(中间可夹空白):空变量替换后留下的「,,」「。,」「, 。」等
256
+
257
257
  _PUNCT_RUN = re.compile(r"[,。、;,;.]\s*(?:[,。、;,;.]\s*)+")
258
258
  _LEADING_PUNCT = re.compile(r"^[\s,。、;,;.]+")
259
259
  _TRAILING_SOFT_PUNCT = re.compile(r"[\s,、;,;]+$")
@@ -266,12 +266,12 @@ def _collapse_punct_run(match: "re.Match[str]") -> str:
266
266
 
267
267
 
268
268
  def _tidy_punctuation(text: str) -> str:
269
- """清理模板变量为空时留下的标点噪声。
270
269
 
271
- ``{style_suffix}`` / ``{character_sheet}`` 这类变量常为空,替换后会留下
272
- 「,,」「。。」「()」和行首孤立的逗号。连续分隔符合并成一个(含句号就留句号),
273
- 去掉空括号、行首标点、行尾逗号,多余空格压成一个。单词间的单个空格不动。
274
- """
270
+
271
+
272
+
273
+
274
+
275
275
  if not text:
276
276
  return ""
277
277
  out = _PUNCT_BEFORE_CLOSE.sub("", text)
@@ -283,9 +283,9 @@ def _tidy_punctuation(text: str) -> str:
283
283
  return out.strip()
284
284
 
285
285
 
286
- # ---------------------------------------------------------------------------
287
- # 一站式入口
288
- # ---------------------------------------------------------------------------
286
+
287
+
288
+
289
289
 
290
290
 
291
291
  def enrich_asset_payload(
@@ -295,14 +295,14 @@ def enrich_asset_payload(
295
295
  dsl_meta: Optional[dict] = None,
296
296
  variant_id: str = "",
297
297
  ) -> dict:
298
- """增强 asset payload 中的图片 prompt。
299
298
 
300
- - prompt 已超过 WELL_CRAFTED_THRESHOLD 字符的,仅补充 negativePrompt(如缺失)
301
- - 否则用 build_enhanced_prompt 重新构建
302
299
 
303
- Returns:
304
- 新的 payload dict(不修改原始对象)
305
- """
300
+
301
+
302
+
303
+
304
+
305
+
306
306
  payload = dict(payload)
307
307
  current_prompt = payload.get("prompt", "")
308
308
 
@@ -312,7 +312,7 @@ def enrich_asset_payload(
312
312
 
313
313
  style_guide = resolve_style_guide(template, variant_id)
314
314
 
315
- # 已精心编写的 prompt → 仅补充 negativePrompt
315
+
316
316
  if len(current_prompt) > WELL_CRAFTED_THRESHOLD:
317
317
  if "negativePrompt" not in payload and style_guide.get("negativePrompt"):
318
318
  payload["negativePrompt"] = style_guide["negativePrompt"]
@@ -320,7 +320,7 @@ def enrich_asset_payload(
320
320
  payload["guidanceScale"] = style_guide["guidanceScale"]
321
321
  return payload
322
322
 
323
- # 从 scene 提取上下文
323
+
324
324
  scene_ctx = extract_scene_context(scene) if scene else {}
325
325
  purpose = scene_ctx.get("purpose", "point")
326
326
  narration_text = scene_ctx.get("narration_text", "")
@@ -344,9 +344,9 @@ def enrich_asset_payload(
344
344
  return payload
345
345
 
346
346
 
347
- # ---------------------------------------------------------------------------
348
- # compose 模式:agent 写「画什么」,模板管「怎么画」
349
- # ---------------------------------------------------------------------------
347
+
348
+
349
+
350
350
 
351
351
 
352
352
  def is_compose_mode(style_guide: Optional[dict]) -> bool:
@@ -368,19 +368,19 @@ def compose_image_payload(
368
368
  dsl_meta: Optional[dict] = None,
369
369
  variant_id: str = "",
370
370
  ) -> dict:
371
- """把模板的 imageStyleGuide 套到一个 gen-image 素材的 payload 上(compose 模式专用)。
372
371
 
373
- ``payload.prompt`` 在 compose 模式下的语义是**画面主体**(这一页画什么),不含画风。
374
- 这里拼出最终 prompt = promptTemplate(主体 / 角色设定 / 文字层…) + styleModifier +
375
- baseStyle + colorDirective,并补上 negativePrompt / ratio / guidanceScale / seed。
376
372
 
377
- - 非 compose 模板:原样返回(不改变 legacy 模板的行为)。
378
- - 幂等:``styleGuideApplied`` 等于当前模板版本时原样返回;版本变了则从
379
- ``promptSubject``(上次 compose 前的主体)重新拼,画风不会叠两遍。
380
- - 主体为空或仍是骨架占位时退回本场景的旁白全文 / 文字层。
381
373
 
382
- 返回新 dict,不修改入参。
383
- """
374
+
375
+
376
+
377
+
378
+
379
+
380
+
381
+
382
+
383
+
384
384
  guide = resolve_style_guide(template, variant_id)
385
385
  if not is_compose_mode(guide):
386
386
  return payload
@@ -392,7 +392,7 @@ def compose_image_payload(
392
392
  ctx = extract_scene_context(scene) if scene else {}
393
393
  purpose = ctx.get("purpose") or "point"
394
394
 
395
- # 上次 compose 过(模板升版后重跑)→ 主体在 promptSubject 里,prompt 已是成品
395
+
396
396
  subject = str(payload.get("promptSubject") or payload.get("prompt") or "").strip()
397
397
  if _is_placeholder_subject(subject):
398
398
  subject = (
@@ -413,15 +413,15 @@ def compose_image_payload(
413
413
  "character_sheet": str(meta.get("characterSheet") or "").strip(),
414
414
  "topic": str(meta.get("topic") or meta.get("title") or ""),
415
415
  "style_suffix": f"{style}风格" if style else "",
416
- # 旧变量名兼容:legacy promptTemplate 里写的 {narration_summary} 在这里就是主体
416
+
417
417
  "narration_summary": subject,
418
418
  }
419
419
 
420
420
  overrides = guide.get("purposeOverrides", {}) or {}
421
421
  override = overrides.get(purpose) or {}
422
422
  parts = [
423
- # 没有针对本 purpose 的模板(如 cta / ending)时角色设定照样注入 —— 否则恰好
424
- # 这一页的主角会换个样子。
423
+
424
+
425
425
  _safe_format(override.get("promptTemplate") or COMPOSE_DEFAULT_TEMPLATE, vars_),
426
426
  override.get("styleModifier", ""),
427
427
  guide.get("baseStyle", ""),
@@ -456,11 +456,11 @@ def _scene_refs(node, asset_id: str) -> bool:
456
456
 
457
457
 
458
458
  def compose_dsl_image_prompts(dsl: dict, template: Optional[dict]) -> list[str]:
459
- """对整份 DSL 里待生成的图片素材逐个 compose(就地改 dsl),返回日志行。
460
459
 
461
- 只处理 ``type=image, source=gen-image, status∈{planned, missing}``;已生成 / 自带的图
462
- 不动。非 compose 模板是零成本 no-op(返回空列表)。
463
- """
460
+
461
+
462
+
463
+
464
464
  meta = dsl.get("meta") or {}
465
465
  variant_id = str(meta.get("variantId") or "")
466
466
  if not is_compose_mode(resolve_style_guide(template, variant_id)):