@remixmate/cli 0.1.0 → 0.1.1

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.
Files changed (122) hide show
  1. package/README.md +100 -83
  2. package/README.zh-CN.md +183 -0
  3. package/dist/handlers/gen-digital-human.d.ts +12 -0
  4. package/dist/handlers/gen-digital-human.js +157 -0
  5. package/dist/handlers/gen-image.js +5 -41
  6. package/dist/handlers/gen-video.d.ts +18 -0
  7. package/dist/handlers/gen-video.js +153 -0
  8. package/dist/handlers/gen-voice.js +2 -12
  9. package/dist/handlers/index.js +4 -2
  10. package/dist/handlers/shared.d.ts +26 -0
  11. package/dist/handlers/shared.js +66 -0
  12. package/dist/http.d.ts +4 -2
  13. package/dist/http.js +5 -3
  14. package/dist/manifest.json +27 -24
  15. package/dist/registry.d.ts +5 -16
  16. package/dist/registry.js +11 -17
  17. package/dist/skill-schema.d.ts +47 -0
  18. package/dist/skill-schema.js +49 -0
  19. package/package.json +5 -4
  20. package/skills/export-jianying/scripts/gen_jianying_draft.py +4 -4
  21. package/skills/gen-digital-human/SKILL.md +23 -46
  22. package/skills/gen-digital-human/skill.json +3 -2
  23. package/skills/gen-digital-human/version.json +1 -1
  24. package/skills/gen-image/SKILL.md +2 -2
  25. package/skills/gen-image/skill.json +2 -1
  26. package/skills/gen-image/version.json +1 -1
  27. package/skills/gen-script/SKILL.md +13 -13
  28. package/skills/gen-script/scripts/gen_script.py +11 -12
  29. package/skills/gen-video/SKILL.md +22 -33
  30. package/skills/gen-video/skill.json +3 -2
  31. package/skills/gen-video/version.json +1 -1
  32. package/skills/gen-voice/SKILL.md +1 -1
  33. package/skills/gen-voice/version.json +1 -1
  34. package/skills/prepare-video-assets/SKILL.md +2 -2
  35. package/skills/prepare-video-assets/skill.json +1 -1
  36. package/skills/render-video/scripts/render_video.py +8 -8
  37. package/skills/{template-bind → template-registry}/README.md +4 -4
  38. package/skills/{template-bind → template-registry}/SKILL.md +14 -12
  39. package/skills/template-registry/scripts/list_templates.py +129 -0
  40. package/skills/{template-bind → template-registry}/scripts/match_template.py +4 -3
  41. package/skills/{template-bind → template-registry}/scripts/registry_loader.py +1 -1
  42. package/skills/{template-bind → template-registry}/scripts/template_paths.py +7 -7
  43. package/skills/{template-bind → template-registry}/skill.json +6 -5
  44. package/skills/template-registry/version.json +7 -0
  45. package/skills/{template-bind → template-registry}/video_dsl/README.md +2 -2
  46. package/skills/{template-bind → template-registry}/video_dsl/runtime/__init__.py +1 -1
  47. package/skills/video-parser/SKILL.md +47 -160
  48. package/skills/video-parser/skill.json +3 -4
  49. package/skills/video-parser/version.json +1 -1
  50. package/skills/web-capture/SKILL.md +407 -116
  51. package/skills/web-capture/scripts/_media_screenshot/__init__.py +21 -0
  52. package/skills/web-capture/scripts/_media_screenshot/bootstrap.py +34 -0
  53. package/skills/web-capture/scripts/_media_screenshot/browser.py +86 -0
  54. package/skills/web-capture/scripts/_media_screenshot/cli_args.py +111 -0
  55. package/skills/web-capture/scripts/_media_screenshot/js/arrow.js +86 -0
  56. package/skills/web-capture/scripts/_media_screenshot/js/caption.js +27 -0
  57. package/skills/web-capture/scripts/_media_screenshot/js/clear_overlay.js +5 -0
  58. package/skills/web-capture/scripts/_media_screenshot/js/cursor_bootstrap.js +26 -0
  59. package/skills/web-capture/scripts/_media_screenshot/js/cursor_hide.js +6 -0
  60. package/skills/web-capture/scripts/_media_screenshot/js/cursor_move.js +12 -0
  61. package/skills/web-capture/scripts/_media_screenshot/js/cursor_ripple.js +25 -0
  62. package/skills/web-capture/scripts/_media_screenshot/js/element_zoom.js +72 -0
  63. package/skills/web-capture/scripts/_media_screenshot/js/element_zoom_restore.js +21 -0
  64. package/skills/web-capture/scripts/_media_screenshot/js/fade_out_overlays.js +10 -0
  65. package/skills/web-capture/scripts/_media_screenshot/js/focus.js +38 -0
  66. package/skills/web-capture/scripts/_media_screenshot/js/highlight.js +97 -0
  67. package/skills/web-capture/scripts/_media_screenshot/js/overlay_bootstrap.js +19 -0
  68. package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_in.js +54 -0
  69. package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_out.js +11 -0
  70. package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_restore.js +13 -0
  71. package/skills/web-capture/scripts/_media_screenshot/js/redact.js +33 -0
  72. package/skills/web-capture/scripts/_media_screenshot/js/scroll.js +26 -0
  73. package/skills/web-capture/scripts/_media_screenshot/js/scroll_through.js +18 -0
  74. package/skills/web-capture/scripts/_media_screenshot/js/sequence.js +76 -0
  75. package/skills/web-capture/scripts/_media_screenshot/js/settled_check.js +15 -0
  76. package/skills/web-capture/scripts/_media_screenshot/js/title_card.js +71 -0
  77. package/skills/web-capture/scripts/_media_screenshot/js_loader.py +14 -0
  78. package/skills/web-capture/scripts/_media_screenshot/overlay.py +17 -0
  79. package/skills/web-capture/scripts/_media_screenshot/recording.py +100 -0
  80. package/skills/web-capture/scripts/_media_screenshot/scenes/__init__.py +39 -0
  81. package/skills/web-capture/scripts/_media_screenshot/scenes/arrow.py +50 -0
  82. package/skills/web-capture/scripts/_media_screenshot/scenes/caption.py +20 -0
  83. package/skills/web-capture/scripts/_media_screenshot/scenes/click.py +35 -0
  84. package/skills/web-capture/scripts/_media_screenshot/scenes/code_lines.py +54 -0
  85. package/skills/web-capture/scripts/_media_screenshot/scenes/focus.py +24 -0
  86. package/skills/web-capture/scripts/_media_screenshot/scenes/highlight.py +54 -0
  87. package/skills/web-capture/scripts/_media_screenshot/scenes/hover.py +28 -0
  88. package/skills/web-capture/scripts/_media_screenshot/scenes/redact.py +32 -0
  89. package/skills/web-capture/scripts/_media_screenshot/scenes/scroll.py +32 -0
  90. package/skills/web-capture/scripts/_media_screenshot/scenes/sequence.py +43 -0
  91. package/skills/web-capture/scripts/_media_screenshot/scenes/title_card.py +20 -0
  92. package/skills/web-capture/scripts/_media_screenshot/scenes/type.py +42 -0
  93. package/skills/web-capture/scripts/_media_screenshot/scenes/wait.py +9 -0
  94. package/skills/web-capture/scripts/_media_screenshot/scenes/zoom.py +72 -0
  95. package/skills/web-capture/scripts/_media_screenshot/screenshot.py +166 -0
  96. package/skills/web-capture/scripts/_media_screenshot/storyboard.py +88 -0
  97. package/skills/web-capture/scripts/_media_screenshot/template.py +119 -0
  98. package/skills/web-capture/scripts/_media_screenshot/trim.py +82 -0
  99. package/skills/web-capture/scripts/record.py +186 -0
  100. package/skills/web-capture/scripts/screenshot.py +94 -0
  101. package/skills/web-capture/skill.json +14 -14
  102. package/skills/web-capture/templates/github-code-tour.json +59 -0
  103. package/skills/web-capture/templates/github-readme-tour.json +77 -0
  104. package/skills/web-capture/templates/github-repo-intro.json +74 -0
  105. package/skills/web-capture/version.json +2 -2
  106. package/dist/handlers/template-bind.d.ts +0 -15
  107. package/dist/handlers/template-bind.js +0 -149
  108. package/skills/gen-digital-human/scripts/gen_digital_human_video.py +0 -574
  109. package/skills/gen-video/scripts/gen_video.py +0 -591
  110. package/skills/template-bind/version.json +0 -7
  111. package/skills/web-capture/scripts/web_capture.py +0 -461
  112. /package/skills/{template-bind → template-registry}/scripts/check_contracts.py +0 -0
  113. /package/skills/{template-bind → template-registry}/scripts/render_job_client.py +0 -0
  114. /package/skills/{template-bind → template-registry}/scripts/sync_registry.py +0 -0
  115. /package/skills/{template-bind → template-registry}/video_dsl/runtime/dsl_validator.py +0 -0
  116. /package/skills/{template-bind → template-registry}/video_dsl/runtime/prompt_enhancer.py +0 -0
  117. /package/skills/{template-bind → template-registry}/video_dsl/runtime/template_binder.py +0 -0
  118. /package/skills/{template-bind → template-registry}/video_dsl/runtime/timeline_compiler.py +0 -0
  119. /package/skills/{template-bind → template-registry}/video_dsl/schema/render-plan-v1alpha1.json +0 -0
  120. /package/skills/{template-bind → template-registry}/video_dsl/schema/template-binding-v1alpha1.json +0 -0
  121. /package/skills/{template-bind → template-registry}/video_dsl/schema/template-definition-v1alpha1.json +0 -0
  122. /package/skills/{template-bind → template-registry}/video_dsl/schema/video-dsl-v1alpha1.json +0 -0
@@ -9,10 +9,9 @@
9
9
  * - Async response (generationId + status:'generating'/'pending') is polled
10
10
  * against /model/getImgStatus until completed/failed/timeout
11
11
  */
12
- import { readFileSync, existsSync, statSync } from 'node:fs';
13
- import path from 'node:path';
14
12
  import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
15
13
  import { emitProgress } from '../progress.js';
14
+ import { isTrue, resolveImageInput, toNumber, toStringArray } from './shared.js';
16
15
  const DEFAULT_IMAGE_MODEL = 'doubao/doubao-seedream-5-0-260128';
17
16
  // Maps aspect-ratio shorthand → (width, height) for Seedream models.
18
17
  const SIZE_PRESETS = {
@@ -23,13 +22,6 @@ const SIZE_PRESETS = {
23
22
  '3:4': [1728, 2304],
24
23
  '2:3': [1664, 2496],
25
24
  };
26
- const MIME_BY_EXT = {
27
- '.png': 'image/png',
28
- '.jpg': 'image/jpeg',
29
- '.jpeg': 'image/jpeg',
30
- '.webp': 'image/webp',
31
- '.gif': 'image/gif',
32
- };
33
25
  // Per-model reference-image caps (matches ab-api validation).
34
26
  const SEEDREAM_REF_LIMIT = 14;
35
27
  const GEMINI_REF_LIMIT = 4;
@@ -37,38 +29,6 @@ function isSeedreamModel(model) {
37
29
  const lower = model.toLowerCase();
38
30
  return lower.includes('seedream') || lower.includes('seedance');
39
31
  }
40
- function resolveImageInput(pathOrUrl) {
41
- const trimmed = pathOrUrl.trim();
42
- if (!trimmed)
43
- return '';
44
- const lower = trimmed.toLowerCase();
45
- if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('data:')) {
46
- return trimmed;
47
- }
48
- if (!existsSync(trimmed) || !statSync(trimmed).isFile()) {
49
- throw new SkillError(`❌ image path does not exist or is not a file: ${trimmed}`);
50
- }
51
- const ext = path.extname(trimmed).toLowerCase();
52
- const mime = MIME_BY_EXT[ext] ?? 'image/jpeg';
53
- const b64 = readFileSync(trimmed).toString('base64');
54
- return `data:${mime};base64,${b64}`;
55
- }
56
- /** Coerce a CLI flag value (string|string[]|boolean) into an array of strings. */
57
- function toStringArray(value) {
58
- if (value === undefined || value === null)
59
- return [];
60
- if (Array.isArray(value))
61
- return value.map(String);
62
- return [String(value)];
63
- }
64
- function toNumber(value, name) {
65
- if (value === undefined || value === null)
66
- return undefined;
67
- const n = Number(value);
68
- if (!Number.isFinite(n))
69
- throw new SkillError(`❌ ${name} must be a number, received: ${value}`);
70
- return n;
71
- }
72
32
  /** Resolve {width, height} for Seedream from --size (preset key or WxH). */
73
33
  function resolveSeedreamSize(size) {
74
34
  if (SIZE_PRESETS[size]) {
@@ -153,6 +113,10 @@ export async function genImage(input, ctxIn) {
153
113
  if (imageUrls.length === 0) {
154
114
  throw new SkillError(`❌ API response did not contain any image URL: ${JSON.stringify(data)}`);
155
115
  }
116
+ if (isTrue(input.json_output)) {
117
+ process.stdout.write(JSON.stringify({ urls: imageUrls }) + '\n');
118
+ return;
119
+ }
156
120
  process.stdout.write(`\n📸 Generated ${imageUrls.length} image(s)\n`);
157
121
  process.stdout.write(`\n🔗 Image URLs (renders directly, no download needed):\n`);
158
122
  for (const url of imageUrls)
@@ -0,0 +1,18 @@
1
+ /**
2
+ * gen-video handler — POST ab-api /model/genVideo, then poll /model/getVideoStatus.
3
+ *
4
+ * Ported from the previous `scripts/gen_video.py` (entry.type python →
5
+ * entry.type http) so it shares auth / base-URL / business-code / polling with
6
+ * the other atomic generators via http.ts instead of re-implementing them.
7
+ *
8
+ * Model families (aligned with the ab-web AI_VIDEO_MODELS dropdown):
9
+ * - Seedance 1.5 Pro (Volcano): audio, first/last frame, fixed camera,
10
+ * adaptive ratio, duration 4–12s
11
+ * - Veo 3.1 / Veo 3.1 Fast (Google): first/last frame, reference images,
12
+ * negative prompt, 4K, duration 4/6/8s
13
+ *
14
+ * Local image paths for first/last/reference frames are read and base64-encoded
15
+ * into a data: URI; https / data: URIs pass through unchanged.
16
+ */
17
+ import type { HandlerContext, HandlerInput } from './index.js';
18
+ export declare function genVideo(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * gen-video handler — POST ab-api /model/genVideo, then poll /model/getVideoStatus.
3
+ *
4
+ * Ported from the previous `scripts/gen_video.py` (entry.type python →
5
+ * entry.type http) so it shares auth / base-URL / business-code / polling with
6
+ * the other atomic generators via http.ts instead of re-implementing them.
7
+ *
8
+ * Model families (aligned with the ab-web AI_VIDEO_MODELS dropdown):
9
+ * - Seedance 1.5 Pro (Volcano): audio, first/last frame, fixed camera,
10
+ * adaptive ratio, duration 4–12s
11
+ * - Veo 3.1 / Veo 3.1 Fast (Google): first/last frame, reference images,
12
+ * negative prompt, 4K, duration 4/6/8s
13
+ *
14
+ * Local image paths for first/last/reference frames are read and base64-encoded
15
+ * into a data: URI; https / data: URIs pass through unchanged.
16
+ */
17
+ import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
18
+ import { emitProgress } from '../progress.js';
19
+ import { isTrue, resolveImageInput, toNumber, toStringArray } from './shared.js';
20
+ const DEFAULT_VIDEO_MODEL = 'doubao-seedance-1-5-pro-251215';
21
+ // CLI shorthand → full LiteLLM / gateway model id (matches the frontend dropdown).
22
+ const MODEL_ALIASES = {
23
+ seedance: 'doubao-seedance-1-5-pro-251215',
24
+ 'seedance-1.5': 'doubao-seedance-1-5-pro-251215',
25
+ 'seedance-1.5-pro': 'doubao-seedance-1-5-pro-251215',
26
+ veo: 'veo-3.1-generate-001',
27
+ 'veo-3.1': 'veo-3.1-generate-001',
28
+ 'veo-3.1-generate': 'veo-3.1-generate-001',
29
+ 'veo-fast': 'veo-3.1-fast-generate-001',
30
+ 'veo-3.1-fast': 'veo-3.1-fast-generate-001',
31
+ 'veo-3.1-fast-generate': 'veo-3.1-fast-generate-001',
32
+ };
33
+ const SEEDANCE_RATIOS = new Set(['adaptive', '16:9', '4:3', '1:1', '3:4', '9:16', '21:9']);
34
+ const SEEDANCE_RESOLUTIONS = new Set(['480p', '720p', '1080p']);
35
+ const VEO_RATIOS = new Set(['16:9', '9:16']);
36
+ const VEO_RESOLUTIONS = new Set(['720p', '1080p', '4k']);
37
+ const VEO_DURATIONS = new Set([4, 6, 8]);
38
+ const VEO_MAX_REFERENCE_IMAGES = 3;
39
+ function normalizeModelId(model) {
40
+ const key = model.trim().toLowerCase();
41
+ return MODEL_ALIASES[key] ?? model.trim();
42
+ }
43
+ function isSeedanceModel(model) {
44
+ return model.toLowerCase().includes('seedance');
45
+ }
46
+ function isVeoModel(model) {
47
+ return model.toLowerCase().includes('veo');
48
+ }
49
+ function validateParamsForModel(model, duration, ratio, resolution, refCount) {
50
+ if (isSeedanceModel(model)) {
51
+ if (duration < 4 || duration > 12) {
52
+ throw new SkillError(`❌ Seedance duration must be 4–12 s, got: ${duration}`);
53
+ }
54
+ if (!SEEDANCE_RATIOS.has(ratio)) {
55
+ throw new SkillError(`❌ Seedance aspect ratio must be one of: ${[...SEEDANCE_RATIOS].join(', ')}; got: ${ratio}`);
56
+ }
57
+ if (!SEEDANCE_RESOLUTIONS.has(resolution)) {
58
+ throw new SkillError(`❌ Seedance resolution must be one of: ${[...SEEDANCE_RESOLUTIONS].join(', ')}; got: ${resolution}`);
59
+ }
60
+ if (refCount > 0) {
61
+ throw new SkillError('❌ Seedance 1.5 Pro does not support a reference-image array (first / last frame only); use --first-frame / --last-frame instead');
62
+ }
63
+ }
64
+ else if (isVeoModel(model)) {
65
+ if (!VEO_DURATIONS.has(duration)) {
66
+ throw new SkillError(`❌ Veo duration must be one of: ${[...VEO_DURATIONS].join(', ')} s; got: ${duration}`);
67
+ }
68
+ if (!VEO_RATIOS.has(ratio)) {
69
+ throw new SkillError(`❌ Veo aspect ratio must be one of: ${[...VEO_RATIOS].join(', ')}; got: ${ratio}`);
70
+ }
71
+ if (!VEO_RESOLUTIONS.has(resolution)) {
72
+ throw new SkillError(`❌ Veo resolution must be one of: ${[...VEO_RESOLUTIONS].join(', ')}; got: ${resolution}`);
73
+ }
74
+ if (refCount > VEO_MAX_REFERENCE_IMAGES) {
75
+ throw new SkillError(`❌ Veo accepts at most ${VEO_MAX_REFERENCE_IMAGES} reference images; got: ${refCount}`);
76
+ }
77
+ }
78
+ }
79
+ async function pollVideoStatus(ctx, taskId) {
80
+ return pollUntil(async () => {
81
+ const data = await mmPost(ctx, '/model/getVideoStatus', { taskId }, { timeoutMs: 30_000 });
82
+ const status = data.status ?? 'unknown';
83
+ emitProgress({ phase: 'gen-video:poll', taskId, status });
84
+ if (status === 'succeeded' || status === 'completed') {
85
+ return data.videoUrl ?? '';
86
+ }
87
+ if (status === 'failed') {
88
+ const err = data.errorMsg ?? data.error ?? data.message ?? 'unknown error';
89
+ throw new SkillError(`❌ video generation failed: ${err}`);
90
+ }
91
+ return null;
92
+ }, { intervalMs: 10_000, timeoutMs: 300_000 });
93
+ }
94
+ export async function genVideo(input, ctxIn) {
95
+ const model = normalizeModelId(input.model ?? process.env.MM_VIDEO_MODEL ?? DEFAULT_VIDEO_MODEL);
96
+ const firstFrame = input.first_frame ? resolveImageInput(String(input.first_frame)) : '';
97
+ const lastFrame = input.last_frame ? resolveImageInput(String(input.last_frame)) : '';
98
+ const references = toStringArray(input.reference).map(resolveImageInput).filter(Boolean);
99
+ const prompt = (input.prompt ?? '').trim();
100
+ const hasVisual = Boolean(firstFrame || lastFrame || references.length > 0);
101
+ if (!prompt && !hasVisual) {
102
+ throw new SkillError('❌ Provide at least one of: a non-empty --prompt, or --first-frame / --last-frame / --reference');
103
+ }
104
+ const ratio = input.ratio ?? '16:9';
105
+ const resolution = input.resolution ?? '720p';
106
+ const duration = toNumber(input.duration, 'duration') ?? (isVeoModel(model) ? 8 : 5);
107
+ const seed = toNumber(input.seed, 'seed');
108
+ validateParamsForModel(model, duration, ratio, resolution, references.length);
109
+ const ctx = resolveHttpContext(ctxIn.skillName, {
110
+ apiBaseUrl: input.api_base_url,
111
+ privateToken: input.priv_token,
112
+ });
113
+ const payload = {
114
+ model,
115
+ duration,
116
+ ratio,
117
+ resolution,
118
+ generateAudio: isTrue(input.generate_audio),
119
+ cameraFixed: isTrue(input.camera_fixed),
120
+ };
121
+ if (prompt)
122
+ payload.prompt = prompt;
123
+ if (firstFrame)
124
+ payload.firstFrameImage = firstFrame;
125
+ if (lastFrame)
126
+ payload.lastFrameImage = lastFrame;
127
+ if (references.length > 0)
128
+ payload.referenceImages = references;
129
+ if (input.negative_prompt && String(input.negative_prompt).trim()) {
130
+ payload.negativePrompt = String(input.negative_prompt).trim();
131
+ }
132
+ if (seed !== undefined)
133
+ payload.seed = seed;
134
+ if (input.person_generation)
135
+ payload.personGeneration = String(input.person_generation);
136
+ emitProgress({ phase: 'gen-video:request', model, ratio, resolution, duration });
137
+ const data = await mmPost(ctx, '/model/genVideo', payload, { timeoutMs: 120_000 });
138
+ const taskId = data.taskId;
139
+ if (!taskId) {
140
+ throw new SkillError(`❌ could not parse the task id; API returned: ${JSON.stringify(data)}`);
141
+ }
142
+ emitProgress({ phase: 'gen-video:async', taskId });
143
+ const videoUrl = await pollVideoStatus(ctx, taskId);
144
+ if (!videoUrl) {
145
+ throw new SkillError(`❌ video generated but no URL was returned, task id: ${taskId}`);
146
+ }
147
+ if (isTrue(input.json_output)) {
148
+ process.stdout.write(JSON.stringify({ url: videoUrl }) + '\n');
149
+ return;
150
+ }
151
+ process.stdout.write(`\n🔗 Video URL (renders directly, no download needed):\n${videoUrl}\n`);
152
+ process.stdout.write(`\n🎉 Video generation done!\n`);
153
+ }
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { mmPost, resolveHttpContext, SkillError } from '../http.js';
16
16
  import { emitProgress } from '../progress.js';
17
+ import { isTrue, toNumber } from './shared.js';
17
18
  const DEFAULT_VOICE_ID = 'Chinese (Mandarin)_Male_Announcer';
18
19
  /**
19
20
  * Fallback voice catalog printed when `--list-voices --local` is passed.
@@ -27,17 +28,6 @@ const LOCAL_VOICE_CATALOG = [
27
28
  // TODO: replace once gen-voice service confirms an English default voice id.
28
29
  { id: 'english-male-friendly-01', language: 'en', name: 'Friendly Male (English, TBD)' },
29
30
  ];
30
- function toNumber(value, name, defaultValue) {
31
- if (value === undefined || value === null || value === '')
32
- return defaultValue;
33
- const n = Number(value);
34
- if (!Number.isFinite(n))
35
- throw new SkillError(`❌ ${name} must be a number, received: ${value}`);
36
- return n;
37
- }
38
- function isTrue(value) {
39
- return value === true || value === 'true';
40
- }
41
31
  async function listVoicesRemote(ctx) {
42
32
  const data = await mmPost(ctx, '/voice/page', { current: 1, pageSize: 100 }, { timeoutMs: 30_000 });
43
33
  const voices = data.list ?? [];
@@ -75,7 +65,7 @@ export async function genVoice(input, ctxIn) {
75
65
  throw new SkillError('❌ Please provide --text to specify the text to synthesize');
76
66
  }
77
67
  const voiceId = input.voice_id ?? DEFAULT_VOICE_ID;
78
- const speed = toNumber(input.speed, 'speed', 1.0);
68
+ const speed = toNumber(input.speed, 'speed') ?? 1.0;
79
69
  if (speed < 0.5 || speed > 2.0) {
80
70
  throw new SkillError('❌ speed must be in the range 0.5~2.0');
81
71
  }
@@ -6,9 +6,11 @@
6
6
  */
7
7
  import { genImage } from './gen-image.js';
8
8
  import { genVoice } from './gen-voice.js';
9
- import { templateBind } from './template-bind.js';
9
+ import { genVideo } from './gen-video.js';
10
+ import { genDigitalHuman } from './gen-digital-human.js';
10
11
  export const HANDLERS = {
11
12
  'gen-image': genImage,
12
13
  'gen-voice': genVoice,
13
- 'template-bind': templateBind,
14
+ 'gen-video': genVideo,
15
+ 'gen-digital-human': genDigitalHuman,
14
16
  };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared helpers for TS skill handlers.
3
+ *
4
+ * These were previously copy-pasted across gen-image / gen-video /
5
+ * gen-digital-human / gen-voice. Centralizing them keeps argument coercion and
6
+ * image-input resolution identical across every atomic generator.
7
+ *
8
+ * Lives under handlers/ (not src/ root) so the compiled `dist/handlers/shared.js`
9
+ * travels with the rest of the handler bundle when ab-agent copies `dist/`.
10
+ */
11
+ /** Truthy check tolerant of CLI string values (`--flag` → true, `--flag true` → 'true'). */
12
+ export declare function isTrue(value: unknown): boolean;
13
+ /**
14
+ * Coerce a flag value into a finite number, or `undefined` when absent/empty.
15
+ * Throws SkillError on a non-numeric value so callers fail fast with a clear msg.
16
+ */
17
+ export declare function toNumber(value: unknown, name: string): number | undefined;
18
+ /** Coerce a repeatable flag (string | string[] | undefined) into a string array. */
19
+ export declare function toStringArray(value: unknown): string[];
20
+ /**
21
+ * Resolve an image reference into a value the backend accepts:
22
+ * - http(s) URLs and data: URIs pass through unchanged
23
+ * - local file paths are read and base64-encoded into a `data:<mime>;base64,…` URI
24
+ * Throws SkillError when a local path does not exist or is not a file.
25
+ */
26
+ export declare function resolveImageInput(pathOrUrl: string): string;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Shared helpers for TS skill handlers.
3
+ *
4
+ * These were previously copy-pasted across gen-image / gen-video /
5
+ * gen-digital-human / gen-voice. Centralizing them keeps argument coercion and
6
+ * image-input resolution identical across every atomic generator.
7
+ *
8
+ * Lives under handlers/ (not src/ root) so the compiled `dist/handlers/shared.js`
9
+ * travels with the rest of the handler bundle when ab-agent copies `dist/`.
10
+ */
11
+ import { readFileSync, existsSync, statSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ import { SkillError } from '../http.js';
14
+ const MIME_BY_EXT = {
15
+ '.png': 'image/png',
16
+ '.jpg': 'image/jpeg',
17
+ '.jpeg': 'image/jpeg',
18
+ '.webp': 'image/webp',
19
+ '.gif': 'image/gif',
20
+ };
21
+ /** Truthy check tolerant of CLI string values (`--flag` → true, `--flag true` → 'true'). */
22
+ export function isTrue(value) {
23
+ return value === true || value === 'true';
24
+ }
25
+ /**
26
+ * Coerce a flag value into a finite number, or `undefined` when absent/empty.
27
+ * Throws SkillError on a non-numeric value so callers fail fast with a clear msg.
28
+ */
29
+ export function toNumber(value, name) {
30
+ if (value === undefined || value === null || value === '')
31
+ return undefined;
32
+ const n = Number(value);
33
+ if (!Number.isFinite(n))
34
+ throw new SkillError(`❌ ${name} must be a number, received: ${value}`);
35
+ return n;
36
+ }
37
+ /** Coerce a repeatable flag (string | string[] | undefined) into a string array. */
38
+ export function toStringArray(value) {
39
+ if (value === undefined || value === null)
40
+ return [];
41
+ if (Array.isArray(value))
42
+ return value.map(String);
43
+ return [String(value)];
44
+ }
45
+ /**
46
+ * Resolve an image reference into a value the backend accepts:
47
+ * - http(s) URLs and data: URIs pass through unchanged
48
+ * - local file paths are read and base64-encoded into a `data:<mime>;base64,…` URI
49
+ * Throws SkillError when a local path does not exist or is not a file.
50
+ */
51
+ export function resolveImageInput(pathOrUrl) {
52
+ const trimmed = pathOrUrl.trim();
53
+ if (!trimmed)
54
+ return '';
55
+ const lower = trimmed.toLowerCase();
56
+ if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('data:')) {
57
+ return trimmed;
58
+ }
59
+ if (!existsSync(trimmed) || !statSync(trimmed).isFile()) {
60
+ throw new SkillError(`❌ image path does not exist or is not a file: ${trimmed}`);
61
+ }
62
+ const ext = path.extname(trimmed).toLowerCase();
63
+ const mime = MIME_BY_EXT[ext] ?? 'image/jpeg';
64
+ const b64 = readFileSync(trimmed).toString('base64');
65
+ return `data:${mime};base64,${b64}`;
66
+ }
package/dist/http.d.ts CHANGED
@@ -43,8 +43,10 @@ export declare function mmPost<T = unknown>(ctx: HttpContext, pathOrUrl: string,
43
43
  timeoutMs?: number;
44
44
  }): Promise<T>;
45
45
  /**
46
- * Generic polling helper. Repeatedly invokes `probe()` every `intervalMs`
47
- * until it returns a non-null value or `timeoutMs` elapses.
46
+ * Generic polling helper. Invokes `probe()` immediately, then every
47
+ * `intervalMs`, until it returns a non-null value or `timeoutMs` elapses.
48
+ * Probing first (rather than sleeping first) avoids a wasted initial interval
49
+ * when the backend job is already complete.
48
50
  */
49
51
  export declare function pollUntil<T>(probe: () => Promise<T | null>, opts?: {
50
52
  intervalMs?: number;
package/dist/http.js CHANGED
@@ -93,18 +93,20 @@ export async function mmPost(ctx, pathOrUrl, body, opts = {}) {
93
93
  return parsed.data ?? undefined;
94
94
  }
95
95
  /**
96
- * Generic polling helper. Repeatedly invokes `probe()` every `intervalMs`
97
- * until it returns a non-null value or `timeoutMs` elapses.
96
+ * Generic polling helper. Invokes `probe()` immediately, then every
97
+ * `intervalMs`, until it returns a non-null value or `timeoutMs` elapses.
98
+ * Probing first (rather than sleeping first) avoids a wasted initial interval
99
+ * when the backend job is already complete.
98
100
  */
99
101
  export async function pollUntil(probe, opts = {}) {
100
102
  const intervalMs = opts.intervalMs ?? 5_000;
101
103
  const timeoutMs = opts.timeoutMs ?? 120_000;
102
104
  const start = Date.now();
103
105
  while (Date.now() - start < timeoutMs) {
104
- await new Promise((r) => setTimeout(r, intervalMs));
105
106
  const result = await probe();
106
107
  if (result !== null)
107
108
  return result;
109
+ await new Promise((r) => setTimeout(r, intervalMs));
108
110
  }
109
111
  throw new SkillError(`❌ polling timed out (${Math.round(timeoutMs / 1000)}s)`);
110
112
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0",
4
- "generatedAt": "2026-06-06T15:02:44.292Z",
3
+ "version": "0.1.1",
4
+ "generatedAt": "2026-06-08T05:59:50.921Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "export-jianying",
@@ -37,10 +37,10 @@
37
37
  "Use Jimeng or HiFly to drive an avatar"
38
38
  ],
39
39
  "entry": {
40
- "type": "python",
41
- "scriptPath": "scripts/gen_digital_human_video.py"
40
+ "type": "http",
41
+ "handler": "gen-digital-human"
42
42
  },
43
- "runtime": "python",
43
+ "runtime": "node",
44
44
  "envVars": [
45
45
  "PRIV_TOKEN",
46
46
  "MM_API_BASE_URL",
@@ -103,10 +103,10 @@
103
103
  "Image-to-video, first-frame / last-frame, reference-image-to-video"
104
104
  ],
105
105
  "entry": {
106
- "type": "python",
107
- "scriptPath": "scripts/gen_video.py"
106
+ "type": "http",
107
+ "handler": "gen-video"
108
108
  },
109
- "runtime": "python",
109
+ "runtime": "node",
110
110
  "envVars": [
111
111
  "PRIV_TOKEN",
112
112
  "MM_API_BASE_URL",
@@ -190,20 +190,18 @@
190
190
  ]
191
191
  },
192
192
  {
193
- "id": "template-bind",
193
+ "id": "template-registry",
194
194
  "tier": "orchestration",
195
- "title": "Template Registry (List-only)",
195
+ "title": "Video Template Registry",
196
196
  "summary": "List all available video templates (templateId / name / aspect ratio / style tags). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
197
197
  "triggers": [
198
- "View available templates / list every template",
199
- "Bind a template to a DSL (a template-id must be supplied)",
200
- "Agent has a Video DSL and needs a TemplateBinding"
198
+ "View available templates / list every template"
201
199
  ],
202
200
  "entry": {
203
- "type": "builtin",
204
- "handler": "template-bind"
201
+ "type": "python",
202
+ "scriptPath": "scripts/list_templates.py"
205
203
  },
206
- "runtime": "node",
204
+ "runtime": "python",
207
205
  "envVars": [
208
206
  "PRIV_TOKEN",
209
207
  "VIDEO_TEMPLATE_REGISTRY",
@@ -227,25 +225,30 @@
227
225
  },
228
226
  "runtime": "python",
229
227
  "envVars": [
228
+ "RENDER_API_URL",
230
229
  "PRIV_TOKEN",
231
- "MM_API_BASE_URL",
232
- "AGENT_NAME",
233
- "RENDER_API_URL"
230
+ "CONVERSATION_ID"
234
231
  ]
235
232
  },
236
233
  {
237
234
  "id": "web-capture",
238
235
  "tier": "tool",
239
236
  "title": "Web Page Capture",
240
- "summary": "Drive a headless browser (Playwright) to capture a web page: viewport/full-page screenshots, or a smooth top-to-bottom scrolling video. Produces PNG / MP4 assets that feed the screen-walkthrough template.",
237
+ "summary": "Drive a headless browser (Playwright Python) to capture any URL to local files: full-page / element / region screenshots, fixed-duration / condition-triggered / auto-scroll recordings, and storyboard videos (highlight / focus / zoom / scroll / virtual-cursor click / caption / title-card / arrow / numbered sequence / redact / code-line highlight). Two entry scripts: scripts/screenshot.py (stills) and scripts/record.py (webm video, storyboards, templates).",
241
238
  "triggers": [
242
- "Screenshot a web page / website / a specific URL, full-page screenshot",
243
- "Record a page scrolling top-to-bottom into a video, \"scroll video\" of a site",
244
- "Produce screen-recording / walkthrough footage of a GitHub repo, landing page, or product page"
239
+ "网页截图、网页截屏、整页截图、长截图、full page screenshot",
240
+ "截某个元素 / 区域、局部截屏、focus 某个区域",
241
+ "录屏、网页录制、录制视频、录一段操作、生成 webm",
242
+ "滚动录屏、页面从头划到尾的视频",
243
+ "多场景视频、分镜视频、storyboard、按时间线编排",
244
+ "演示点击 / 输入 / 悬停(虚拟鼠标 + 涟漪 / 打字机 / 触发 tooltip)",
245
+ "加箭头、加字幕条、加标题卡 / 片头片尾、按顺序编号高亮多个区域",
246
+ "截图前隐藏元素 / 涂盖打码、给静态截图加注释(红框 / 箭头 / 标签)",
247
+ "移动端 / 设备模拟、带 cookie / 登录态截屏 / 录屏"
245
248
  ],
246
249
  "entry": {
247
250
  "type": "python",
248
- "scriptPath": "scripts/web_capture.py"
251
+ "scriptPath": "scripts/screenshot.py"
249
252
  },
250
253
  "runtime": "python",
251
254
  "envVars": [
@@ -1,24 +1,13 @@
1
1
  /**
2
2
  * Skill registry — scan skills/ and load skill.json definitions.
3
3
  *
4
- * skill.json schema:
5
- * { name, toolName, description, parameters, entry?, scriptPath? }
6
- *
7
- * `entry` is the dispatcher hint. Missing entry falls back to the legacy
8
- * top-level scriptPath, treated as { type:'python', scriptPath } so older
9
- * skills keep working without a touch.
4
+ * Validation and entry-normalization live in `skill-schema.ts` (shared with
5
+ * the build-time manifest generator and smoke test). A record that fails
6
+ * validation is skipped with a warning rather than crashing the whole CLI.
10
7
  */
8
+ import { type SkillEntry } from './skill-schema.js';
11
9
  export declare const SKILLS_DIR: string;
12
- export type SkillEntry = {
13
- type: 'python';
14
- scriptPath: string;
15
- } | {
16
- type: 'http';
17
- handler: string;
18
- } | {
19
- type: 'builtin';
20
- handler: string;
21
- };
10
+ export type { SkillEntry } from './skill-schema.js';
22
11
  export interface SkillDef {
23
12
  name: string;
24
13
  toolName: string;
package/dist/registry.js CHANGED
@@ -1,29 +1,21 @@
1
1
  /**
2
2
  * Skill registry — scan skills/ and load skill.json definitions.
3
3
  *
4
- * skill.json schema:
5
- * { name, toolName, description, parameters, entry?, scriptPath? }
6
- *
7
- * `entry` is the dispatcher hint. Missing entry falls back to the legacy
8
- * top-level scriptPath, treated as { type:'python', scriptPath } so older
9
- * skills keep working without a touch.
4
+ * Validation and entry-normalization live in `skill-schema.ts` (shared with
5
+ * the build-time manifest generator and smoke test). A record that fails
6
+ * validation is skipped with a warning rather than crashing the whole CLI.
10
7
  */
11
8
  import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
12
9
  import path from 'node:path';
13
10
  import { fileURLToPath } from 'node:url';
11
+ import { normalizeEntry, validateSkillJson, } from './skill-schema.js';
14
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
13
  export const SKILLS_DIR = path.resolve(__dirname, '..', 'skills');
16
- function normalizeEntry(raw) {
17
- if (raw.entry)
18
- return raw.entry;
19
- if (raw.scriptPath)
20
- return { type: 'python', scriptPath: raw.scriptPath };
21
- return null;
22
- }
23
14
  function loadOne(skillDir) {
24
15
  const skillJsonPath = path.join(skillDir, 'skill.json');
25
16
  if (!existsSync(skillJsonPath))
26
17
  return null;
18
+ const skillId = path.basename(skillDir);
27
19
  let raw;
28
20
  try {
29
21
  raw = JSON.parse(readFileSync(skillJsonPath, 'utf-8'));
@@ -32,16 +24,18 @@ function loadOne(skillDir) {
32
24
  console.error(`[remixmate] failed to parse ${skillJsonPath}:`, err);
33
25
  return null;
34
26
  }
35
- const entry = normalizeEntry(raw);
36
- if (!entry) {
37
- console.warn(`[remixmate] skipping ${raw.name}: missing entry/scriptPath`);
27
+ const errors = validateSkillJson(raw, skillId);
28
+ if (errors.length > 0) {
29
+ console.warn(`[remixmate] skipping ${skillId}:\n${errors.map((e) => ` - ${e}`).join('\n')}`);
38
30
  return null;
39
31
  }
32
+ // validateSkillJson guarantees a resolvable entry.
33
+ const entry = normalizeEntry(raw);
40
34
  const def = {
41
35
  name: raw.name,
42
36
  toolName: raw.toolName,
43
37
  description: raw.description,
44
- parameters: raw.parameters,
38
+ parameters: raw.parameters ?? {},
45
39
  entry,
46
40
  skillDir,
47
41
  };