@remixmate/cli 0.9.45 → 0.9.46
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.
- package/dist/manifest.json +2 -2
- package/package.json +1 -1
- package/skills/gen-script/scripts/gen_script.py +31 -2
- package/skills/render-video/scripts/render_video.py +11 -2
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/scripts/payload_hydrator.py +65 -3
- package/skills/template-registry/version.json +1 -1
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.46",
|
|
4
4
|
"description": "AI media generation skills for Claude Code / Codex — 12 skills covering image, video, voice, digital human, web screenshot, web recording, script, template registry, rendering, Jianying export, and video deconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -328,7 +328,11 @@ def plan_scenes(
|
|
|
328
328
|
opening_duration = max(3, round(duration * 0.12))
|
|
329
329
|
ending_duration = max(3, round(duration * 0.12))
|
|
330
330
|
content_total = duration - opening_duration - ending_duration
|
|
331
|
-
|
|
331
|
+
# 余数摊到前几段,保证各段加总正好等于 duration。以前是 round(均值) × 段数:
|
|
332
|
+
# 2026-09-23 线上 ppt-to-video 传 --duration 600 --scenes 23,21 段 × round(21.7)=22
|
|
333
|
+
# 加总 606s,撞上模板 supportedDurations.max=600 被自己的时长门禁拒掉。
|
|
334
|
+
# 每段仍有 3s 下限 —— 段数多到摊不开时加总会超出,那是真超出,交给门禁报。
|
|
335
|
+
base_content, extra_content = divmod(max(content_total, 0), content_scenes)
|
|
332
336
|
|
|
333
337
|
scenes.append({
|
|
334
338
|
"purpose": "opening",
|
|
@@ -339,7 +343,7 @@ def plan_scenes(
|
|
|
339
343
|
for i in range(content_scenes):
|
|
340
344
|
scenes.append({
|
|
341
345
|
"purpose": "point",
|
|
342
|
-
"duration":
|
|
346
|
+
"duration": max(3, base_content + (1 if i < extra_content else 0)),
|
|
343
347
|
"label": _localize_label(language, "label_point", n=i + 1),
|
|
344
348
|
})
|
|
345
349
|
|
|
@@ -617,6 +621,7 @@ def _resolve_contract(template_config: dict | None) -> dict:
|
|
|
617
621
|
"payload_defaults": payload_defaults,
|
|
618
622
|
"primary_visual_type": primary_visual_type,
|
|
619
623
|
"default_layout": caps.get("defaultLayout"),
|
|
624
|
+
"authored_scenes": caps.get("authoredScenes") is True,
|
|
620
625
|
}
|
|
621
626
|
|
|
622
627
|
|
|
@@ -791,6 +796,16 @@ def _plan_contract_scenes(
|
|
|
791
796
|
|
|
792
797
|
fixed = contract["fixed_scenes"] or [{"purpose": "opening"}]
|
|
793
798
|
n = len(fixed)
|
|
799
|
+
# 非 arc 策略的场景数由模板钉死,--scenes 不生效 —— 但必须出声。2026-09-23 线上
|
|
800
|
+
# screen-walkthrough 传 --scenes 6 拿回 2 场,没有任何提示,模型只能自己猜为什么。
|
|
801
|
+
if scene_count is not None and scene_count != n:
|
|
802
|
+
print(
|
|
803
|
+
f"⚠️ requested {scene_count} scene(s) but this template's sceneStrategy="
|
|
804
|
+
f"{contract['scene_strategy']} always plans {n}: --scenes is ignored here.\n"
|
|
805
|
+
f" The DSL below has {n} scenes — read the count back from it before showing "
|
|
806
|
+
f"the script to the user.",
|
|
807
|
+
file=sys.stderr,
|
|
808
|
+
)
|
|
794
809
|
per = max(1, round(duration / n))
|
|
795
810
|
plans = []
|
|
796
811
|
for i, fs in enumerate(fixed):
|
|
@@ -1709,6 +1724,20 @@ def build_dsl(
|
|
|
1709
1724
|
|
|
1710
1725
|
_record_source_document(dsl, template_config, template_id, source_document_url)
|
|
1711
1726
|
|
|
1727
|
+
if contract["authored_scenes"]:
|
|
1728
|
+
# 场景表由调用方按用户素材编写(如按操作时间点切录屏),骨架只提供 meta/global。
|
|
1729
|
+
# 这个标记必须由骨架携带:ab-agent 看到它才会整份采用模型提交的 DSL,
|
|
1730
|
+
# 否则它以骨架为基底合并,新增场景、existing 素材、visuals 都会被静默丢掉。
|
|
1731
|
+
dsl["meta"]["authoredScenes"] = True
|
|
1732
|
+
print(
|
|
1733
|
+
f"ℹ️ template '{template_id}' declares capabilities.authoredScenes: the scene list below "
|
|
1734
|
+
f"is only a placeholder.\n"
|
|
1735
|
+
f" Write the full DSL yourself from the user's material (scenes, assets, visuals, "
|
|
1736
|
+
f"customPayload) and submit all of it to prepare_video_assets — it is used as-is, "
|
|
1737
|
+
f"not merged onto this skeleton. Keep meta.templateId.",
|
|
1738
|
+
file=sys.stderr,
|
|
1739
|
+
)
|
|
1740
|
+
|
|
1712
1741
|
_enforce_supported_duration(dsl, template_config, template_id)
|
|
1713
1742
|
|
|
1714
1743
|
return dsl
|
|
@@ -110,7 +110,10 @@ def _rebalance_subtitle_segments(segs: list[dict]) -> list[dict]:
|
|
|
110
110
|
for p in parts:
|
|
111
111
|
ratio = len(p) / total_chars
|
|
112
112
|
frames = max(int(total_frames * ratio), 1)
|
|
113
|
+
# `**seg` 而不是只写三个键:切出来的各部分仍属于**同一句**,
|
|
114
|
+
# 所以 sentenceIndex 等字段要原样继承(见 build_render_plan 处的说明)。
|
|
113
115
|
expanded.append({
|
|
116
|
+
**seg,
|
|
114
117
|
"text": _strip_subtitle_trailing_punct(p.strip()),
|
|
115
118
|
"startFrame": cur_frame,
|
|
116
119
|
"endFrame": cur_frame + frames,
|
|
@@ -848,9 +851,13 @@ def build_render_plan(dsl: dict, binding: dict) -> dict:
|
|
|
848
851
|
local_subs = split_subtitle_from_lines(lines, duration_frames, fps)
|
|
849
852
|
else:
|
|
850
853
|
local_subs = split_subtitle(narration_text, duration_frames, fps)
|
|
854
|
+
# ⚠️ 平移帧号时**必须保留原有字段**(`**s`),不要只挑三个键重建。
|
|
855
|
+
# 这里曾经只搬 text/startFrame/endFrame,于是 timeline_compiler 给每段打上的
|
|
856
|
+
# `sentenceIndex`(模板用它把动作对齐到**句**而不是段)在这一步被静默丢掉 ——
|
|
857
|
+
# 下游模板拿不到句边界,只能退回按段对齐,也就是那个错位 bug 本身。
|
|
851
858
|
subtitle_segments = [
|
|
852
859
|
{
|
|
853
|
-
|
|
860
|
+
**s,
|
|
854
861
|
"startFrame": s["startFrame"] + current_frame,
|
|
855
862
|
"endFrame": s["endFrame"] + current_frame,
|
|
856
863
|
}
|
|
@@ -1519,6 +1526,7 @@ def adjust_timeline_to_audio(render_plan: dict) -> dict:
|
|
|
1519
1526
|
# 起点为终点。
|
|
1520
1527
|
end_f = scene_dur if i == len(old_subs) - 1 else starts[i + 1]
|
|
1521
1528
|
new_subs.append({
|
|
1529
|
+
**s,
|
|
1522
1530
|
"text": text,
|
|
1523
1531
|
"startFrame": current_frame + start_f,
|
|
1524
1532
|
"endFrame": current_frame + end_f,
|
|
@@ -1538,6 +1546,7 @@ def adjust_timeline_to_audio(render_plan: dict) -> dict:
|
|
|
1538
1546
|
ratio = _plain(text) / total_chars
|
|
1539
1547
|
seg_frames = max(int(sub_span * ratio), fps // 2)
|
|
1540
1548
|
new_subs.append({
|
|
1549
|
+
**s,
|
|
1541
1550
|
"text": text,
|
|
1542
1551
|
"startFrame": current_frame + cursor,
|
|
1543
1552
|
"endFrame": current_frame + cursor + seg_frames,
|
|
@@ -1560,7 +1569,7 @@ def adjust_timeline_to_audio(render_plan: dict) -> dict:
|
|
|
1560
1569
|
rel_start = s["startFrame"] - old_scene_start
|
|
1561
1570
|
rel_end = s["endFrame"] - old_scene_start
|
|
1562
1571
|
new_subs.append({
|
|
1563
|
-
|
|
1572
|
+
**s,
|
|
1564
1573
|
"startFrame": current_frame + rel_start,
|
|
1565
1574
|
"endFrame": current_frame + rel_end,
|
|
1566
1575
|
})
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V28",
|
|
6
6
|
"skillDescription": "Final-render skill: loads a persisted RenderPlan by `job_id` and drives the Remotion engine to produce the final video.\n\nUse this skill as soon as the user mentions any of these intents (after assets are already prepared):\n- Render the video, composite the video, export the video\n- Turn the prepared assets into the final clip\n- Render with Remotion\n\nPrerequisite: assets must already be generated via `prepare_video_assets`. This skill never resolves or regenerates assets — pass it a `job_id` from a previous `prepare_video_assets` call.\n\n⚠️ Stop-and-confirm gate: never call this skill until the user has explicitly confirmed the assets prepared by `prepare_video_assets`. If those assets were prepared in the current turn and the user has not replied since, stop and ask instead of rendering."
|
|
7
7
|
}
|
|
@@ -56,6 +56,44 @@ SUPPORTED_SOURCES = ("doc-extract",)
|
|
|
56
56
|
DEFAULT_INDEX_FIELD = "slideIndex"
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def declared_subfields(template_config: Optional[dict], payload_field: str) -> Optional[set[str]]:
|
|
60
|
+
"""模板为 ``customPayload.<payload_field>`` 声明的子字段名集合。
|
|
61
|
+
|
|
62
|
+
注入必须按这个集合**投影**,不能把抽取器返回的整个对象原样塞进去 ——
|
|
63
|
+
两边的字段集根本不是一回事:
|
|
64
|
+
|
|
65
|
+
抽取器返回(每页) slideIndex / title / text / rawNotes / verdict / coverage /
|
|
66
|
+
flags / nodes / beats / mode / modeReason / ambientIds /
|
|
67
|
+
background / recommendedSec / canvas …
|
|
68
|
+
模板声明(slide) nodes / beats / mode / modeReason / ambientIds /
|
|
69
|
+
background / recommendedSec / canvas
|
|
70
|
+
|
|
71
|
+
前者多出来的那些是**内容层与抽取器自己的记账字段**(备注原文、质检结论、覆盖率),
|
|
72
|
+
渲染端一个都不读。而 ``dsl_validator._check_custom_payload_fields`` 会逐字段核对
|
|
73
|
+
声明表,遇到没声明的就报「the slide does not read 'x' — the value is silently
|
|
74
|
+
dropped」并整单拒绝。2026-09-23 线上 conv_StrnauS7TaZNUQV4BfLX 就是这样:
|
|
75
|
+
注入明明成功(23/23 页),却因为多了 5 个字段被校验整单拒掉,而且**重试必然复现**。
|
|
76
|
+
|
|
77
|
+
为什么用投影而不是写个黑名单:抽取器以后再加字段(verdict / coverage 就是后加的)
|
|
78
|
+
黑名单就会再漏一次。声明表是渲染端真正读的那一份,按它投影才不会二次失效。
|
|
79
|
+
|
|
80
|
+
声明表读不到时返回 None = 不投影。这与校验层的行为一致:没有
|
|
81
|
+
``customPayloadSchema`` 时它也不查字段名,所以不投影不会引入新的失败。
|
|
82
|
+
"""
|
|
83
|
+
if not template_config:
|
|
84
|
+
return None
|
|
85
|
+
cps = template_config.get("customPayloadSchema")
|
|
86
|
+
if not isinstance(cps, dict):
|
|
87
|
+
return None
|
|
88
|
+
node = cps.get(payload_field)
|
|
89
|
+
if not isinstance(node, dict):
|
|
90
|
+
return None
|
|
91
|
+
props = node.get("properties")
|
|
92
|
+
if not isinstance(props, dict) or not props:
|
|
93
|
+
return None
|
|
94
|
+
return set(props.keys())
|
|
95
|
+
|
|
96
|
+
|
|
59
97
|
class HydrationError(RuntimeError):
|
|
60
98
|
"""注入失败。message 必须直接告诉调用方下一步做什么。
|
|
61
99
|
|
|
@@ -206,6 +244,18 @@ def hydrate_dsl(
|
|
|
206
244
|
filled = 0
|
|
207
245
|
missing: list[str] = []
|
|
208
246
|
|
|
247
|
+
# 按模板声明投影。抽取结果里混着内容层与抽取器记账字段,渲染端一个都不读,
|
|
248
|
+
# 而校验层见到未声明字段会整单拒绝(见 declared_subfields 的说明)。
|
|
249
|
+
allowed = declared_subfields(template_config, field)
|
|
250
|
+
dropped: set[str] = set()
|
|
251
|
+
|
|
252
|
+
# 页号只有在模板**声明**了它的时候才写回去。没声明就是未知字段,写了会被校验
|
|
253
|
+
# 拒掉 —— 这正是上一版的第二个错:既投影不足,又多写了一个 customPayload.slideIndex。
|
|
254
|
+
index_is_declared = index_field in (declared_subfields(template_config, index_field) or ())
|
|
255
|
+
if not index_is_declared:
|
|
256
|
+
cps = (template_config or {}).get("customPayloadSchema")
|
|
257
|
+
index_is_declared = isinstance(cps, dict) and index_field in cps
|
|
258
|
+
|
|
209
259
|
for position, scene in enumerate(scenes, start=1):
|
|
210
260
|
payload = _scene_payload(scene)
|
|
211
261
|
if payload.get(field) is not None:
|
|
@@ -216,9 +266,14 @@ def hydrate_dsl(
|
|
|
216
266
|
if page is None:
|
|
217
267
|
missing.append(f"scene {scene.get('id', position)} → page {page_no}")
|
|
218
268
|
continue
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
269
|
+
if allowed is None:
|
|
270
|
+
payload[field] = page
|
|
271
|
+
else:
|
|
272
|
+
dropped |= {k for k in page if k not in allowed}
|
|
273
|
+
payload[field] = {k: v for k, v in page.items() if k in allowed}
|
|
274
|
+
# 留下"这一场用的是第几页"的痕迹 —— 仅当模板声明了这个字段。
|
|
275
|
+
if index_is_declared:
|
|
276
|
+
payload.setdefault(index_field, page_no)
|
|
222
277
|
filled += 1
|
|
223
278
|
|
|
224
279
|
source_meta = result.get("source") or {}
|
|
@@ -227,6 +282,13 @@ def hydrate_dsl(
|
|
|
227
282
|
f"{source_meta.get('slideCount', len(pages))} page(s) of "
|
|
228
283
|
f"{source_meta.get('name', 'the source document')}"
|
|
229
284
|
)
|
|
285
|
+
if dropped:
|
|
286
|
+
# 说清丢了什么:这些是内容层/记账字段,渲染端本来就不读。写出来是为了
|
|
287
|
+
# 抽取器哪天把一个**该读**的字段也归到这里时,有人看得见。
|
|
288
|
+
report.append(
|
|
289
|
+
f" (已按模板声明投影,忽略 {len(dropped)} 个渲染端不读的字段:"
|
|
290
|
+
f"{', '.join(sorted(dropped)[:8])})"
|
|
291
|
+
)
|
|
230
292
|
|
|
231
293
|
if missing:
|
|
232
294
|
# 页号对不上不是警告而是错误:少一页 = 成片里一个空白场景,而它渲得出来。
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "template-registry",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "475",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V21",
|
|
6
6
|
"skillDescription": "Video-template registry skill. Stores every video-template definition and lists the available templates (templateId / name / aspect ratio / style tags).\n\nUse this skill as soon as the user mentions any of these intents:\n- View available templates / list every template\n\nNote: DSL→TemplateBinding is no longer a separate exposed step — once prepare_video_assets receives a template_id it builds the binding internally."
|
|
7
7
|
}
|