@remixmate/cli 0.9.55 → 0.9.57

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,12 +1,17 @@
1
1
  /**
2
- * gen-digital-human handler — talking-head video, three modes:
3
- * --list-avatars true → POST /digital-human/avatar/page (list)
4
- * --check-status --generation-id <id> → POST /digital-human/video/status (one-shot)
5
- * default (--avatar-id <id> ...) → generate: resolve avatar → submit → poll
2
+ * gen-digital-human handler — talking-head video + custom avatars, five modes:
3
+ * --list-avatars true → POST /digital-human/avatar/page (list)
4
+ * --clone --clone-url <url> --name <n> → POST /digital-human/avatar/clone, then poll clone-status
5
+ * --clone-status --avatar-id <id> → POST /digital-human/avatar/clone-status (one-shot)
6
+ * --check-status --generation-id <id> → POST /digital-human/video/status (one-shot)
7
+ * default (--avatar-id <id> ...) → generate: resolve avatar → submit → poll
6
8
  *
7
9
  * Ported from `scripts/gen_digital_human_video.py` (python → http) so it shares
8
10
  * auth / base-URL / business-code handling with the other generators via
9
- * http.ts. Endpoints and payloads match the backend DigitalHumanVideoGenerateDTO.
11
+ * http.ts. Endpoints and payloads match the backend DigitalHuman* DTOs.
12
+ *
13
+ * Avatar ids are the backend's opaque uids (`dh_...`); legacy numeric ids are
14
+ * still accepted as strings.
10
15
  */
11
16
  import type { HandlerContext, HandlerInput } from './index.js';
12
17
  export declare function genDigitalHuman(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
@@ -1,21 +1,37 @@
1
1
  /**
2
- * gen-digital-human handler — talking-head video, three modes:
3
- * --list-avatars true → POST /digital-human/avatar/page (list)
4
- * --check-status --generation-id <id> → POST /digital-human/video/status (one-shot)
5
- * default (--avatar-id <id> ...) → generate: resolve avatar → submit → poll
2
+ * gen-digital-human handler — talking-head video + custom avatars, five modes:
3
+ * --list-avatars true → POST /digital-human/avatar/page (list)
4
+ * --clone --clone-url <url> --name <n> → POST /digital-human/avatar/clone, then poll clone-status
5
+ * --clone-status --avatar-id <id> → POST /digital-human/avatar/clone-status (one-shot)
6
+ * --check-status --generation-id <id> → POST /digital-human/video/status (one-shot)
7
+ * default (--avatar-id <id> ...) → generate: resolve avatar → submit → poll
6
8
  *
7
9
  * Ported from `scripts/gen_digital_human_video.py` (python → http) so it shares
8
10
  * auth / base-URL / business-code handling with the other generators via
9
- * http.ts. Endpoints and payloads match the backend DigitalHumanVideoGenerateDTO.
11
+ * http.ts. Endpoints and payloads match the backend DigitalHuman* DTOs.
12
+ *
13
+ * Avatar ids are the backend's opaque uids (`dh_...`); legacy numeric ids are
14
+ * still accepted as strings.
10
15
  */
11
16
  import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
12
17
  import { getCapabilities } from '../capabilities.js';
13
18
  import { emitProgress } from '../progress.js';
14
19
  import { isTrue, toNumber } from './shared.js';
20
+ const CLONE_TYPES = ['video', 'image'];
21
+ /**
22
+ * The only provider currently wired end to end (HiFly). HiFly's generate APIs
23
+ * take no aspect ratio or action prompt — the output keeps the cloned source's
24
+ * framing — so the handler sends neither.
25
+ */
26
+ const ACTIVE_SOURCE = 'hifly';
27
+ function toId(value) {
28
+ if (value === undefined || value === null)
29
+ return '';
30
+ return String(value).trim();
31
+ }
15
32
  async function listAvatars(ctx, input) {
16
- const payload = { current: 1, pageSize: 100 };
17
- if (input.source)
18
- payload.source = String(input.source);
33
+ // Only avatars of the active provider can be driven, so only those are listed.
34
+ const payload = { current: 1, pageSize: 100, source: ACTIVE_SOURCE };
19
35
  if (input.gender)
20
36
  payload.gender = String(input.gender);
21
37
  if (isTrue(input.mine))
@@ -32,31 +48,107 @@ async function listAvatars(ctx, input) {
32
48
  process.stdout.write('⚠️ No avatars available\n');
33
49
  return;
34
50
  }
35
- process.stdout.write(`\n${'ID'.padEnd(8)} ${'Name'.padEnd(16)} ${'Source'.padEnd(10)} ${'Gender'.padEnd(8)} Ratio\n`);
36
- process.stdout.write(`${'-'.repeat(8)} ${'-'.repeat(16)} ${'-'.repeat(10)} ${'-'.repeat(8)} ${'-'.repeat(10)}\n`);
51
+ process.stdout.write(`\n${'ID'.padEnd(24)} ${'Name'.padEnd(16)} ${'Source'.padEnd(14)} ${'Gender'.padEnd(8)} ${'Ratio'.padEnd(6)} Clone\n`);
52
+ process.stdout.write(`${'-'.repeat(24)} ${'-'.repeat(16)} ${'-'.repeat(14)} ${'-'.repeat(8)} ${'-'.repeat(6)} ${'-'.repeat(8)}\n`);
37
53
  for (const a of avatars) {
38
- process.stdout.write(`${String(a.id ?? '').padEnd(8)} ${String(a.name ?? '').padEnd(16)} ${String(a.source ?? '').padEnd(10)} ${String(a.gender ?? '').padEnd(8)} ${a.aspectRatio ?? ''}\n`);
54
+ process.stdout.write(`${toId(a.id).padEnd(24)} ${String(a.name ?? '').padEnd(16)} ${String(a.source ?? '').padEnd(14)} ${String(a.gender ?? '').padEnd(8)} ${String(a.aspectRatio ?? '').padEnd(6)} ${a.cloneStatus || '-'}\n`);
39
55
  }
40
- process.stdout.write(`\n${avatars.length} avatar(s) available\n`);
56
+ const total = data.pager?.total ?? avatars.length;
57
+ process.stdout.write(`\n${avatars.length} avatar(s) shown${total > avatars.length ? ` of ${total}; narrow with --name` : ''}\n`);
41
58
  process.stdout.write('💡 Use --avatar-id <ID> to pick an avatar for generation\n');
42
59
  }
43
- /** Look up one avatar's metadata; checks the normal page then `mine: true`. */
60
+ /**
61
+ * Look up one avatar's metadata. Only the caller's own avatars are searched: they
62
+ * are few, and they are the only ones that can be mid-clone or failed. The public
63
+ * HiFly catalog has ~1000 entries, far beyond one page, so a public id that is not
64
+ * among "mine" is passed through as-is and the backend validates it (unknown ids
65
+ * come back as a business error).
66
+ */
44
67
  async function fetchAvatarInfo(ctx, avatarId) {
45
- for (const extra of [{}, { mine: true }]) {
46
- const data = await mmPost(ctx, '/digital-human/avatar/page', { current: 1, pageSize: 100, ...extra }, { timeoutMs: 30_000 });
47
- const found = (data.list ?? []).find((a) => String(a.id) === String(avatarId));
48
- if (found)
49
- return found;
50
- }
51
- throw new SkillError(`❌ avatar_id=${avatarId} not found; use --list-avatars [--mine] to see what is available`);
68
+ const data = await mmPost(ctx, '/digital-human/avatar/page', { current: 1, pageSize: 100, mine: true, source: ACTIVE_SOURCE }, { timeoutMs: 30_000 });
69
+ const found = (data.list ?? []).find((a) => toId(a.id) === avatarId);
70
+ return found ?? { id: avatarId, source: ACTIVE_SOURCE };
52
71
  }
53
- function resolveSource(explicit, avatar, validSources) {
54
- const src = (explicit ?? '').trim() || (avatar.source ?? '').trim();
72
+ function resolveSource(avatar, validSources) {
73
+ const src = (avatar.source ?? '').trim() || ACTIVE_SOURCE;
55
74
  if (!validSources.includes(src)) {
56
- throw new SkillError(`❌ Could not determine a valid source (${validSources.join('/')}). Pass --source, or confirm the avatar carries a source field.`);
75
+ throw new SkillError(`❌ avatar uses provider "${src}", which is not available (supported: ${validSources.join('/')}). Pick another avatar from --list-avatars.`);
57
76
  }
58
77
  return src;
59
78
  }
79
+ // ── Avatar cloning (HiFly) ─────────────────────────────────────────────────
80
+ async function cloneAvatar(ctx, input) {
81
+ const fileUrl = toId(input.clone_url);
82
+ if (!/^https?:\/\//i.test(fileUrl)) {
83
+ throw new SkillError('❌ --clone needs --clone-url <public http(s) URL of a video or portrait image> (local files are not uploaded)');
84
+ }
85
+ const name = toId(input.name);
86
+ if (!name)
87
+ throw new SkillError('❌ --clone needs --name <avatar name>');
88
+ const sourceType = toId(input.clone_type) || 'video';
89
+ if (!CLONE_TYPES.includes(sourceType)) {
90
+ throw new SkillError(`❌ --clone-type must be one of ${CLONE_TYPES.join(' / ')}, received: ${sourceType}`);
91
+ }
92
+ const payload = { name, sourceType, fileUrl };
93
+ if (input.thumbnail_url)
94
+ payload.thumbnailUrl = toId(input.thumbnail_url);
95
+ if (input.gender)
96
+ payload.gender = toId(input.gender);
97
+ emitProgress({ phase: 'gen-digital-human:clone-request', sourceType });
98
+ const data = await mmPost(ctx, '/digital-human/avatar/clone', payload, { timeoutMs: 60_000 });
99
+ const avatarId = toId(data.id);
100
+ if (!avatarId) {
101
+ throw new SkillError(`❌ could not parse the new avatar id; API returned: ${JSON.stringify(data)}`);
102
+ }
103
+ emitProgress({ phase: 'gen-digital-human:clone-async', avatarId });
104
+ const json = isTrue(input.json_output);
105
+ if (input.wait !== undefined && !isTrue(input.wait)) {
106
+ if (json) {
107
+ process.stdout.write(JSON.stringify({ avatarId, cloneStatus: data.cloneStatus ?? 'cloning' }) + '\n');
108
+ return;
109
+ }
110
+ process.stdout.write(`\n🧬 Clone submitted, avatar id: ${avatarId}\n`);
111
+ process.stdout.write(`💡 Check progress with --clone-status --avatar-id ${avatarId}\n`);
112
+ return;
113
+ }
114
+ let final;
115
+ try {
116
+ final = await pollUntil(async () => {
117
+ const s = await mmPost(ctx, '/digital-human/avatar/clone-status', { id: avatarId }, { timeoutMs: 30_000 });
118
+ const status = s.cloneStatus ?? 'unknown';
119
+ emitProgress({ phase: 'gen-digital-human:clone-poll', avatarId, status });
120
+ if (status === 'done')
121
+ return s;
122
+ if (status === 'failed')
123
+ throw new SkillError(`❌ avatar clone failed: ${s.cloneError || 'unknown error'}`);
124
+ return null;
125
+ }, { intervalMs: 10_000, timeoutMs: 20 * 60_000 });
126
+ }
127
+ catch (err) {
128
+ if (err instanceof SkillError && /polling timed out/.test(err.message)) {
129
+ throw new SkillError(`❌ avatar ${avatarId} is still cloning; rerun --clone-status --avatar-id ${avatarId} later`);
130
+ }
131
+ throw err;
132
+ }
133
+ if (json) {
134
+ process.stdout.write(JSON.stringify({ avatarId, cloneStatus: final.cloneStatus }) + '\n');
135
+ return;
136
+ }
137
+ process.stdout.write(`\n✅ Avatar cloned, id: ${avatarId}\n`);
138
+ process.stdout.write(`💡 Generate with --avatar-id ${avatarId} --text "..."\n`);
139
+ }
140
+ async function checkCloneStatus(ctx, avatarId, json) {
141
+ const s = await mmPost(ctx, '/digital-human/avatar/clone-status', { id: avatarId }, { timeoutMs: 30_000 });
142
+ const status = s.cloneStatus || 'unknown';
143
+ if (json) {
144
+ process.stdout.write(JSON.stringify({ avatarId, cloneStatus: status, error: s.cloneError || null }) + '\n');
145
+ return;
146
+ }
147
+ process.stdout.write(`\nclone status: ${status}\n`);
148
+ if (status === 'failed')
149
+ process.stdout.write(` failure reason: ${s.cloneError || 'unknown error'}\n`);
150
+ }
151
+ // ── Video generation ───────────────────────────────────────────────────────
60
152
  async function checkStatus(ctx, generationId, json) {
61
153
  const data = await mmPost(ctx, '/digital-human/video/status', { generationId }, { timeoutMs: 30_000 });
62
154
  const status = data.status ?? 'unknown';
@@ -93,25 +185,40 @@ export async function genDigitalHuman(input, ctxIn) {
93
185
  privateToken: input.priv_token,
94
186
  preflightToken: ctxIn.auth?.token,
95
187
  });
188
+ const json = isTrue(input.json_output);
96
189
  // ── Mode 1: list avatars ────────────────────────────────────────────────
97
190
  if (isTrue(input.list_avatars)) {
98
191
  return listAvatars(ctx, input);
99
192
  }
100
- // ── Mode 3: check status ────────────────────────────────────────────────
193
+ // ── Mode 2: clone a custom avatar (HiFly) ───────────────────────────────
194
+ if (isTrue(input.clone)) {
195
+ return cloneAvatar(ctx, input);
196
+ }
197
+ // ── Mode 3: clone status ────────────────────────────────────────────────
198
+ if (isTrue(input.clone_status)) {
199
+ const avatarId = toId(input.avatar_id);
200
+ if (!avatarId)
201
+ throw new SkillError('❌ clone-status mode requires --avatar-id');
202
+ return checkCloneStatus(ctx, avatarId, json);
203
+ }
204
+ // ── Mode 4: check generation status ─────────────────────────────────────
101
205
  if (isTrue(input.check_status)) {
102
206
  const generationId = toNumber(input.generation_id, 'generation_id');
103
207
  if (generationId === undefined) {
104
208
  throw new SkillError('❌ check-status mode requires --generation-id');
105
209
  }
106
- return checkStatus(ctx, generationId, isTrue(input.json_output));
210
+ return checkStatus(ctx, generationId, json);
107
211
  }
108
- // ── Mode 2: generate (default) ──────────────────────────────────────────
109
- const avatarId = toNumber(input.avatar_id, 'avatar_id');
110
- if (avatarId === undefined) {
212
+ // ── Mode 5: generate (default) ──────────────────────────────────────────
213
+ const avatarId = toId(input.avatar_id);
214
+ if (!avatarId) {
111
215
  throw new SkillError('❌ provide --avatar-id to pick the avatar (use --list-avatars to see available avatars)');
112
216
  }
113
217
  const audioUrl = (input.audio_url ?? '').trim();
114
218
  const text = input.text ?? '';
219
+ if (audioUrl && !/^https?:\/\//i.test(audioUrl)) {
220
+ throw new SkillError(`❌ --audio-url needs a public http(s) URL (local files are not uploaded), received: ${audioUrl}`);
221
+ }
115
222
  if (!audioUrl) {
116
223
  if (!text.trim()) {
117
224
  throw new SkillError('❌ in TTS mode provide --text, or use --audio-url for audio-driven mode');
@@ -120,12 +227,16 @@ export async function genDigitalHuman(input, ctxIn) {
120
227
  const dh = (await getCapabilities(input.api_base_url)).digitalHuman;
121
228
  emitProgress({ phase: 'gen-digital-human:resolve-avatar', avatarId });
122
229
  const avatar = await fetchAvatarInfo(ctx, avatarId);
123
- const source = resolveSource(input.source, avatar, dh.sources);
230
+ if (avatar.cloneStatus === 'cloning') {
231
+ throw new SkillError(`❌ avatar ${avatarId} is still cloning; check with --clone-status --avatar-id ${avatarId}`);
232
+ }
233
+ if (avatar.cloneStatus === 'failed') {
234
+ throw new SkillError(`❌ avatar ${avatarId} failed to clone (${avatar.cloneError || 'unknown error'}); clone it again`);
235
+ }
236
+ const source = resolveSource(avatar, dh.sources);
124
237
  const payload = { avatarId, source };
125
238
  if (audioUrl) {
126
239
  payload.audioUrl = audioUrl;
127
- if (text.trim())
128
- payload.text = text.trim();
129
240
  }
130
241
  else {
131
242
  payload.text = text;
@@ -133,11 +244,6 @@ export async function genDigitalHuman(input, ctxIn) {
133
244
  if (input.voice_name)
134
245
  payload.voiceName = String(input.voice_name);
135
246
  }
136
- const aspectRatio = input.aspect_ratio ?? avatar.aspectRatio;
137
- if (aspectRatio)
138
- payload.aspectRatio = aspectRatio;
139
- if (input.prompt)
140
- payload.prompt = String(input.prompt);
141
247
  emitProgress({ phase: 'gen-digital-human:request', avatarId, source, mode: audioUrl ? 'audio' : 'tts' });
142
248
  const data = await mmPost(ctx, '/digital-human/video/generate', payload, { timeoutMs: 60_000 });
143
249
  const generationId = data.generationId;
@@ -149,7 +255,7 @@ export async function genDigitalHuman(input, ctxIn) {
149
255
  if (!videoUrl) {
150
256
  throw new SkillError(`❌ video generated but no URL was returned; rerun --check-status --generation-id ${generationId} later`);
151
257
  }
152
- if (isTrue(input.json_output)) {
258
+ if (json) {
153
259
  process.stdout.write(JSON.stringify({ url: videoUrl, generationId }) + '\n');
154
260
  return;
155
261
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.55",
4
- "generatedAt": "2026-09-28T12:06:33.998Z",
3
+ "version": "0.9.57",
4
+ "generatedAt": "2026-09-29T07:05:55.130Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
@@ -169,12 +169,12 @@
169
169
  "tier": "atomic",
170
170
  "category": "asset",
171
171
  "title": "Digital-Human Talking-Head",
172
- "summary": "Digital-human video: list available avatars; produce a talking-head video from text via TTS, or drive an avatar from an existing audio URL.",
172
+ "summary": "Digital-human video (HiFly): list or clone avatars; produce a talking-head video from text via TTS, or drive an avatar from an existing audio URL. Aspect ratio follows the avatar's cloned source and cannot be chosen.",
173
173
  "triggers": [
174
174
  "Digital human, talking-head video, AI presenter, virtual host",
175
175
  "Make a talking-head video out of this script / this audio file",
176
176
  "List or browse digital-human avatars",
177
- "Use Jimeng or HiFly to drive an avatar"
177
+ "Clone / create a custom avatar from a video or photo"
178
178
  ],
179
179
  "entry": {
180
180
  "type": "http",
@@ -199,15 +199,7 @@
199
199
  },
200
200
  "name": {
201
201
  "type": "string",
202
- "description": "With list_avatars=true: fuzzy-filter avatars by name"
203
- },
204
- "source": {
205
- "type": "string",
206
- "enum": [
207
- "jimeng",
208
- "hifly"
209
- ],
210
- "description": "Provider: jimeng is image-driven, hifly is video-driven. Usually inferred from the avatar; pass it explicitly when the avatar declares no source."
202
+ "description": "With list_avatars=true: fuzzy-filter avatars by name. With clone=true: the new avatar's name (required)."
211
203
  },
212
204
  "gender": {
213
205
  "type": "string",
@@ -218,33 +210,53 @@
218
210
  "description": "Filter by gender"
219
211
  },
220
212
  "avatar_id": {
221
- "type": "number",
222
- "description": "Avatar id"
213
+ "type": "string",
214
+ "description": "Avatar id as returned by list_avatars (opaque string like dh_xxx). Also used with clone_status=true."
215
+ },
216
+ "clone": {
217
+ "type": "boolean",
218
+ "description": "Clone mode: create a custom HiFly avatar from a video (free) or a portrait image (costs HiFly credits). Requires clone_url and name. Waits until cloning finishes unless wait=false."
219
+ },
220
+ "clone_url": {
221
+ "type": "string",
222
+ "description": "With clone=true: public http(s) URL of the source. Video: mp4/mov (h264), 5s-30min, 360p-4K, ≤500MB, one clearly visible speaking person. Image: a front-facing portrait."
223
+ },
224
+ "clone_type": {
225
+ "type": "string",
226
+ "enum": [
227
+ "video",
228
+ "image"
229
+ ],
230
+ "description": "With clone=true: source type. Defaults to video."
231
+ },
232
+ "thumbnail_url": {
233
+ "type": "string",
234
+ "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."
235
+ },
236
+ "wait": {
237
+ "type": "boolean",
238
+ "description": "With clone=true: set false to return right after submission (then poll with clone_status=true). Defaults to waiting up to ~20 minutes."
239
+ },
240
+ "clone_status": {
241
+ "type": "boolean",
242
+ "description": "Clone-status mode: report cloning / done / failed for avatar_id."
223
243
  },
224
244
  "text": {
225
245
  "type": "string",
226
- "description": "Narration text (TTS mode)"
246
+ "description": "Narration text (TTS mode, ≤ 10000 chars). Synthesized with voice_id, then drives the avatar; the speech must last at least 5 s. Ignored when audio_url is given."
227
247
  },
228
248
  "audio_url": {
229
249
  "type": "string",
230
- "description": "Audio URL (audio-driven mode)"
250
+ "description": "Audio-driven mode: public http(s) URL of mp3 / m4a / wav, 5 s – 30 min, ≤ 100 MB. Takes precedence over text / voice_id."
231
251
  },
232
252
  "voice_id": {
233
253
  "type": "string",
234
- "description": "Voice id (TTS mode). Shares the Minimax catalog with gen-voice — call gen_voice with list_voices=true to see available ids rather than inventing one."
254
+ "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."
235
255
  },
236
256
  "voice_name": {
237
257
  "type": "string",
238
258
  "description": "Voice display name, recorded alongside voice_id for bookkeeping. Does not affect synthesis."
239
259
  },
240
- "aspect_ratio": {
241
- "type": "string",
242
- "description": "Aspect ratio: 9:16 / 16:9 / 3:4 / 1:1. Defaults to the avatar's own ratio."
243
- },
244
- "prompt": {
245
- "type": "string",
246
- "description": "Action prompt describing how the avatar should perform, e.g. 'more hand gestures'"
247
- },
248
260
  "check_status": {
249
261
  "type": "boolean",
250
262
  "description": "Status-check mode: poll an earlier job instead of starting a new one. Requires generation_id. Use this when a generate call timed out."
@@ -255,7 +267,7 @@
255
267
  },
256
268
  "json_output": {
257
269
  "type": "boolean",
258
- "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }) instead of human-readable output"
270
+ "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }; clone / clone-status: { avatarId, cloneStatus }) instead of human-readable output"
259
271
  }
260
272
  },
261
273
  "required": []
@@ -263,18 +275,20 @@
263
275
  "ui": {
264
276
  "primary": [
265
277
  "avatar_id",
266
- "text"
267
- ],
268
- "advanced": [
269
- "source",
278
+ "text",
270
279
  "voice_id",
271
- "aspect_ratio",
272
- "prompt",
273
280
  "audio_url"
274
281
  ],
282
+ "advanced": [],
275
283
  "hidden": [
276
284
  "json_output",
277
285
  "list_avatars",
286
+ "clone",
287
+ "clone_url",
288
+ "clone_type",
289
+ "thumbnail_url",
290
+ "wait",
291
+ "clone_status",
278
292
  "mine",
279
293
  "name",
280
294
  "gender",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.55",
3
+ "version": "0.9.57",
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",
@@ -1,111 +1,78 @@
1
1
  ---
2
2
  name: gen-digital-human
3
3
  description: |
4
- Digital-human (talking-head) skill: list available avatars, run TTS-based or audio-driven lip-sync, and check the status of pending jobs. Backed by ab-api's digital-human endpoints (Jimeng / HiFly providers).
4
+ Digital-human (talking-head) skill backed by HiFly: list avatars, clone a custom avatar from a video or photo, drive an avatar with text (TTS) or an existing audio URL, and check the status of pending jobs.
5
5
 
6
6
  Use this skill immediately whenever the user asks for any of:
7
7
  - Digital human, talking-head video, AI presenter, virtual host
8
8
  - "Make a talking-head video out of this script / this audio file"
9
9
  - List or browse digital-human avatars
10
- - Use Jimeng or HiFly to drive an avatar
10
+ - Clone / create a custom avatar from a video or photo
11
11
 
12
12
  Even without an explicit "use AI", any request that turns text or audio into a synthesized presenter video should route here.
13
13
  triggers:
14
14
  - Digital human, talking-head video, AI presenter, virtual host
15
15
  - "Make a talking-head video out of this script / this audio file"
16
16
  - List or browse digital-human avatars
17
- - Use Jimeng or HiFly to drive an avatar
17
+ - Clone / create a custom avatar from a video or photo
18
18
  ---
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**. Two providers: **Jimeng** (image-driven) and **HiFly** (video-driven). Generation is async — the handler submits the task and polls `/digital-human/video/status` until completion.
23
-
24
- > 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_digital_human`, same params as in `skill.json`); local repro goes through `remixmate gen-digital-human ...`.
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.
25
23
 
26
24
  ## Capabilities
27
25
 
28
26
  | Capability | Description | API |
29
27
  |------------|-------------|-----|
30
- | List avatars | Page through available avatars; filter by source / gender / name | `POST /digital-human/avatar/page` |
31
- | TTS lip-sync | avatar + text + voice → talking-head video | `POST /digital-human/video/generate` |
28
+ | List avatars | Public avatars, or your own with `--mine` | `POST /digital-human/avatar/page` |
29
+ | Text-driven | avatar + text + voice → speech is synthesized, then drives the avatar | `POST /digital-human/video/generate` |
32
30
  | Audio-driven | avatar + existing audio URL → talking-head video | `POST /digital-human/video/generate` |
33
- | Check status | Poll the progress of a generation job | `POST /digital-human/video/status` |
31
+ | Check status | Poll a generation job | `POST /digital-human/video/status` |
32
+ | Clone avatar | Custom avatar from a video (free) or portrait photo (costs HiFly credits) | `POST /digital-human/avatar/clone` |
33
+ | Clone status | `cloning` / `done` / `failed` for a cloned avatar | `POST /digital-human/avatar/clone-status` |
34
34
 
35
- **Cross-skill dependency**: voice ids are shared with `gen-voice` (same Minimax catalog). Use `gen-voice --list-voices` to see available voices.
35
+ Avatar ids are opaque strings (e.g. `dh_xxx`) — always copy them from `--list-avatars`, never invent them.
36
36
 
37
- ## Auth & environment
37
+ **What you cannot choose**: aspect ratio and on-camera actions. The output keeps the framing of the source the avatar was cloned from (a 9:16 clone produces 9:16 videos). To get another ratio, clone the avatar from footage in that ratio.
38
38
 
39
- No skill-local env file — the executing process inherits the system environment.
39
+ **Cross-skill dependency**: voice ids are shared with `gen-voice` (including cloned voices). Use `gen-voice --list-voices` to see them.
40
40
 
41
- - **Enterprise OpenClaw**: auth is already injected, **no need** for `PRIV_TOKEN` / `--priv-token`.
42
- - **Other environments**: configure the token. Without a token, non-interactive runs fail.
41
+ ## Auth & environment
43
42
 
44
43
  | Env var | Description | Default |
45
44
  |---------|-------------|---------|
46
- | `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
45
+ | `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides (not needed in Enterprise OpenClaw) | none |
47
46
  | `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.ai/api` |
48
47
  | `AGENT_NAME` | Optional `x-invoke-agent` header | none |
49
48
 
50
49
  ## Operations
51
50
 
52
- By default only the URL is printed; generation is async and the handler polls until done (typically 1–5 minutes), emitting `__progress__` lines. The resulting video URL is cloud-persisted — show it directly, no download needed.
51
+ Generation typically takes 1–5 minutes; the handler polls and emits `__progress__` lines. The resulting video URL is cloud-persisted — show it directly, no download needed.
53
52
 
54
53
  ### 1. List avatars
55
54
 
56
55
  ```bash
57
- remixmate gen-digital-human --list-avatars
56
+ remixmate gen-digital-human --list-avatars --name "教师" # public avatars (~1000; always narrow with --name)
57
+ remixmate gen-digital-human --list-avatars --mine # your cloned avatars
58
+ remixmate gen-digital-human --list-avatars --gender female --name "alice"
58
59
  ```
59
60
 
60
- Optional filters:
61
-
62
- ```bash
63
- # Filter by source (jimeng = image-driven, hifly = video-driven)
64
- remixmate gen-digital-human --list-avatars --source jimeng
65
- remixmate gen-digital-human --list-avatars --source hifly
66
-
67
- # Filter by gender
68
- remixmate gen-digital-human --list-avatars --gender female
61
+ ### 2. Generate a video
69
62
 
70
- # Show your custom avatars
71
- remixmate gen-digital-human --list-avatars --mine
72
-
73
- # Filter by name
74
- remixmate gen-digital-human --list-avatars --name "alice"
75
- ```
76
-
77
- > Custom avatars require `--mine`. When generating a video the handler auto-falls back from the public list to `mine: true` if needed.
78
-
79
- ### 2. Generate a digital-human video
80
-
81
- #### TTS mode
63
+ Pick exactly one driver: **text** (+ voice) or **audio**. With `--audio-url`, text and voice are ignored.
82
64
 
83
65
  ```bash
84
- remixmate gen-digital-human \
85
- --avatar-id 7 \
86
- --text "Hi everyone, welcome to the live stream — today I'll introduce a new product." \
66
+ # Text-driven
67
+ remixmate gen-digital-human --avatar-id dh_xxx \
68
+ --text "Hi everyone, today I'll introduce a new product." \
87
69
  --voice-id "male-qn-qingse"
88
- ```
89
70
 
90
- #### TTS + custom voice + aspect ratio
91
-
92
- ```bash
93
- remixmate gen-digital-human \
94
- --avatar-id 7 \
95
- --text "Hi class, here's our brand-new course." \
96
- --voice-id "female-shaonv" \
97
- --aspect-ratio "9:16"
71
+ # Audio-driven
72
+ remixmate gen-digital-human --avatar-id dh_xxx --audio-url "https://example.com/voice.mp3"
98
73
  ```
99
74
 
100
- #### Audio-driven mode
101
-
102
- ```bash
103
- remixmate gen-digital-human \
104
- --avatar-id 39 \
105
- --source hifly \
106
- --audio-url "https://example.com/voice.mp3" \
107
- --aspect-ratio "16:9"
108
- ```
75
+ Limits: speech/audio must last **5 s – 30 min**; audio is mp3 / m4a / wav, ≤ 100 MB, public http(s) URL; text ≤ 10000 characters.
109
76
 
110
77
  ### 3. Check job status
111
78
 
@@ -113,67 +80,42 @@ remixmate gen-digital-human \
113
80
  remixmate gen-digital-human --check-status --generation-id 123
114
81
  ```
115
82
 
116
- ## Common CLI flags
117
-
118
- ### List avatars
119
-
120
- | Flag | Description | Default |
121
- |------|-------------|---------|
122
- | `--list-avatars` | List avatars and exit | — |
123
- | `--source` | Filter by source: `jimeng` / `hifly` | no filter |
124
- | `--gender` | Filter by gender: `male` / `female` | no filter |
125
- | `--mine` | Limit to user's custom avatars | off |
126
- | `--name` | Fuzzy filter by name | no filter |
127
-
128
- ### Generate video
129
-
130
- | Flag | Description | Default |
131
- |------|-------------|---------|
132
- | `--avatar-id` | Avatar id (required) | — |
133
- | `-t` / `--text` | Speech text (required for TTS mode; optional with `--audio-url`) | — |
134
- | `--audio-url` | Audio URL (audio-driven mode) | none |
135
- | `--voice-id` | Voice id (TTS mode) | `male-qn-qingse` |
136
- | `--voice-name` | Voice display name (recordkeeping) | none |
137
- | `--source` | `jimeng` / `hifly` (inferable from avatar) | auto |
138
- | `--aspect-ratio` | `9:16` / `16:9` / `3:4` / `1:1` | avatar's default |
139
- | `--prompt` | Action prompt (e.g. "more hand gestures") | none |
140
- | `--api-base-url` | Override API root | see env vars |
141
- | `--priv-token` | Override token | see env vars |
142
-
143
- ### Check status
83
+ ### 4. Clone a custom avatar
144
84
 
145
- | Flag | Description | Default |
146
- |------|-------------|---------|
147
- | `--check-status` | Enter status-check mode | — |
148
- | `--generation-id` | Job id (required) | — |
85
+ The source must be a public http(s) URL (local files are not uploaded). Video: mp4/mov (h264), 5 s – 30 min, 360p – 4K, ≤ 500 MB, one person speaking to camera. Cloning takes minutes; the handler waits (up to ~20 min) and prints the new avatar id. The avatar also shows up under 素材库 → 数字人 in the web app.
149
86
 
150
- ## Typical flows
151
-
152
- ### Flow A — TTS
153
-
154
- ```
155
- 1. List avatars → --list-avatars [--gender female]
156
- 2. Pick avatar-id
157
- 3. Confirm or polish the speech text
158
- 4. (Optional) pick a voice → use gen-voice --list-voices
159
- 5. Generate → --avatar-id <id> --text "..." --voice-id "female-shaonv"
160
- 6. Show URL
161
- ```
87
+ ```bash
88
+ # Video clone (free)
89
+ remixmate gen-digital-human --clone --name "My presenter" --clone-url "https://example.com/me.mp4"
162
90
 
163
- ### Flow B — Audio-driven
91
+ # Portrait photo clone (costs HiFly credits)
92
+ remixmate gen-digital-human --clone --clone-type image --name "My presenter" --clone-url "https://example.com/me.jpg"
164
93
 
94
+ # Submit without waiting, then poll
95
+ remixmate gen-digital-human --clone --wait false --name "My presenter" --clone-url "https://example.com/me.mp4"
96
+ remixmate gen-digital-human --clone-status --avatar-id dh_xxx
165
97
  ```
166
- 1. List avatars → --list-avatars [--mine]
167
- 2. Prepare an audio URL (gen-voice can produce one)
168
- 3. Generate → --avatar-id <id> --audio-url "https://..." [--source hifly]
169
- 4. Show URL
170
- ```
171
-
172
- ## Text guidance (TTS)
173
98
 
174
- - Punctuation drives pacing — commas short, periods long.
175
- - Keep individual jobs under ~500 characters.
176
- - Tone and style of the script affect the perceived voice.
99
+ ## Flags
100
+
101
+ | Flag | Mode | Description | Default |
102
+ |------|------|-------------|---------|
103
+ | `--list-avatars` | list | List avatars and exit | — |
104
+ | `--mine` | list | Only your own avatars | off |
105
+ | `--gender` | list / clone | `male` / `female` | none |
106
+ | `--name` | list / clone | Fuzzy filter; with `--clone` the new avatar's name (required) | — |
107
+ | `--avatar-id` | generate / clone-status | Avatar id (required) | — |
108
+ | `-t` / `--text` | generate | Script (text-driven) | — |
109
+ | `--voice-id` | generate | Voice id (text-driven) | `male-qn-qingse` |
110
+ | `--voice-name` | generate | Voice display name (recordkeeping only) | none |
111
+ | `--audio-url` | generate | Audio URL (audio-driven) | none |
112
+ | `--check-status` / `--generation-id` | status | Poll a generation job | — |
113
+ | `--clone` / `--clone-url` | clone | Clone mode and source URL (required) | — |
114
+ | `--clone-type` | clone | `video` / `image` | `video` |
115
+ | `--thumbnail-url` | clone | Cover shown in lists | image itself / none |
116
+ | `--wait` | clone | `false` to return right after submission | wait |
117
+ | `--clone-status` | clone-status | Report clone status of `--avatar-id` | — |
118
+ | `--api-base-url` / `--priv-token` | all | Overrides | see env vars |
177
119
 
178
120
  ## Credits
179
121
 
@@ -192,9 +134,10 @@ generation cost, and the balance is the only warning before a run fails with
192
134
  - **401** / **token missing** (non-OpenClaw): set `PRIV_TOKEN`.
193
135
  - **Business `code != 0`**: read `msg` on stderr.
194
136
  - **429**: rate-limited; retry later.
195
- - **Generation timeout**: the handler caps polling at ~10 minutes; use `--check-status --generation-id <id>` later.
196
- - **Empty TTS text**: provide non-empty `--text` or use `--audio-url`.
137
+ - **Generation timeout**: polling caps at ~10 minutes; use `--check-status --generation-id <id>` later.
138
+ - **Speech/audio under 5 s**: lengthen the text or audio.
197
139
  - **Invalid voice id**: re-check via `gen-voice --list-voices`.
198
140
  - **Avatar id not found**: re-check via `--list-avatars` (or `--list-avatars --mine`).
199
- - **Cannot infer source**: when the avatar lacks a `source` and `--source` is not set, pass `--source jimeng` or `--source hifly` explicitly.
141
+ - **Avatar still cloning / clone failed**: poll `--clone-status --avatar-id <id>`; on failure, re-clone with a clearer source (single speaker facing the camera, h264 mp4).
142
+ - **User asks for a different aspect ratio or gestures**: not supported at generation time — explain that the ratio follows the cloned source, and offer to clone from footage in the wanted ratio.
200
143
  - **Network**: verify connectivity and `MM_API_BASE_URL`.
@@ -4,7 +4,7 @@
4
4
  "tier": "atomic",
5
5
  "category": "asset",
6
6
  "title": "Digital-Human Talking-Head",
7
- "description": "Digital-human video: list available avatars; produce a talking-head video from text via TTS, or drive an avatar from an existing audio URL.",
7
+ "description": "Digital-human video (HiFly): list or clone avatars; produce a talking-head video from text via TTS, or drive an avatar from an existing audio URL. Aspect ratio follows the avatar's cloned source and cannot be chosen.",
8
8
  "auth": "required",
9
9
  "joinsTake": true,
10
10
  "envVars": [
@@ -29,15 +29,7 @@
29
29
  },
30
30
  "name": {
31
31
  "type": "string",
32
- "description": "With list_avatars=true: fuzzy-filter avatars by name"
33
- },
34
- "source": {
35
- "type": "string",
36
- "enum": [
37
- "jimeng",
38
- "hifly"
39
- ],
40
- "description": "Provider: jimeng is image-driven, hifly is video-driven. Usually inferred from the avatar; pass it explicitly when the avatar declares no source."
32
+ "description": "With list_avatars=true: fuzzy-filter avatars by name. With clone=true: the new avatar's name (required)."
41
33
  },
42
34
  "gender": {
43
35
  "type": "string",
@@ -48,33 +40,53 @@
48
40
  "description": "Filter by gender"
49
41
  },
50
42
  "avatar_id": {
51
- "type": "number",
52
- "description": "Avatar id"
43
+ "type": "string",
44
+ "description": "Avatar id as returned by list_avatars (opaque string like dh_xxx). Also used with clone_status=true."
45
+ },
46
+ "clone": {
47
+ "type": "boolean",
48
+ "description": "Clone mode: create a custom HiFly avatar from a video (free) or a portrait image (costs HiFly credits). Requires clone_url and name. Waits until cloning finishes unless wait=false."
49
+ },
50
+ "clone_url": {
51
+ "type": "string",
52
+ "description": "With clone=true: public http(s) URL of the source. Video: mp4/mov (h264), 5s-30min, 360p-4K, ≤500MB, one clearly visible speaking person. Image: a front-facing portrait."
53
+ },
54
+ "clone_type": {
55
+ "type": "string",
56
+ "enum": [
57
+ "video",
58
+ "image"
59
+ ],
60
+ "description": "With clone=true: source type. Defaults to video."
61
+ },
62
+ "thumbnail_url": {
63
+ "type": "string",
64
+ "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."
65
+ },
66
+ "wait": {
67
+ "type": "boolean",
68
+ "description": "With clone=true: set false to return right after submission (then poll with clone_status=true). Defaults to waiting up to ~20 minutes."
69
+ },
70
+ "clone_status": {
71
+ "type": "boolean",
72
+ "description": "Clone-status mode: report cloning / done / failed for avatar_id."
53
73
  },
54
74
  "text": {
55
75
  "type": "string",
56
- "description": "Narration text (TTS mode)"
76
+ "description": "Narration text (TTS mode, ≤ 10000 chars). Synthesized with voice_id, then drives the avatar; the speech must last at least 5 s. Ignored when audio_url is given."
57
77
  },
58
78
  "audio_url": {
59
79
  "type": "string",
60
- "description": "Audio URL (audio-driven mode)"
80
+ "description": "Audio-driven mode: public http(s) URL of mp3 / m4a / wav, 5 s – 30 min, ≤ 100 MB. Takes precedence over text / voice_id."
61
81
  },
62
82
  "voice_id": {
63
83
  "type": "string",
64
- "description": "Voice id (TTS mode). Shares the Minimax catalog with gen-voice — call gen_voice with list_voices=true to see available ids rather than inventing one."
84
+ "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."
65
85
  },
66
86
  "voice_name": {
67
87
  "type": "string",
68
88
  "description": "Voice display name, recorded alongside voice_id for bookkeeping. Does not affect synthesis."
69
89
  },
70
- "aspect_ratio": {
71
- "type": "string",
72
- "description": "Aspect ratio: 9:16 / 16:9 / 3:4 / 1:1. Defaults to the avatar's own ratio."
73
- },
74
- "prompt": {
75
- "type": "string",
76
- "description": "Action prompt describing how the avatar should perform, e.g. 'more hand gestures'"
77
- },
78
90
  "check_status": {
79
91
  "type": "boolean",
80
92
  "description": "Status-check mode: poll an earlier job instead of starting a new one. Requires generation_id. Use this when a generate call timed out."
@@ -85,7 +97,7 @@
85
97
  },
86
98
  "json_output": {
87
99
  "type": "boolean",
88
- "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }) instead of human-readable output"
100
+ "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }; clone / clone-status: { avatarId, cloneStatus }) instead of human-readable output"
89
101
  }
90
102
  },
91
103
  "required": []
@@ -93,18 +105,20 @@
93
105
  "ui": {
94
106
  "primary": [
95
107
  "avatar_id",
96
- "text"
97
- ],
98
- "advanced": [
99
- "source",
108
+ "text",
100
109
  "voice_id",
101
- "aspect_ratio",
102
- "prompt",
103
110
  "audio_url"
104
111
  ],
112
+ "advanced": [],
105
113
  "hidden": [
106
114
  "json_output",
107
115
  "list_avatars",
116
+ "clone",
117
+ "clone_url",
118
+ "clone_type",
119
+ "thumbnail_url",
120
+ "wait",
121
+ "clone_status",
108
122
  "mine",
109
123
  "name",
110
124
  "gender",
@@ -2,6 +2,6 @@
2
2
  "skillName": "gen-digital-human",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "336",
5
- "version": "V8",
6
- "skillDescription": "Digital-human (talking-head) skill: list available avatars, run TTS-based or audio-driven lip-sync, and check the status of pending jobs. Backed by ab-api's digital-human endpoints (Jimeng / HiFly providers).\n\nUse this skill immediately whenever the user asks for any of:\n- Digital human, talking-head video, AI presenter, virtual host\n- \"Make a talking-head video out of this script / this audio file\"\n- List or browse digital-human avatars\n- Use Jimeng or HiFly to drive an avatar\n\nEven without an explicit \"use AI\", any request that turns text or audio into a synthesized presenter video should route here."
5
+ "version": "V9",
6
+ "skillDescription": "Digital-human (talking-head) skill backed by HiFly: list avatars, clone a custom avatar from a video or photo, drive an avatar with text (TTS) or an existing audio URL, and check the status of pending jobs.\n\nUse this skill immediately whenever the user asks for any of:\n- Digital human, talking-head video, AI presenter, virtual host\n- \"Make a talking-head video out of this script / this audio file\"\n- List or browse digital-human avatars\n- Clone / create a custom avatar from a video or photo\n\nEven without an explicit \"use AI\", any request that turns text or audio into a synthesized presenter video should route here."
7
7
  }