@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
@@ -56,6 +56,16 @@
56
56
  "type": "string",
57
57
  "description": "Template id (from template_registry). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
58
58
  },
59
+ "template_source": {
60
+ "type": "string",
61
+ "enum": ["user-explicit", "user-confirmed", "auto"],
62
+ "description": "How template_id was chosen; recorded at meta.templateSelection.source. **Always pass it with template_id.** `user-explicit`: the user named this template — its id or name, a pasted template detail-page prompt or link, or it was settled earlier in this conversation. `user-confirmed`: the user did not name one, you offered candidates and the user picked or approved this one in the chat. `auto`: you picked it yourself and the user has not confirmed — gen_script refuses, so instead of calling it, show the candidates and end the turn. Never mark a template user-confirmed that the user has not seen and approved."
63
+ },
64
+ "template_candidates": {
65
+ "type": "array",
66
+ "items": { "type": "string" },
67
+ "description": "The template ids you offered the user, recommended first; recorded at meta.templateSelection.candidates. Pass with template_source=user-confirmed — template_id must then be one of them (a template the user named outside the shortlist is user-explicit)."
68
+ },
59
69
  "source_document_url": {
60
70
  "type": "string",
61
71
  "description": "Source document URL (.pptx) for a template that builds its per-scene picture from that document (capabilities.payloadHydration). **Must** pass it for such a template: it is recorded at meta.sourceDocument, and prepare_video_assets then fetches the layout itself and fills the customPayload field the template declares (payloadHydration.payloadField) on every scene. Pass the same URL you gave doc_parse. You are NOT expected to author or even read that layout data — one page is hundreds of KB, so it never travels through the conversation; without this reference the DSL is rejected downstream after you have already written all the narration."
@@ -128,6 +138,8 @@
128
138
  "variant",
129
139
  "background_preset",
130
140
  "template_id",
141
+ "template_source",
142
+ "template_candidates",
131
143
  "source_document_url",
132
144
  "headline",
133
145
  "subheadline",
@@ -2,6 +2,6 @@
2
2
  "skillName": "gen-script",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "474",
5
- "version": "V18",
5
+ "version": "V19",
6
6
  "skillDescription": "Video-script generation skill. Turns a user-supplied topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.\n\nUse this skill as soon as the user mentions any of these intents:\n- Write a video script, generate a video script, plan a video, write storyboards\n- Create a short video, plan video content, generate a Video DSL\n- Turn a topic into a video structure / video content plan\n\nEven when the user does not say \"generate the DSL\", use this skill whenever they want to turn a topic into a structured video plan.\n\n⚠️ Stop-and-confirm gate: after this skill returns a DSL, show the full script and wait for the user's explicit confirmation. Never call `prepare_video_assets` in the same turn."
7
7
  }
@@ -1,21 +1,21 @@
1
1
  #!/usr/bin/env python3
2
- """Thin wrapper that delegates to the canonical render_video.py with --resolve-only forced on.
3
2
 
4
- The two-phase video pipeline shares one Python implementation:
5
3
 
6
- prepare_video_assets → render_video.py --resolve-only ... (this file)
7
- render_video → render_video.py ... (sibling skill)
8
4
 
9
- By exec'ing the canonical script we get verbatim stdout / stderr / exit-code / signal forwarding
10
- without any subprocess wrapper layer. The agent layer's stdout parsing for the
11
- "📦 render job jobId: N" line therefore continues to work unchanged.
12
5
 
13
- The wrapper unconditionally injects --resolve-only as the first argument. The MCP tool schema for
14
- this skill excludes job_id / render_plan / resolve_only / upload_title / no_upload / renderer, so
15
- the LLM cannot push this entry point into Phase 3 territory; even if a shell caller passes those
16
- flags directly to this wrapper, --resolve-only still wins because asset-resolve-and-exit always
17
- short-circuits before the render step in render_video.py.
18
- """
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
19
  from __future__ import annotations
20
20
 
21
21
  import os
@@ -23,14 +23,14 @@ import sys
23
23
 
24
24
 
25
25
  def _canonical_script_path() -> str:
26
- """Resolve the absolute path to the render-video skill's render_video.py.
27
26
 
28
- Layout (relative to this file):
29
- apps/ab-skill/skills/prepare-video-assets/scripts/prepare_video_assets.py <-- HERE
30
- apps/ab-skill/skills/render-video/scripts/render_video.py <-- target
31
27
 
32
- The relative jump is ../../render-video/scripts/render_video.py.
33
- """
28
+
29
+
30
+
31
+
32
+
33
+
34
34
  here = os.path.dirname(os.path.abspath(__file__))
35
35
  return os.path.normpath(
36
36
  os.path.join(here, "..", "..", "render-video", "scripts", "render_video.py")
@@ -40,7 +40,7 @@ def _canonical_script_path() -> str:
40
40
  def main() -> None:
41
41
  script = _canonical_script_path()
42
42
  if not os.path.isfile(script):
43
- # Fail fast with a human-readable message rather than letting execv raise OSError.
43
+
44
44
  print(
45
45
  f"❌ prepare_video_assets wrapper could not locate the canonical render script.\n"
46
46
  f" Expected at: {script}\n"
@@ -48,7 +48,7 @@ def main() -> None:
48
48
  file=sys.stderr,
49
49
  )
50
50
  sys.exit(2)
51
- # Force --resolve-only into argv before any user-supplied flag.
51
+
52
52
  forwarded = ["--resolve-only", *sys.argv[1:]]
53
53
  os.execv(sys.executable, [sys.executable, script, *forwarded])
54
54
 
@@ -1,11 +1,11 @@
1
- """Chrome Headless vendor sync — separated from render_video.py.
2
1
 
3
- 把 Chrome 下载/解压/共享缓存这块"部署/运维相关"的逻辑收拢到一个文件,让
4
- render_video.py 只剩"调度 + 渲染"。日后如果要把这块搬到 ab-render Dockerfile
5
- 或独立 prepare 脚本,只需要换掉这个模块的实现,render_video 不用动。
6
2
 
7
- 公共入口只有一个:sync_chrome_headless_vendor()。其它都是辅助函数,给 sync 用。
8
- """
3
+
4
+
5
+
6
+
7
+
8
+
9
9
  from __future__ import annotations
10
10
 
11
11
  import builtins
@@ -13,7 +13,7 @@ import json
13
13
  import os
14
14
  import platform
15
15
  import shutil
16
- import subprocess # noqa: F401 (kept for parity with original imports)
16
+ import subprocess
17
17
  import sys
18
18
  import tempfile
19
19
  import urllib.error
@@ -29,14 +29,14 @@ def _now_iso() -> str:
29
29
 
30
30
 
31
31
  def _log(msg: str) -> None:
32
- """Tiny stderr-with-timestamp helper, mirrors render_video.LogPrint format."""
32
+
33
33
  now = datetime.now()
34
34
  stamp = now.strftime("%Y%m%d %H%M%S") + f":{now.microsecond // 1000:03d}"
35
35
  builtins.print(f"[{stamp}] {msg}", file=sys.stderr)
36
36
 
37
37
 
38
38
  def _detect_remotion_chrome_platform() -> Optional[str]:
39
- """与 Remotion BrowserFetcher 一致的平台标识;不支持时返回 None。"""
39
+
40
40
  system = platform.system()
41
41
  machine = platform.machine().lower()
42
42
  if system == "Darwin":
@@ -118,7 +118,7 @@ def _chrome_headless_share_zip_path(share_root: str, rem_platform: str) -> str:
118
118
 
119
119
 
120
120
  def _share_chrome_zip_ready(share_root: str, rem_platform: str, expected_version: str) -> bool:
121
- """共享目录仅保留 VERSION + 各平台 zip 时,判断是否可用。"""
121
+
122
122
  version_path = os.path.join(share_root, "VERSION")
123
123
  if not os.path.isfile(version_path):
124
124
  return False
@@ -136,7 +136,7 @@ def _share_chrome_zip_ready(share_root: str, rem_platform: str, expected_version
136
136
 
137
137
 
138
138
  def _extract_chrome_headless_zip_to_dest(zip_path: str, dest_root: str, rem_platform: str) -> None:
139
- """将官方 headless shell zip 解压到 Remotion 缓存根目录下的平台子目录。"""
139
+
140
140
  platform_dest = os.path.join(dest_root, rem_platform)
141
141
  if os.path.isdir(platform_dest):
142
142
  shutil.rmtree(platform_dest)
@@ -146,13 +146,13 @@ def _extract_chrome_headless_zip_to_dest(zip_path: str, dest_root: str, rem_plat
146
146
 
147
147
 
148
148
  def sync_chrome_headless_vendor(renderer_dir: str, render_plan: dict) -> None:
149
- """
150
- 共享目录(默认 /tmp/agent-share/chrome-headless)只存 VERSION + chrome-headless-shell-<平台>.zip;
151
- 若 zip 与版本匹配则本地解压到 node_modules/.remotion/chrome-headless-shell/,否则从 OSS 下载,
152
- 成功后把 zip 写入共享目录(不存解压后的目录树)。
153
149
 
154
- 所有 progress / warning 经 stderr 打印;render_plan["logs"] 上记录关键事件。
155
- """
150
+
151
+
152
+
153
+
154
+
155
+
156
156
  if os.environ.get("REMOTION_SKIP_VENDOR_CHROME", "").strip().lower() in ("1", "true", "yes"):
157
157
  render_plan["logs"].append({
158
158
  "phase": "render",
@@ -1,16 +1,16 @@
1
- """数字人连续段:分段、驱动音频规划、起播帧注入。
2
1
 
3
- 一个 ``source=gen-digital-human`` 的 avatar 资产被**相邻**几个场景的
4
- ``props.avatarAssetId`` 同时引用 = 一个连续段:生成一段视频,人物跨场景不重启。
5
- 要让第 k 个场景的第 0 帧正好对上视频里的对应位置,驱动音频必须是「段内各场景
6
- 旁白、每段补静音到场景最终时长」拼成的一条——所以数字人只能在**排轴之后**生成
7
- (场景时长由真实 TTS 时长决定,见 render_video.adjust_timeline_to_audio)。
8
2
 
9
- 本模块只放纯函数与 ffmpeg 拼接;调度(何时生成、如何回写 RenderPlan 状态)在
10
- render_video.resolve_digital_human_segments。全部按 RenderPlan 的通用字段工作,
11
- 不识别具体模板:模板是否支持由 template.json 的 ``capabilities.digitalHuman`` 声明,
12
- 写没写 avatar 资产由上游决定。
13
- """
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
14
 
15
15
  from __future__ import annotations
16
16
 
@@ -21,10 +21,10 @@ from typing import Optional
21
21
 
22
22
  DIGITAL_HUMAN_SOURCE = "gen-digital-human"
23
23
 
24
- #: 驱动音频下限。飞影要求 5 秒 ~ 30 分钟;不足时在段尾补静音。
24
+
25
25
  MIN_DRIVE_SEC = 5.0
26
26
 
27
- #: 拼接输出的采样率 / 声道。口型驱动不需要立体声,统一成单声道避免混格式失败。
27
+
28
28
  _SAMPLE_RATE = 44100
29
29
 
30
30
 
@@ -42,11 +42,11 @@ def digital_human_asset_ids(render_plan: dict) -> set[str]:
42
42
 
43
43
 
44
44
  def collect_segments(render_plan: dict) -> dict[str, list[int]]:
45
- """avatar 资产 id → 引用它的 timeline 下标(按出场顺序)。
46
45
 
47
- 只收 gen-digital-human 资产:``existing`` 的数字人视频是作者自备的,起播点由作者
48
- 自己负责(``digitalHuman.startFromSec``),管线不去猜。
49
- """
46
+
47
+
48
+
49
+
50
50
  dh_ids = digital_human_asset_ids(render_plan)
51
51
  segments: dict[str, list[int]] = {}
52
52
  for idx, entry in enumerate(render_plan.get("timeline", [])):
@@ -69,11 +69,11 @@ def segment_role(pos: int, count: int) -> str:
69
69
 
70
70
 
71
71
  def inject_segment_props(render_plan: dict, segments: dict[str, list[int]]) -> list[str]:
72
- """给段内每个场景写 ``avatarOffsetFrames`` / ``avatarSegmentRole``。
73
72
 
74
- 偏移用 RenderPlan 里最终的 ``durationFrames`` 累加,不按秒重算,避免累计舍入。
75
- 返回告警(不相邻的同一资产:偏移仍按出场顺序累加,但中间隔开的场景会让口型错位)。
76
- """
73
+
74
+
75
+
76
+
77
77
  warnings: list[str] = []
78
78
  timeline = render_plan.get("timeline", [])
79
79
  for asset_id, indices in segments.items():
@@ -94,11 +94,11 @@ def inject_segment_props(render_plan: dict, segments: dict[str, list[int]]) -> l
94
94
 
95
95
 
96
96
  def plan_drive_pieces(render_plan: dict, indices: list[int]) -> list[dict]:
97
- """段内每个场景一块:``{"sceneId", "url" | None, "durationSec"}``。
98
97
 
99
- ``url`` 取场景旁白资产(``props.narrationAssetId``)已生成的地址;没有旁白的场景
100
- 是整段静音。块长 = 场景最终时长;总长不足 MIN_DRIVE_SEC 时把最后一块拉长。
101
- """
98
+
99
+
100
+
101
+
102
102
  fps = (render_plan.get("renderConfig") or {}).get("fps") or 30
103
103
  assets = {a.get("assetId"): a for a in render_plan.get("resolvedAssets", [])}
104
104
  timeline = render_plan.get("timeline", [])
@@ -119,10 +119,10 @@ def plan_drive_pieces(render_plan: dict, indices: list[int]) -> list[dict]:
119
119
 
120
120
 
121
121
  def direct_drive_url(render_plan: dict, indices: list[int]) -> Optional[str]:
122
- """单场景段、且旁白本身就够长时,直接拿旁白音频驱动,不必拼接。
123
122
 
124
- 视频比场景短几百毫秒(旁白尾巴)无妨:模板默认 ``onEnd: freeze`` 停在最后一帧。
125
- """
123
+
124
+
125
+
126
126
  if len(indices) != 1:
127
127
  return None
128
128
  entry = render_plan.get("timeline", [])[indices[0]]
@@ -137,11 +137,11 @@ def direct_drive_url(render_plan: dict, indices: list[int]) -> Optional[str]:
137
137
 
138
138
 
139
139
  def split_segment_per_scene(render_plan: dict, asset_id: str, indices: list[int]) -> list[str]:
140
- """把一个连续段拆成每场景一段(拿不到 ffmpeg 拼接时的降级)。
141
140
 
142
- 人物会在场景边界重新起播,但每段都能直接用本场景旁白驱动,口型不会错位。
143
- 新资产 id = ``<原 id>.<sceneId>``;原资产从 resolvedAssets 移除,场景改指新 id。
144
- """
141
+
142
+
143
+
144
+
145
145
  assets = render_plan.get("resolvedAssets", [])
146
146
  original = next((a for a in assets if a.get("assetId") == asset_id), None)
147
147
  if original is None:
@@ -160,7 +160,7 @@ def split_segment_per_scene(render_plan: dict, asset_id: str, indices: list[int]
160
160
 
161
161
 
162
162
  def build_concat_command(pieces: list[dict], out_path: str, ffmpeg: str = "ffmpeg") -> list[str]:
163
- """ffmpeg 参数:每块重采样成单声道 → 补静音 → 精确截到块长 → 顺序拼接成 mp3。"""
163
+
164
164
  cmd = [ffmpeg, "-y", "-loglevel", "error"]
165
165
  filters: list[str] = []
166
166
  labels: list[str] = []
@@ -190,7 +190,7 @@ def find_ffmpeg() -> Optional[str]:
190
190
 
191
191
 
192
192
  def concat_drive_audio(pieces: list[dict], out_dir: str, name: str, ffmpeg: str, timeout: int = 300) -> str:
193
- """拼出驱动音频,返回本地文件路径。失败抛 RuntimeError。"""
193
+
194
194
  os.makedirs(out_dir, exist_ok=True)
195
195
  safe = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in name)
196
196
  out_path = os.path.join(out_dir, f"dh-drive-{safe}.mp3")
@@ -1,15 +1,15 @@
1
- """Video duration probe — separated from render_video.py.
2
1
 
3
- screen-walkthrough 这种"按 TTS 时间轴 + 视频素材池"的模板需要知道远端视频的
4
- 真实时长才能选出正确的 adaptStrategy(fit-exact / trim-and-speed / freeze-tail
5
- / static-fallback)。这里给出三条策略,按速度排序:
6
2
 
7
- 1. 通过 mediaId(从 CDN URL 中切出)查 ab-api /file/get
8
- 2. HEAD 请求按 Content-Length 估算(粗略)
9
- 3. 下载前 2 MB 用 ffprobe 解析(精确但贵)
10
3
 
11
- 公共入口只有一个:probe_video_duration(url) -> Optional[float]。
12
- """
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
13
  from __future__ import annotations
14
14
 
15
15
  import builtins
@@ -31,12 +31,12 @@ def _log(msg: str) -> None:
31
31
 
32
32
 
33
33
  def _extract_media_id_from_url(url: str) -> Optional[str]:
34
- """Extract mediaId (vodVideoId) from CDN URL.
35
34
 
36
- Supported formats:
37
- - http://cdn.aibearer.com/{mediaId}/{hash}-ld.mp4
38
- - https://cdn.aibearer.com/{mediaId}/...
39
- """
35
+
36
+
37
+
38
+
39
+
40
40
  try:
41
41
  from urllib.parse import urlparse
42
42
  parsed = urlparse(url)
@@ -51,10 +51,10 @@ def _extract_media_id_from_url(url: str) -> Optional[str]:
51
51
 
52
52
 
53
53
  def _query_duration_from_api(url: str) -> Optional[float]:
54
- """Query ab-api /file/get by mediaId to get the precise duration from database.
55
54
 
56
- Returns duration in seconds, or None if query fails or duration not available.
57
- """
55
+
56
+
57
+
58
58
  media_id = _extract_media_id_from_url(url)
59
59
  if not media_id:
60
60
  return None
@@ -99,15 +99,15 @@ def _query_duration_from_api(url: str) -> Optional[float]:
99
99
 
100
100
 
101
101
  def probe_video_duration(url: str) -> Optional[float]:
102
- """Probe the duration (in seconds) of a remote video URL.
103
102
 
104
- Strategy (ordered by priority):
105
- 1. Query ab-api /file/get by mediaId extracted from CDN URL (fastest, most accurate).
106
- 2. HEAD request to estimate from Content-Length.
107
- 3. Download first 2MB + ffprobe (if available).
108
103
 
109
- Returns duration in seconds, or None on failure.
110
- """
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
111
  api_duration = _query_duration_from_api(url)
112
112
  if api_duration is not None:
113
113
  return api_duration
@@ -178,16 +178,16 @@ def probe_video_duration(url: str) -> Optional[float]:
178
178
 
179
179
 
180
180
  def probe_video_meta(url: str, timeout: float = 20.0) -> dict:
181
- """Probe a remote video's pixel size and duration with ffprobe (reads the URL directly).
182
181
 
183
- Returns ``{"width": int, "height": int, "duration_ms": int}`` with only the fields it
184
- could read; ``{}`` when ffprobe is missing or the probe fails. Callers treat every
185
- field as optional.
186
182
 
187
- 为什么要:数字人盒子的宽高比缺省按 3:4 算,而克隆素材多是 9:16 左右的竖版
188
- (实测 720×1264)。资产上没有宽高时,模板用 3:4 的盒子 object-fit: cover 裁掉
189
- 人物的头顶和下半身(conv_XKTQxEBLdZy3i_jIB7IG)。
190
- """
183
+
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
191
  try:
192
192
  result = subprocess.run(
193
193
  [
@@ -212,7 +212,7 @@ def probe_video_meta(url: str, timeout: float = 20.0) -> dict:
212
212
  s = streams[0]
213
213
  w, h = s.get("width"), s.get("height")
214
214
  if isinstance(w, int) and isinstance(h, int) and w > 0 and h > 0:
215
- # 手机素材常带 90° / 270° 旋转元数据:播放器按旋转后的方向显示。
215
+
216
216
  rotation = 0
217
217
  for side in s.get("side_data_list") or []:
218
218
  if isinstance(side.get("rotation"), (int, float)):
@@ -1,12 +1,12 @@
1
- """VOD playback URL polling — separated from render_video.py.
2
1
 
3
- ab-render 通过远程渲染服务把视频上传到阿里云 VOD 后,会立即返回一个
4
- `vod://` 协议地址 + fileId。但前端需要的是 CDN 加速地址(`http://cdn.aibearer.com/...`),
5
- 这要等阿里云转码回调到达 ab-api、回写 `file.fileUrl` 之后才能拿到。
6
2
 
7
- 这个模块负责轮询 ab-api /file/get 直到拿到 CDN URL(或超时返回 OSS 原始地址)。
8
- 公共入口只有一个:poll_vod_playback_url(file_id, priv_token, ...) -> Optional[str]。
9
- """
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
10
  from __future__ import annotations
11
11
 
12
12
  import builtins
@@ -33,13 +33,13 @@ def poll_vod_playback_url(
33
33
  max_wait: float = 60.0,
34
34
  interval: float = 3.0,
35
35
  ) -> Optional[str]:
36
- """轮询 ab-api /file/get,等待 VOD CDN URL 就绪。
37
36
 
38
- VOD 上传后通常 10-30 秒完成转码。此函数策略:
39
- - 一旦看到非 `vod://` 且非阿里云 OSS 域名的 URL(说明 CDN 转码完成),立即返回
40
- - 看到阿里云 OSS 直链时记为 fallback,继续等 CDN
41
- - 超时仍未拿到 CDN 时退回到 OSS 原始地址
42
- """
37
+
38
+
39
+
40
+
41
+
42
+
43
43
  api_base = os.environ.get(
44
44
  "MM_API_BASE_URL",
45
45
  os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.ai/api"),
@@ -1,28 +1,28 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- 远程渲染 HTTP 客户端 — 对接独立部署的 remotion-renderer 服务。
4
-
5
- 接口(位于 /remotion-renderer/server/index.ts):
6
- - POST /render → { code, data: { taskId } }
7
- body 可选 coverCompositionId(如 "SpotlightCardCover");传了的话视频
8
- 渲染完成后服务端会再串行渲一帧静态封面、一并上传。
9
- - POST /renderStatus → { code, data: { status, progress, fileUrl?,
10
- coverUrl?, coverError?, error? } }
11
- coverUrl 视频成功且封面成功时存在
12
- coverError 视频成功但封面失败时存在(视频本身仍然 succeeded)
13
-
14
- 认证头(复用 X-Priv-Token 规范,服务端通过后端 API 校验):
15
- X-Priv-Token: <PRIV_TOKEN>
16
- x-invoke-skill: render-video
17
- x-invoke-agent: <AGENT_NAME> # optional
18
-
19
- Base URL 优先级:
20
- 1. 调用方显式传入 base_url
21
- 2. 环境变量 REMOTION_RENDER_API_URL(指向本地/staging 时在此覆盖)
22
- 3. 默认 https://api-render.remixmate.ai(生产,零配置可用)
23
-
24
- 本模块只负责 HTTP 层;调用方(render_video.py)负责把结果写回 render_plan。
25
- """
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
26
 
27
27
  from __future__ import annotations
28
28
 
@@ -61,16 +61,16 @@ TERMINAL_FAILURE = {STATUS_FAILED, STATUS_CANCELLED}
61
61
 
62
62
 
63
63
  class RemoteRenderError(RuntimeError):
64
- """抛出至调用方,由上层写入 render_plan.errors。"""
64
+ pass
65
65
 
66
66
 
67
67
  class RemoteRenderTimeout(RemoteRenderError):
68
- """轮询超时 —— 任务**没有失败**,只是我们不等了。
69
68
 
70
- 与普通 RemoteRenderError 分开,是因为上层的处置完全相反:
71
- 真失败 → 可以重新提交一个任务;超时 → 后端还在渲,重新提交等于白烧一遍,
72
- 必须保留 taskId 让下一次调用续上。
73
- """
69
+
70
+
71
+
72
+
73
+
74
74
 
75
75
  def __init__(self, message: str, task_id: str):
76
76
  super().__init__(message)
@@ -144,7 +144,7 @@ def start_render(
144
144
  conversation_id: Optional[str] = None,
145
145
  path: str = "/render",
146
146
  ) -> str:
147
- """POST <path>(默认 /render,私有模板动态渲染传 /renderDraft);返回 taskId。"""
147
+
148
148
  result = _request(
149
149
  "POST",
150
150
  path,
@@ -174,16 +174,16 @@ def poll_render(
174
174
  adaptive_interval: bool = False,
175
175
  status_path: str = "/renderStatus",
176
176
  ) -> dict:
177
- """POST <status_path>(默认 /renderStatus,私有模板传 /renderDraftStatus)轮询直到完成/失败/超时。
178
177
 
179
- 成功时返回 data dict(至少包含 fileUrl),否则抛 RemoteRenderError。
180
- on_progress 回调在每次成功请求后触发,参数为完整 data dict。
181
178
 
182
- adaptive_interval=True 时根据渲染进度自动调整轮询间隔:
183
- 0-20%: max(interval * 0.5, 1.0)(起步阶段采样加密,短渲染也能看到进度)
184
- 20-80%: interval(正常渲染阶段)
185
- 80-100%: max(interval * 0.6, 1.0)(编码收尾阶段,更频繁检查)
186
- """
179
+
180
+
181
+
182
+
183
+
184
+
185
+
186
+
187
187
  start = time.monotonic()
188
188
  consecutive_errors = 0
189
189
  current_progress = 0.0
@@ -233,7 +233,7 @@ def poll_render(
233
233
  if on_progress is not None:
234
234
  try:
235
235
  on_progress(data)
236
- except Exception: # noqa: BLE001 — 进度回调异常不应中断轮询
236
+ except Exception:
237
237
  pass
238
238
 
239
239
  status = str(data.get("status") or "").lower()
@@ -243,11 +243,11 @@ def poll_render(
243
243
  err = data.get("error") or data.get("errorMsg") or data.get("message") or "unknown"
244
244
  raise RemoteRenderError(f"remote render failed ({status}): {err}")
245
245
 
246
- # Adaptive interval: adjust sleep based on current progress
246
+
247
247
  current_progress = float(data.get("progress") or 0.0)
248
248
  if adaptive_interval:
249
249
  if current_progress < 0.2:
250
- # 起步阶段(含服务端很快就渲完的短片)采样加密,避免 0%→完成的跳变
250
+
251
251
  sleep_time = max(interval * 0.5, 1.0)
252
252
  elif current_progress >= 0.8:
253
253
  sleep_time = max(interval * 0.6, 1.0)