@remixmate/cli 0.13.7 → 0.13.9

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.
@@ -1,39 +1,39 @@
1
- var k=Object.defineProperty;var f=(t,e)=>k(t,"name",{value:e,configurable:!0});import{mmPost as p,pollUntil as y,resolveHttpContext as U,SkillError as i}from"../http.js";import{getCapabilities as b}from"../capabilities.js";import{emitProgress as m}from"../progress.js";import{isTrue as w,toNumber as M}from"./shared.js";const E=["video","image"],S="hifly";function l(t){return t==null?"":String(t).trim()}f(l,"toId");async function N(t,e){const r=w(e.mine),a=!r&&w(e.all),n=!r&&!a,s={current:1,pageSize:100};n?s.featured=!0:s.source=S,r&&(s.mine=!0),e.gender&&(s.gender=String(e.gender)),e.name&&(s.name=String(e.name));const g=await p(t,"/digital-human/avatar/page",s,{timeoutMs:3e4}),o=g.list??[];if(w(e.json_output)){process.stdout.write(JSON.stringify({avatars:o})+`
2
- `);return}if(o.length===0){process.stdout.write(n?`⚠️ No curated public avatars match. Try --all for the full inventory, or --mine for your own.
1
+ var U=Object.defineProperty;var w=(t,e)=>U(t,"name",{value:e,configurable:!0});import{mmPost as h,pollUntil as y,resolveHttpContext as b,SkillError as i}from"../http.js";import{getCapabilities as N}from"../capabilities.js";import{emitProgress as v}from"../progress.js";import{isTrue as g,toNumber as I}from"./shared.js";const E=["video","image"],k=["hifly","heygen"],S="hifly";function c(t){return t==null?"":String(t).trim()}w(c,"toId");async function M(t,e){const o=g(e.mine),n=!o&&g(e.all),a=!o&&!n,r={current:1,pageSize:100};a?r.featured=!0:n&&(r.source=c(e.source)||S),o&&(r.mine=!0),e.gender&&(r.gender=String(e.gender)),e.name&&(r.name=String(e.name));const u=await h(t,"/digital-human/avatar/page",r,{timeoutMs:3e4}),l=u.list??[];if(g(e.json_output)){process.stdout.write(JSON.stringify({avatars:l})+`
2
+ `);return}if(l.length===0){process.stdout.write(a?`⚠️ No curated public avatars match. Try --all for the full inventory, or --mine for your own.
3
3
  `:`⚠️ No avatars available
4
- `);return}if(n){process.stdout.write(`
4
+ `);return}if(a){process.stdout.write(`
5
5
  ${"ID".padEnd(24)} ${"Name".padEnd(16)} ${"Gender".padEnd(8)} ${"Ratio".padEnd(6)} ${"Scenes".padEnd(22)} Voice
6
6
  `),process.stdout.write(`${"-".repeat(24)} ${"-".repeat(16)} ${"-".repeat(8)} ${"-".repeat(6)} ${"-".repeat(22)} ${"-".repeat(12)}
7
- `);for(const d of o)process.stdout.write(`${l(d.id).padEnd(24)} ${String(d.name??"").padEnd(16)} ${String(d.gender??"").padEnd(8)} ${String(d.aspectRatio??"").padEnd(6)} ${(d.tags??[]).join(",").padEnd(22)} ${d.defaultVoiceId||"-"}
7
+ `);for(const d of l)process.stdout.write(`${c(d.id).padEnd(24)} ${String(d.name??"").padEnd(16)} ${String(d.gender??"").padEnd(8)} ${String(d.aspectRatio??"").padEnd(6)} ${(d.tags??[]).join(",").padEnd(22)} ${d.defaultVoiceId||"-"}
8
8
  `)}else{process.stdout.write(`
9
9
  ${"ID".padEnd(24)} ${"Name".padEnd(16)} ${"Source".padEnd(14)} ${"Gender".padEnd(8)} ${"Ratio".padEnd(6)} Clone
10
10
  `),process.stdout.write(`${"-".repeat(24)} ${"-".repeat(16)} ${"-".repeat(14)} ${"-".repeat(8)} ${"-".repeat(6)} ${"-".repeat(8)}
11
- `);for(const d of o)process.stdout.write(`${l(d.id).padEnd(24)} ${String(d.name??"").padEnd(16)} ${String(d.source??"").padEnd(14)} ${String(d.gender??"").padEnd(8)} ${String(d.aspectRatio??"").padEnd(6)} ${d.cloneStatus||"-"}
12
- `)}const u=g.pager?.total??o.length;process.stdout.write(`
13
- ${o.length} avatar(s) shown${u>o.length?` of ${u}; narrow with --name`:""}
14
- `),process.stdout.write(n?`💡 Use --avatar-id <ID> to pick an avatar; pass its Voice as --voice-id for a matching voice
11
+ `);for(const d of l)process.stdout.write(`${c(d.id).padEnd(24)} ${String(d.name??"").padEnd(16)} ${String(d.source??"").padEnd(14)} ${String(d.gender??"").padEnd(8)} ${String(d.aspectRatio??"").padEnd(6)} ${d.cloneStatus||"-"}
12
+ `)}const s=u.pager?.total??l.length;process.stdout.write(`
13
+ ${l.length} avatar(s) shown${s>l.length?` of ${s}; narrow with --name`:""}
14
+ `),process.stdout.write(a?`💡 Use --avatar-id <ID> to pick an avatar; pass its Voice as --voice-id for a matching voice
15
15
  `:`💡 Use --avatar-id <ID> to pick an avatar for generation
16
- `)}f(N,"listAvatars");async function I(t,e){return((await p(t,"/digital-human/avatar/page",{current:1,pageSize:100,mine:!0,source:S},{timeoutMs:3e4})).list??[]).find(n=>l(n.id)===e)??{id:e,source:S}}f(I,"fetchAvatarInfo");function O(t,e){const r=(t.source??"").trim()||S;if(!e.includes(r))throw new i(`❌ avatar uses provider "${r}", which is not available (supported: ${e.join("/")}). Pick another avatar from --list-avatars.`);return r}f(O,"resolveSource");async function T(t,e){const r=l(e.clone_url);if(!/^https?:\/\//i.test(r))throw new i("❌ --clone needs --clone-url <public http(s) URL of a video or portrait image> (local files are not uploaded)");const a=l(e.name);if(!a)throw new i("❌ --clone needs --name <avatar name>");const n=l(e.clone_type)||"video";if(!E.includes(n))throw new i(`❌ --clone-type must be one of ${E.join(" / ")}, received: ${n}`);const s={name:a,sourceType:n,fileUrl:r};e.thumbnail_url&&(s.thumbnailUrl=l(e.thumbnail_url)),e.gender&&(s.gender=l(e.gender)),m({phase:"gen-digital-human:clone-request",sourceType:n});const g=await p(t,"/digital-human/avatar/clone",s,{timeoutMs:6e4}),o=l(g.id);if(!o)throw new i(`❌ could not parse the new avatar id; API returned: ${JSON.stringify(g)}`);m({phase:"gen-digital-human:clone-async",avatarId:o});const u=w(e.json_output);if(e.wait!==void 0&&!w(e.wait)){if(u){process.stdout.write(JSON.stringify({avatarId:o,cloneStatus:g.cloneStatus??"cloning"})+`
16
+ `)}w(M,"listAvatars");async function O(t,e){for(const o of[{mine:!0},{featured:!0}]){const a=((await h(t,"/digital-human/avatar/page",{current:1,pageSize:100,...o},{timeoutMs:3e4})).list??[]).find(r=>c(r.id)===e);if(a)return a}return{id:e,source:S}}w(O,"fetchAvatarInfo");function R(t,e){const o=(t.source??"").trim()||S;if(!e.includes(o))throw new i(`❌ avatar uses provider "${o}", which is not available (supported: ${e.join("/")}). Pick another avatar from --list-avatars.`);return o}w(R,"resolveSource");async function T(t,e){const o=c(e.clone_url);if(!/^https?:\/\//i.test(o))throw new i("❌ --clone needs --clone-url <public http(s) URL of a video or portrait image> (local files are not uploaded)");const n=c(e.name);if(!n)throw new i("❌ --clone needs --name <avatar name>");const a=c(e.clone_type)||"video";if(!E.includes(a))throw new i(`❌ --clone-type must be one of ${E.join(" / ")}, received: ${a}`);const r=c(e.clone_provider)||S;if(!k.includes(r))throw new i(`❌ --clone-provider must be one of ${k.join(" / ")}, received: ${r}`);if(r==="heygen"&&a!=="image")throw new i("❌ HeyGen avatars are created from a photo only; add --clone-type image");const u={name:n,sourceType:a,fileUrl:o};r!==S&&(u.provider=r),e.thumbnail_url&&(u.thumbnailUrl=c(e.thumbnail_url)),e.gender&&(u.gender=c(e.gender)),v({phase:"gen-digital-human:clone-request",sourceType:a,provider:r});const l=await h(t,"/digital-human/avatar/clone",u,{timeoutMs:6e4}),s=c(l.id);if(!s)throw new i(`❌ could not parse the new avatar id; API returned: ${JSON.stringify(l)}`);v({phase:"gen-digital-human:clone-async",avatarId:s});const d=g(e.json_output);if(e.wait!==void 0&&!g(e.wait)){if(d){process.stdout.write(JSON.stringify({avatarId:s,cloneStatus:l.cloneStatus??"cloning"})+`
17
17
  `);return}process.stdout.write(`
18
- 🧬 Clone submitted, avatar id: ${o}
19
- `),process.stdout.write(`💡 Check progress with --clone-status --avatar-id ${o}
20
- `);return}let d;try{d=await y(async()=>{const c=await p(t,"/digital-human/avatar/clone-status",{id:o},{timeoutMs:3e4}),h=c.cloneStatus??"unknown";if(m({phase:"gen-digital-human:clone-poll",avatarId:o,status:h}),h==="done")return c;if(h==="failed")throw new i(`❌ avatar clone failed: ${c.cloneError||"unknown error"}`);return null},{intervalMs:1e4,timeoutMs:20*6e4})}catch(c){throw c instanceof i&&/polling timed out/.test(c.message)?new i(`❌ avatar ${o} is still cloning; rerun --clone-status --avatar-id ${o} later`):c}if(u){process.stdout.write(JSON.stringify({avatarId:o,cloneStatus:d.cloneStatus})+`
18
+ 🧬 Clone submitted, avatar id: ${s}
19
+ `),process.stdout.write(`💡 Check progress with --clone-status --avatar-id ${s}
20
+ `);return}let m;try{m=await y(async()=>{const f=await h(t,"/digital-human/avatar/clone-status",{id:s},{timeoutMs:3e4}),p=f.cloneStatus??"unknown";if(v({phase:"gen-digital-human:clone-poll",avatarId:s,status:p}),p==="done")return f;if(p==="failed")throw new i(`❌ avatar clone failed: ${f.cloneError||"unknown error"}`);return null},{intervalMs:1e4,timeoutMs:20*6e4})}catch(f){throw f instanceof i&&/polling timed out/.test(f.message)?new i(`❌ avatar ${s} is still cloning; rerun --clone-status --avatar-id ${s} later`):f}if(d){process.stdout.write(JSON.stringify({avatarId:s,cloneStatus:m.cloneStatus})+`
21
21
  `);return}process.stdout.write(`
22
- ✅ Avatar cloned, id: ${o}
23
- `),process.stdout.write(`💡 Generate with --avatar-id ${o} --text "..."
24
- `)}f(T,"cloneAvatar");async function R(t,e,r){const a=await p(t,"/digital-human/avatar/clone-status",{id:e},{timeoutMs:3e4}),n=a.cloneStatus||"unknown";if(r){process.stdout.write(JSON.stringify({avatarId:e,cloneStatus:n,error:a.cloneError||null})+`
22
+ ✅ Avatar cloned, id: ${s}
23
+ `),process.stdout.write(`💡 Generate with --avatar-id ${s} --text "..."
24
+ `)}w(T,"cloneAvatar");async function L(t,e,o){const n=await h(t,"/digital-human/avatar/clone-status",{id:e},{timeoutMs:3e4}),a=n.cloneStatus||"unknown";if(o){process.stdout.write(JSON.stringify({avatarId:e,cloneStatus:a,error:n.cloneError||null})+`
25
25
  `);return}process.stdout.write(`
26
- clone status: ${n}
27
- `),n==="failed"&&process.stdout.write(` failure reason: ${a.cloneError||"unknown error"}
28
- `)}f(R,"checkCloneStatus");async function C(t,e,r){const a=await p(t,"/digital-human/video/status",{generationId:e},{timeoutMs:3e4}),n=a.status??"unknown";if(r){process.stdout.write(JSON.stringify({status:n,url:a.videoUrl??null,generationId:e})+`
26
+ clone status: ${a}
27
+ `),a==="failed"&&process.stdout.write(` failure reason: ${n.cloneError||"unknown error"}
28
+ `)}w(L,"checkCloneStatus");async function j(t,e,o){const n=await h(t,"/digital-human/video/status",{generationId:e},{timeoutMs:3e4}),a=n.status??"unknown";if(o){process.stdout.write(JSON.stringify({status:a,url:n.videoUrl??null,generationId:e})+`
29
29
  `);return}if(process.stdout.write(`
30
- task status: ${n}
31
- `),a.videoUrl&&process.stdout.write(` video URL: ${a.videoUrl}
32
- `),n==="failed"){const s=a.errorMessage??a.error??a.message??"unknown error";process.stdout.write(` failure reason: ${s}
33
- `)}}f(C,"checkStatus");const j=30*6e4;async function J(t,e){return await y(async()=>{const a=await p(t,"/digital-human/video/status",{generationId:e},{timeoutMs:3e4}),n=a.status??"unknown";if(m({phase:"gen-digital-human:poll",generationId:e,status:n}),n==="completed")return a.videoUrl??"";if(n==="failed"){const s=a.errorMessage??a.error??a.message??"unknown error";throw new i(`❌ video generation failed: ${s}`)}return null},{intervalMs:5e3,timeoutMs:j}).catch(a=>{throw a instanceof i&&/polling timed out/.test(a.message)?new i(`${a.message}; still generating on the server — resume with --check-status --generation-id ${e}`):a})}f(J,"pollVideoStatus");async function x(t,e){const r=await U(e.skillName,{apiBaseUrl:t.api_base_url,privateToken:t.priv_token,preflightToken:e.auth?.token}),a=w(t.json_output);if(w(t.list_avatars))return N(r,t);if(w(t.clone))return T(r,t);if(w(t.clone_status)){const $=l(t.avatar_id);if(!$)throw new i("❌ clone-status mode requires --avatar-id");return R(r,$,a)}if(w(t.check_status)){const $=M(t.generation_id,"generation_id");if($===void 0)throw new i("❌ check-status mode requires --generation-id");return C(r,$,a)}const n=l(t.avatar_id);if(!n)throw new i("❌ provide --avatar-id to pick the avatar (use --list-avatars to see available avatars)");const s=(t.audio_url??"").trim(),g=t.text??"";if(s&&!/^https?:\/\//i.test(s))throw new i(`❌ --audio-url needs a public http(s) URL (local files are not uploaded), received: ${s}`);if(!s&&!g.trim())throw new i("❌ in TTS mode provide --text, or use --audio-url for audio-driven mode");const o=(await b(t.api_base_url)).digitalHuman;m({phase:"gen-digital-human:resolve-avatar",avatarId:n});const u=await I(r,n);if(u.cloneStatus==="cloning")throw new i(`❌ avatar ${n} is still cloning; check with --clone-status --avatar-id ${n}`);if(u.cloneStatus==="failed")throw new i(`❌ avatar ${n} failed to clone (${u.cloneError||"unknown error"}); clone it again`);const d=O(u,o.sources),c={avatarId:n,source:d};s?c.audioUrl=s:(c.text=g,c.voiceId=t.voice_id??o.defaultVoiceId,t.voice_name&&(c.voiceName=String(t.voice_name))),m({phase:"gen-digital-human:request",avatarId:n,source:d,mode:s?"audio":"tts"});const h=await p(r,"/digital-human/video/generate",c,{timeoutMs:6e4}),v=h.generationId;if(!v)throw new i(`❌ could not parse generationId; API returned: ${JSON.stringify(h)}`);m({phase:"gen-digital-human:async",generationId:v});const _=await J(r,v);if(!_)throw new i(`❌ video generated but no URL was returned; rerun --check-status --generation-id ${v} later`);if(a){process.stdout.write(JSON.stringify({url:_,generationId:v})+`
30
+ task status: ${a}
31
+ `),n.videoUrl&&process.stdout.write(` video URL: ${n.videoUrl}
32
+ `),a==="failed"){const r=n.errorMessage??n.error??n.message??"unknown error";process.stdout.write(` failure reason: ${r}
33
+ `)}}w(j,"checkStatus");const C=30*6e4;async function D(t,e){return await y(async()=>{const n=await h(t,"/digital-human/video/status",{generationId:e},{timeoutMs:3e4}),a=n.status??"unknown";if(v({phase:"gen-digital-human:poll",generationId:e,status:a}),a==="completed")return n.videoUrl??"";if(a==="failed"){const r=n.errorMessage??n.error??n.message??"unknown error";throw new i(`❌ video generation failed: ${r}`)}return null},{intervalMs:5e3,timeoutMs:C}).catch(n=>{throw n instanceof i&&/polling timed out/.test(n.message)?new i(`${n.message}; still generating on the server — resume with --check-status --generation-id ${e}`):n})}w(D,"pollVideoStatus");async function q(t,e){const o=await b(e.skillName,{apiBaseUrl:t.api_base_url,privateToken:t.priv_token,preflightToken:e.auth?.token}),n=g(t.json_output);if(g(t.list_avatars))return M(o,t);if(g(t.clone))return T(o,t);if(g(t.clone_status)){const $=c(t.avatar_id);if(!$)throw new i("❌ clone-status mode requires --avatar-id");return L(o,$,n)}if(g(t.check_status)){const $=I(t.generation_id,"generation_id");if($===void 0)throw new i("❌ check-status mode requires --generation-id");return j(o,$,n)}const a=c(t.avatar_id);if(!a)throw new i("❌ provide --avatar-id to pick the avatar (use --list-avatars to see available avatars)");const r=(t.audio_url??"").trim(),u=t.text??"";if(r&&!/^https?:\/\//i.test(r))throw new i(`❌ --audio-url needs a public http(s) URL (local files are not uploaded), received: ${r}`);if(!r&&!u.trim())throw new i("❌ in TTS mode provide --text, or use --audio-url for audio-driven mode");const l=(await N(t.api_base_url)).digitalHuman;v({phase:"gen-digital-human:resolve-avatar",avatarId:a});const s=await O(o,a);if(s.cloneStatus==="cloning")throw new i(`❌ avatar ${a} is still cloning; check with --clone-status --avatar-id ${a}`);if(s.cloneStatus==="failed")throw new i(`❌ avatar ${a} failed to clone (${s.cloneError||"unknown error"}); clone it again`);const d=R(s,l.sources),m={avatarId:a,source:d};r?m.audioUrl=r:(m.text=u,m.voiceId=c(t.voice_id)||c(s.defaultVoiceId)||l.defaultVoiceId,t.voice_name&&(m.voiceName=String(t.voice_name))),v({phase:"gen-digital-human:request",avatarId:a,source:d,mode:r?"audio":"tts"});const f=await h(o,"/digital-human/video/generate",m,{timeoutMs:6e4}),p=f.generationId;if(!p)throw new i(`❌ could not parse generationId; API returned: ${JSON.stringify(f)}`);v({phase:"gen-digital-human:async",generationId:p});const _=await D(o,p);if(!_)throw new i(`❌ video generated but no URL was returned; rerun --check-status --generation-id ${p} later`);if(n){process.stdout.write(JSON.stringify({url:_,generationId:p})+`
34
34
  `);return}process.stdout.write(`
35
35
  🔗 Video URL (renders directly, no download needed):
36
36
  ${_}
37
37
  `),process.stdout.write(`
38
38
  🎉 Digital-human video generation done!
39
- `)}f(x,"genDigitalHuman");export{x as genDigitalHuman};
39
+ `)}w(q,"genDigitalHuman");export{q as genDigitalHuman};
@@ -1,19 +1,23 @@
1
- var w=Object.defineProperty;var d=(e,i)=>w(e,"name",{value:i,configurable:!0});import{mmPost as m,resolveHttpContext as v,SkillError as c}from"../http.js";import{getCapabilities as y}from"../capabilities.js";import{emitProgress as x}from"../progress.js";import{isTrue as u,toNumber as _}from"./shared.js";const b=[{id:"Chinese (Mandarin)_Male_Announcer",language:"zh",name:"Mandarin Male Announcer (legacy default)"},{id:"english-male-friendly-01",language:"en",name:"Friendly Male (English, TBD)"}];async function $(e){const o=(await m(e,"/voice/page",{current:1,pageSize:100},{timeoutMs:3e4})).list??[];if(o.length===0){process.stdout.write(`⚠️ No voices available
2
- `);return}process.stdout.write(`
3
- ${"Voice ID".padEnd(30)} Name
4
- `),process.stdout.write(`${"-".repeat(30)} ${"-".repeat(20)}
5
- `);for(const s of o)process.stdout.write(`${(s.uniqId??"").padEnd(30)} ${s.name??""}
6
- `);process.stdout.write(`
7
- ${o.length} voice(s) available
8
- `),process.stdout.write(`💡 Use --voice-id <id> to select a voice for synthesis
9
- `)}d($,"listVoicesRemote");function k(){process.stderr.write(`⚠️ --local prints a hardcoded 2-entry fallback list, NOT the available voices. Drop --local to query the live catalog.
10
- `);for(const e of b)process.stdout.write(`${e.id} ${e.language} ${e.name}
11
- `)}d(k,"listVoicesLocal");async function L(e,i){if(u(e.list_voices)){if(u(e.local))return k();const t=await v(i.skillName,{apiBaseUrl:e.api_base_url,privateToken:e.priv_token,preflightToken:i.auth?.token});return $(t)}const o=e.text;if(!o||typeof o!="string"||!o.trim())throw new c("❌ Please provide --text to specify the text to synthesize");const s=e.api_base_url,n=(await y(s)).voice,p=e.voice_id??n.defaultVoiceId,r=_(e.speed,"speed")??1;if(r<n.speedMin||r>n.speedMax)throw new c(`❌ speed must be in the range ${n.speedMin}~${n.speedMax}`);const g=e.language_boost?.trim()||void 0,h=await v(i.skillName,{apiBaseUrl:s,privateToken:e.priv_token,preflightToken:i.auth?.token});x({phase:"gen-voice:request",voiceId:p,speed:r});const a=await m(h,"/tool/tts",{provider:n.provider,text:o,voiceSetting:{voiceId:p,speed:r},outputFormat:"url",...g?{languageBoost:g}:{}},{timeoutMs:12e4}),l=a.audio;if(!l)throw new c(`❌ API response did not contain an audio URL: ${JSON.stringify(a)}`);if(u(e.json_output)){process.stdout.write(JSON.stringify({url:l,audio_length_ms:a.extraInfo?.audioLength??null,subtitles:(a.subtitles??[]).map(t=>({text:t.text??"",timeBegin:t.timeBegin??0,timeEnd:t.timeEnd??0,...typeof t.textBegin=="number"&&typeof t.textEnd=="number"?{textBegin:t.textBegin,textEnd:t.textEnd}:{}}))})+`
12
- `);return}const f=a.extraInfo?.audioLength;typeof f=="number"&&process.stdout.write(`
13
- 🕐 Audio length: ${(f/1e3).toFixed(1)} s
1
+ var T=Object.defineProperty;var l=(e,o)=>T(e,"name",{value:o,configurable:!0});import{mmPost as w,resolveHttpContext as $,SkillError as h}from"../http.js";import{getCapabilities as b}from"../capabilities.js";import{emitProgress as A}from"../progress.js";import{isTrue as m,toNumber as y}from"./shared.js";const L=[{id:"Chinese (Mandarin)_Male_Announcer",language:"zh",name:"Mandarin Male Announcer (legacy default)"},{id:"english-male-friendly-01",language:"en",name:"Friendly Male (English, TBD)"}],E=100,_=1,x=3,M={male:"男",m:"男",man:"男",female:"女",f:"女",woman:"女"};function f(e){if(e==null)return;const o=String(e).trim();return o||void 0}l(f,"optString");function v(e,o){return(e.tagItems??[]).filter(t=>t.category===o&&t.name).map(t=>t.name).join("/")}l(v,"tagsOf");function N(e){const o=y(e.page,"page")??1;if(!Number.isInteger(o)||o<1)throw new h(`❌ page must be a positive integer, received: ${e.page}`);const t={current:o,pageSize:E},p=f(e.name);p&&(t.name=p);const r=f(e.language);r&&(t.language=r);const i=f(e.gender);if(i&&(t.gender=M[i.toLowerCase()]??i),m(e.mine))return{path:"/voice/collect/myPage",body:t};const s=f(e.provider)?.toLowerCase();s&&(t.provider=s);const a=f(e.voice_type)?.toLowerCase();return a&&(t.voiceType=a),{path:"/voice/page",body:t}}l(N,"buildVoiceListRequest");async function k(e,o){const{path:t,body:p}=N(o),r=await w(e,t,p,{timeoutMs:3e4}),i=r.list??[],s=t!=="/voice/page",a=Number(p.current),g=r.pager?.total??i.length;if(m(o.json_output)){process.stdout.write(JSON.stringify({page:a,total:g,voices:i.map(n=>({id:n.uniqId??"",name:n.name??"",provider:n.provider??"",type:n.voiceType??"",gender:v(n,x),language:v(n,_)}))})+`
2
+ `);return}if(i.length===0){process.stdout.write(a>1?`⚠️ No voices on page ${a} (total ${g})
3
+ `:s?`⚠️ You have no cloned voices
4
+ `:`⚠️ No voices match these filters
5
+ `);return}const d=Math.min(48,Math.max(30,...i.map(n=>(n.uniqId??"").length)));process.stdout.write(`
6
+ ${"Voice ID".padEnd(d)} ${"Provider".padEnd(10)} ${"Type".padEnd(6)} ${"Gender".padEnd(6)} ${"Language".padEnd(12)} Name
7
+ `),process.stdout.write(`${"-".repeat(d)} ${"-".repeat(10)} ${"-".repeat(6)} ${"-".repeat(6)} ${"-".repeat(12)} ${"-".repeat(20)}
8
+ `);for(const n of i)process.stdout.write(`${(n.uniqId??"").padEnd(d)} ${(n.provider??"").padEnd(10)} ${(n.voiceType??"").padEnd(6)} ${v(n,x).padEnd(6)} ${v(n,_).padEnd(12)} ${n.name??""}
9
+ `);const u=(a-1)*E+i.length;process.stdout.write(`
10
+ ${i.length} voice(s) on page ${a}, ${g} in total
11
+ `),g>u&&process.stdout.write(`💡 More voices: add --page ${a+1}, or narrow with --provider / --gender / --language / --name
12
+ `),process.stdout.write(`💡 Use --voice-id <id> to select a voice; the provider is picked from the voice automatically
13
+ `)}l(k,"listVoicesRemote");function C(){process.stderr.write(`⚠️ --local prints a hardcoded 2-entry fallback list, NOT the available voices. Drop --local to query the live catalog.
14
+ `);for(const e of L)process.stdout.write(`${e.id} ${e.language} ${e.name}
15
+ `)}l(C,"listVoicesLocal");async function R(e,o){if(m(e.list_voices)){if(m(e.local))return C();const c=await $(o.skillName,{apiBaseUrl:e.api_base_url,privateToken:e.priv_token,preflightToken:o.auth?.token});return k(c,e)}const t=e.text;if(!t||typeof t!="string"||!t.trim())throw new h("❌ Please provide --text to specify the text to synthesize");const p=e.api_base_url,r=(await b(p)).voice,i=e.voice_id??r.defaultVoiceId,s=y(e.speed,"speed")??1;if(s<r.speedMin||s>r.speedMax)throw new h(`❌ speed must be in the range ${r.speedMin}~${r.speedMax}`);const a=e.language_boost?.trim()||void 0,g=await $(o.skillName,{apiBaseUrl:p,privateToken:e.priv_token,preflightToken:o.auth?.token});A({phase:"gen-voice:request",voiceId:i,speed:s});const d=await w(g,"/tool/tts",{provider:r.provider,text:t,voiceSetting:{voiceId:i,speed:s},outputFormat:"url",...a?{languageBoost:a}:{}},{timeoutMs:12e4}),u=d.audio;if(!u)throw new h(`❌ API response did not contain an audio URL: ${JSON.stringify(d)}`);if(m(e.json_output)){process.stdout.write(JSON.stringify({url:u,audio_length_ms:d.extraInfo?.audioLength??null,subtitles:(d.subtitles??[]).map(c=>({text:c.text??"",timeBegin:c.timeBegin??0,timeEnd:c.timeEnd??0,...typeof c.textBegin=="number"&&typeof c.textEnd=="number"?{textBegin:c.textBegin,textEnd:c.textEnd}:{}}))})+`
16
+ `);return}const n=d.extraInfo?.audioLength;typeof n=="number"&&process.stdout.write(`
17
+ 🕐 Audio length: ${(n/1e3).toFixed(1)} s
14
18
  `),process.stdout.write(`
15
19
  🔗 Audio URL (plays directly, no download needed):
16
- ${l}
20
+ ${u}
17
21
  `),process.stdout.write(`
18
22
  🎉 TTS synthesis done!
19
- `)}d(L,"genVoice");export{L as genVoice};
23
+ `)}l(R,"genVoice");export{N as buildVoiceListRequest,R as genVoice};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.13.7",
4
- "generatedAt": "2026-10-30T09:41:49.930Z",
3
+ "version": "0.13.9",
4
+ "generatedAt": "2026-11-03T15:23:12.126Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
@@ -203,7 +203,15 @@
203
203
  },
204
204
  "all": {
205
205
  "type": "boolean",
206
- "description": "With list_avatars=true: list the full public HiFly inventory (500+, mostly uncurated, no scene tags) instead of the curated public avatars. Only for when the curated set has nothing suitable."
206
+ "description": "With list_avatars=true: list one provider's full public inventory (HiFly by default, 500+, mostly uncurated, no scene tags) instead of the curated public avatars. Only for when the curated set has nothing suitable."
207
+ },
208
+ "source": {
209
+ "type": "string",
210
+ "enum": [
211
+ "hifly",
212
+ "heygen"
213
+ ],
214
+ "description": "With list_avatars=true and all=true: which provider's full public inventory to list. Defaults to hifly. heygen is only available when the capabilities catalog lists it."
207
215
  },
208
216
  "name": {
209
217
  "type": "string",
@@ -223,7 +231,7 @@
223
231
  },
224
232
  "clone": {
225
233
  "type": "boolean",
226
- "description": "Clone mode: create a custom HiFly avatar from a video (free) or a portrait image (costs credits per clone, refunded automatically if cloning fails; see the pricing page). Requires clone_url and name. Waits until cloning finishes unless wait=false."
234
+ "description": "Clone mode: create a custom avatar (HiFly by default, or HeyGen with clone_provider=heygen) from a video (free) or a portrait image (costs credits per clone, refunded automatically if cloning fails; see the pricing page). Requires clone_url and name. Waits until cloning finishes unless wait=false."
227
235
  },
228
236
  "clone_url": {
229
237
  "type": "string",
@@ -237,6 +245,14 @@
237
245
  ],
238
246
  "description": "With clone=true: source type. Defaults to video."
239
247
  },
248
+ "clone_provider": {
249
+ "type": "string",
250
+ "enum": [
251
+ "hifly",
252
+ "heygen"
253
+ ],
254
+ "description": "With clone=true: where to create the avatar. hifly (default) accepts a video or a photo; heygen accepts a photo only (clone_type=image, jpg/png ≤ 32MB) and gives more natural lip-sync for non-Chinese speech at a higher per-second price. Only use heygen when the capabilities catalog lists it."
255
+ },
240
256
  "thumbnail_url": {
241
257
  "type": "string",
242
258
  "description": "With clone=true: optional cover image URL shown in avatar lists (image clones default to the image itself). The output aspect ratio always follows the cloned source; it cannot be chosen at generation time."
@@ -259,7 +275,7 @@
259
275
  },
260
276
  "voice_id": {
261
277
  "type": "string",
262
- "description": "Voice id (TTS mode). Shares the catalog with gen-voice (including cloned voices) — call gen_voice with list_voices=true to see available ids rather than inventing one."
278
+ "description": "Voice id (TTS mode). Optional: defaults to the avatar's own voice (a curated avatar's suggested voice, or a HeyGen digital twin's cloned voice), else the service default. Shares the catalog with gen-voice (any provider, including cloned voices) — call gen_voice with list_voices=true to see available ids rather than inventing one."
263
279
  },
264
280
  "voice_name": {
265
281
  "type": "string",
@@ -294,11 +310,13 @@
294
310
  "clone",
295
311
  "clone_url",
296
312
  "clone_type",
313
+ "clone_provider",
297
314
  "thumbnail_url",
298
315
  "wait",
299
316
  "clone_status",
300
317
  "mine",
301
318
  "all",
319
+ "source",
302
320
  "name",
303
321
  "gender",
304
322
  "check_status",
@@ -614,7 +632,7 @@
614
632
  "on",
615
633
  "off"
616
634
  ],
617
- "description": "Narration on / off, overriding the template's needsNarration (auto = the template decides). Templates where narration is optional (image sequences, galton-board) default to none — **must** pass `on` when the replicate card says `旁白: <名称>(narrated)`, otherwise the narration you write is dropped without an error and the video comes back silent. `off` drops narration from a narrated template."
635
+ "description": "Narration on / off, overriding the template's needsNarration (auto = the template decides). Templates declaring narration optional default to none — **must** pass `on` when the replicate card says `旁白: <名称>(narrated)`, otherwise the narration you write is dropped without an error and the video comes back silent. `off` drops narration from a narrated template."
618
636
  },
619
637
  "background_asset": {
620
638
  "type": "string",
@@ -925,11 +943,11 @@
925
943
  "toolName": "gen_voice",
926
944
  "tier": "atomic",
927
945
  "category": "asset",
928
- "title": "Text-to-Speech (Minimax)",
929
- "summary": "Text-to-speech (TTS): synthesize narration audio from text via the Minimax TTS model. Returns the persisted audio URL — no download needed.",
946
+ "title": "Text-to-Speech",
947
+ "summary": "Text-to-speech (TTS): synthesize narration audio from text. The voice catalog spans several providers (MiniMax, 豆包/Volcengine, HeyGen, and the caller's own cloned voices); the provider is picked from the voice id automatically. Returns the persisted audio URL — no download needed.",
930
948
  "triggers": [
931
949
  "AI voice-over, TTS, text-to-speech, generate narration audio",
932
- "Use Minimax for speech synthesis",
950
+ "Use MiniMax / 豆包 (Doubao) / HeyGen voices for speech synthesis",
933
951
  "Read a piece of text aloud / produce an audio file from text"
934
952
  ],
935
953
  "entry": {
@@ -951,15 +969,57 @@
951
969
  },
952
970
  "voice_id": {
953
971
  "type": "string",
954
- "description": "Voice id. Default 'Chinese (Mandarin)_Male_Announcer'. When unsure, call with list_voices=true first to see what's available — do not invent ids, and do not declare a voice unavailable unless it is missing from that list."
972
+ "description": "Voice id from list_voices (any provider; synthesis routes to the voice's own provider). Default 'Chinese (Mandarin)_Male_Announcer'. When unsure, call with list_voices=true first — do not invent ids, and do not declare a voice unavailable unless it is missing from the filtered list."
955
973
  },
956
974
  "speed": {
957
975
  "type": "number",
958
- "description": "Speech rate, 0.5-2.0"
976
+ "description": "Speech rate, 0.5-2.0. Some HeyGen cloned voices ignore it."
959
977
  },
960
978
  "list_voices": {
961
979
  "type": "boolean",
962
- "description": "List the available voices and exit. This queries the live voice service and is the authoritative catalog — one call is enough, and its absence from this list is the only evidence that a voice id is invalid."
980
+ "description": "List voices and exit (one page of up to 100, with provider / type / gender / language columns and the total). This queries the live voice service and is the authoritative catalog. The public catalog has a few hundred voices, so narrow it with provider / gender / language / name, or page through with page; mine=true lists the caller's own cloned voices instead."
981
+ },
982
+ "mine": {
983
+ "type": "boolean",
984
+ "description": "With list_voices=true: list the caller's own cloned voices instead of the public catalog (provider / voice_type do not apply)."
985
+ },
986
+ "provider": {
987
+ "type": "string",
988
+ "enum": [
989
+ "minimax",
990
+ "volcengine",
991
+ "heygen"
992
+ ],
993
+ "description": "With list_voices=true: only voices from this provider (volcengine = 豆包). Not needed for synthesis."
994
+ },
995
+ "voice_type": {
996
+ "type": "string",
997
+ "enum": [
998
+ "system",
999
+ "clone"
1000
+ ],
1001
+ "description": "With list_voices=true: system (stock) or clone voices."
1002
+ },
1003
+ "gender": {
1004
+ "type": "string",
1005
+ "enum": [
1006
+ "male",
1007
+ "female"
1008
+ ],
1009
+ "description": "With list_voices=true: filter by gender."
1010
+ },
1011
+ "language": {
1012
+ "type": "string",
1013
+ "description": "With list_voices=true: filter by language tag, exact tag as shown in the Language column (e.g. 中文-普通话, 中文-粤语, 英语, 日语)."
1014
+ },
1015
+ "name": {
1016
+ "type": "string",
1017
+ "description": "With list_voices=true: fuzzy-filter by voice name."
1018
+ },
1019
+ "page": {
1020
+ "type": "integer",
1021
+ "minimum": 1,
1022
+ "description": "With list_voices=true: page number (100 per page). Default 1."
963
1023
  },
964
1024
  "json_output": {
965
1025
  "type": "boolean",
@@ -978,7 +1038,14 @@
978
1038
  ],
979
1039
  "hidden": [
980
1040
  "json_output",
981
- "list_voices"
1041
+ "list_voices",
1042
+ "mine",
1043
+ "provider",
1044
+ "voice_type",
1045
+ "gender",
1046
+ "language",
1047
+ "name",
1048
+ "page"
982
1049
  ]
983
1050
  }
984
1051
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.13.7",
3
+ "version": "0.13.9",
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",
@@ -19,7 +19,7 @@ triggers:
19
19
 
20
20
  # Digital-Human Skill
21
21
 
22
- Wraps ab-api's digital-human endpoints (the same ones the web studio uses), authenticated with the **Tianyan privateToken**. The provider is **HiFly**: avatars are cloned from a video or photo, then driven by audio. Generation is async — the handler submits the task and polls `/digital-human/video/status` until completion.
22
+ Wraps ab-api's digital-human endpoints (the same ones the web studio uses), authenticated with the **Tianyan privateToken**. Providers are **HiFly** (default) and, when the capabilities catalog lists it, **HeyGen**: avatars are cloned from a video or photo (HeyGen: photo only), then driven by audio. Each avatar belongs to one provider and always renders there — you never pick the provider at generation time. HeyGen costs more per second (the curated list and the studio picker mark HeyGen avatars); prefer it only for non-Chinese speech or when the user asks. Generation is async — the handler submits the task and polls `/digital-human/video/status` until completion.
23
23
 
24
24
  ## Capabilities
25
25
 
@@ -57,6 +57,7 @@ remixmate gen-digital-human --list-avatars # curated public a
57
57
  remixmate gen-digital-human --list-avatars --gender female # curated, filtered by gender
58
58
  remixmate gen-digital-human --list-avatars --mine # your cloned avatars
59
59
  remixmate gen-digital-human --list-avatars --all --name "教师" # full HiFly inventory (500+; always narrow with --name)
60
+ remixmate gen-digital-human --list-avatars --all --source heygen --name "Anna" # full HeyGen public inventory
60
61
  ```
61
62
 
62
63
  The default list is the **curated public library**: avatars an admin picked, with a cover, scene tags (`education` / `medical` / `news` / `business` / `culture` / `outdoor` / `lifestyle` / `festival` / `ecommerce`) and usually a suggested voice. Prefer it — pick the avatar whose scenes match the script, and pass its `Voice` column as `--voice-id` unless the user chose a voice. Fall back to `--all` only when nothing curated fits; those entries have no tags or suggested voice.
@@ -94,6 +95,9 @@ remixmate gen-digital-human --clone --name "My presenter" --clone-url "https://e
94
95
  # Portrait photo clone (charged per clone)
95
96
  remixmate gen-digital-human --clone --clone-type image --name "My presenter" --clone-url "https://example.com/me.jpg"
96
97
 
98
+ # HeyGen photo avatar (photo only, jpg/png ≤ 32MB; more natural lip-sync for English and other languages)
99
+ remixmate gen-digital-human --clone --clone-type image --clone-provider heygen --name "My presenter" --clone-url "https://example.com/me.jpg"
100
+
97
101
  # Submit without waiting, then poll
98
102
  remixmate gen-digital-human --clone --wait false --name "My presenter" --clone-url "https://example.com/me.mp4"
99
103
  remixmate gen-digital-human --clone-status --avatar-id dh_xxx
@@ -109,12 +113,14 @@ remixmate gen-digital-human --clone-status --avatar-id dh_xxx
109
113
  | `--name` | list / clone | Fuzzy filter; with `--clone` the new avatar's name (required) | — |
110
114
  | `--avatar-id` | generate / clone-status | Avatar id (required) | — |
111
115
  | `-t` / `--text` | generate | Script (text-driven) | — |
112
- | `--voice-id` | generate | Voice id (text-driven) | `male-qn-qingse` |
116
+ | `--voice-id` | generate | Voice id (text-driven) | the avatar's own voice (curated suggestion / HeyGen twin's cloned voice), else the service default |
113
117
  | `--voice-name` | generate | Voice display name (recordkeeping only) | none |
114
118
  | `--audio-url` | generate | Audio URL (audio-driven) | none |
115
119
  | `--check-status` / `--generation-id` | status | Poll a generation job | — |
116
120
  | `--clone` / `--clone-url` | clone | Clone mode and source URL (required) | — |
117
121
  | `--clone-type` | clone | `video` / `image` | `video` |
122
+ | `--clone-provider` | clone | `hifly` / `heygen` (heygen needs `--clone-type image`) | `hifly` |
123
+ | `--source` | list with `--all` | Which provider's full inventory: `hifly` / `heygen` | `hifly` |
118
124
  | `--thumbnail-url` | clone | Cover shown in lists | image itself / none |
119
125
  | `--wait` | clone | `false` to return right after submission | wait |
120
126
  | `--clone-status` | clone-status | Report clone status of `--avatar-id` | — |
@@ -29,7 +29,15 @@
29
29
  },
30
30
  "all": {
31
31
  "type": "boolean",
32
- "description": "With list_avatars=true: list the full public HiFly inventory (500+, mostly uncurated, no scene tags) instead of the curated public avatars. Only for when the curated set has nothing suitable."
32
+ "description": "With list_avatars=true: list one provider's full public inventory (HiFly by default, 500+, mostly uncurated, no scene tags) instead of the curated public avatars. Only for when the curated set has nothing suitable."
33
+ },
34
+ "source": {
35
+ "type": "string",
36
+ "enum": [
37
+ "hifly",
38
+ "heygen"
39
+ ],
40
+ "description": "With list_avatars=true and all=true: which provider's full public inventory to list. Defaults to hifly. heygen is only available when the capabilities catalog lists it."
33
41
  },
34
42
  "name": {
35
43
  "type": "string",
@@ -49,7 +57,7 @@
49
57
  },
50
58
  "clone": {
51
59
  "type": "boolean",
52
- "description": "Clone mode: create a custom HiFly avatar from a video (free) or a portrait image (costs credits per clone, refunded automatically if cloning fails; see the pricing page). Requires clone_url and name. Waits until cloning finishes unless wait=false."
60
+ "description": "Clone mode: create a custom avatar (HiFly by default, or HeyGen with clone_provider=heygen) from a video (free) or a portrait image (costs credits per clone, refunded automatically if cloning fails; see the pricing page). Requires clone_url and name. Waits until cloning finishes unless wait=false."
53
61
  },
54
62
  "clone_url": {
55
63
  "type": "string",
@@ -63,6 +71,14 @@
63
71
  ],
64
72
  "description": "With clone=true: source type. Defaults to video."
65
73
  },
74
+ "clone_provider": {
75
+ "type": "string",
76
+ "enum": [
77
+ "hifly",
78
+ "heygen"
79
+ ],
80
+ "description": "With clone=true: where to create the avatar. hifly (default) accepts a video or a photo; heygen accepts a photo only (clone_type=image, jpg/png ≤ 32MB) and gives more natural lip-sync for non-Chinese speech at a higher per-second price. Only use heygen when the capabilities catalog lists it."
81
+ },
66
82
  "thumbnail_url": {
67
83
  "type": "string",
68
84
  "description": "With clone=true: optional cover image URL shown in avatar lists (image clones default to the image itself). The output aspect ratio always follows the cloned source; it cannot be chosen at generation time."
@@ -85,7 +101,7 @@
85
101
  },
86
102
  "voice_id": {
87
103
  "type": "string",
88
- "description": "Voice id (TTS mode). Shares the catalog with gen-voice (including cloned voices) — call gen_voice with list_voices=true to see available ids rather than inventing one."
104
+ "description": "Voice id (TTS mode). Optional: defaults to the avatar's own voice (a curated avatar's suggested voice, or a HeyGen digital twin's cloned voice), else the service default. Shares the catalog with gen-voice (any provider, including cloned voices) — call gen_voice with list_voices=true to see available ids rather than inventing one."
89
105
  },
90
106
  "voice_name": {
91
107
  "type": "string",
@@ -120,11 +136,13 @@
120
136
  "clone",
121
137
  "clone_url",
122
138
  "clone_type",
139
+ "clone_provider",
123
140
  "thumbnail_url",
124
141
  "wait",
125
142
  "clone_status",
126
143
  "mine",
127
144
  "all",
145
+ "source",
128
146
  "name",
129
147
  "gender",
130
148
  "check_status",
@@ -491,7 +491,7 @@ python3 <SkillDir>/scripts/gen_script.py \
491
491
  | `--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 |
492
492
  | `--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 |
493
493
  | `--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 |
494
- | `--motion` | Camera motion for every scene (`animationHints.motion`): `static` / `kenburns-in` / `kenburns-out` / `pan-left` / `pan-right` / `float`. `auto` removes the motion from every scene so the template picks (image-slide rotates push / pan / pull / pan); nothing is locked then. | skeleton alternates in / out |
494
+ | `--motion` | Camera motion for every scene (`animationHints.motion`): `static` / `kenburns-in` / `kenburns-out` / `pan-left` / `pan-right` / `float`. `auto` removes the motion from every scene so the template picks its own motion; nothing is locked then. | skeleton alternates in / out |
495
495
  | `--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 |
496
496
  | `--font-name` | Display name of the picked font → `global.font.fontName` (display only, never used for rendering). Pass it with `--font-id`. | — |
497
497
  | `--list-fonts` | Print the live font library (`/font/page`: id, name, lang, category, license) and exit; no script is generated and `--topic` is not needed. The ids it prints are the only valid `--font-id` values — use it when the user asks which fonts exist or names a font without its id. | — |
@@ -510,7 +510,7 @@ python3 <SkillDir>/scripts/gen_script.py \
510
510
 
511
511
  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.
512
512
 
513
- For image-to-video replicate cards, map **转场效果 / Transition effect** to both `--payload sequence.defaultEffect=<id>` and `--payload sequence.overrideItemEffects=true`. Use the id in parentheses (`fade`, `wipe-left`, `slide-left`, `none`), preserving the chosen preset and all typography. **跟随样片 / Follow example (`follow`)** omits both overrides; `follow` is not a valid effect. Map **转场时长 / Transition duration** to `--payload sequence.transitionMs=<milliseconds>` (e.g. `0.6 秒(600)` → `600`); omit when following the sample or using `none`. These payload keys are locked into every generated scene, so do not re-author them in a saved DSL.
513
+ For replicate cards exposing transition effects or duration, read the template's `customPayloadSchema` and `llmHint` for the exact field paths, units, allowed values and any paired override switch. Pass each explicit selection through `--payload <declared-path>=<value>`, preserving the chosen preset and typography; following the sample omits the overrides. These payload keys are locked into every generated scene, so do not re-author them in a saved DSL.
514
514
 
515
515
  ## DSL generation principles
516
516
 
@@ -643,6 +643,8 @@ def _template_needs_image(template_config: dict | None) -> bool:
643
643
 
644
644
 
645
645
 
646
+
647
+
646
648
 
647
649
 
648
650
 
@@ -707,6 +709,7 @@ def _resolve_contract(template_config: dict | None) -> dict:
707
709
  "duration_strategy": duration_strategy,
708
710
  "duration_model": caps.get("durationModel"),
709
711
  "payload_defaults": payload_defaults,
712
+ "media_binding": caps.get("mediaBinding"),
710
713
  "primary_visual_type": primary_visual_type,
711
714
  "background_strategy": "shared" if caps.get("backgroundStrategy") == "shared" else "per-scene",
712
715
  "default_layout": caps.get("defaultLayout"),
@@ -1275,6 +1278,8 @@ def _media_option_choice(option_schema: dict, value: str | None, flag: str,
1275
1278
 
1276
1279
 
1277
1280
 
1281
+
1282
+
1278
1283
 
1279
1284
  _MEDIA_OPTION_FLAGS = {"preset": "--sequence-preset", "fit": "--sequence-fit"}
1280
1285
  _LEGACY_MEDIA_BINDINGS = {
@@ -2493,7 +2498,7 @@ def _attach_narration(scene: dict, narration_text: str, narration_asset_id: str
2493
2498
  def _visual_scene(
2494
2499
  ctx: _SkeletonContext, idx: int, scene_id: str, plan: dict, visual_asset_id: str,
2495
2500
  ) -> dict:
2496
- return {
2501
+ scene = {
2497
2502
  "id": scene_id,
2498
2503
  "purpose": plan["purpose"],
2499
2504
  "duration": plan["duration"],
@@ -2507,6 +2512,24 @@ def _visual_scene(
2507
2512
  }
2508
2513
 
2509
2514
 
2515
+
2516
+ binding = ctx.contract.get("media_binding")
2517
+ if isinstance(binding, dict) and "assetRefKey" in binding:
2518
+ path = binding.get("itemsPath")
2519
+ ref_key = binding.get("assetRefKey")
2520
+ if not isinstance(path, str) or not all(path.split(".")) or not isinstance(ref_key, str) or not ref_key:
2521
+ raise ValueError("mediaBinding needs non-empty itemsPath and assetRefKey")
2522
+ item = deepcopy(binding.get("item", {}))
2523
+ if not isinstance(item, dict):
2524
+ raise ValueError("mediaBinding.item must be an object")
2525
+ item[ref_key] = visual_asset_id
2526
+ payload = _build_custom_payload(ctx.template_config)
2527
+ _write_payload(payload, [(path.split("."), [item])])
2528
+ scene["customPayload"] = payload
2529
+ del scene["visuals"]
2530
+ return scene
2531
+
2532
+
2510
2533
  def _payload_scene(ctx: _SkeletonContext, scene_id: str, plan: dict) -> dict:
2511
2534
 
2512
2535
 
@@ -2979,7 +3002,7 @@ Examples:
2979
3002
  "--motion", default=None,
2980
3003
  help="Camera motion for every scene (animationHints.motion): static / kenburns-in / kenburns-out / "
2981
3004
  "pan-left / pan-right / float. Replaces the skeleton's alternating default. `auto` removes the "
2982
- "motion from every scene so the template picks (image-slide rotates push / pan / pull / pan).",
3005
+ "motion from every scene so the template picks its own motion.",
2983
3006
  )
2984
3007
  parser.add_argument("--allow-digital-human", action="store_true", help="Allow digital-human assets")
2985
3008
  parser.add_argument("--avatar-id", default=None, help="Digital-human avatar id (dh_xxx) for templates declaring capabilities.digitalHuman: adds one gen-digital-human asset referenced by every scene's visuals.avatar.assetRef (one continuous presenter driven by the narration). Placement stays the template default unless set with --payload digitalHuman.<field>=...")
@@ -67,7 +67,7 @@
67
67
  "narration": {
68
68
  "type": "string",
69
69
  "enum": ["auto", "on", "off"],
70
- "description": "Narration on / off, overriding the template's needsNarration (auto = the template decides). Templates where narration is optional (image sequences, galton-board) default to none — **must** pass `on` when the replicate card says `旁白: <名称>(narrated)`, otherwise the narration you write is dropped without an error and the video comes back silent. `off` drops narration from a narrated template."
70
+ "description": "Narration on / off, overriding the template's needsNarration (auto = the template decides). Templates declaring narration optional default to none — **must** pass `on` when the replicate card says `旁白: <名称>(narrated)`, otherwise the narration you write is dropped without an error and the video comes back silent. `off` drops narration from a narrated template."
71
71
  },
72
72
  "background_asset": {
73
73
  "type": "string",
@@ -2,6 +2,6 @@
2
2
  "skillName": "gen-script",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "474",
5
- "version": "V25",
5
+ "version": "V26",
6
6
  "skillDescription": "Video-script generation skill. Turns a user-supplied topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.\n\nUse this skill as soon as the user mentions any of these intents:\n- Write a video script, generate a video script, plan a video, write storyboards\n- Create a short video, plan video content, generate a Video DSL\n- Turn a topic into a video structure / video content plan\n\nEven when the user does not say \"generate the DSL\", use this skill whenever they want to turn a topic into a structured video plan.\n\n⚠️ Stop-and-confirm gate: after this skill returns a DSL, show the full script and wait for the user's explicit confirmation. Never call `prepare_video_assets` in the same turn."
7
7
  }
@@ -1,23 +1,23 @@
1
1
  ---
2
2
  name: gen-voice
3
3
  description: |
4
- Text-to-speech (TTS) skill: synthesize narration audio from text via ab-api's unified `/tool/tts` endpoint with `provider: "minimax"` (Minimax TTS).
4
+ Text-to-speech (TTS) skill: synthesize narration audio from text via ab-api's unified `/tool/tts` endpoint. Voices come from several providers (MiniMax, 豆包/Volcengine, HeyGen, plus the caller's cloned voices); the backend routes each voice id to its own provider.
5
5
 
6
6
  Use this skill immediately whenever the user asks for any of:
7
7
  - AI voice-over, TTS, text-to-speech, generate narration audio
8
- - Use Minimax for speech synthesis
8
+ - Use MiniMax / 豆包 (Doubao) / HeyGen voices for speech synthesis
9
9
  - Read a piece of text aloud / produce an audio file from text
10
10
 
11
11
  Even when the user does not explicitly say "AI", any request that turns text into speech should route here.
12
12
  triggers:
13
13
  - AI voice-over, TTS, text-to-speech, generate narration audio
14
- - Use Minimax for speech synthesis
14
+ - Use MiniMax / 豆包 (Doubao) / HeyGen voices for speech synthesis
15
15
  - Read a piece of text aloud / produce an audio file from text
16
16
  ---
17
17
 
18
18
  # Text-to-Speech (TTS) Skill
19
19
 
20
- Wraps ab-api's unified `POST /tool/tts` with `provider: "minimax"` (the same endpoint the web studio uses), authenticated with the **Tianyan privateToken**, backed by the **Minimax TTS** model.
20
+ Wraps ab-api's unified `POST /tool/tts` (the same endpoint the web studio uses), authenticated with the **Tianyan privateToken**. You never pick a provider: ab-api looks the voice id up and synthesizes with that voice's provider (MiniMax, 豆包/Volcengine, HeyGen). Cloned voices (豆包 voice clones, HeyGen digital-twin voices) are usable only by their owner.
21
21
 
22
22
  ## Auth & environment
23
23
 
@@ -37,16 +37,21 @@ There is no skill-local env file — the executing process inherits the system e
37
37
  > This skill was migrated from a Python script to an remixmate CLI HTTP handler (`entry.type: http`). The agent invocation is unchanged (same tool name `gen_voice`, same params as in `skill.json`); local repro goes through `remixmate gen-voice ...`. The legacy `--download` flag has been removed — audio URLs are persisted in the cloud and play directly.
38
38
 
39
39
  1. **Text**: confirm what to synthesize. Punctuation drives pauses (commas short, periods long).
40
- 2. **Voice**: if the user prefers a specific voice, run `--list-voices` first and pick a matching id.
40
+ 2. **Voice**: if the user prefers a specific voice, run `--list-voices` (with filters) first and pick a matching id.
41
41
  3. **Output**: by default the audio URL is printed; add `--json-output` for a structured result that includes per-line subtitle timestamps.
42
42
 
43
43
  ### List available voices
44
44
 
45
45
  ```bash
46
- remixmate gen-voice --list-voices
46
+ remixmate gen-voice --list-voices # page 1 of the public catalog
47
+ remixmate gen-voice --list-voices --provider volcengine --gender female --language 中文-普通话
48
+ remixmate gen-voice --list-voices --page 2 # next 100
49
+ remixmate gen-voice --list-voices --mine # your own cloned voices
47
50
  ```
48
51
 
49
- The list above is the **authoritative** catalog — a voice id is invalid only if it is missing from it.
52
+ Each row shows the voice id, provider, type (system / clone), gender, language and name; the footer gives the total and the next `--page` when there is more. The public catalog has a few hundred voices, so filter before paging. `--json-output` returns `{page, total, voices:[{id,name,provider,type,gender,language}]}`.
53
+
54
+ The live list is the **authoritative** catalog — a voice id is invalid only if it is missing from it (check the filters and later pages before concluding that).
50
55
 
51
56
  For offline development (no token / no network) there is a `--local` escape hatch. It prints the resolver's two hardcoded fallback ids, **not** the available voices, so never use it to decide whether a voice exists:
52
57
 
@@ -83,9 +88,16 @@ remixmate gen-voice --text "<text-to-synthesize>" --json-output
83
88
  |------|-------------|---------|
84
89
  | `--text` | Text to synthesize (required, except with `--list-voices`) | — |
85
90
  | `--voice-id` | Voice id (use `--list-voices` to discover) | `Chinese (Mandarin)_Male_Announcer` |
86
- | `--speed` | Speech rate, 0.5–2.0 | `1.0` |
87
- | `--language-boost` | CLI-only. Language hint for MiniMax (`auto` / `Chinese` / `English` / …). Mixed-script narration ("只有四个字:Agent Loop") mispronounces the Latin terms without one, so the service already applies its own default — pass this only to pin a specific language. Usually set from the DSL (`global.narration.languageBoost`, scene-level overrides) rather than by hand. Not exposed to the agent — see the handler comment. | service default |
88
- | `--list-voices` | List available voices and exit | — |
91
+ | `--speed` | Speech rate, 0.5–2.0 (some HeyGen cloned voices ignore it) | `1.0` |
92
+ | `--language-boost` | CLI-only. Language hint (MiniMax voices only) (`auto` / `Chinese` / `English` / …). Mixed-script narration ("只有四个字:Agent Loop") mispronounces the Latin terms without one, so the service already applies its own default — pass this only to pin a specific language. Usually set from the DSL (`global.narration.languageBoost`, scene-level overrides) rather than by hand. Not exposed to the agent — see the handler comment. | service default |
93
+ | `--list-voices` | List voices and exit (100 per page) | — |
94
+ | `--mine` | With `--list-voices`: your own cloned voices instead of the public catalog | off |
95
+ | `--provider` | With `--list-voices`: `minimax` / `volcengine` (豆包) / `heygen` | all |
96
+ | `--voice-type` | With `--list-voices`: `system` / `clone` | all |
97
+ | `--gender` | With `--list-voices`: `male` / `female` | all |
98
+ | `--language` | With `--list-voices`: exact language tag, e.g. `中文-普通话`, `英语` | all |
99
+ | `--name` | With `--list-voices`: fuzzy name filter | — |
100
+ | `--page` | With `--list-voices`: page number | `1` |
89
101
  | `--local` | CLI-only. With `--list-voices`: print the 2-entry hardcoded fallback list instead of querying the service (offline dev). Not exposed to the agent — see the handler comment. | off |
90
102
  | `--json-output` | Emit JSON (url, audio_length_ms, subtitles) | off |
91
103
  | `--priv-token` | Override token | env var |
@@ -3,8 +3,8 @@
3
3
  "toolName": "gen_voice",
4
4
  "tier": "atomic",
5
5
  "category": "asset",
6
- "title": "Text-to-Speech (Minimax)",
7
- "description": "Text-to-speech (TTS): synthesize narration audio from text via the Minimax TTS model. Returns the persisted audio URL — no download needed.",
6
+ "title": "Text-to-Speech",
7
+ "description": "Text-to-speech (TTS): synthesize narration audio from text. The voice catalog spans several providers (MiniMax, 豆包/Volcengine, HeyGen, and the caller's own cloned voices); the provider is picked from the voice id automatically. Returns the persisted audio URL — no download needed.",
8
8
  "auth": "required",
9
9
  "joinsTake": true,
10
10
  "envVars": [
@@ -25,15 +25,57 @@
25
25
  },
26
26
  "voice_id": {
27
27
  "type": "string",
28
- "description": "Voice id. Default 'Chinese (Mandarin)_Male_Announcer'. When unsure, call with list_voices=true first to see what's available — do not invent ids, and do not declare a voice unavailable unless it is missing from that list."
28
+ "description": "Voice id from list_voices (any provider; synthesis routes to the voice's own provider). Default 'Chinese (Mandarin)_Male_Announcer'. When unsure, call with list_voices=true first — do not invent ids, and do not declare a voice unavailable unless it is missing from the filtered list."
29
29
  },
30
30
  "speed": {
31
31
  "type": "number",
32
- "description": "Speech rate, 0.5-2.0"
32
+ "description": "Speech rate, 0.5-2.0. Some HeyGen cloned voices ignore it."
33
33
  },
34
34
  "list_voices": {
35
35
  "type": "boolean",
36
- "description": "List the available voices and exit. This queries the live voice service and is the authoritative catalog — one call is enough, and its absence from this list is the only evidence that a voice id is invalid."
36
+ "description": "List voices and exit (one page of up to 100, with provider / type / gender / language columns and the total). This queries the live voice service and is the authoritative catalog. The public catalog has a few hundred voices, so narrow it with provider / gender / language / name, or page through with page; mine=true lists the caller's own cloned voices instead."
37
+ },
38
+ "mine": {
39
+ "type": "boolean",
40
+ "description": "With list_voices=true: list the caller's own cloned voices instead of the public catalog (provider / voice_type do not apply)."
41
+ },
42
+ "provider": {
43
+ "type": "string",
44
+ "enum": [
45
+ "minimax",
46
+ "volcengine",
47
+ "heygen"
48
+ ],
49
+ "description": "With list_voices=true: only voices from this provider (volcengine = 豆包). Not needed for synthesis."
50
+ },
51
+ "voice_type": {
52
+ "type": "string",
53
+ "enum": [
54
+ "system",
55
+ "clone"
56
+ ],
57
+ "description": "With list_voices=true: system (stock) or clone voices."
58
+ },
59
+ "gender": {
60
+ "type": "string",
61
+ "enum": [
62
+ "male",
63
+ "female"
64
+ ],
65
+ "description": "With list_voices=true: filter by gender."
66
+ },
67
+ "language": {
68
+ "type": "string",
69
+ "description": "With list_voices=true: filter by language tag, exact tag as shown in the Language column (e.g. 中文-普通话, 中文-粤语, 英语, 日语)."
70
+ },
71
+ "name": {
72
+ "type": "string",
73
+ "description": "With list_voices=true: fuzzy-filter by voice name."
74
+ },
75
+ "page": {
76
+ "type": "integer",
77
+ "minimum": 1,
78
+ "description": "With list_voices=true: page number (100 per page). Default 1."
37
79
  },
38
80
  "json_output": {
39
81
  "type": "boolean",
@@ -52,7 +94,14 @@
52
94
  ],
53
95
  "hidden": [
54
96
  "json_output",
55
- "list_voices"
97
+ "list_voices",
98
+ "mine",
99
+ "provider",
100
+ "voice_type",
101
+ "gender",
102
+ "language",
103
+ "name",
104
+ "page"
56
105
  ]
57
106
  }
58
107
  }
@@ -2,6 +2,6 @@
2
2
  "skillName": "template-registry",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "475",
5
- "version": "V31",
5
+ "version": "V32",
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
  }
@@ -1273,6 +1273,44 @@ def _check_narration_language_strict(dsl: dict) -> list[ValidationError]:
1273
1273
  _TEXT_LAYER_SKELETON_PREFIXES = ("[骨架待填充]", "[skeleton placeholder]")
1274
1274
 
1275
1275
 
1276
+ def _check_presenter_visibility(dsl: dict) -> list[ValidationError]:
1277
+
1278
+
1279
+
1280
+
1281
+ tpl = _get_template_cfg(_pick_template_id(dsl)) or {}
1282
+ caps = tpl.get("capabilities")
1283
+ caps = caps if isinstance(caps, dict) else {}
1284
+ spec = caps.get("digitalHuman") or {}
1285
+ if not isinstance(spec, dict) or spec.get("backgroundOccludes") is not True:
1286
+ return []
1287
+ presenter_scenes = []
1288
+ for scene in dsl.get("scenes") or []:
1289
+ if not isinstance(scene, dict):
1290
+ continue
1291
+ visuals = scene.get("visuals") or {}
1292
+ if not isinstance(visuals, dict):
1293
+ continue
1294
+ if not isinstance(visuals.get("avatar"), dict):
1295
+ continue
1296
+ if (visuals.get("avatar") or {}).get("assetRef"):
1297
+ presenter_scenes.append(scene)
1298
+ if not presenter_scenes or not all(
1299
+ isinstance(scene["visuals"].get("background"), dict)
1300
+ and scene["visuals"]["background"].get("assetRef")
1301
+ for scene in presenter_scenes
1302
+ ):
1303
+ return []
1304
+ binding = caps.get("mediaBinding") or {}
1305
+ path = binding.get("itemsPath") if isinstance(binding, dict) else None
1306
+ hint = f" Use timed media items in customPayload.{path} instead." if path else " Leave presenter scenes without full-scene backgrounds."
1307
+ return [ValidationError(
1308
+ "scenes[].visuals.background.assetRef",
1309
+ "Full-scene backgrounds cover every presenter scene; the presenter would never be visible."
1310
+ + hint,
1311
+ )]
1312
+
1313
+
1276
1314
  def _check_text_layer_rules(dsl: dict) -> list[ValidationError]:
1277
1315
 
1278
1316
 
@@ -1398,6 +1436,7 @@ def validate_integrity(dsl: dict) -> list[ValidationError]:
1398
1436
  errors += _check_step_narration_alignment(dsl)
1399
1437
  errors += _check_slide_cross_field(dsl)
1400
1438
  errors += _check_text_layer_rules(dsl)
1439
+ errors += _check_presenter_visibility(dsl)
1401
1440
 
1402
1441
  errors += _check_duration_estimate(dsl)
1403
1442
  return errors
@@ -1435,6 +1474,7 @@ def validate_dsl(dsl: dict) -> list[ValidationError]:
1435
1474
  errors += _check_step_narration_alignment(dsl)
1436
1475
  errors += _check_slide_cross_field(dsl)
1437
1476
  errors += _check_text_layer_rules(dsl)
1477
+ errors += _check_presenter_visibility(dsl)
1438
1478
 
1439
1479
  errors += _check_duration_estimate(dsl)
1440
1480
  return errors