@remixmate/cli 0.9.72 → 0.9.74

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,14 +1,14 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- 解构产物 → 视频结构分析报告
4
2
 
5
- 从 deconstruction.json(video-parser 的输出)提取钩子、叙事结构、文案节奏、
6
- 场景编排等关键元素,输出结构化的分析报告 analysis.json。
7
3
 
8
- 用法:
9
- python analyze_video.py -i ./deconstructed_xxx/deconstruction.json
10
- python analyze_video.py -i ./deconstructed_xxx/ -o ./reports/
11
- """
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
12
 
13
13
  import argparse
14
14
  import json
@@ -19,9 +19,9 @@ import sys
19
19
  QUIET = False
20
20
 
21
21
 
22
- # ---------------------------------------------------------------------------
23
- # Logging
24
- # ---------------------------------------------------------------------------
22
+
23
+
24
+
25
25
 
26
26
  def log(msg: str) -> None:
27
27
  if not QUIET:
@@ -32,12 +32,12 @@ def log_err(msg: str) -> None:
32
32
  print(msg, file=sys.stderr, flush=True)
33
33
 
34
34
 
35
- # ---------------------------------------------------------------------------
36
- # Load
37
- # ---------------------------------------------------------------------------
35
+
36
+
37
+
38
38
 
39
39
  def load_deconstruction(input_path: str):
40
- """Load deconstruction.json, return (dict, input_dir)."""
40
+
41
41
  if os.path.isdir(input_path):
42
42
  json_path = os.path.join(input_path, "deconstruction.json")
43
43
  else:
@@ -54,27 +54,27 @@ def load_deconstruction(input_path: str):
54
54
  return data, input_dir
55
55
 
56
56
 
57
- # ---------------------------------------------------------------------------
58
- # Language detection
59
- # ---------------------------------------------------------------------------
57
+
58
+
59
+
60
60
 
61
61
  def detect_language(text: str) -> str:
62
- """Simple heuristic: if >30% CJK chars, treat as zh-CN, else en."""
62
+
63
63
  if not text:
64
64
  return "zh-CN"
65
65
  cjk_count = sum(1 for ch in text if '\u4e00' <= ch <= '\u9fff')
66
66
  return "zh-CN" if cjk_count > len(text) * 0.3 else "en"
67
67
 
68
68
 
69
- # ---------------------------------------------------------------------------
70
- # Utterance ↔ scene matching
71
- # ---------------------------------------------------------------------------
69
+
70
+
71
+
72
72
 
73
73
  def match_utterances_to_scenes(scenes: list, utterances: list) -> dict:
74
- """Assign ASR utterances to scenes by time overlap.
75
74
 
76
- Returns {scene_index: [utterance, ...]}
77
- """
75
+
76
+
77
+
78
78
  result = {s["index"]: [] for s in scenes}
79
79
  for utt in utterances:
80
80
  utt_start = utt.get("startTime", 0) / 1000.0
@@ -90,15 +90,15 @@ def match_utterances_to_scenes(scenes: list, utterances: list) -> dict:
90
90
  return result
91
91
 
92
92
 
93
- # ---------------------------------------------------------------------------
94
- # Scene roles
95
- # ---------------------------------------------------------------------------
93
+
94
+
95
+
96
96
 
97
97
  def infer_scene_roles(scene_count: int) -> list:
98
- """Infer role labels from scene count.
99
98
 
100
- Returns list like ["opening", "point", ..., "ending"].
101
- """
99
+
100
+
101
+
102
102
  if scene_count <= 0:
103
103
  return ["opening"]
104
104
  if scene_count == 1:
@@ -114,21 +114,21 @@ def infer_scene_roles(scene_count: int) -> list:
114
114
  return roles
115
115
 
116
116
 
117
- # ---------------------------------------------------------------------------
118
- # Hook extraction
119
- # ---------------------------------------------------------------------------
117
+
118
+
119
+
120
120
 
121
121
  def analyze_hook(scenes: list, utterance_map: dict, asr: dict) -> dict:
122
- """Extract hook from the first 3 seconds of the first scene."""
122
+
123
123
  if not scenes:
124
124
  return {"text": "", "durationSec": 0.0, "strategy": "none"}
125
125
 
126
126
  first_scene = scenes[0]
127
127
  hook_end = first_scene["startTimeSec"] + 3.0
128
128
 
129
- # Collect words that fall within the hook window.
130
- # Space tokens (startTime == -1) are included between valid words
131
- # to preserve natural text spacing.
129
+
130
+
131
+
132
132
  hook_words = []
133
133
  utterances = asr.get("utterances", [])
134
134
  exceeded = False
@@ -138,7 +138,7 @@ def analyze_hook(scenes: list, utterance_map: dict, asr: dict) -> dict:
138
138
  for w in utt.get("words", []):
139
139
  w_start = w.get("startTime", -1) / 1000.0
140
140
  if w_start < 0:
141
- # Space/punctuation token — include if we haven't exceeded the window
141
+
142
142
  hook_words.append(w["text"])
143
143
  continue
144
144
  if w_start >= hook_end:
@@ -148,12 +148,12 @@ def analyze_hook(scenes: list, utterance_map: dict, asr: dict) -> dict:
148
148
 
149
149
  hook_text = "".join(hook_words).strip()
150
150
  if not hook_text:
151
- # Fallback: use the first utterance text in the first scene
151
+
152
152
  first_utts = utterance_map.get(first_scene["index"], [])
153
153
  if first_utts:
154
154
  hook_text = first_utts[0].get("text", "")
155
155
 
156
- # Determine hook duration (end of last word in hook window)
156
+
157
157
  hook_duration = 3.0
158
158
  for utt in utterances:
159
159
  for w in utt.get("words", []):
@@ -162,7 +162,7 @@ def analyze_hook(scenes: list, utterance_map: dict, asr: dict) -> dict:
162
162
  if 0 <= w_start < hook_end and w_end > 0:
163
163
  hook_duration = min(3.0, w_end)
164
164
 
165
- # Classify hook strategy
165
+
166
166
  strategy = "none"
167
167
  if hook_text:
168
168
  stripped = hook_text.strip()
@@ -173,7 +173,7 @@ def analyze_hook(scenes: list, utterance_map: dict, asr: dict) -> dict:
173
173
  else:
174
174
  strategy = "direct-content"
175
175
 
176
- # Word count: non-space characters for CJK, space-separated tokens for English
176
+
177
177
  hook_word_count = len(hook_text.replace(" ", "")) if any(
178
178
  '\u4e00' <= ch <= '\u9fff' for ch in hook_text
179
179
  ) else len(hook_text.split())
@@ -186,12 +186,12 @@ def analyze_hook(scenes: list, utterance_map: dict, asr: dict) -> dict:
186
186
  }
187
187
 
188
188
 
189
- # ---------------------------------------------------------------------------
190
- # Narrative structure
191
- # ---------------------------------------------------------------------------
189
+
190
+
191
+
192
192
 
193
193
  def build_narrative_structure(scenes: list, utterance_map: dict, total_duration: float) -> list:
194
- """Build per-scene narrative structure entries."""
194
+
195
195
  roles = infer_scene_roles(len(scenes))
196
196
  structure = []
197
197
  for i, scene in enumerate(scenes):
@@ -215,12 +215,12 @@ def build_narrative_structure(scenes: list, utterance_map: dict, total_duration:
215
215
  return structure
216
216
 
217
217
 
218
- # ---------------------------------------------------------------------------
219
- # Pacing analysis
220
- # ---------------------------------------------------------------------------
218
+
219
+
220
+
221
221
 
222
222
  def analyze_pacing(scenes: list, total_duration: float) -> dict:
223
- """Analyze scene pacing / rhythm."""
223
+
224
224
  durations = []
225
225
  for s in scenes:
226
226
  d = round(s["endTimeSec"] - s["startTimeSec"], 1)
@@ -239,7 +239,7 @@ def analyze_pacing(scenes: list, total_duration: float) -> dict:
239
239
  mn = min(durations)
240
240
  mx = max(durations)
241
241
 
242
- # Determine rhythm
242
+
243
243
  if len(durations) == 1:
244
244
  rhythm = "uniform"
245
245
  elif mx - mn < 1.0:
@@ -272,23 +272,23 @@ def analyze_pacing(scenes: list, total_duration: float) -> dict:
272
272
  }
273
273
 
274
274
 
275
- # ---------------------------------------------------------------------------
276
- # Keywords & silence detection
277
- # ---------------------------------------------------------------------------
275
+
276
+
277
+
278
278
 
279
279
  def extract_keywords(words: list, full_text: str, language: str) -> list:
280
- """Extract meaningful keywords from ASR data.
281
280
 
282
- For English: use word-level tokens, filter stopwords and punctuation.
283
- For Chinese: split full text on punctuation into phrases, then extract
284
- multi-character content tokens (simple segmentation by punctuation boundaries).
285
- """
281
+
282
+
283
+
284
+
285
+
286
286
  if language.startswith("zh"):
287
287
  return _extract_keywords_zh(full_text)
288
288
  return _extract_keywords_en(words)
289
289
 
290
290
 
291
- # Common Chinese stopwords (function words with little semantic value)
291
+
292
292
  _ZH_STOPWORDS = {
293
293
  "的", "了", "在", "是", "我", "们", "有", "和", "就", "不", "人", "都",
294
294
  "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着",
@@ -301,8 +301,8 @@ _ZH_STOPWORDS = {
301
301
 
302
302
 
303
303
  def _extract_keywords_zh(full_text: str) -> list:
304
- """Extract Chinese keywords by splitting on punctuation and filtering."""
305
- # Split on common punctuation to get phrase segments
304
+
305
+
306
306
  segments = re.split(r'[,,。、!?!?.…::;;\-—–\s\'\"()()\[\]【】]+', full_text)
307
307
 
308
308
  keywords = []
@@ -311,8 +311,8 @@ def _extract_keywords_zh(full_text: str) -> list:
311
311
  seg = seg.strip()
312
312
  if not seg:
313
313
  continue
314
- # Extract multi-char tokens from each segment using a simple greedy approach:
315
- # scan for runs of CJK characters (2+ chars) and non-CJK tokens (e.g. "1V1", "3500")
314
+
315
+
316
316
  tokens = re.findall(r'[\u4e00-\u9fff]{2,}|[A-Za-z0-9]+(?:[A-Za-z0-9]+)*', seg)
317
317
  for tok in tokens:
318
318
  lower = tok.lower()
@@ -320,7 +320,7 @@ def _extract_keywords_zh(full_text: str) -> list:
320
320
  continue
321
321
  if tok in _ZH_STOPWORDS:
322
322
  continue
323
- # Skip very short CJK tokens that are likely function words
323
+
324
324
  if len(tok) == 2 and all('\u4e00' <= c <= '\u9fff' for c in tok) and tok in _ZH_STOPWORDS:
325
325
  continue
326
326
  seen.add(lower)
@@ -329,7 +329,7 @@ def _extract_keywords_zh(full_text: str) -> list:
329
329
 
330
330
 
331
331
  def _extract_keywords_en(words: list) -> list:
332
- """Extract English keywords from word-level ASR data."""
332
+
333
333
  stop_pattern = re.compile(r'^[\s,,。、!?!?.…::;;\-—–\'\"()()\[\]【】]+$')
334
334
  en_stopwords = {
335
335
  "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
@@ -362,12 +362,12 @@ def _extract_keywords_en(words: list) -> list:
362
362
 
363
363
 
364
364
  def detect_silence_segments(utterances: list, total_duration: float) -> list:
365
- """Find gaps between utterances longer than 1 second."""
365
+
366
366
  if not utterances:
367
367
  return []
368
368
 
369
369
  segments = []
370
- # Gap before first utterance
370
+
371
371
  first_start = utterances[0].get("startTime", 0) / 1000.0
372
372
  if first_start > 1.0:
373
373
  segments.append({
@@ -376,7 +376,7 @@ def detect_silence_segments(utterances: list, total_duration: float) -> list:
376
376
  "durationSec": round(first_start, 1),
377
377
  })
378
378
 
379
- # Gaps between utterances
379
+
380
380
  for i in range(len(utterances) - 1):
381
381
  cur_end = utterances[i].get("endTime", 0) / 1000.0
382
382
  next_start = utterances[i + 1].get("startTime", 0) / 1000.0
@@ -388,7 +388,7 @@ def detect_silence_segments(utterances: list, total_duration: float) -> list:
388
388
  "durationSec": round(gap, 1),
389
389
  })
390
390
 
391
- # Gap after last utterance
391
+
392
392
  last_end = utterances[-1].get("endTime", 0) / 1000.0
393
393
  if total_duration - last_end > 1.0:
394
394
  segments.append({
@@ -400,37 +400,37 @@ def detect_silence_segments(utterances: list, total_duration: float) -> list:
400
400
  return segments
401
401
 
402
402
 
403
- # ---------------------------------------------------------------------------
404
- # Text structure
405
- # ---------------------------------------------------------------------------
403
+
404
+
405
+
406
406
 
407
407
  def analyze_text_structure(asr: dict, scenes: list, utterance_map: dict, total_duration: float) -> dict:
408
- """Analyze text structure: speech rate, silence segments, keywords."""
408
+
409
409
  full_text = asr.get("text", "")
410
410
  utterances = asr.get("utterances", [])
411
411
 
412
- # Collect all words from all utterances
412
+
413
413
  all_words = []
414
414
  for utt in utterances:
415
415
  all_words.extend(utt.get("words", []))
416
416
 
417
- # Speech duration (sum of utterance durations)
417
+
418
418
  speech_duration = 0.0
419
419
  for utt in utterances:
420
420
  s = utt.get("startTime", 0) / 1000.0
421
421
  e = utt.get("endTime", 0) / 1000.0
422
422
  speech_duration += max(0, e - s)
423
423
 
424
- # Text length (non-space characters)
424
+
425
425
  text_len = len(full_text.replace(" ", ""))
426
426
 
427
- # Speech rate: chars per second of speech
427
+
428
428
  speech_rate = round(text_len / speech_duration, 1) if speech_duration > 0 else 0.0
429
429
 
430
- # Silence segments
430
+
431
431
  silence_segments = detect_silence_segments(utterances, total_duration)
432
432
 
433
- # Keywords
433
+
434
434
  keywords = extract_keywords(all_words, full_text, detect_language(full_text))
435
435
 
436
436
  return {
@@ -441,18 +441,18 @@ def analyze_text_structure(asr: dict, scenes: list, utterance_map: dict, total_d
441
441
  }
442
442
 
443
443
 
444
- # ---------------------------------------------------------------------------
445
- # Audio pattern
446
- # ---------------------------------------------------------------------------
444
+
445
+
446
+
447
447
 
448
448
  def analyze_audio_pattern(asr: dict, audio: dict, total_duration: float) -> dict:
449
- """Analyze audio pattern: narration presence, speech/silence ratio."""
449
+
450
450
  utterances = asr.get("utterances", [])
451
451
  has_narration = len(utterances) > 0
452
452
 
453
453
  narration_url = audio.get("url", "")
454
454
 
455
- # Total speech duration
455
+
456
456
  speech_duration = 0.0
457
457
  for utt in utterances:
458
458
  s = utt.get("startTime", 0) / 1000.0
@@ -470,15 +470,15 @@ def analyze_audio_pattern(asr: dict, audio: dict, total_duration: float) -> dict
470
470
  }
471
471
 
472
472
 
473
- # ---------------------------------------------------------------------------
474
- # CTA detection
475
- # ---------------------------------------------------------------------------
473
+
474
+
475
+
476
476
 
477
477
  CTA_KEYWORDS = [
478
- # English
478
+
479
479
  "subscribe", "like", "follow", "share", "comment", "click", "link",
480
480
  "sign up", "buy now", "check out", "tap", "swipe up", "join",
481
- # Chinese — verbs / phrases commonly used in CTA
481
+
482
482
  "关注", "点赞", "转发", "评论", "订阅", "分享", "点击", "链接",
483
483
  "扫码", "加入", "报名", "下单", "购买", "赶紧", "抓紧", "安排",
484
484
  "左下角", "右下角", "小黄车", "橱窗", "主页", "私信", "留言",
@@ -486,26 +486,26 @@ CTA_KEYWORDS = [
486
486
 
487
487
 
488
488
  def analyze_cta(scenes: list, utterance_map: dict, utterances: list) -> dict:
489
- """Detect call-to-action in the ending portion of the video.
490
489
 
491
- When there are multiple scenes, check the last scene's text.
492
- When there is only one scene, check the last utterance(s) to avoid
493
- treating the entire video text as the ending.
494
- """
490
+
491
+
492
+
493
+
494
+
495
495
  if not scenes:
496
496
  return {"hasExplicitCta": False, "endingText": "", "endingDurationSec": 0.0}
497
497
 
498
498
  last_scene = scenes[-1]
499
499
  scene_duration = round(last_scene["endTimeSec"] - last_scene["startTimeSec"], 1)
500
500
 
501
- # Determine ending text: use last scene's utterances,
502
- # but if there's only one scene, narrow down to the last utterance(s)
501
+
502
+
503
503
  utts = utterance_map.get(last_scene["index"], [])
504
504
  if len(scenes) == 1 and len(utts) > 1:
505
- # Single-scene video: take the last 1-2 utterances as ending
505
+
506
506
  ending_utts = utts[-2:] if len(utts) >= 2 else utts[-1:]
507
507
  ending_text = " ".join(u.get("text", "") for u in ending_utts).strip()
508
- # Approximate ending duration from last utterance timestamps
508
+
509
509
  first_end_utt = ending_utts[0]
510
510
  ending_duration = round(
511
511
  (last_scene["endTimeSec"] - first_end_utt.get("startTime", 0) / 1000.0), 1
@@ -514,12 +514,12 @@ def analyze_cta(scenes: list, utterance_map: dict, utterances: list) -> dict:
514
514
  ending_text = " ".join(u.get("text", "") for u in utts).strip()
515
515
  ending_duration = scene_duration
516
516
 
517
- # Keyword matching
517
+
518
518
  lower_text = ending_text.lower()
519
519
  matched_count = sum(1 for kw in CTA_KEYWORDS if kw in lower_text)
520
520
  has_cta = matched_count > 0
521
521
 
522
- # Strength: none / weak / strong
522
+
523
523
  if matched_count == 0:
524
524
  strength = "none"
525
525
  elif matched_count == 1:
@@ -535,12 +535,12 @@ def analyze_cta(scenes: list, utterance_map: dict, utterances: list) -> dict:
535
535
  }
536
536
 
537
537
 
538
- # ---------------------------------------------------------------------------
539
- # Utterance metrics
540
- # ---------------------------------------------------------------------------
538
+
539
+
540
+
541
541
 
542
542
  def analyze_utterance_metrics(utterances: list) -> dict:
543
- """Compute per-utterance statistics: char counts, durations, density."""
543
+
544
544
  if not utterances:
545
545
  return {
546
546
  "count": 0,
@@ -587,13 +587,13 @@ def analyze_utterance_metrics(utterances: list) -> dict:
587
587
  }
588
588
 
589
589
 
590
- # ---------------------------------------------------------------------------
591
- # Speech rhythm
592
- # ---------------------------------------------------------------------------
590
+
591
+
592
+
593
593
 
594
594
  def analyze_speech_rhythm(utterances: list) -> dict:
595
- """Analyze word-level micro-rhythm: gaps, pauses, tempo profile."""
596
- # Collect all words with valid timestamps across all utterances
595
+
596
+
597
597
  all_words = []
598
598
  for utt in utterances:
599
599
  for w in utt.get("words", []):
@@ -608,7 +608,7 @@ def analyze_speech_rhythm(utterances: list) -> dict:
608
608
  "tempoProfile": "steady",
609
609
  }
610
610
 
611
- # Sort words by start time
611
+
612
612
  all_words.sort(key=lambda w: w["startTime"])
613
613
 
614
614
  gaps = []
@@ -630,7 +630,7 @@ def analyze_speech_rhythm(utterances: list) -> dict:
630
630
  avg_gap = int(sum(gaps) / len(gaps)) if gaps else 0
631
631
  max_gap = int(max(gaps)) if gaps else 0
632
632
 
633
- # Tempo profile: compare first-half vs second-half avg gap
633
+
634
634
  if len(gaps) < 4:
635
635
  tempo = "steady"
636
636
  else:
@@ -654,16 +654,16 @@ def analyze_speech_rhythm(utterances: list) -> dict:
654
654
  }
655
655
 
656
656
 
657
- # ---------------------------------------------------------------------------
658
- # Repetition analysis
659
- # ---------------------------------------------------------------------------
657
+
658
+
659
+
660
660
 
661
661
  def analyze_repetition(full_text: str, words: list, language: str) -> dict:
662
- """Detect repeated words/phrases and compute repetition ratio."""
662
+
663
663
  from collections import Counter
664
664
 
665
665
  if language.startswith("zh"):
666
- # Chinese: tokenize via punctuation-split + CJK/alphanum extraction (no dedup)
666
+
667
667
  segments = re.split(r'[,,。、!?!?.…::;;\-—–\s\'\"()()\[\]【】]+', full_text)
668
668
  tokens = []
669
669
  for seg in segments:
@@ -675,7 +675,7 @@ def analyze_repetition(full_text: str, words: list, language: str) -> dict:
675
675
  if tok not in _ZH_STOPWORDS:
676
676
  tokens.append(tok)
677
677
  else:
678
- # English: use word-level tokens, filter stopwords
678
+
679
679
  en_stopwords = {
680
680
  "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
681
681
  "have", "has", "had", "do", "does", "did", "will", "would", "could",
@@ -720,9 +720,9 @@ def analyze_repetition(full_text: str, words: list, language: str) -> dict:
720
720
  }
721
721
 
722
722
 
723
- # ---------------------------------------------------------------------------
724
- # Sentence type classification
725
- # ---------------------------------------------------------------------------
723
+
724
+
725
+
726
726
 
727
727
  _IMPERATIVE_STARTERS_ZH = [
728
728
  "点", "看", "赶紧", "抓紧", "快", "记得", "一定", "别忘",
@@ -738,8 +738,8 @@ _IMPERATIVE_STARTERS_EN = [
738
738
 
739
739
 
740
740
  def analyze_sentence_types(full_text: str, language: str) -> dict:
741
- """Classify sentences by type: declarative, interrogative, imperative, exclamatory."""
742
- # Split into sentences by sentence-ending punctuation
741
+
742
+
743
743
  sentences = re.split(r'[。!?!?.]', full_text)
744
744
  sentences = [s.strip() for s in sentences if s.strip()]
745
745
 
@@ -757,10 +757,10 @@ def analyze_sentence_types(full_text: str, language: str) -> dict:
757
757
  },
758
758
  }
759
759
 
760
- # To classify we need the original ending punctuation, so re-scan full_text
761
- # Build list of (sentence_text, ending_punctuation)
760
+
761
+
762
762
  parts = re.findall(r'([^。!?!?.]+)([。!?!?.])', full_text)
763
- # If regex didn't capture all, fallback to simple classification
763
+
764
764
  if not parts:
765
765
  parts = [(s, "。") for s in sentences]
766
766
 
@@ -792,12 +792,12 @@ def analyze_sentence_types(full_text: str, language: str) -> dict:
792
792
  }
793
793
 
794
794
 
795
- # ---------------------------------------------------------------------------
796
- # Assets summary
797
- # ---------------------------------------------------------------------------
795
+
796
+
797
+
798
798
 
799
799
  def build_assets_summary(decon: dict) -> dict:
800
- """Build assets summary from deconstruction data."""
800
+
801
801
  audio = decon.get("audio", {})
802
802
  keyframes = decon.get("keyframes", [])
803
803
 
@@ -824,9 +824,9 @@ def build_assets_summary(decon: dict) -> dict:
824
824
  return result
825
825
 
826
826
 
827
- # ---------------------------------------------------------------------------
828
- # Main
829
- # ---------------------------------------------------------------------------
827
+
828
+
829
+
830
830
 
831
831
  def main():
832
832
  parser = argparse.ArgumentParser(
@@ -862,9 +862,9 @@ Examples:
862
862
  global QUIET
863
863
  QUIET = args.json_output
864
864
 
865
- # ------------------------------------------------------------------
866
- # [1] Load deconstruction
867
- # ------------------------------------------------------------------
865
+
866
+
867
+
868
868
  log("[1/13] Loading deconstruction data...")
869
869
  decon, input_dir = load_deconstruction(args.input)
870
870
  log(f" loaded: {input_dir}/deconstruction.json")
@@ -872,9 +872,9 @@ Examples:
872
872
  output_dir = args.output_dir or input_dir
873
873
  os.makedirs(output_dir, exist_ok=True)
874
874
 
875
- # ------------------------------------------------------------------
876
- # [2] Basic info
877
- # ------------------------------------------------------------------
875
+
876
+
877
+
878
878
  log("[2/13] Basic analysis...")
879
879
  source = decon.get("source", {})
880
880
  duration_ms = source.get("durationMs", 0)
@@ -909,76 +909,76 @@ Examples:
909
909
 
910
910
  log(f" duration: {duration_sec:.1f}s, scenes: {len(scenes)}, language: {language}")
911
911
 
912
- # ------------------------------------------------------------------
913
- # [3] Scene structure (utterance mapping)
914
- # ------------------------------------------------------------------
912
+
913
+
914
+
915
915
  log("[3/13] Scene-structure analysis...")
916
916
  utterance_map = match_utterances_to_scenes(scenes, utterances) if scenes and utterances else {}
917
917
  narrative = build_narrative_structure(scenes, utterance_map, duration_sec)
918
918
  for ns in narrative:
919
919
  log(f" scene {ns['sceneIndex']}: {ns['role']} ({ns['durationSec']}s, {ns['durationPct']}%)")
920
920
 
921
- # ------------------------------------------------------------------
922
- # [4] Hook extraction
923
- # ------------------------------------------------------------------
921
+
922
+
923
+
924
924
  log("[4/13] Hook extraction...")
925
925
  hook = analyze_hook(scenes, utterance_map, asr)
926
926
  log(f" strategy: {hook['strategy']}, word count: {hook['wordCount']}, text: {hook['text'][:40]}...")
927
927
 
928
- # ------------------------------------------------------------------
929
- # [5] Pacing analysis
930
- # ------------------------------------------------------------------
928
+
929
+
930
+
931
931
  log("[5/13] Pacing analysis...")
932
932
  pacing = analyze_pacing(scenes, duration_sec)
933
933
  log(f" rhythm: {pacing['rhythm']}, scene durations: {pacing['sceneDurations']}")
934
934
 
935
- # ------------------------------------------------------------------
936
- # [6] Text structure
937
- # ------------------------------------------------------------------
935
+
936
+
937
+
938
938
  log("[6/13] Copy-structure analysis...")
939
939
  text_structure = analyze_text_structure(asr, scenes, utterance_map, duration_sec)
940
940
  log(f" speech rate: {text_structure['speechRate']} chars/sec, "
941
941
  f"关键词: {len(text_structure['keywords'])} 个, "
942
942
  f"静默段: {len(text_structure['silenceSegments'])} 处")
943
943
 
944
- # ------------------------------------------------------------------
945
- # [7] Audio pattern
946
- # ------------------------------------------------------------------
944
+
945
+
946
+
947
947
  log("[7/13] Audio-pattern analysis...")
948
948
  audio_pattern = analyze_audio_pattern(asr, audio, duration_sec)
949
949
  log(f" speech ratio: {audio_pattern['speechRatio']}, "
950
950
  f"静默比例: {audio_pattern['silenceRatio']}")
951
951
 
952
- # ------------------------------------------------------------------
953
- # [8] CTA detection
954
- # ------------------------------------------------------------------
952
+
953
+
954
+
955
955
  log("[8/13] CTA detection...")
956
956
  cta = analyze_cta(scenes, utterance_map, utterances)
957
957
  log(f" explicit CTA: {cta['hasExplicitCta']}, "
958
958
  f"强度: {cta['strength']}, "
959
959
  f"结尾文案: {cta['endingText'][:30]}")
960
960
 
961
- # ------------------------------------------------------------------
962
- # [9] Utterance metrics
963
- # ------------------------------------------------------------------
961
+
962
+
963
+
964
964
  log("[9/13] Sentence statistics...")
965
965
  utterance_metrics = analyze_utterance_metrics(utterances)
966
966
  log(f" sentences: {utterance_metrics['count']}, "
967
967
  f"平均字数: {utterance_metrics['avgCharCount']}, "
968
968
  f"平均时长: {utterance_metrics['avgDurationSec']}s")
969
969
 
970
- # ------------------------------------------------------------------
971
- # [10] Speech rhythm
972
- # ------------------------------------------------------------------
970
+
971
+
972
+
973
973
  log("[10/13] Speech rhythm...")
974
974
  speech_rhythm = analyze_speech_rhythm(utterances)
975
975
  log(f" avg word gap: {speech_rhythm['avgWordGapMs']}ms, "
976
976
  f"有意停顿: {len(speech_rhythm['intentionalPauses'])} 处, "
977
977
  f"节奏: {speech_rhythm['tempoProfile']}")
978
978
 
979
- # ------------------------------------------------------------------
980
- # [11] Repetition
981
- # ------------------------------------------------------------------
979
+
980
+
981
+
982
982
  log("[11/13] Repetition patterns...")
983
983
  all_words = []
984
984
  for utt in utterances:
@@ -988,9 +988,9 @@ Examples:
988
988
  log(f" repetition ratio: {repetition['repetitionRatio']}, "
989
989
  f"高频词: {top3 or '无'}")
990
990
 
991
- # ------------------------------------------------------------------
992
- # [12] Sentence types
993
- # ------------------------------------------------------------------
991
+
992
+
993
+
994
994
  log("[12/13] Sentence-type distribution...")
995
995
  sentence_types = analyze_sentence_types(full_text, language)
996
996
  log(f" declarative: {sentence_types['declarative']}, "
@@ -998,9 +998,9 @@ Examples:
998
998
  f"祈使: {sentence_types['imperative']}, "
999
999
  f"感叹: {sentence_types['exclamatory']}")
1000
1000
 
1001
- # ------------------------------------------------------------------
1002
- # [13] Output
1003
- # ------------------------------------------------------------------
1001
+
1002
+
1003
+
1004
1004
  log("[13/13] Writing analysis report...")
1005
1005
  assets = build_assets_summary(decon)
1006
1006