@remixmate/cli 0.9.25 → 0.9.26
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/README.md +4 -0
- package/dist/manifest.json +7 -2
- package/package.json +2 -1
- package/skills/gen-script/SKILL.md +1 -0
- package/skills/gen-script/scripts/gen_script.py +51 -2
- package/skills/gen-script/skill.json +5 -0
- package/skills/gen-script/version.json +1 -1
- package/skills/render-video/scripts/render_video.py +32 -0
- package/skills/render-video/version.json +1 -1
package/README.md
CHANGED
|
@@ -268,6 +268,10 @@ npm run regress:update
|
|
|
268
268
|
# Run only the render-plan snapshot layer:
|
|
269
269
|
npm run test:render-plan
|
|
270
270
|
|
|
271
|
+
# Narration speed wiring (gen_script → render plan → gen-voice --speed).
|
|
272
|
+
# Fully offline: no registry, no TTS call.
|
|
273
|
+
npm run test:narration-speed
|
|
274
|
+
|
|
271
275
|
# L3 — selective, real Remotion render of chosen template(s). NOT full-library
|
|
272
276
|
# by design (rendering is slow + needs asset generation). Catches crashes /
|
|
273
277
|
# black frames the static layers can't. Needs PRIV_TOKEN for narration TTS;
|
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"version": "0.9.26",
|
|
4
|
+
"generatedAt": "2026-09-13T00:51:13.551Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "export-jianying",
|
|
@@ -382,6 +382,10 @@
|
|
|
382
382
|
"type": "string",
|
|
383
383
|
"description": "Narration voice id. Default depends on the bound template's outputLanguage; query gen_voice with list_voices=true to see available ids."
|
|
384
384
|
},
|
|
385
|
+
"speed": {
|
|
386
|
+
"type": "number",
|
|
387
|
+
"description": "Narration speech rate, 0.5-2.0 (default 1.0). Stored at global.narration.speed and applied when prepare_video_assets synthesizes the voice-over. **Must** pass when the user picked a speech rate — a template's 语速 / speed parameter arrives as a multiplier like 1.1, and without this flag it is silently dropped and the narration comes back at 1.0. Above ~1.3 subtitle alignment drifts and the delivery sounds mechanical; suggest shortening the script instead of raising it further."
|
|
388
|
+
},
|
|
385
389
|
"template_id": {
|
|
386
390
|
"type": "string",
|
|
387
391
|
"description": "Template id (e.g. html-slide). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
|
|
@@ -444,6 +448,7 @@
|
|
|
444
448
|
"ratio",
|
|
445
449
|
"scenes",
|
|
446
450
|
"voice_id",
|
|
451
|
+
"speed",
|
|
447
452
|
"template_id",
|
|
448
453
|
"headline",
|
|
449
454
|
"subheadline",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.26",
|
|
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",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"test:validators": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-validators.py",
|
|
24
24
|
"test:template-pipeline": "PYTHONDONTWRITEBYTECODE=1 node dist/cli.js exec -- python3 scripts/test-template-pipeline.py",
|
|
25
25
|
"test:props-contract": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-props-contract.py",
|
|
26
|
+
"test:narration-speed": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-narration-speed.py",
|
|
26
27
|
"test:render-plan": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-render-plan-snapshot.py",
|
|
27
28
|
"test:render-plan:update": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-render-plan-snapshot.py --update",
|
|
28
29
|
"test:contracts": "PYTHONDONTWRITEBYTECODE=1 python3 skills/template-registry/scripts/check_contracts.py",
|
|
@@ -401,6 +401,7 @@ python3 <SkillDir>/scripts/gen_script.py \
|
|
|
401
401
|
| `--ratio` | Aspect ratio. | `16:9` |
|
|
402
402
|
| `--scenes` | Scene count. | auto-planned |
|
|
403
403
|
| `--voice-id` | Narration voice id. | resolved from template + language fallback |
|
|
404
|
+
| `--speed` | Narration speech rate (0.5–2.0), written to `global.narration.speed` and applied when `prepare_video_assets` calls gen-voice. Pass it whenever the user picked a speed (the replicate form's 语速 field sends one). Above ~1.3 subtitle alignment drifts and the delivery turns mechanical — shorten the script instead. | `1.0` |
|
|
404
405
|
| `--allow-digital-human` | Whether to allow digital-human assets. | off |
|
|
405
406
|
| `--allow-ai-video` | Whether to allow AI-generated video assets. | off |
|
|
406
407
|
| `--validate` | Validate-only mode: only validate the input DSL. | — |
|
|
@@ -80,6 +80,24 @@ FALLBACK_VOICE = {
|
|
|
80
80
|
"en": "english-male-friendly-01",
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
# ── Narration speed ───────────────────────────────────────────────────────────
|
|
84
|
+
# Written to global.narration.speed; render_video copies it down into the
|
|
85
|
+
# gen-voice asset payload (same route as narration text) and passes it to
|
|
86
|
+
# `gen-voice --speed`. The envelope mirrors gen-voice's own 0.5–2.0 contract;
|
|
87
|
+
# the real per-voice limits (capabilities speedMin/speedMax) are only known at
|
|
88
|
+
# synthesis time, which is several confirmation steps and a few paid image
|
|
89
|
+
# generations later — so an obviously-out-of-range value fails here instead.
|
|
90
|
+
DEFAULT_NARRATION_SPEED = 1.0
|
|
91
|
+
SPEED_MIN = 0.5
|
|
92
|
+
SPEED_MAX = 2.0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _resolve_narration_speed(speed):
|
|
96
|
+
"""CLI speed → the value written to global.narration.speed. Total function."""
|
|
97
|
+
if speed is None:
|
|
98
|
+
return DEFAULT_NARRATION_SPEED
|
|
99
|
+
return float(speed)
|
|
100
|
+
|
|
83
101
|
|
|
84
102
|
def _resolve_template_language_meta(template_config):
|
|
85
103
|
"""Tolerant reader for outputLanguage / defaultVoiceId on a template dict.
|
|
@@ -750,6 +768,7 @@ def _build_carousel_caption_dsl(
|
|
|
750
768
|
resolution: str,
|
|
751
769
|
output_language: str,
|
|
752
770
|
resolved_voice_id: str,
|
|
771
|
+
narration_speed: float,
|
|
753
772
|
font_id: str | None,
|
|
754
773
|
font_name: str | None,
|
|
755
774
|
narration_enabled: bool,
|
|
@@ -907,7 +926,7 @@ def _build_carousel_caption_dsl(
|
|
|
907
926
|
# 无配音模板:关闭 CC 字幕(字幕段是 narration→TTS 的派生物),
|
|
908
927
|
# 并省略 global.narration,避免下游误判存在旁白。
|
|
909
928
|
"subtitle": {"enabled": narration_enabled, "style": "bottom"},
|
|
910
|
-
**({"narration": {"voiceId": resolved_voice_id, "speed":
|
|
929
|
+
**({"narration": {"voiceId": resolved_voice_id, "speed": narration_speed}} if narration_enabled else {}),
|
|
911
930
|
**({"font": {"fontId": font_id, **({"fontName": font_name} if font_name else {})}} if font_id else {}),
|
|
912
931
|
"bgm": {"enabled": True, "volume": 0.12},
|
|
913
932
|
},
|
|
@@ -961,6 +980,7 @@ def build_dsl(
|
|
|
961
980
|
scene_count: int | None,
|
|
962
981
|
allow_digital_human: bool,
|
|
963
982
|
allow_ai_video: bool,
|
|
983
|
+
speed: float | None = None,
|
|
964
984
|
template_id: str | None = None,
|
|
965
985
|
stub_image_url: str = "",
|
|
966
986
|
stub_video_url: str = "",
|
|
@@ -992,6 +1012,10 @@ def build_dsl(
|
|
|
992
1012
|
图给第 i 个场景,没被占到的场景照常走 gen-image 补图。这条规则不是随便定的——
|
|
993
1013
|
见 docs/asset-annotation-design.md §6:另外两种立场(模型自由挑选 / 严格一一对应)
|
|
994
1014
|
都会让"只传两张图"要么失去可预期性,要么直接跑不通。
|
|
1015
|
+
|
|
1016
|
+
speed 是旁白语速倍率,落在 global.narration.speed(不给 = 1.0)。它**不写进
|
|
1017
|
+
gen-voice 资产的 payload**——与旁白文本同一条规矩:唯一来源在 DSL 上,
|
|
1018
|
+
render_video 在调 TTS 前按 assetRef 回查注入,免得两处存一个值各自漂移。
|
|
995
1019
|
"""
|
|
996
1020
|
# Load template config(registry 单一数据源)→ 归一化 contract
|
|
997
1021
|
template_config = _load_template_config(template_id) if template_id else None
|
|
@@ -1010,6 +1034,7 @@ def build_dsl(
|
|
|
1010
1034
|
# values fall back to "zh" so old templates keep their current behavior.
|
|
1011
1035
|
output_language, template_default_voice_id = _resolve_template_language_meta(template_config)
|
|
1012
1036
|
resolved_voice_id = _resolve_voice_id(voice_id, template_default_voice_id, output_language)
|
|
1037
|
+
narration_speed = _resolve_narration_speed(speed)
|
|
1013
1038
|
if resolved_voice_id != voice_id:
|
|
1014
1039
|
print(
|
|
1015
1040
|
f"ℹ️ voice resolution: cli={voice_id!r} → resolved={resolved_voice_id!r} "
|
|
@@ -1087,6 +1112,7 @@ def build_dsl(
|
|
|
1087
1112
|
resolution=resolution,
|
|
1088
1113
|
output_language=output_language,
|
|
1089
1114
|
resolved_voice_id=resolved_voice_id,
|
|
1115
|
+
narration_speed=narration_speed,
|
|
1090
1116
|
font_id=font_id,
|
|
1091
1117
|
font_name=font_name,
|
|
1092
1118
|
narration_enabled=needs_narration,
|
|
@@ -1377,7 +1403,7 @@ def build_dsl(
|
|
|
1377
1403
|
# 无配音模板:关闭 CC 字幕(字幕段是 narration→TTS 的派生物),并省略
|
|
1378
1404
|
# global.narration,避免下游误判存在旁白。
|
|
1379
1405
|
"subtitle": {"enabled": needs_narration, "style": "bottom"},
|
|
1380
|
-
**({"narration": {"voiceId": resolved_voice_id, "speed":
|
|
1406
|
+
**({"narration": {"voiceId": resolved_voice_id, "speed": narration_speed}} if needs_narration else {}),
|
|
1381
1407
|
**({"font": {"fontId": font_id, **({"fontName": font_name} if font_name else {})}} if font_id else {}),
|
|
1382
1408
|
"bgm": {"enabled": True, "volume": 0.12},
|
|
1383
1409
|
},
|
|
@@ -1417,6 +1443,17 @@ Examples:
|
|
|
1417
1443
|
parser.add_argument("--resolution", default="1080p", help="Resolution (default: 1080p)")
|
|
1418
1444
|
parser.add_argument("--scenes", type=int, default=None, help="Scene count (default: auto-planned)")
|
|
1419
1445
|
parser.add_argument("--voice-id", default=None, help="Narration voice id. When omitted, the resolver picks template.defaultVoiceId, then the language-keyed fallback (zh→Chinese (Mandarin)_Male_Announcer, en→TBD English voice). gen_voice --list-voices --local prints the language-tagged catalog.")
|
|
1446
|
+
parser.add_argument(
|
|
1447
|
+
"--speed",
|
|
1448
|
+
type=float,
|
|
1449
|
+
default=None,
|
|
1450
|
+
help=(
|
|
1451
|
+
f"Narration speech rate ({SPEED_MIN}-{SPEED_MAX}, default {DEFAULT_NARRATION_SPEED}). "
|
|
1452
|
+
"Stored at global.narration.speed and applied by render_video when it calls gen-voice. "
|
|
1453
|
+
"Above ~1.3 subtitle alignment starts to drift and the delivery turns mechanical — "
|
|
1454
|
+
"shorten the script instead."
|
|
1455
|
+
),
|
|
1456
|
+
)
|
|
1420
1457
|
parser.add_argument(
|
|
1421
1458
|
"--font-id",
|
|
1422
1459
|
default=None,
|
|
@@ -1521,6 +1558,15 @@ Examples:
|
|
|
1521
1558
|
parser.print_help()
|
|
1522
1559
|
sys.exit(1)
|
|
1523
1560
|
|
|
1561
|
+
if args.speed is not None and not (SPEED_MIN <= args.speed <= SPEED_MAX):
|
|
1562
|
+
print(
|
|
1563
|
+
f"❌ --speed must be in the range {SPEED_MIN}~{SPEED_MAX} (got {args.speed}).\n"
|
|
1564
|
+
" Speech rate is a multiplier, not a percentage: 1.0 is the voice's own pace, "
|
|
1565
|
+
"1.1 is slightly brisker.",
|
|
1566
|
+
file=sys.stderr,
|
|
1567
|
+
)
|
|
1568
|
+
sys.exit(1)
|
|
1569
|
+
|
|
1524
1570
|
# ── 模板感知的 ratio 自动推导 ────────────────────────────────────────────
|
|
1525
1571
|
# 用户没显式传 --ratio 时:
|
|
1526
1572
|
# 1. 若指定了 --template-id,取模板 supportedAspectRatios[0](避免横竖屏不匹配)
|
|
@@ -1552,6 +1598,8 @@ Examples:
|
|
|
1552
1598
|
print(f" platform: {args.platform}", file=sys.stderr)
|
|
1553
1599
|
print(f" duration: {args.duration}s", file=sys.stderr)
|
|
1554
1600
|
print(f" ratio: {args.ratio}", file=sys.stderr)
|
|
1601
|
+
if args.speed is not None:
|
|
1602
|
+
print(f" narration speed: {args.speed}x", file=sys.stderr)
|
|
1555
1603
|
|
|
1556
1604
|
# CLI flag takes precedence; env vars act as fallback with a visible warning
|
|
1557
1605
|
# so silent cross-session leakage is always observable.
|
|
@@ -1592,6 +1640,7 @@ Examples:
|
|
|
1592
1640
|
ratio=args.ratio,
|
|
1593
1641
|
resolution=args.resolution,
|
|
1594
1642
|
voice_id=args.voice_id,
|
|
1643
|
+
speed=args.speed,
|
|
1595
1644
|
font_id=args.font_id,
|
|
1596
1645
|
font_name=args.font_name,
|
|
1597
1646
|
scene_count=args.scenes,
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"type": "string",
|
|
53
53
|
"description": "Narration voice id. Default depends on the bound template's outputLanguage; query gen_voice with list_voices=true to see available ids."
|
|
54
54
|
},
|
|
55
|
+
"speed": {
|
|
56
|
+
"type": "number",
|
|
57
|
+
"description": "Narration speech rate, 0.5-2.0 (default 1.0). Stored at global.narration.speed and applied when prepare_video_assets synthesizes the voice-over. **Must** pass when the user picked a speech rate — a template's 语速 / speed parameter arrives as a multiplier like 1.1, and without this flag it is silently dropped and the narration comes back at 1.0. Above ~1.3 subtitle alignment drifts and the delivery sounds mechanical; suggest shortening the script instead of raising it further."
|
|
58
|
+
},
|
|
55
59
|
"template_id": {
|
|
56
60
|
"type": "string",
|
|
57
61
|
"description": "Template id (e.g. html-slide). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
|
|
@@ -114,6 +118,7 @@
|
|
|
114
118
|
"ratio",
|
|
115
119
|
"scenes",
|
|
116
120
|
"voice_id",
|
|
121
|
+
"speed",
|
|
117
122
|
"template_id",
|
|
118
123
|
"headline",
|
|
119
124
|
"subheadline",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "gen-script",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "474",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V12",
|
|
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
|
}
|
|
@@ -159,6 +159,22 @@ RESOLUTION_MAP = {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def _narration_speed(narration: dict) -> Optional[float]:
|
|
163
|
+
"""Read a narration block's speech-rate multiplier, or None when unset.
|
|
164
|
+
|
|
165
|
+
Tolerant on purpose: the DSL is hand-editable, and a malformed speed must
|
|
166
|
+
not take down a render that would otherwise be fine — it falls back to the
|
|
167
|
+
voice's own pace. 0 and negatives are treated as unset for the same reason
|
|
168
|
+
ab-api does (`speed: 0` there means "follow the global setting").
|
|
169
|
+
"""
|
|
170
|
+
if not isinstance(narration, dict):
|
|
171
|
+
return None
|
|
172
|
+
raw = narration.get("speed")
|
|
173
|
+
if isinstance(raw, bool) or not isinstance(raw, (int, float)):
|
|
174
|
+
return None
|
|
175
|
+
return float(raw) if raw > 0 else None
|
|
176
|
+
|
|
177
|
+
|
|
162
178
|
def extract_narration_lines(narration: dict) -> Optional[tuple[list[str], int, list[Optional[float]]]]:
|
|
163
179
|
"""If narration uses the structured {intro, items, outro} form, return
|
|
164
180
|
(lines, intro_line_count, at_sec_list). `at_sec_list` is parallel to
|
|
@@ -621,6 +637,8 @@ def build_render_plan(dsl: dict, binding: dict) -> dict:
|
|
|
621
637
|
# so adjust_timeline_to_audio can later auto-derive highlightMap from
|
|
622
638
|
# per-line TTS timestamps.
|
|
623
639
|
dsl_assets_by_id = {a["assetId"]: a for a in dsl.get("assets", [])}
|
|
640
|
+
global_narration = (dsl.get("global") or {}).get("narration") or {}
|
|
641
|
+
global_speed = _narration_speed(global_narration)
|
|
624
642
|
for scene in dsl.get("scenes", []):
|
|
625
643
|
narration = (scene.get("audio") or {}).get("narration") or {}
|
|
626
644
|
ref = narration.get("assetRef")
|
|
@@ -628,6 +646,15 @@ def build_render_plan(dsl: dict, binding: dict) -> dict:
|
|
|
628
646
|
if not asset:
|
|
629
647
|
continue
|
|
630
648
|
payload = asset.setdefault("payload", {})
|
|
649
|
+
# Speech rate follows the same route as the narration text: single source
|
|
650
|
+
# of truth on the DSL, copied down here because resolve_asset_audio only
|
|
651
|
+
# ever sees the asset. Scene-level overrides global (mirrors the narration
|
|
652
|
+
# editor's "行级覆盖 > 全局"); neither set = gen-voice's own 1.0 default.
|
|
653
|
+
speed = _narration_speed(narration)
|
|
654
|
+
if speed is None:
|
|
655
|
+
speed = global_speed
|
|
656
|
+
if speed is not None:
|
|
657
|
+
payload["speed"] = speed
|
|
631
658
|
extracted = extract_narration_lines(narration)
|
|
632
659
|
if extracted:
|
|
633
660
|
lines, intro_lines, _ = extracted
|
|
@@ -862,6 +889,11 @@ def resolve_asset_audio(asset: dict, private_token: str, timeout: int) -> dict:
|
|
|
862
889
|
cmd.extend(["--text", tts_text, "--json-output"])
|
|
863
890
|
if payload.get("voiceId"):
|
|
864
891
|
cmd.extend(["--voice-id", payload["voiceId"]])
|
|
892
|
+
# Injected by build_render_plan from the DSL (scene narration > global).
|
|
893
|
+
# Absent = let gen-voice apply its own default rather than pinning 1.0 here.
|
|
894
|
+
speed = _narration_speed(payload)
|
|
895
|
+
if speed is not None:
|
|
896
|
+
cmd.extend(["--speed", str(speed)])
|
|
865
897
|
if private_token:
|
|
866
898
|
cmd.extend(["--priv-token", private_token])
|
|
867
899
|
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V20",
|
|
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
|
}
|