@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,39 +1,39 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- 模板 registry 加载器 - 单一数据源(ab-api HTTP)。
4
-
5
- 为什么独立成一个模块:
6
- template-registry、render-video 等多个 skill 都要读 registry,集中在一处便于:
7
- - 缓存策略一致(HTTP 拉取 5 分钟 TTL + ETag 复用)
8
- - 错误降级一致(HTTP 故障时复用上一次磁盘缓存)
9
-
10
- 单一数据源(设计取舍):
11
- registry 只从 ab-api HTTP 接口加载,避免「本地文件 / monorepo 源码 / 数据库」
12
- 多源并存导致的不一致 —— 私有 / 多租户模板只存在于 ab-api,本地源永远不全。
13
-
14
- URL 解析:
15
- 1. 显式 VIDEO_TEMPLATE_REGISTRY_URL(若设置则直接用)
16
- 2. 否则按 CLI 后端约定推导:
17
- <MM_API_BASE_URL>/remotionTemplate/registry
18
- MM_API_BASE_URL 默认 https://api.remixmate.ai/api(与 src/http.ts 一致),
19
- 兼容 ab-agent 注入的 MM_BACKEND_API_URL。默认值内置在 CLI 自身,独立安装
20
- (codex / npm i -g)开箱即用,无需宿主在 spawn 时注入。
21
-
22
- 鉴权:只读 PRIV_TOKEN 环境变量。凭证库 / 钥匙串 / 设备授权流由 CLI 的 Node 侧
23
- 唯一鉴权入口负责(src/auth/ensure.ts),并在 spawn 时注入本进程环境。
24
-
25
- monorepo / 显式本地文件等其它来源已**移除**。模板仓库自己的维护工具住在
26
- template-library,不经过本模块。
27
-
28
- P1.2 — list/detail split (待后端就绪):
29
- 100+ 模板时 ``GET /api/template/list`` 应该只返摘要(id / name / aspects /
30
- styleTags / status / llmHint),``GET /api/template/{id}`` 按需返完整定义。
31
- 目前本模块一次拉整个 registry。后端 endpoint 就绪后只需在 ``_fetch_http``
32
- 下新增 ``_fetch_summary`` / ``_fetch_detail`` 两个函数,``load_registry_data``
33
- 改成「先拉摘要,触发 ``get_template`` 时再补全」,公共 API 不变。
34
- 调用方今天用 ``get_template(id)`` / ``list_templates()`` 已经是按需消费的形态,
35
- 迁移时不需要改任何 caller。
36
- """
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
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
37
 
38
38
  from __future__ import annotations
39
39
 
@@ -63,54 +63,54 @@ __all__ = [
63
63
  "EXIT_BACKEND_UNREACHABLE",
64
64
  ]
65
65
 
66
- # 与 Node 侧 src/errors.ts 的 EXIT 常量保持一致:宿主据此判断「该引导用户授权」
67
- # 还是「后端故障,等一会儿再试」,而不是把两者都当成一次普通失败。
66
+
67
+
68
68
  EXIT_NOT_AUTHENTICATED = 4
69
69
  EXIT_BACKEND_UNREACHABLE = 5
70
70
 
71
71
 
72
72
  class RegistryAuthError(RuntimeError):
73
- """凭证缺失 / 失效 / 被吊销 —— 与「后端不可达」区分开的独立失败模式。
73
+ pass
74
+
75
+
76
+
74
77
 
75
- 这两种情况以前被压成同一段「请确认 ab-api 可达…并已设置有效 PRIV_TOKEN」的
76
- 文案,用户无从判断该去登录还是该等后端恢复。
77
- """
78
78
 
79
79
 
80
80
  class RegistryUnreachableError(RuntimeError):
81
- """网络不通 / 超时 / 5xx —— 用户无需重新登录,重试或修配置即可。"""
81
+ pass
82
82
 
83
83
 
84
84
  class TemplateStatusGatedError(RuntimeError):
85
- """模板存在,但它的 ``status`` 不在当前进程的可见集合里(见 ``_allowed_statuses``)。
85
+ pass
86
+
87
+
88
+
89
+
90
+
86
91
 
87
- 与「id 不存在」分开的独立失败模式:前者要改 id,后者要开门控开关
88
- (``ENABLE_BETA_TEMPLATES=1``)或把模板转正。压成同一个 ``None`` 返回值的后果见
89
- ``require_visible_template`` 的文档。
90
- """
91
92
 
92
- # 默认缓存 TTL(秒)。HTTP 拉取后该时间内不再重新请求。
93
93
  _DEFAULT_TTL_SECONDS = 300
94
94
 
95
- # HTTP 请求超时(秒)。registry 体积小,超时设短一些避免阻塞。
95
+
96
96
  _DEFAULT_HTTP_TIMEOUT = 5
97
97
 
98
- # 缓存目录:默认 /tmp,CI/容器中也能写。
98
+
99
99
  _CACHE_DIR = os.environ.get(
100
100
  "VIDEO_TEMPLATE_REGISTRY_CACHE_DIR",
101
101
  os.path.join(tempfile.gettempdir(), "ab-template-registry"),
102
102
  )
103
103
 
104
104
 
105
- # CLI 默认后端 base,与 remixmate-cli/src/http.ts 的 DEFAULT_API_BASE_URL 保持一致。
105
+
106
106
  _DEFAULT_API_BASE_URL = "https://api.remixmate.ai/api"
107
107
 
108
108
 
109
109
  def api_base_url() -> str:
110
- """ab-api base(不带尾斜杠)。registry 与其它挂在 ab-api 上的只读接口共用这一份解析。
111
110
 
112
- 顺序:MM_API_BASE_URL → MM_BACKEND_API_URL → 生产默认,见 ``_default_registry_url``。
113
- """
111
+
112
+
113
+
114
114
  return (
115
115
  os.environ.get("MM_API_BASE_URL", "").strip()
116
116
  or os.environ.get("MM_BACKEND_API_URL", "").strip()
@@ -119,28 +119,28 @@ def api_base_url() -> str:
119
119
 
120
120
 
121
121
  def _default_registry_url() -> str:
122
- """未显式配置 VIDEO_TEMPLATE_REGISTRY_URL 时,推导默认 ab-api registry endpoint。
123
122
 
124
- 默认值内置在 CLI 自身(而非依赖 ab-agent 等宿主在 spawn 时注入),这样
125
- ``remixmate-cli`` 独立安装(codex / ``npm i -g``)也能开箱即用。
126
123
 
127
- base 解析顺序与 CLI / ab-agent 对齐:
128
- MM_API_BASE_URL(CLI ``src/http.ts`` 约定)
129
- → MM_BACKEND_API_URL(ab-agent 约定,向后兼容)
130
- → https://api.remixmate.ai/api(生产默认,零配置)
131
- 路径段固定为 ``/remotionTemplate/registry``(POST,返回 ``{code,msg,data}``)。
132
- """
124
+
125
+
126
+
127
+
128
+
129
+
130
+
131
+
132
+
133
133
  return f"{api_base_url()}/remotionTemplate/registry"
134
134
 
135
135
 
136
136
  def _cache_path_for(url: str) -> str:
137
- """根据 URL 生成本地缓存文件路径,URL 不同则缓存隔离。"""
137
+
138
138
  digest = hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
139
139
  return os.path.join(_CACHE_DIR, f"registry-{digest}.json")
140
140
 
141
141
 
142
142
  def _read_cached(cache_file: str) -> Optional[dict]:
143
- """读取缓存元数据 + 内容;任何异常都返回 None。"""
143
+
144
144
  if not os.path.exists(cache_file):
145
145
  return None
146
146
  try:
@@ -151,7 +151,7 @@ def _read_cached(cache_file: str) -> Optional[dict]:
151
151
 
152
152
 
153
153
  def _write_cached(cache_file: str, payload: dict) -> None:
154
- """原子化写入缓存(先写临时文件再 rename,避免半写文件)。"""
154
+
155
155
  os.makedirs(os.path.dirname(cache_file), exist_ok=True)
156
156
  tmp_fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(cache_file))
157
157
  try:
@@ -166,31 +166,31 @@ def _write_cached(cache_file: str, payload: dict) -> None:
166
166
  raise
167
167
 
168
168
 
169
- # ── PrivToken 解析 ────────────────────────────────────────────────────────
170
- #
171
- # 只读 PRIV_TOKEN 环境变量 —— 凭证库 / 系统钥匙串 / 设备授权流全部由 Node 侧的
172
- # 唯一鉴权入口(src/auth/ensure.ts)负责,并在 spawn 时把解析结果注入本进程的
173
- # 环境变量(src/runner.ts)。
174
- #
175
- # 这里曾经自己读 ~/.config/remixmate/credentials.json,带来两个问题:一是与 Node
176
- # 侧的解析逻辑各写一份、只有本 skill 享受得到;二是「只有一条凭证就直接用」的兜底
177
- # 会把某个后端(如本地开发环境)的 token 发往另一个后端(如生产),属于跨源泄露。
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+
177
+
178
178
 
179
179
 
180
180
  def _resolve_priv_token(_url: str) -> str:
181
- """PrivToken 解析:只认由 CLI 注入 / 宿主设置的 PRIV_TOKEN 环境变量。"""
181
+
182
182
  return os.environ.get("PRIV_TOKEN", "").strip()
183
183
 
184
184
 
185
185
  def _fetch_http(url: str, timeout: int) -> dict:
186
- """执行 HTTP 请求,自动加 X-Priv-Token / If-None-Match 头。
187
186
 
188
- 支持两种 endpoint 形态:
189
- - 老形态(@ab-templates/metadata 静态文件):GET 返回纯 registry JSON
190
- - ab-api 形态:POST 返回 {code, msg, data: <registry>} 包装
191
187
 
192
- 通过环境变量 VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD 切换,默认 POST 适配 ab-api。
193
- """
188
+
189
+
190
+
191
+
192
+
193
+
194
194
  cache_file = _cache_path_for(url)
195
195
  cached = _read_cached(cache_file)
196
196
 
@@ -213,11 +213,11 @@ def _fetch_http(url: str, timeout: int) -> dict:
213
213
  etag = resp.headers.get("ETag", "")
214
214
  parsed = json.loads(raw)
215
215
 
216
- # ab-api 风格:{code, msg, data} → 取 data;其他场景视作直接 registry
216
+
217
217
  if isinstance(parsed, dict) and "data" in parsed and "code" in parsed:
218
218
  code = parsed.get("code")
219
219
  if str(code) in ("401", "403"):
220
- # ab-api 用 HTTP 200 + code=401 表达鉴权失败,所以这里也要判。
220
+
221
221
  raise RegistryAuthError(str(parsed.get("msg") or f"code={code}"))
222
222
  if code not in (0, "0"):
223
223
  raise RuntimeError(
@@ -233,7 +233,7 @@ def _fetch_http(url: str, timeout: int) -> dict:
233
233
  )
234
234
  return data
235
235
  except urllib.error.HTTPError as e:
236
- # 304 Not Modified — 复用缓存内容
236
+
237
237
  if e.code == 304 and cached:
238
238
  cached["fetchedAt"] = time.time()
239
239
  _write_cached(cache_file, cached)
@@ -244,9 +244,9 @@ def _fetch_http(url: str, timeout: int) -> dict:
244
244
 
245
245
 
246
246
  def _load_from_url(url: str, ttl: int, timeout: int) -> dict:
247
- """先看缓存是否在 TTL 内;超期则远程拉取。失败时降级到磁盘缓存(同一 URL
248
- 上一份成功响应),无缓存则抛错 —— 不再回退本地文件 / monorepo,保证单一数据源。
249
- """
247
+
248
+
249
+
250
250
  cache_file = _cache_path_for(url)
251
251
  cached = _read_cached(cache_file)
252
252
  now = time.time()
@@ -257,14 +257,14 @@ def _load_from_url(url: str, ttl: int, timeout: int) -> dict:
257
257
  try:
258
258
  return _fetch_http(url, timeout)
259
259
  except RegistryAuthError as exc:
260
- # 鉴权失败不降级到缓存:缓存可能属于另一个账号,静默返回它会把「凭证已失效」
261
- # 掩盖成一份看似正常、实则不属于当前用户的模板列表。
260
+
261
+
262
262
  raise RegistryAuthError(
263
263
  f"凭证无效或已过期({exc})。运行 remixmate login 重新授权,"
264
264
  f"或设置有效的 PRIV_TOKEN。当前后端:{url}"
265
265
  ) from exc
266
266
  except Exception as exc:
267
- # HTTP 故障时降级:有磁盘缓存就继续用(同一数据源的容错,不是第二个源)
267
+
268
268
  if cached:
269
269
  print(
270
270
  f"⚠️ registry HTTP 拉取失败,降级使用本地缓存: {exc}",
@@ -282,14 +282,14 @@ def load_registry_data(
282
282
  ttl_seconds: int = _DEFAULT_TTL_SECONDS,
283
283
  http_timeout: int = _DEFAULT_HTTP_TIMEOUT,
284
284
  ) -> dict:
285
- """加载 registry 完整文档(含 version / generatedAt / templates)。
286
-
287
- 单一数据源:ab-api HTTP。URL 取显式 ``VIDEO_TEMPLATE_REGISTRY_URL``,
288
- 未设置则按 ``MM_API_BASE_URL`` 推导(见 ``_default_registry_url``)。
289
- 带磁盘缓存(TTL + ETag/304),返回 ``data.get("templates", [])`` 形状的 dict。
290
- """
291
- # 这两行是实现细节(endpoint 从哪来),对终端用户没有意义,却出现在每一次
292
- # 成功调用的 stderr 上。默认收起,排障时用 REMIXMATE_DEBUG=1 打开。
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
293
  debug = os.environ.get("REMIXMATE_DEBUG", "").strip() == "1"
294
294
  url = os.environ.get("VIDEO_TEMPLATE_REGISTRY_URL", "").strip()
295
295
  if url:
@@ -307,43 +307,43 @@ def load_registry_data(
307
307
 
308
308
 
309
309
  def registry_page_urls() -> dict[str, str]:
310
- """Document-level ``pageUrls`` (templateId → web detail page), built by ab-api.
311
310
 
312
- Kept beside ``templates`` rather than inside each definition: a definition is
313
- the template's authoring contract, a link is site information. Absent on
314
- older backends, offline registries, or when the backend has no site address
315
- configured — returns ``{}`` then (never raises: it is display-only).
316
- """
311
+
312
+
313
+
314
+
315
+
316
+
317
317
  try:
318
318
  raw = load_registry_data().get("pageUrls")
319
- except Exception: # noqa: BLE001 — the caller already loaded the registry once
319
+ except Exception:
320
320
  return {}
321
321
  if not isinstance(raw, dict):
322
322
  return {}
323
323
  return {k: v for k, v in raw.items() if isinstance(k, str) and isinstance(v, str) and v}
324
324
 
325
325
 
326
- # ════════════════════════════════════════════════════════════════════════════
327
- # Indexed views — O(1) template-id lookup + status / beta gating
328
- #
329
- # `load_registry_data` returns the raw registry document; consumers that
330
- # repeatedly look up templates by id (validator, render_video, gen-script)
331
- # used to do `for tpl in registry.get("templates", []): if tpl["templateId"]
332
- # == X: ...` — O(N) per call. With 100+ templates that becomes 100 scans for
333
- # every validate() invocation. The helpers below materialize the list into a
334
- # dict[templateId → tpl] once per process and serve everyone from there.
335
- #
336
- # `status` / `version` are P2.2 fields on template.json. When templates
337
- # don't declare them yet, we treat them as stable + version "0.0.0" — pure
338
- # additive behavior, no regressions on legacy templates.
339
- # ════════════════════════════════════════════════════════════════════════════
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
338
+
339
+
340
340
 
341
341
  _DEFAULT_STATUS = "stable"
342
342
  _BETA_GATE_ENV = "ENABLE_BETA_TEMPLATES"
343
343
 
344
344
 
345
345
  def _truthy(value: str) -> bool:
346
- """统一解析布尔型环境变量:1 / true / yes / on(大小写不敏感)。"""
346
+
347
347
  return value.strip().lower() in ("1", "true", "yes", "on")
348
348
 
349
349
 
@@ -359,12 +359,12 @@ def _resolve_status(tpl: dict) -> str:
359
359
 
360
360
 
361
361
  def _allowed_statuses() -> set[str]:
362
- """Which template statuses the current process is allowed to see.
363
362
 
364
- Default: only `stable`. Set `ENABLE_BETA_TEMPLATES=1` (or any truthy
365
- value) to also include `beta`. `deprecated` is always hidden — callers
366
- that explicitly need it can call ``load_registry_data`` directly.
367
- """
363
+
364
+
365
+
366
+
367
+
368
368
  allowed = {"stable"}
369
369
  if _truthy_env(_BETA_GATE_ENV):
370
370
  allowed.add("beta")
@@ -373,12 +373,12 @@ def _allowed_statuses() -> set[str]:
373
373
 
374
374
  @lru_cache(maxsize=4)
375
375
  def _indexed_view(allowed_statuses_key: Optional[tuple[str, ...]]) -> dict[str, dict]:
376
- """Build (and memoize) ``{templateId: tpl}`` keyed by the allowed-status set.
377
376
 
378
- The key is a sorted tuple so equal sets map to the same cache entry. The
379
- HTTP/file load underneath has its own TTL — this layer only avoids
380
- re-iterating the list across the typical 10-100 lookups per render run.
381
- """
377
+
378
+
379
+
380
+
381
+
382
382
  raw = load_registry_data()
383
383
  allow_filter = allowed_statuses_key is not None
384
384
  allowed = set(allowed_statuses_key) if allowed_statuses_key else set()
@@ -399,36 +399,36 @@ def load_registry_indexed(
399
399
  http_timeout: int = _DEFAULT_HTTP_TIMEOUT,
400
400
  include_all_statuses: bool = False,
401
401
  ) -> dict[str, dict]:
402
- """Return registry as ``{templateId: template_dict}``.
403
402
 
404
- Filters by the process-wide status gate unless ``include_all_statuses``
405
- is set (e.g. render-video reading an already-chosen template). Templates
406
- missing ``status`` are treated as ``stable`` for backwards compatibility.
407
403
 
408
- ``ttl_seconds`` / ``http_timeout`` are accepted for API symmetry with
409
- ``load_registry_data`` but the memoization layer below is per-process
410
- rather than time-based; call ``invalidate_cache()`` to force a reread.
411
- """
404
+
405
+
406
+
407
+
408
+
409
+
410
+
411
+
412
412
  if include_all_statuses:
413
413
  return _indexed_view(None)
414
414
  return _indexed_view(tuple(sorted(_allowed_statuses())))
415
415
 
416
416
 
417
417
  def get_template(template_id: str, *, include_all_statuses: bool = False) -> Optional[dict]:
418
- """Return a single template dict by id, or None when missing/filtered out."""
418
+
419
419
  if not template_id:
420
420
  return None
421
421
  return load_registry_indexed(include_all_statuses=include_all_statuses).get(template_id)
422
422
 
423
423
 
424
424
  def gated_status(template_id: str) -> Optional[str]:
425
- """模板存在、但被状态门控挡在可见集合外时返回它的真实 status;否则 None。
426
425
 
427
- ``get_template`` 用 ``None`` 同时表达「registry 里没有这个 id」和「有,但被
428
- 门控挡了」两件完全不同的事。前者是调用方拼错了 id,后者是运行时配置问题
429
- (模板确实在线,只是当前进程没开 beta)—— 两者的处置方式不同,却在一个返回
430
- 值里混成一样。这个函数把后者单独识别出来。
431
- """
426
+
427
+
428
+
429
+
430
+
431
+
432
432
  if not template_id:
433
433
  return None
434
434
  if get_template(template_id) is not None:
@@ -438,16 +438,16 @@ def gated_status(template_id: str) -> Optional[str]:
438
438
 
439
439
 
440
440
  def require_visible_template(template_id: str) -> Optional[dict]:
441
- """与 ``get_template`` 相同,但模板**被状态门控挡住**时抛异常而不是返回 None。
442
441
 
443
- 为什么需要它:``get_template`` 返回 None 之后,调用方普遍的写法是「拿不到就用
444
- 默认值继续」—— 对"这个 id 不存在"是合理的容错,对"模板在线但本进程看不到"却是
445
- 灾难性的静默降级:模板的 llmHint / customPayloadSchema / capabilities 全部蒸发,
446
- 脚本按通用骨架生成、payload 契约整段跳过,最后烧掉渲染积分换一条废片,而日志里
447
- 一个字都没有。
448
442
 
449
- 返回 None 只剩一个含义:registry 里没有这个 templateId。
450
- """
443
+
444
+
445
+
446
+
447
+
448
+
449
+
450
+
451
451
  tpl = get_template(template_id)
452
452
  if tpl is not None:
453
453
  return tpl
@@ -462,14 +462,14 @@ def require_visible_template(template_id: str) -> Optional[dict]:
462
462
 
463
463
 
464
464
  def list_templates(*, include_all_statuses: bool = False) -> list[dict]:
465
- """Return the visible templates as a list (status-filtered)."""
465
+
466
466
  return list(load_registry_indexed(include_all_statuses=include_all_statuses).values())
467
467
 
468
468
 
469
469
  def invalidate_cache() -> None:
470
- """Drop the indexed-view memoization. Mostly for tests / hot reload.
471
470
 
472
- Note: the on-disk HTTP cache is independent and respects its own TTL;
473
- this only resets the in-process memoization built by ``lru_cache``.
474
- """
471
+
472
+
473
+
474
+
475
475
  _indexed_view.cache_clear()