@remixmate/cli 0.12.8 → 0.13.0
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 +7 -2
- package/README.zh-CN.md +1 -1
- package/dist/manifest.json +21 -8
- package/dist/template/preview.js +7 -7
- package/dist/template/publish.js +8 -8
- package/dist/template/scene-props.js +1 -0
- package/dist/template/still-cases.js +1 -1
- package/package.json +1 -1
- package/skills/export-jianying/scripts/gen_jianying_draft.py +38 -33
- package/skills/export-jianying/version.json +1 -1
- package/skills/gen-script/SKILL.md +15 -13
- package/skills/gen-script/scripts/gen_script.py +906 -547
- package/skills/gen-script/skill.json +19 -6
- package/skills/gen-script/version.json +1 -1
- package/skills/render-video/scripts/_video_probe.py +0 -1
- package/skills/render-video/scripts/remote_renderer_client.py +9 -7
- package/skills/render-video/scripts/render_video.py +553 -391
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/SKILL.md +1 -1
- package/skills/template-registry/scripts/match_template.py +2 -2
- package/skills/template-registry/scripts/payload_hydrator.py +9 -1
- package/skills/template-registry/scripts/render_job_client.py +4 -0
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +50 -24
- package/skills/template-registry/video_dsl/schema/render-plan-v1alpha1.json +7 -1
- package/skills/video-parser/scripts/analyze_video.py +2 -0
- package/skills/video-parser/version.json +1 -1
package/README.md
CHANGED
|
@@ -256,6 +256,9 @@ gen-script → ✅ user confirms script → prepare-video-assets(--template-id)
|
|
|
256
256
|
# List all skills (name, tool, entry type)
|
|
257
257
|
remixmate --list
|
|
258
258
|
|
|
259
|
+
# Everything offline in one go: test:cli + test:unit + smoke (what CI runs)
|
|
260
|
+
npm test
|
|
261
|
+
|
|
259
262
|
# Offline registry smoke + spec guards
|
|
260
263
|
npm run smoke
|
|
261
264
|
|
|
@@ -265,8 +268,10 @@ npm run test:cli
|
|
|
265
268
|
# ── Unit tests for the Python pipeline ──
|
|
266
269
|
# Fully offline. They exercise the CLI's generic mechanisms against the synthetic
|
|
267
270
|
# templates in scripts/fixtures/ — never against a real template library, so
|
|
268
|
-
# template changes cannot turn them red.
|
|
271
|
+
# template changes cannot turn them red. Every scripts/test-*.py is picked up
|
|
272
|
+
# automatically; pass name fragments to run a subset.
|
|
269
273
|
npm run test:unit
|
|
274
|
+
npm run test:unit -- scene-payload countdown
|
|
270
275
|
|
|
271
276
|
# ── Integration: run the CLI against a real template library ──
|
|
272
277
|
# Validation, binding, RenderPlan and props contracts over every template (and
|
|
@@ -311,7 +316,7 @@ remixmate template-registry --list-templates
|
|
|
311
316
|
@skills/gen-voice/SKILL.md Generate a voiceover introducing panda habits, around 100 words + follow this doc strictly
|
|
312
317
|
@skills/gen-digital-human/SKILL.md Get the digital human list + follow this doc strictly
|
|
313
318
|
@skills/template-registry/SKILL.md Get the template list + follow this doc strictly
|
|
314
|
-
@skills/gen-script/SKILL.md Based on the
|
|
319
|
+
@skills/gen-script/SKILL.md Based on the <templateId> template, create a video about AI learning methods + follow this doc strictly
|
|
315
320
|
```
|
|
316
321
|
|
|
317
322
|
## License
|
package/README.zh-CN.md
CHANGED
|
@@ -251,7 +251,7 @@ remixmate template-registry --list-templates
|
|
|
251
251
|
@skills/gen-voice/SKILL.md 生成一段语音,介绍熊猫的习性,大概100字左右 + 严格按该文档执行
|
|
252
252
|
@skills/gen-digital-human/SKILL.md 获取数字人列表 + 严格按该文档执行
|
|
253
253
|
@skills/template-registry/SKILL.md 获取模版列表 + 严格按该文档执行
|
|
254
|
-
@skills/gen-script/SKILL.md 基于模版
|
|
254
|
+
@skills/gen-script/SKILL.md 基于模版 <templateId>,创作一个关于AI学习方法的视频 + 严格按该文档执行
|
|
255
255
|
```
|
|
256
256
|
|
|
257
257
|
## 许可证
|
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.
|
|
4
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"generatedAt": "2026-10-25T02:08:12.079Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "doc-parse",
|
|
@@ -592,15 +592,15 @@
|
|
|
592
592
|
},
|
|
593
593
|
"background_preset": {
|
|
594
594
|
"type": "string",
|
|
595
|
-
"description": "Scene background preset, one of the template's customPayloadSchema.background.preset values. Written to every scene's customPayload.background.preset. **Must** pass when the user picked a background: the replicate card shows `背景样式:
|
|
595
|
+
"description": "Scene background preset, one of the template's customPayloadSchema.background.preset values. Written to every scene's customPayload.background.preset. **Must** pass when the user picked a background: the replicate card shows `背景样式: <名称>(<id>)` → pass `<id>`. Without it the scene keeps the template default. Requires template_id; unsupported values are rejected. `image` / `video` need the user's file: pass it as background_asset (which also sets the preset)."
|
|
596
596
|
},
|
|
597
597
|
"sequence_preset": {
|
|
598
598
|
"type": "string",
|
|
599
|
-
"description": "
|
|
599
|
+
"description": "Style preset for templates declaring capabilities.mediaBinding.options.preset; one of that option's enum in the template's customPayloadSchema. Written next to the template's media list; the preset sets transitions, motion, text layout and pacing, and the scene duration follows. **Must** pass when the user picked a style preset: the replicate card shows `风格预设: <名称>(<id>)` → pass `<id>`; `none` keeps the template defaults. Without it the choice is silently dropped. Rejected on templates without that option; unsupported values are rejected."
|
|
600
600
|
},
|
|
601
601
|
"sequence_fit": {
|
|
602
602
|
"type": "string",
|
|
603
|
-
"description": "
|
|
603
|
+
"description": "Media fill mode for templates declaring capabilities.mediaBinding.options.fit; one of that option's enum in the template's customPayloadSchema (its descriptions explain each value). Beats the preset's own fit. **Must** pass when the user picked it: `显示方式: <名称>(<id>)` → pass `<id>`; `auto` keeps the template / preset default."
|
|
604
604
|
},
|
|
605
605
|
"background_asset": {
|
|
606
606
|
"type": "string",
|
|
@@ -611,7 +611,7 @@
|
|
|
611
611
|
"items": {
|
|
612
612
|
"type": "string"
|
|
613
613
|
},
|
|
614
|
-
"description": "Template-specific card choices as `KEY=VALUE`, written to every scene's customPayload and locked so the dsl_json you save cannot overwrite them. KEY is a dotted path into the template's customPayloadSchema (e.g.
|
|
614
|
+
"description": "Template-specific card choices as `KEY=VALUE`, written to every scene's customPayload and locked so the dsl_json you save cannot overwrite them. KEY is a dotted path into the template's customPayloadSchema (e.g. `<field>=<value>`, `<object>.<field>=<value>`); VALUE is the id inside the card line's parentheses, or the text verbatim for free-text fields. Booleans take true/false, numbers are range-checked, enums are checked — an undeclared key or value is rejected with the allowed list. **Must** pass every card line that the system prompt maps to a payload key; without it the choice is silently dropped. Requires template_id."
|
|
615
615
|
},
|
|
616
616
|
"motion": {
|
|
617
617
|
"type": "string",
|
|
@@ -625,6 +625,17 @@
|
|
|
625
625
|
],
|
|
626
626
|
"description": "Camera motion for every scene (scenes[].animationHints.motion), replacing the skeleton's alternating push-in / pull-out. **Must** pass when the user picked one: `运镜效果: 缓慢推近(kenburns-in)` → `kenburns-in`."
|
|
627
627
|
},
|
|
628
|
+
"ending_payload": {
|
|
629
|
+
"type": "array",
|
|
630
|
+
"items": {
|
|
631
|
+
"type": "string"
|
|
632
|
+
},
|
|
633
|
+
"description": "Like payload, but `KEY=VALUE` is written only to the ending scene and locked there (e.g. the replicate card's 片尾引导 line → `<object>.<field>=<closing line>`, key as the card shows it). On countdown templates (Top-N lists) it also appends the ending scene after #1 — fill that scene's narration (a closing line) and its item field with the #1 entry. Without it a countdown ends on the #1 card. Requires template_id."
|
|
634
|
+
},
|
|
635
|
+
"avatar_id": {
|
|
636
|
+
"type": "string",
|
|
637
|
+
"description": "Digital-human avatar id (`dh_xxx`, the replicate card's 数字人主讲 / 数字人讲师 line) for templates declaring capabilities.digitalHuman. Adds one gen-digital-human asset referenced by every scene's visuals.avatar.assetRef: one continuous presenter whose lips follow the narration. Placement stays the template default; adjust with payload `digitalHuman.<field>=…`. Copy the id verbatim, never invent one. Costs digital-human credits, so pass it only when the user picked an avatar. Requires template_id."
|
|
638
|
+
},
|
|
628
639
|
"font_id": {
|
|
629
640
|
"type": "string",
|
|
630
641
|
"description": "Font library key (font.uniq_id) for the on-screen text and subtitles. Written to global.font.fontId; match_template folds it into the binding's typography and ab-render resolves and installs the real font before rendering. **Must** pass when the user picked a font: the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)` → pass the id in the parentheses. Pass the opaque key, never a CSS family name or the display name — those fall back to the template font with no error. When the user names a font without its id, call gen_script with list_fonts=true first and take the id from that list; do not invent ids. Without it the choice is silently dropped and the video renders in the template's own font. Omit to follow the template's font pairing."
|
|
@@ -670,7 +681,7 @@
|
|
|
670
681
|
"items": {
|
|
671
682
|
"type": "string"
|
|
672
683
|
},
|
|
673
|
-
"description": "Media URLs for the template's image/video
|
|
684
|
+
"description": "Media URLs for the template's image/video list. When provided together with a template_id whose capabilities.payloadStyle=carousel-caption, these URLs are written as existing assets into the media list the template declares in capabilities.mediaBinding.itemsPath — NO AI image generation is triggered. **Must** pass when the user explicitly provides image/video URLs for such a template. Each element is a full URL string. Video URLs (.mp4/.mov/.webm) are accepted too; when the template declares mediaBinding.videoDurationKey gen_script probes each clip's real length and writes it, and an unreadable video is rejected."
|
|
674
685
|
},
|
|
675
686
|
"scene_images": {
|
|
676
687
|
"type": "array",
|
|
@@ -684,7 +695,7 @@
|
|
|
684
695
|
"items": {
|
|
685
696
|
"type": "string"
|
|
686
697
|
},
|
|
687
|
-
"description": "On-screen caption lines for
|
|
698
|
+
"description": "On-screen caption lines for carousel-caption templates; capabilities.mediaBinding.captions says where they go — one text block, or one caption per media item in order (at most one line per item). A replicate asset line's `(… desc: <text>)` suffix is the user's caption for that image: pass it here in the same order (use an empty string for images without one). Otherwise each element is one line. Line-count limits and whether **emphasis** is parsed are declared by the template — read its llmHint, which gen_script prints in full on stderr. **Must** pass when the user explicitly provides bullet-point text / bottom copy. **Also must pass — written by you — when the user did NOT provide any copy but the template is typewriter-driven** (capabilities.durationStrategy=fit-caption): such templates have no narration, so these lines are both the video's content and the thing that decides its duration. Draft them from the material you researched (repo README, page screenshots, the topic). Leaving this empty for a fit-caption template is rejected: nothing auto-generates caption text, and an empty caption renders a titled carousel with no words at all."
|
|
688
699
|
},
|
|
689
700
|
"stub_image_url": {
|
|
690
701
|
"type": "string",
|
|
@@ -728,6 +739,8 @@
|
|
|
728
739
|
"sequence_fit",
|
|
729
740
|
"background_asset",
|
|
730
741
|
"payload",
|
|
742
|
+
"ending_payload",
|
|
743
|
+
"avatar_id",
|
|
731
744
|
"motion",
|
|
732
745
|
"font_id",
|
|
733
746
|
"font_name",
|
package/dist/template/preview.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var O=Object.defineProperty;var h=(
|
|
2
|
-
✗ 被 ab-render 的安全扫描拒绝(${
|
|
3
|
-
${
|
|
4
|
-
先在本地运行 remixmate template check,它与平台使用同一份安全策略。`),!0}h(
|
|
5
|
-
加长等待:--timeout-sec 900`);await new Promise(w=>setTimeout(w,_));const
|
|
1
|
+
var O=Object.defineProperty;var h=(e,t)=>O(e,"name",{value:t,configurable:!0});import{existsSync as U,readdirSync as N,readFileSync as C,statSync as B}from"node:fs";import $ from"node:path";import{EXIT as y,SkillError as S}from"../errors.js";import{resolveHttpContext as G}from"../http.js";import{emitProgress as v}from"../progress.js";import{IDENTITY_FILE as L,PREVIEW_MAX_LONG_EDGE as R,PREVIEW_MAX_SECONDS as k,PREVIEW_MAX_SHORT_EDGE as M}from"./constants.js";import{runCheck as X}from"./check.js";import{bindSceneProps as W}from"./scene-props.js";import{Output as V,readJsonFile as j,requireProjectRoot as H,requireTemplateDir as J,validateTemplateName as Y}from"./project.js";import{RenderBusinessError as q,renderPost as z,resolveRenderBaseUrl as K}from"./render-client.js";import{hashTemplateSource as Q,writePreviewRecord as Z}from"./state.js";const I=30,tt=5,et=180,ot=new Set(["node_modules","dist","__pycache__",".git"]),nt=new Set([".ts",".tsx",".js",".jsx",".mts",".cts",".json",".css",".scss",".md"]),st=512*1024,rt=20*1024*1024,D={"16:9":[16,9],"9:16":[9,16],"1:1":[1,1],"3:4":[3,4],"4:3":[4,3]},A=`草稿预览超出上限:长边 ≤ ${R}px、短边 ≤ ${M}px、时长 ≤ ${k} 秒。请缩短 --duration-sec,或减少 --props-json 的页数;正式出片用 remixmate render-video,不受此限。`;function it(e,t=()=>{}){const s={};let o=0;const r=h(c=>{for(const p of N(c,{withFileTypes:!0})){const n=$.join(c,p.name);if(p.isDirectory()){ot.has(p.name)||r(n);continue}if(!nt.has($.extname(p.name))||/\.(test|spec)\./.test(p.name)||c===e&&p.name===L)continue;const l=B(n).size;if(l>st){t(` ⚠ 跳过超大文件(${(l/1024).toFixed(0)} KB):${$.relative(e,n)}`);continue}o+=l,s[$.relative(e,n).split($.sep).join("/")]=C(n,"utf8")}},"walk");return r(e),{files:s,bytes:o}}h(it,"collectFiles");function at(e,t,s,o,r=()=>{}){const c=$.join(e,"dsl-example.json"),p=U(c)?j(c):null,n=Array.isArray(p?.assets)?p.assets:[];if(o!==void 0){let u;try{u=JSON.parse(o)}catch(d){throw new S(`❌ --props-json 解析失败: ${d.message}`,y.USAGE)}return{props:u,assets:n,durationSec:null}}const l=(p?.scenes??[])[0];if(!l)return{props:{},assets:n,durationSec:null};const f=(t.compositions??[]).find(u=>u.compositionId===s)?.slot,a=f?t.slotMapping?.[f]?.propExtractors:void 0;return f&&f!=="cover"&&!a&&r(` ⚠ template.json 的 slotMapping.${f} 未声明 propExtractors —— 预览将只带 customPayload 透传的字段。`),{props:W(l,a),assets:n,durationSec:typeof l.duration=="number"?l.duration:null,scene:l}}h(at,"buildSampleProps");function ct(e,t){const s=e?.audio?.narration;if(!s)return[];const o=h(a=>a.filter(m=>m.text).map((m,u,d)=>({text:m.text,startFrame:Math.max(0,Math.round(m.startFrame)),endFrame:u===d.length-1?t:Math.round(d[u+1].startFrame)})).filter(m=>m.endFrame>m.startFrame),"finish"),r=Array.isArray(s.items)?s.items:null;if(r&&r.length>0&&r.some(a=>typeof a?.atSec=="number")){let a=0;return o(r.map(m=>{const u=typeof m?.atSec=="number"?m.atSec*I:a;return a=u,{text:m?.text??"",startFrame:u}}))}const c=typeof s.text=="string"?s.text.trim():"";if(!c)return[];const p=c.split(/(?<=[。!?!?])/).map(a=>a.trim()).filter(Boolean);if(p.length===0)return[];const n=p.map(a=>Math.max(1,a.length)),l=n.reduce((a,m)=>a+m,0);let f=0;return o(p.map((a,m)=>{const u=f/l*t;return f+=n[m],{text:a,startFrame:u}}))}h(ct,"buildSubtitleSegments");function pt(e){const[t,s]=D[e]??D["16:9"],o=t>=s,r=o?t:s,c=o?s:t,p=Math.min(R/r,M/c),n=h(a=>Math.floor(a/2)*2,"even"),l=n(t*p),f=n(s*p);return[l,f]}h(pt,"previewDimensions");function lt(e,t,s=()=>{}){const o=j($.join(e,"template.json")),r=Array.isArray(o.compositions)?o.compositions:[],c=t.compositionId??r[0]?.compositionId;if(!c)throw new S("❌ template.json 未声明 compositions",y.USAGE);if(t.compositionId&&!r.some(E=>E.compositionId===c))throw new S(`❌ composition "${c}" 不在该模板中。可选: ${r.map(E=>E.compositionId).join(", ")}`,y.USAGE);const{files:p,bytes:n}=it(e,s);if(Object.keys(p).length===0)throw new S("❌ 模板目录没有可提交的源文件");n>rt&&s(` ⚠ 模板总体积 ${(n/1024/1024).toFixed(1)} MB,接近 ab-render 的请求上限`);const l=at(e,o,c,t.propsJson,s),f=(o.supportedAspectRatios??[])[0]??"16:9",a=Number(t.durationSec??l.durationSec??o.supportedDurations?.min??tt),[m,u]=pt(f),d=Array.isArray(l.props)?l.props:null,x=Math.max(1,Math.round(a*I)),_=d?x*d.length:x,g=_/I;if(g>k)throw new S(`❌ ${A}(本次 ${g.toFixed(1)} 秒)`,y.USAGE);const F=d?[]:ct(l.scene,x),w={timeline:(d??[l.props]).map((E,P)=>{const T=P*x,b=T+x;return{sceneId:d?`preview-${String(P+1).padStart(2,"0")}`:"preview",compositionId:c,startFrame:T,endFrame:b,durationFrames:x,startTime:Number((T/I).toFixed(2)),endTime:Number((b/I).toFixed(2)),props:E,layers:[],subtitleSegments:F,transition:{type:"none",durationFrames:0}}}),renderConfig:{width:m,height:u,fps:I,totalFrames:_,totalDuration:Number(g.toFixed(2)),codec:"h264",crf:18,outputFormat:"mp4"},resolvedAssets:l.assets,globalTypography:o.defaultTypography,colorScheme:o.defaultColorScheme,motionPreset:o.defaultMotionPreset};return{files:p,inputProps:w,compositionId:c,totalSeconds:g}}h(lt,"buildPreviewPlan");function mt(e){return JSON.stringify(e.payload??{}).includes("draft_preview_limit_exceeded")}h(mt,"isLimitError");function dt(e,t){const o=t.payload?.data?.issues??t.payload?.issues;if(!Array.isArray(o)||o.length===0)return!1;e.warn(`
|
|
2
|
+
✗ 被 ab-render 的安全扫描拒绝(${o.length} 处):`);for(const r of o)e.warn(typeof r=="string"?` · ${r}`:` · ${r.file}
|
|
3
|
+
${r.reason}${r.detail?`: ${r.detail}`:""}`);return e.warn(`
|
|
4
|
+
先在本地运行 remixmate template check,它与平台使用同一份安全策略。`),!0}h(dt,"printScanIssues");async function Ft(e){const t=new V(!!e.json),s=Y(e.name),o=H(e.cwd),r=J(o,s);v({phase:"template-preview:check",progress:.05});const c=await X({name:s,cwd:o,stdio:e.json?"stderr":"inherit"});if(c!==0)throw new S(`❌ 本地校验未通过(check 退出码 ${c}),已中止预览。`,c===2?y.USAGE:y.ERROR);const p=Q(r),n=lt(r,e,i=>t.warn(i)),l=await G("template-preview",{privateToken:e.token,apiBaseUrl:e.apiBaseUrl}),f=K(),{width:a,height:m}=n.inputProps.renderConfig;t.info(`模板 ${s}`),t.info(`composition ${n.compositionId}`),t.info(`画幅 ${a}×${m} ${n.totalSeconds.toFixed(1)}s @${I}fps`),t.info(`文件 ${Object.keys(n.files).length} 个`),t.info(`ab-render ${f}`);const u=h((i,w)=>z(f,i,w,{token:l.privateToken,skillName:"template-preview"}),"post");let d;try{v({phase:"template-preview:submit",progress:.1});const i=await u("/renderDraft",{files:n.files,inputProps:n.inputProps,upload:!1});if(!i.taskId)throw new S("❌ /renderDraft 未返回 taskId");d=i.taskId}catch(i){if(i instanceof q){if(mt(i))throw new S(`❌ ${A}`,y.ERROR);if(dt(t,i))throw new S("❌ 预览被安全扫描拒绝",y.ERROR)}throw i}t.info(`taskId ${d}`);const x=e.timeoutSec??et,_=e.pollIntervalMs??1500,g=Date.now()+x*1e3;let F="";for(;;){if(Date.now()>g)throw new S(`❌ 轮询超时(${x}s),任务 ${d} 仍在 ab-render 排队/渲染。
|
|
5
|
+
加长等待:--timeout-sec 900`);await new Promise(w=>setTimeout(w,_));const i=await u("/renderDraftStatus",{taskId:d});if(i.status!==F&&(F=i.status??"",t.info(i.status==="pending"?"排队中…(队列前面还有任务)":`状态: ${i.status}`)),v({phase:`template-preview:${i.status??"unknown"}`,progress:.1+.9*(i.progress??0),taskId:d}),i.status==="succeeded"){const w=i.previewUrl?.startsWith("http")?i.previewUrl:`${f}${i.previewUrl??""}`;t.info(`
|
|
6
6
|
✅ 预览就绪
|
|
7
|
-
${w}`),t.info(" 临时产物,最长保留 24 小时(服务重启后可能更早失效)。"),
|
|
8
|
-
⏸ 确认点:请把预览链接交给用户查看动效与节奏,等用户确认后再发布。`),t.info(" 之后若再改动模板代码,发布前需要重新预览。");const
|
|
7
|
+
${w}`),t.info(" 临时产物,最长保留 24 小时(服务重启后可能更早失效)。"),Z(o,s,{sourceHash:p,taskId:d,url:w,compositionId:n.compositionId,durationSec:n.totalSeconds,at:new Date().toISOString()}),t.info(`
|
|
8
|
+
⏸ 确认点:请把预览链接交给用户查看动效与节奏,等用户确认后再发布。`),t.info(" 之后若再改动模板代码,发布前需要重新预览。");const E={ok:!0,name:s,taskId:d,url:w,compositionId:n.compositionId,durationSec:n.totalSeconds,awaitingConfirmation:!0};return t.result({...E}),E}if(i.status==="failed"){const w=i.error??"(无错误详情)";throw w.includes("draft_preview_limit_exceeded")?new S(`❌ ${A}`):new S(`❌ 渲染失败: ${w}`)}}}h(Ft,"runPreview");export{A as PREVIEW_LIMIT_MESSAGE,lt as buildPreviewPlan,at as buildSampleProps,ct as buildSubtitleSegments,it as collectFiles,pt as previewDimensions,Ft as runPreview};
|
package/dist/template/publish.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
var _=Object.defineProperty;var s=(r,e)=>_(r,"name",{value:e,configurable:!0});import{mkdtempSync as F,readFileSync as L,rmSync as P,writeFileSync as z}from"node:fs";import H from"node:os";import d from"node:path";import{EXIT as
|
|
2
|
-
${e.preview.url}`:""}`)}s(B,"printSummary");function pe(r,e){const t=["-czf",e];for(const
|
|
1
|
+
var _=Object.defineProperty;var s=(r,e)=>_(r,"name",{value:e,configurable:!0});import{mkdtempSync as F,readFileSync as L,rmSync as P,writeFileSync as z}from"node:fs";import H from"node:os";import d from"node:path";import{EXIT as y,SkillError as f}from"../errors.js";import{copyDirSync as q}from"../fs-copy.js";import{mmPost as k,resolveHttpContext as X}from"../http.js";import{emitProgress as m}from"../progress.js";import{IDENTITY_FILE as U,TAR_EXCLUDES as V}from"./constants.js";import{runCheck as G}from"./check.js";import{Output as M,readIdentity as W,readJsonFile as Y,requireProjectRoot as Z,requireTemplateDir as Q,resolveTemplateKey as J,validateTemplateName as ee,writeIdentity as re}from"./project.js";import{previewStatus as te}from"./state.js";import{runProcess as oe}from"./proc.js";import{fetchCoreSurface as ne,resolveRenderBaseUrl as ie}from"./render-client.js";function Re(r){return r.needsConfirmation===!0}s(Re,"isPublishConfirmation");function ae(r,e,t,i,a){const n=e.customPayloadSchema,o=n&&typeof n=="object"&&n.type==="object"&&n.properties&&typeof n.properties=="object"?n.properties:n;return{name:r,displayName:i,templateKey:t?.templateKey??null,created:!t,currentVersion:t?t.version:null,coreVersion:typeof e.coreVersion=="string"?e.coreVersion:"",aspectRatios:Array.isArray(e.supportedAspectRatios)?e.supportedAspectRatios.map(String):[],durations:e.supportedDurations&&typeof e.supportedDurations=="object"?{min:e.supportedDurations.min,max:e.supportedDurations.max}:null,params:o&&typeof o=="object"&&!Array.isArray(o)?Object.keys(o):[],preview:{state:a.state,url:a.record?.url||null,at:a.record?.at||null}}}s(ae,"buildPublishSummary");const se={fresh:"已预览,且之后源码未改动",stale:"⚠ 预览之后源码有改动,预览已过期",missing:"⚠ 尚未做过远程预览"};function B(r,e){r.info(`模板 ${e.name}`),r.info(`名称 ${e.displayName}`),r.info(e.created?"templateKey 首次发布时生成(新建私有模板)":`templateKey ${e.templateKey}(⚠ 覆盖更新已发布的模板,当前 v${e.currentVersion??"?"})`),e.aspectRatios.length&&r.info(`画幅 ${e.aspectRatios.join(", ")}`),e.durations&&r.info(`时长 ${e.durations.min??"?"}–${e.durations.max??"?"} 秒`),r.info(`可调参数 ${e.params.length?e.params.join(", "):"(customPayloadSchema 未声明)"}`),r.info(`coreVersion ${e.coreVersion||"(未声明)"}`),r.info(`预览 ${se[e.preview.state]}${e.preview.url?`
|
|
2
|
+
${e.preview.url}`:""}`)}s(B,"printSummary");function pe(r,e){const t=["-czf",e];for(const i of V)t.push(`--exclude=${i}`);return t.push("-C",r,"."),t}s(pe,"tarArgs");function le(r,e,t=()=>{}){if(!e)return"ab-render /health 未返回 coreSurface —— 该实例版本过旧,无法校验 core 契约";if(!e.available)return`ab-render 无法读取 template-core:${e.error??"未知原因"}`;if(!Array.isArray(r.coreExports))return"template.json 缺少 coreExports —— 先运行 remixmate template check 生成";const i=new Set(e.exports??[]),a=r.coreExports.filter(h=>!i.has(h)),n=typeof r.coreVersion=="string"?r.coreVersion:null,o=Array.isArray(e.versions)&&e.versions.length>0?e.versions:e.packageVersion?[e.packageVersion]:[];return a.length>0?`目标 ab-render 的 template-core 缺少导出:${a.join(", ")}。模板声明需要 core@${n??"(未声明)"},平台当前提供 ${o.length?o.map(h=>`core@${h}`).join(", "):"(未知版本)"}。请改用平台已有的导出,或等平台升级 core 后再发布。`:(n&&o.length>0&&!o.includes(n)&&t(` ⚠ 模板声明 core@${n},平台提供 ${o.join(", ")} —— 导出齐全可以渲染,但效果未必与本地完全一致`),null)}s(le,"evaluateCoreSurface");function ce(r,e){return oe("tar",pe(r,e),{stdio:"pipe"}).then(t=>{if(t.code!==0)throw new f(`❌ 打包失败(tar 退出码 ${t.code}): ${t.stderr.trim().slice(0,300)}`)})}s(ce,"tarball");async function me(r,e){let t;try{t=await fetch(r,{method:"PUT",headers:{"Content-Type":"application/gzip"},body:new Uint8Array(e)})}catch(i){throw new f(`❌ 上传模板包失败(网络错误): ${i.message}`,y.BACKEND_UNREACHABLE)}if(t.status>=300)throw new f(`❌ 上传模板包失败(OSS HTTP ${t.status})`,t.status>=500?y.BACKEND_UNREACHABLE:y.ERROR)}s(me,"putToOss");function D(r,e){const t=s(i=>/^[A-Za-z0-9._/-]+$/.test(i)?i:`'${i.replace(/'/g,"'\\''")}'`,"quote");return`remixmate template publish ${t(r)}${e?` --name ${t(e)}`:""} --yes`}s(D,"confirmCommandFor");async function Ae(r){const e=new M(!!r.json),t=ee(r.name),i=Z(r.cwd),a=Q(i,t);m({phase:"template-publish:check",progress:.05});const n=await G({name:t,cwd:i,stdio:r.json?"stderr":"inherit"});if(n!==0)throw new f(`❌ 本地校验未通过(check 退出码 ${n}),已中止发布。`,n===2?y.USAGE:y.ERROR);const o=Y(d.join(a,"template.json")),h=W(a),v=r.displayName??(typeof o.name=="string"&&o.name?o.name:t),w=typeof o.coreVersion=="string"?o.coreVersion:"",O=te(i,t,a),l=ae(t,o,h,v,O);if(!r.dryRun&&!r.yes){e.info(`即将发布为私有模板:
|
|
3
3
|
`),B(e,l),e.info(`
|
|
4
|
-
⏸ 确认点:尚未发布。请把以上摘要展示给用户,用户确认后执行:`),e.info(` ${D(t,r.displayName)}`),l.preview.state!=="fresh"&&e.info(" (发布前需要先运行 remixmate template preview,或在用户明确同意时加 --skip-preview-check)"),
|
|
4
|
+
⏸ 确认点:尚未发布。请把以上摘要展示给用户,用户确认后执行:`),e.info(` ${D(t,r.displayName)}`),l.preview.state!=="fresh"&&e.info(" (发布前需要先运行 remixmate template preview,或在用户明确同意时加 --skip-preview-check)"),m({phase:"template-publish:awaiting-confirmation",progress:.1,summary:l});const p={ok:!1,needsConfirmation:!0,exitCode:y.NEEDS_CONFIRMATION,summary:l,confirmCommand:D(t,r.displayName)};return e.result({...p}),p}if(!r.dryRun&&l.preview.state!=="fresh"&&!r.skipPreviewCheck)throw new f(l.preview.state==="missing"?`❌ 模板 ${t} 还没有远程预览过,已中止发布。
|
|
5
5
|
先运行 remixmate template preview ${t},让用户确认预览后再发布。`:`❌ 模板 ${t} 在上次预览(${l.preview.at??"时间未知"})之后源码有改动,用户看到的不是将要发布的版本,已中止发布。
|
|
6
|
-
重新运行 remixmate template preview ${t},让用户确认后再发布。`);const{templateKey:b,existing:
|
|
7
|
-
[dry-run] 将依次调用 prepareSave → presignUpload → 上传 → save(isBuiltin=false)`),e.info(`[dry-run] 打包排除:${V.join(", ")}(保留 dsl-example.json)`);const p={ok:!0,name:t,templateKey:b,version:null,dryRun:!0,created:!
|
|
8
|
-
`,"utf8"),JSON.stringify(S)!==JSON.stringify(o)&&e.warn(" ⚠ 服务端规范化后的 template.json 与本地不同;本次上传使用规范化版本,本地文件未改动。"),
|
|
9
|
-
✅ 已发布为你的私有模板`),e.info(` templateKey ${
|
|
10
|
-
出片:`),e.info(` remixmate render-video --template-id ${
|
|
6
|
+
重新运行 remixmate template preview ${t},让用户确认后再发布。`);const{templateKey:b,existing:R}=J(a);let A=null;r.dryRun||(A=await X("template-publish",{privateToken:r.token,apiBaseUrl:r.apiBaseUrl}));const I=ie();B(e,l),!r.dryRun&&l.preview.state!=="fresh"&&e.warn(" ⚠ 已按 --skip-preview-check 跳过预览校验"),m({phase:"template-publish:preflight",progress:.15});const E=await ne(I),C=le(o,E,p=>e.warn(p));if(C)throw new f(`❌ core 预检未通过:${C}`);if(e.info(`core 预检 通过(${E?.exports.length??0} 个导出)`),r.dryRun){e.info(`
|
|
7
|
+
[dry-run] 将依次调用 prepareSave → presignUpload → 上传 → save(isBuiltin=false)`),e.info(`[dry-run] 打包排除:${V.join(", ")}(保留 dsl-example.json)`);const p={ok:!0,name:t,templateKey:b,version:null,dryRun:!0,created:!R};return e.result({...p}),p}const x=A;let g;try{m({phase:"template-publish:prepare",progress:.3});const p=await k(x,"/remotionTemplate/prepareSave",{templateJson:o,name:v,templateKey:b,...w?{coreVersion:w}:{}}),c=p?.templateKey||b,S={...p?.templateJson||o,templateId:c};g=F(d.join(H.tmpdir(),"remixmate-publish-"));const $=d.join(g,"pkg");q(a,$),P(d.join($,U),{force:!0}),z(d.join($,"template.json"),JSON.stringify(S,null,2)+`
|
|
8
|
+
`,"utf8"),JSON.stringify(S)!==JSON.stringify(o)&&e.warn(" ⚠ 服务端规范化后的 template.json 与本地不同;本次上传使用规范化版本,本地文件未改动。"),m({phase:"template-publish:pack",progress:.45});const K=d.join(g,`${c}.tar.gz`);await ce($,K);const T=L(K);e.info(`打包 ${(T.length/1024).toFixed(0)} KB`),m({phase:"template-publish:presign",progress:.55});const u=await k(x,"/remotionTemplate/presignUpload",{templateKey:c});if(!u?.uploadUrl)throw new f("❌ presignUpload 未返回 uploadUrl");m({phase:"template-publish:upload",progress:.7}),await me(String(u.uploadUrl),T),m({phase:"template-publish:save",progress:.85}),await k(x,"/remotionTemplate/save",{templateKey:c,name:v,isBuiltin:!1,sourceOssKey:u.sourceOssKey,version:u.version,templateJson:S,...w?{coreVersion:w}:{}});const j=typeof u.version=="number"?u.version:Number(u.version)||0;re(a,{templateKey:c,version:j}),m({phase:"template-publish:done",progress:1,templateKey:c}),e.info(`
|
|
9
|
+
✅ 已发布为你的私有模板`),e.info(` templateKey ${c}`),e.info(` version ${j}`),e.info(` (已写入 templates/${t}/${U},再次发布会更新同一个模板)`),e.info(`
|
|
10
|
+
出片:`),e.info(` remixmate render-video --template-id ${c} …`);const N={ok:!0,name:t,templateKey:c,version:j,dryRun:!1,created:!R};return e.result({...N}),N}finally{g&&P(g,{recursive:!0,force:!0})}}s(Ae,"runPublish");export{ae as buildPublishSummary,le as evaluateCoreSurface,Re as isPublishConfirmation,Ae as runPublish,pe as tarArgs};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var p=Object.defineProperty;var s=(o,f)=>p(o,"name",{value:f,configurable:!0});function u(o,f){const n={};for(const[a,r]of Object.entries(f??{})){if(!r||typeof r.from!="string")continue;const c=r.from;if(c==="textLayers"){const t=(o.textLayers??[]).find(i=>i.role===r.role);t?.content!==void 0&&t?.content!==null&&(n[a]=t.content);continue}const e=c.split(".").reduce((t,i)=>t&&typeof t=="object"?t[i]:void 0,o);e!=null&&(n[a]=e)}return n}s(u,"applyExtractors");function d(o,f){const n=u(o,f),a=o.customPayload&&typeof o.customPayload=="object"?o.customPayload:{},r=a.templateData||o.templateData,c=r&&typeof r=="object"?r:{};for(const[e,t]of Object.entries(c))e in n||(n[e]=t);for(const[e,t]of Object.entries(a))e!=="templateData"&&!(e in n)&&(n[e]=t);return n}s(d,"bindSceneProps");export{u as applyExtractors,d as bindSceneProps};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var x=Object.defineProperty;var u=(o,i)=>x(o,"name",{value:i,configurable:!0});import{usageError as m}from"./args.js";import{buildSubtitleSegments as b}from"./preview.js";import{bindSceneProps as $}from"./scene-props.js";const p=30,A=5,y=6,T=80;function g(o,i){const n=new Set;let t=o[i];for(;t?.$ref;){if(n.has(t.$ref))return{};n.add(t.$ref),t=o[t.$ref]}return t??{}}u(g,"resolveSlot");function M(o,i){return typeof i?.duration=="number"&&i.duration>0?i.duration:o.supportedDurations?.min??A}u(M,"sceneSeconds");function S(o,i,n,t,r,e){const f=Math.max(1,Math.round(M(o,n)*p)),s=o.slotMapping??{},a=g(s,e),c=a.compositionId?a:g(s,t.slot),d=n?$(n,t.slot==="cover"?void 0:c.propExtractors):{},h={sceneId:n?.id??`case-${r}`,compositionId:t.compositionId,startFrame:0,endFrame:f,durationFrames:f,startTime:0,endTime:f/p,props:d,subtitleSegments:b(n,f),transition:{type:i?.transitions?.default??"none",durationFrames:Math.round((i?.transitions?.duration??0)*p)}};return{index:r,purpose:e,sceneId:typeof n?.id=="string"?n.id:null,compositionId:String(t.compositionId),durationFrames:f,props:{entry:h,resolvedAssets:Array.isArray(i?.assets)?i.assets:[],typography:o.defaultTypography}}}u(S,"buildCase");function L(o,i){const n=Array.isArray(o.compositions)?o.compositions:[];if(n.length===0)return[];const t=Array.isArray(i?.scenes)?i.scenes:[],r=o.slotMapping??{},e=u(s=>{const a=s?g(r,s).compositionId:void 0;return n.find(c=>c.compositionId===a)??n.find(c=>c.slot===s)??n[0]},"compFor"),f=t.map((s,a)=>S(o,i,s,e(s.purpose),a+1,String(s.purpose??"scene")));for(const s of n){if(f.some(c=>c.compositionId===s.compositionId))continue;const a=t.find(c=>c.purpose===s.slot)??t[0];f.push(S(o,i,a,s,f.length+1,String(s.slot??"composition")))}return f}u(L,"enumerateCases");function C(o,i,n){let t=o;if(n&&(t=t.filter(e=>e.compositionId===n),t.length===0))throw m(`composition "${n}" 不在该模板中。可选: ${[...new Set(o.map(e=>e.compositionId))].join(", ")}`);if(!i||i.trim()==="all")return t;const r=[];for(const e of i.split(",").map(f=>f.trim()).filter(Boolean)){const f=/^\d+$/.test(e)?t.filter(s=>s.index===Number(e)):t.filter(s=>s.purpose===e||s.sceneId===e);if(f.length===0)throw m(`--scenes 里的 '${e}' 不存在。可选:1–${o.length},或 ${[...new Set(o.map(s=>s.purpose))].join(" / ")}`);for(const s of f)r.includes(s)||r.push(s)}return r.sort((e,f)=>e.index-f.index)}u(C,"selectCases");function l(o,i){const n=i-1,t=u(e=>Math.min(Math.max(0,Math.round(e)),n),"clamp");if(typeof o=="number")return t(o*p);const r=o.trim();if(r==="start")return 0;if(r==="end")return n;if(r==="intro")return t(Math.min(1.5*p,i*.2));if(r==="middle"||r==="mid")return t(n/2);if(r==="settled")return t(Math.min(i*.85,n-p/2));if(r==="final")return t(n-p*.2);if(/^\d+(\.\d+)?%$/.test(r))return t(Number(r.slice(0,-1))/100*n);if(/^\d+(\.\d+)?s$/.test(r))return t(Number(r.slice(0,-1))*p);if(/^\d+$/.test(r))return t(Number(r));throw m(`帧取值不合法: '${r}'(可用 settled / intro / middle / final / start / end / 百分比如 33% / 帧号 / 秒数如 2.5s)`)}u(l,"anchorFrame");function F(o,i){const n=o.split(",").map(r=>r.trim()).filter(Boolean);if(n.length===0)throw m("--frames 不能为空");const t=[];for(const r of n){const e=l(r,i);t.some(f=>f.frame===e)||t.push({label:r,frame:e})}if(t.length>y)throw m(`--frames 每个场景最多 ${y} 帧,收到 ${t.length} 帧`);return t}u(F,"parseFrameSpec");function D(o,i,n=()=>{}){const t=o.previewHints?.keyframes;if(t===void 0)return[];if(!Array.isArray(t))return n(" ⚠ previewHints.keyframes 应为数组,已忽略"),[];const r=[];return t.forEach((e,f)=>{const s=e?.scene===void 0?1:e.scene,a=e?.at===void 0?"settled":e.at,c=typeof a=="number"?a>=0:typeof a=="string";if(!Number.isInteger(s)||s<1||s>i||!c){n(` ⚠ previewHints.keyframes[${f}] 无效(scene 应为 1–${i},at 为锚点或秒数),已忽略`);return}try{l(a,2)}catch{n(` ⚠ previewHints.keyframes[${f}].at='${a}' 无法识别,已忽略`);return}r.push({scene:s,at:a,...typeof e.label=="string"?{label:e.label}:{},...typeof e.expect=="string"?{expect:e.expect}:{}})}),r}u(D,"readDeclaredKeyframes");const I=30*p;function w(o,i){return o>=4?["settled"]:o>=2?["intro","settled"]:["intro",...i>I?["33%","67%"]:["middle"],"settled","final"]}u(w,"defaultAnchors");function H(o,i,n,t=o.length){const r=new Map;for(const e of o){if(i){r.set(e,F(i,e.durationFrames));continue}const f=n.filter(a=>a.scene===e.index);if(f.length===0){const a=[];for(const c of w(t,e.durationFrames)){const d=l(c,e.durationFrames);a.some(h=>h.frame===d)||a.push({label:c,frame:d})}r.set(e,a);continue}const s=[];for(const a of f){const c=l(a.at,e.durationFrames);s.some(d=>d.frame===c)||s.push({label:a.label??String(a.at),frame:c,...a.expect?{expect:a.expect}:{}})}r.set(e,s.slice(0,y))}return r}u(H,"planFrames");export{p as FPS,y as MAX_FRAMES_PER_CASE,T as MAX_TOTAL_STILLS,l as anchorFrame,w as defaultAnchors,L as enumerateCases,F as parseFrameSpec,H as planFrames,D as readDeclaredKeyframes,C as selectCases};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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",
|
|
@@ -431,6 +431,27 @@ def build_text_segment(
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
|
|
434
|
+
def _referenced_assets(props, resolved_assets: dict) -> list:
|
|
435
|
+
|
|
436
|
+
found: list = []
|
|
437
|
+
seen: set = set()
|
|
438
|
+
|
|
439
|
+
def walk(value) -> None:
|
|
440
|
+
if isinstance(value, str):
|
|
441
|
+
if value in resolved_assets and value not in seen:
|
|
442
|
+
seen.add(value)
|
|
443
|
+
found.append(resolved_assets[value])
|
|
444
|
+
elif isinstance(value, dict):
|
|
445
|
+
for v in value.values():
|
|
446
|
+
walk(v)
|
|
447
|
+
elif isinstance(value, list):
|
|
448
|
+
for v in value:
|
|
449
|
+
walk(v)
|
|
450
|
+
|
|
451
|
+
walk(props)
|
|
452
|
+
return found
|
|
453
|
+
|
|
454
|
+
|
|
434
455
|
def convert_render_plan_to_scenes(render_plan: dict) -> tuple:
|
|
435
456
|
|
|
436
457
|
|
|
@@ -454,26 +475,18 @@ def convert_render_plan_to_scenes(render_plan: dict) -> tuple:
|
|
|
454
475
|
scene["duration"] = duration_sec
|
|
455
476
|
|
|
456
477
|
|
|
457
|
-
for layer in entry.get("layers", []):
|
|
458
|
-
if layer.get("type") == "visual":
|
|
459
|
-
asset = resolved_assets.get(layer.get("assetId", ""))
|
|
460
|
-
if asset and asset.get("url"):
|
|
461
|
-
asset_type = asset.get("type", "image")
|
|
462
|
-
if asset_type == "video":
|
|
463
|
-
scene["videoUrl"] = asset["url"]
|
|
464
|
-
else:
|
|
465
|
-
scene["imageUrl"] = asset["url"]
|
|
466
|
-
break
|
|
467
478
|
|
|
468
479
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
480
|
+
|
|
481
|
+
for asset in _referenced_assets(entry.get("props") or {}, resolved_assets):
|
|
482
|
+
if not asset.get("url"):
|
|
483
|
+
continue
|
|
484
|
+
if asset.get("type") in ("image", "video") and not (scene.get("videoUrl") or scene.get("imageUrl")):
|
|
485
|
+
scene["videoUrl" if asset["type"] == "video" else "imageUrl"] = asset["url"]
|
|
486
|
+
elif asset.get("type") == "audio" and not scene.get("audioUrl"):
|
|
487
|
+
scene["audioUrl"] = asset["url"]
|
|
488
|
+
if asset.get("duration"):
|
|
489
|
+
scene["audioDuration"] = round(asset["duration"] / 1000.0, 2)
|
|
477
490
|
|
|
478
491
|
|
|
479
492
|
subtitle_segments = entry.get("subtitleSegments", [])
|
|
@@ -483,21 +496,13 @@ def convert_render_plan_to_scenes(render_plan: dict) -> tuple:
|
|
|
483
496
|
scene["subtitleText"] = subtitle_text.strip()
|
|
484
497
|
|
|
485
498
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
if scene.get("subtitleText"):
|
|
496
|
-
text_layers.append({
|
|
497
|
-
"content": scene["subtitleText"],
|
|
498
|
-
"role": "subheadline",
|
|
499
|
-
})
|
|
500
|
-
del scene["subtitleText"]
|
|
499
|
+
|
|
500
|
+
text_layers = [
|
|
501
|
+
{"content": t["content"], "role": t.get("role", "headline")}
|
|
502
|
+
for t in (entry.get("textLayers") or [])
|
|
503
|
+
if isinstance(t, dict) and isinstance(t.get("content"), str) and t["content"].strip()
|
|
504
|
+
]
|
|
505
|
+
if text_layers:
|
|
501
506
|
scene["textLayers"] = text_layers
|
|
502
507
|
|
|
503
508
|
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "export-jianying",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "554",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V8",
|
|
6
6
|
"skillDescription": "Jianying (CapCut) draft-generation skill. Packages asset URLs into a draft ZIP that Jianying can import; supports automatic conversion from a RenderPlan (calls ab-api /file/generateJianYing).\n\nUse this skill as soon as the user mentions any of these intents:\n- Export to Jianying, Jianying draft, package for Jianying, import into Jianying\n- Export materials to the Jianying format, generate a Jianying project\n- Bundle video / image / audio into a Jianying draft\n- Export a Jianying draft from a RenderPlan\n\nEven when the user does not say \"Jianying\" explicitly, use this skill whenever they want to package materials into a draft that can be edited in Jianying."
|
|
7
7
|
}
|
|
@@ -77,7 +77,7 @@ The intent is to avoid the failure mode where "the generic DSL looks compatible
|
|
|
77
77
|
3b. If the template ships `slideSchemas` (multi-layout templates do), step 1 returns them as a **menu** — per layout: description, field names, card-list key / item fields / `maxItems`, and a `narration` note. Pick layouts and draft the script from the menu. Then, before writing `templateData`, call `template_registry` once with `template_id` and `slide_id=[every chosen layout]`, and **author `templateData` straight from those `slideSchemas[slideId]`** — it is the only place the per-layout field names exist. Do not infer them: the same concept is named `name` in one layout, `label` in another and `era` in a third, and a wrong name is dropped in silence (see below).
|
|
78
78
|
4. Combine that with the template's `slotMapping`, `requiredProps`, `optionalProps`, `propExtractors`, `assetRequirements`, `supportedAspectRatios`, `supportedDurations`, `constraints`, and `scenePatterns`.
|
|
79
79
|
5. Generate the DSL in the template's native shape — not the generic DSL shape.
|
|
80
|
-
6. Make sure the DSL explicitly contains every template-specific field, e.g. `templateData
|
|
80
|
+
6. Make sure the DSL explicitly contains every template-specific field, e.g. `templateData.<field>`, `customPayload.slideId`, `visuals.avatar.assetRef`.
|
|
81
81
|
7. After generation, run `gen_script.py --validate -i <dsl.json>` and fix every error it reports, then show the script summary to the user for confirmation. It checks the template contract, not just the DSL shape: undeclared `customPayload` field names, values outside a declared `enum` (e.g. `layout: "bogus"`, `style.preset: "nwes"`), and every `assetRef` / `*AssetRef` (`logoAssetRef`, `avatarAssetRef` …) pointing at an asset that `assets[]` does not declare. These are the same checks render-video runs before spending credits; catching them here saves a confirmed script from being rejected later.
|
|
82
82
|
|
|
83
83
|
### Document-driven templates: pass the source URL, don't author the payload
|
|
@@ -192,7 +192,7 @@ The user is reviewing the narration word by word; a summary they cannot proofrea
|
|
|
192
192
|
> exactly the bug it should surface.
|
|
193
193
|
> - Durations ← `scenes[].duration`, **not** your `--duration` argument (`fit-caption` / `fit-narration`
|
|
194
194
|
> templates recompute it and ignore the target you passed).
|
|
195
|
-
> - On-screen text ← `textLayers[]
|
|
195
|
+
> - On-screen text ← `textLayers[]` and the media / caption fields the template's `capabilities.mediaBinding` names —
|
|
196
196
|
> quoted verbatim, with the counts you actually see.
|
|
197
197
|
> - If something you intended to set is missing or empty in the returned DSL, that is a failed call:
|
|
198
198
|
> say so and re-run `gen_script.py` with the right flags. Listing caption lines that are not in
|
|
@@ -242,7 +242,7 @@ The agent should show the following in clear Markdown:
|
|
|
242
242
|
|
|
243
243
|
Some templates are **media-driven, not prompt-driven**: their on-screen content comes entirely from media URLs the user already has, and the skill does **not** generate any image. These are the templates whose `capabilities.payloadStyle` is `carousel-caption`.
|
|
244
244
|
|
|
245
|
-
For these templates the picture comes from `
|
|
245
|
+
For these templates the picture comes from the media list the template declares in `capabilities.mediaBinding.itemsPath`, which is filled **only** from the `--carousel-items` flag. If the user gives you images but you do not pass `--carousel-items`, that list is empty and the result is a **black, minimum-length clip**. `gen_script.py` now hard-fails in this case instead of producing the degenerate video.
|
|
246
246
|
|
|
247
247
|
### Mandatory behavior
|
|
248
248
|
|
|
@@ -253,7 +253,7 @@ When the user selects a `carousel-caption` template (or any template whose `asse
|
|
|
253
253
|
- `durationStrategy: fit-caption` → **caption lines are mandatory.** The template has no narration and the typewriter copy is both the content and the clock: it is what the video says *and* what decides how long it runs. **If the user did not supply the copy, write it yourself** from the material you researched (repo README, page screenshots, the topic) and pass it. `gen_script.py` hard-fails on an empty caption for these templates.
|
|
254
254
|
- `durationStrategy: fit-images` → purely visual, captions genuinely optional; duration comes from the image count.
|
|
255
255
|
3. **Never call gen_script for a `carousel-caption` template without `--carousel-items`.** If the user picked such a template but provided no media, ask them for the image/video URLs first — do not generate an empty carousel.
|
|
256
|
-
4.
|
|
256
|
+
4. Video URLs (.mp4/.mov/.webm) go in `--carousel-items` like images. When the template declares `mediaBinding.videoDurationKey`, gen_script probes each clip's real length and writes it there, so the scene is timed by the clip, not as a still; an unreadable video is rejected. A replicate asset line ` (…, desc: <text>)` carries the user's caption for that item — pass it as the matching `--caption-lines` entry (empty string for items without one).
|
|
257
257
|
5. Do **not** route these user-provided images through `gen-image`; they are existing assets and go straight into the carousel.
|
|
258
258
|
6. Nothing downstream fills these in for you. `gen_script.py` routes on `capabilities.payloadStyle` alone — omitting the flags does **not** fall back to a generic path that generates images or writes copy; it assembles an empty carousel / empty caption. There is no auto-generation of caption text anywhere in the pipeline.
|
|
259
259
|
|
|
@@ -285,7 +285,7 @@ AI-generated picture. That failure is quiet: the video renders fine, it just isn
|
|
|
285
285
|
the material the user handed you.
|
|
286
286
|
|
|
287
287
|
**One shared background.** A template that declares `capabilities.backgroundStrategy: "shared"`
|
|
288
|
-
|
|
288
|
+
gets a skeleton with a single
|
|
289
289
|
`img-background` asset that every scene references, instead of one image per scene. Keep it
|
|
290
290
|
that way; switch to another image only where the template's `llmHint` says you may (such as a
|
|
291
291
|
new chapter). `--scene-images` still maps the user's own images one per scene.
|
|
@@ -412,7 +412,7 @@ Typical phrasings (non-exhaustive):
|
|
|
412
412
|
|
|
413
413
|
```bash
|
|
414
414
|
python3 <SkillDir>/scripts/gen_script.py \
|
|
415
|
-
--topic "
|
|
415
|
+
--topic "<topic>" \
|
|
416
416
|
--template-id <templateId> \
|
|
417
417
|
--stub-image-url "https://cdn.example.com/placeholder.jpg"
|
|
418
418
|
```
|
|
@@ -481,11 +481,13 @@ python3 <SkillDir>/scripts/gen_script.py \
|
|
|
481
481
|
| `--voice-id` | Narration voice id. | resolved from template + language fallback |
|
|
482
482
|
| `--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` |
|
|
483
483
|
| `--variant` | Template visual variant id (a key of the template's `variants`; `template-registry --list-templates` prints them as `variantIds`). Written to `meta.templateVariant`, which takes precedence over the fuzzy `--style` match, and the variant's own `imageStyleGuide` is what the skeleton's image prompts / `negativePrompt` are built from (pass it to `gen_script` rather than setting `meta.templateVariant` afterwards, or the prompts keep the template's default look). Pass it whenever the user picked a style — the replicate card shows it as `名字(id)`, e.g. `视觉风格: 紫色科技(tech-purple)`; pass the id in the parentheses. Requires `--template-id`; unknown ids are rejected. | template default |
|
|
484
|
-
| `--background-preset` | Scene background preset, one of the template's `customPayloadSchema.background.preset` values. Written to every scene's `customPayload.background.preset` (other keys of the template's default background are kept). Pass it whenever the user picked a background, e.g. `背景样式:
|
|
485
|
-
| `--sequence-preset` |
|
|
486
|
-
| `--sequence-fit` |
|
|
484
|
+
| `--background-preset` | Scene background preset, one of the template's `customPayloadSchema.background.preset` values. Written to every scene's `customPayload.background.preset` (other keys of the template's default background are kept). Pass it whenever the user picked a background, e.g. `背景样式: <名称>(<id>)` → `<id>`. `image` / `video` need the user's file: pass `--background-asset` (which sets the preset itself). Requires `--template-id`; unsupported values are rejected. | template default |
|
|
485
|
+
| `--sequence-preset` | Style preset for templates declaring `capabilities.mediaBinding.options.preset`; one of that option's enum in the template's `customPayloadSchema` (`none` keeps the template defaults). Written next to the media list; the preset sets transitions, motion, text layout and pacing, and the scene duration follows. **Must** pass when the user picked a style preset on the replicate card (pass the id in parentheses, e.g. `风格预设: <名称>(<id>)` → `<id>`); otherwise the choice is silently dropped. Rejected on templates without that option. |
|
|
486
|
+
| `--sequence-fit` | Fill mode for templates declaring `capabilities.mediaBinding.options.fit`; one of that option's enum (`auto` keeps the template / preset default). Beats the preset's own fit. **Must** pass when the user picked it on the replicate card (`显示方式: <名称>(<id>)` → `<id>`). |
|
|
487
487
|
| `--background-asset` | URL of the user's background image or video (`.mp4/.mov/.webm/.m4v` → video). Added as existing asset `asset-background`; every scene's `customPayload.background` gets `preset: image` + `assetRef`, or `preset: video` + `videoAssetRef`. Conflicts with a different `--background-preset`; rejected when the template's background enum lacks that kind. | none |
|
|
488
|
-
| `--payload KEY=VALUE` | Template-specific card choice, repeatable. `KEY` is a dotted path in `customPayloadSchema` (or `capabilities.frameLevelKeys`), e.g.
|
|
488
|
+
| `--payload KEY=VALUE` | Template-specific card choice, repeatable. `KEY` is a dotted path in `customPayloadSchema` (or `capabilities.frameLevelKeys`), e.g. `<field>=<value>`, `<object>.<field>=<value>`. Value is converted to the declared type (true/false, numbers) and checked against enum / range; undeclared keys are rejected. Written to every scene with sibling keys kept, applied before fit-caption duration estimation. | template default |
|
|
489
|
+
| `--ending-payload KEY=VALUE` | Same as `--payload`, but written only to the ending scene, e.g. `<object>.<field>=<closing line>`. On countdown templates it also appends an ending scene after #1 (it carries the #1 index; fill its narration and the item field `capabilities.countdown.itemProp` names). Errors when the script has no ending scene. | none |
|
|
490
|
+
| `--avatar-id` | Digital-human avatar id (`dh_xxx`) for templates declaring `capabilities.digitalHuman`. Adds asset `asset-avatar` (`type: avatar`, `source: gen-digital-human`, `payload.avatarId`) and sets `visuals.avatar.assetRef` on every scene whose slot reads it — one continuous presenter driven by the narration. Placement: template default, or `--payload digitalHuman.<field>=…` (a photo-only presenter is `--payload digitalHuman.posterUrl=<url>` without this flag). | none |
|
|
489
491
|
| `--motion` | Camera motion for every scene (`animationHints.motion`): `static` / `kenburns-in` / `kenburns-out` / `pan-left` / `pan-right` / `float`. | skeleton alternates in / out |
|
|
490
492
|
| `--font-id` | Font library key (`font.uniq_id`), written to `global.font.fontId`; ab-render resolves and installs the real font at render time. Pass it whenever the user picked a font — the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)`; pass the id in the parentheses. Never pass a CSS family or the display name: an unknown key falls back to the template font with no error. | template font |
|
|
491
493
|
| `--font-name` | Display name of the picked font → `global.font.fontName` (display only, never used for rendering). Pass it with `--font-id`. | — |
|
|
@@ -499,11 +501,11 @@ python3 <SkillDir>/scripts/gen_script.py \
|
|
|
499
501
|
| `-o` / `--output` | Output DSL file path. | stdout |
|
|
500
502
|
| `--stub-image-url` | Test mode: every image AssetRef is written as existing + generated + this URL, no prompt (env: `STUB_IMAGE_URL`). | — |
|
|
501
503
|
| `--stub-video-url` | Test mode: every video AssetRef is written as existing + generated + this URL, no prompt (env: `STUB_VIDEO_URL`). | — |
|
|
502
|
-
| `--carousel-items` | Repeatable. Media URL placed directly into `
|
|
504
|
+
| `--carousel-items` | Repeatable. Media URL placed directly into the template's media list (`capabilities.mediaBinding.itemsPath`) for `carousel-caption` templates. Bypasses gen-image. **Required** for `carousel-caption` templates when the user supplies images. | — |
|
|
503
505
|
| `--scene-images` | Repeatable. One scene's visual for a **`visual-overlay`** template, mapped **positionally**. Each entry is a URL (that existing asset) or `ai` / `ai:<prompt>` (that scene goes to gen-image, optionally with the user's own prompt) — so an AI scene can sit anywhere in the order. Scenes past the last entry still get AI images. Scene count is raised (never lowered) to fit the entries when `--scenes` is absent. **Required** whenever the user supplies images for such a template — omitting it silently ignores every image they gave. | — |
|
|
504
|
-
| `--caption-lines` | Repeatable. On-screen caption line for `carousel-caption` templates → `
|
|
506
|
+
| `--caption-lines` | Repeatable. On-screen caption line for `carousel-caption` templates → where `capabilities.mediaBinding.captions` says (one text block, or one caption per media item, in order). Line limits and `**emphasis**` support are declared per template (see its `llmHint`). **Required** for `durationStrategy: fit-caption` templates — write the lines yourself if the user did not supply them. Optional only for `fit-images` templates. | — |
|
|
505
507
|
|
|
506
|
-
Values written by `--background-preset`, `--background-asset`, `--payload` and `--motion` are listed in `meta.lockedScenePaths` (scene-relative paths such as `customPayload
|
|
508
|
+
Values written by `--background-preset`, `--background-asset`, `--payload`, `--ending-payload`, `--avatar-id` and `--motion` are listed in `meta.lockedScenePaths` (scene-relative paths such as `customPayload.<object>.<field>`, `animationHints.motion`). When the agent later saves its own `dsl_json` in merge mode, ab-agent restores those paths from this skeleton after merging, so a model-written `customPayload` object cannot wipe a card choice. Do not hand-write these keys in `dsl_json`; pass the flag. Templates that author their own scenes (`meta.authoredScenes`) are not merged: there ab-agent writes each locked value that the skeleton holds on every scene (with one value) onto every submitted scene, and adds the skeleton assets those values reference (`asset-background`, `asset-avatar`) when the submitted `assets` lacks them.
|
|
507
509
|
|
|
508
510
|
## DSL generation principles
|
|
509
511
|
|