@remixmate/cli 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/capabilities.d.ts +56 -0
- package/dist/capabilities.js +75 -0
- package/dist/handlers/gen-digital-human.js +7 -7
- package/dist/handlers/gen-image.d.ts +12 -6
- package/dist/handlers/gen-image.js +35 -47
- package/dist/handlers/gen-video.d.ts +7 -8
- package/dist/handlers/gen-video.js +41 -65
- package/dist/handlers/gen-voice.d.ts +1 -1
- package/dist/handlers/gen-voice.js +9 -7
- package/dist/http.d.ts +2 -2
- package/dist/http.js +3 -3
- package/dist/manifest.json +2 -2
- package/package.json +7 -1
- package/skills/export-jianying/SKILL.md +1 -1
- package/skills/export-jianying/scripts/gen_jianying_draft.py +2 -2
- package/skills/gen-digital-human/SKILL.md +1 -1
- package/skills/gen-image/SKILL.md +1 -1
- package/skills/gen-video/SKILL.md +1 -1
- package/skills/gen-voice/SKILL.md +3 -3
- package/skills/gen-voice/version.json +1 -1
- package/skills/prepare-video-assets/SKILL.md +2 -2
- package/skills/render-video/SKILL.md +4 -4
- package/skills/render-video/scripts/_video_probe.py +4 -1
- package/skills/render-video/scripts/_vod_polling.py +1 -1
- package/skills/render-video/scripts/remote_renderer_client.py +9 -8
- package/skills/render-video/scripts/render_video.py +10 -4
- package/skills/render-video/scripts/upload_video.py +3 -3
- package/skills/template-registry/README.md +1 -1
- package/skills/template-registry/SKILL.md +3 -3
- package/skills/template-registry/scripts/check_contracts.py +8 -25
- package/skills/template-registry/scripts/match_template.py +100 -28
- package/skills/template-registry/scripts/registry_loader.py +3 -3
- package/skills/template-registry/scripts/render_job_client.py +2 -2
- package/skills/template-registry/scripts/verify_props_contract.py +273 -0
- package/skills/template-registry/video_dsl/README.md +0 -1
- package/skills/video-parser/SKILL.md +1 -1
- package/skills/video-parser/scripts/deconstruct_video.py +2 -2
- package/skills/video-parser/scripts/parse_via_render.py +2 -2
- package/skills/template-registry/video_dsl/schema/template-definition-v1alpha1.json +0 -247
package/README.md
CHANGED
|
@@ -188,6 +188,29 @@ npm run smoke
|
|
|
188
188
|
# CLI unit tests (argv parser + skill schema)
|
|
189
189
|
npm run test:cli
|
|
190
190
|
|
|
191
|
+
# ── Template regression (after changing binding / timeline / template code) ──
|
|
192
|
+
# L1 (contracts) + L2 (render-plan golden snapshots), full coverage, seconds,
|
|
193
|
+
# no render and no generation API calls. Run this before/after touching shared
|
|
194
|
+
# template plumbing to catch "did I break some OTHER template's binding?".
|
|
195
|
+
npm run regress
|
|
196
|
+
|
|
197
|
+
# Accept intended render-plan changes (rewrites L2 baselines):
|
|
198
|
+
npm run regress:update
|
|
199
|
+
|
|
200
|
+
# Run only the render-plan snapshot layer:
|
|
201
|
+
npm run test:render-plan
|
|
202
|
+
|
|
203
|
+
# L3 — selective, real Remotion render of chosen template(s). NOT full-library
|
|
204
|
+
# by design (rendering is slow + needs asset generation). Catches crashes /
|
|
205
|
+
# black frames the static layers can't. Needs PRIV_TOKEN for narration TTS;
|
|
206
|
+
# pass --stub-image-url to keep image-heavy renders cheap.
|
|
207
|
+
npm run check:render -- --template spotlight-card --frame
|
|
208
|
+
|
|
209
|
+
# Template defs come from the registry (ab-api HTTP + /tmp cache), exactly like
|
|
210
|
+
# render_video in production. A warm cache or reachable ab-api is required.
|
|
211
|
+
# In a multi-repo checkout the scripts auto-detect template-library; override
|
|
212
|
+
# with AB_TEMPLATES_SRC=/path/to/template-library/packages/templates/src.
|
|
213
|
+
|
|
191
214
|
# Python skills forward --help to the underlying script:
|
|
192
215
|
remixmate gen-script --help
|
|
193
216
|
remixmate prepare-video-assets --help
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model capabilities client.
|
|
3
|
+
*
|
|
4
|
+
* The model roster — internal model ids, friendly aliases, defaults, and each
|
|
5
|
+
* model's parameter constraints (aspect ratios / resolutions / durations /
|
|
6
|
+
* reference-image caps / Seedream size presets) — is owned by ab-api and served
|
|
7
|
+
* from POST /model/capabilities (public, no auth). The CLI fetches it at runtime
|
|
8
|
+
* instead of hardcoding it, so this npm package never ships the internal model
|
|
9
|
+
* matrix or provider details, and validation can't drift from the backend.
|
|
10
|
+
*
|
|
11
|
+
* Fetched once per process and memoized; the catalog is static server-side.
|
|
12
|
+
*/
|
|
13
|
+
export interface ModelConstraints {
|
|
14
|
+
aspectRatios?: string[];
|
|
15
|
+
resolutions?: string[];
|
|
16
|
+
durations?: number[];
|
|
17
|
+
durationMin?: number;
|
|
18
|
+
durationMax?: number;
|
|
19
|
+
durationDefault?: number;
|
|
20
|
+
refImageMax?: number;
|
|
21
|
+
sizes?: string[];
|
|
22
|
+
/** aspect-ratio → [width, height] pixel preset (Seedream). */
|
|
23
|
+
sizePresets?: Record<string, [number, number]>;
|
|
24
|
+
}
|
|
25
|
+
export interface ModelDescriptor {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
aliases?: string[];
|
|
29
|
+
default?: boolean;
|
|
30
|
+
constraints: ModelConstraints;
|
|
31
|
+
}
|
|
32
|
+
export interface VoiceCapabilities {
|
|
33
|
+
provider: string;
|
|
34
|
+
defaultVoiceId: string;
|
|
35
|
+
speedMin: number;
|
|
36
|
+
speedMax: number;
|
|
37
|
+
}
|
|
38
|
+
export interface DigitalHumanCapabilities {
|
|
39
|
+
sources: string[];
|
|
40
|
+
defaultVoiceId: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ModelCapabilities {
|
|
43
|
+
image: ModelDescriptor[];
|
|
44
|
+
video: ModelDescriptor[];
|
|
45
|
+
voice: VoiceCapabilities;
|
|
46
|
+
digitalHuman: DigitalHumanCapabilities;
|
|
47
|
+
}
|
|
48
|
+
/** Fetch (and memoize) the model capabilities catalog from ab-api. */
|
|
49
|
+
export declare function getCapabilities(apiBaseUrl?: string): Promise<ModelCapabilities>;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a user-supplied model (friendly alias or full id) against a model
|
|
52
|
+
* list. Returns the matched descriptor, or null when nothing matches.
|
|
53
|
+
*/
|
|
54
|
+
export declare function matchModel(list: ModelDescriptor[], input: string): ModelDescriptor | null;
|
|
55
|
+
/** The descriptor flagged `default` in a list, falling back to the first entry. */
|
|
56
|
+
export declare function defaultModel(list: ModelDescriptor[]): ModelDescriptor | undefined;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model capabilities client.
|
|
3
|
+
*
|
|
4
|
+
* The model roster — internal model ids, friendly aliases, defaults, and each
|
|
5
|
+
* model's parameter constraints (aspect ratios / resolutions / durations /
|
|
6
|
+
* reference-image caps / Seedream size presets) — is owned by ab-api and served
|
|
7
|
+
* from POST /model/capabilities (public, no auth). The CLI fetches it at runtime
|
|
8
|
+
* instead of hardcoding it, so this npm package never ships the internal model
|
|
9
|
+
* matrix or provider details, and validation can't drift from the backend.
|
|
10
|
+
*
|
|
11
|
+
* Fetched once per process and memoized; the catalog is static server-side.
|
|
12
|
+
*/
|
|
13
|
+
import { resolveApiBaseUrl, SkillError } from './http.js';
|
|
14
|
+
let cached = null;
|
|
15
|
+
/** Fetch (and memoize) the model capabilities catalog from ab-api. */
|
|
16
|
+
export async function getCapabilities(apiBaseUrl) {
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
const base = resolveApiBaseUrl(apiBaseUrl);
|
|
20
|
+
const url = `${base}/model/capabilities`;
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
23
|
+
let resp;
|
|
24
|
+
try {
|
|
25
|
+
resp = await fetch(url, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: '{}',
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
34
|
+
throw new SkillError(`❌ request timed out fetching model capabilities: ${url}`);
|
|
35
|
+
}
|
|
36
|
+
throw new SkillError(`❌ network error fetching model capabilities: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
}
|
|
41
|
+
const text = await resp.text();
|
|
42
|
+
if (!resp.ok) {
|
|
43
|
+
throw new SkillError(`❌ failed to fetch model capabilities (HTTP ${resp.status}): ${text}`);
|
|
44
|
+
}
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new SkillError(`❌ model capabilities response is not JSON: ${text.slice(0, 200)}`);
|
|
51
|
+
}
|
|
52
|
+
if (parsed.code !== 0 || !parsed.data) {
|
|
53
|
+
throw new SkillError(`❌ model capabilities request failed: ${parsed.msg ?? 'unknown error'} (code=${parsed.code})`);
|
|
54
|
+
}
|
|
55
|
+
cached = parsed.data;
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a user-supplied model (friendly alias or full id) against a model
|
|
60
|
+
* list. Returns the matched descriptor, or null when nothing matches.
|
|
61
|
+
*/
|
|
62
|
+
export function matchModel(list, input) {
|
|
63
|
+
const key = input.trim().toLowerCase();
|
|
64
|
+
for (const m of list) {
|
|
65
|
+
if (m.id.toLowerCase() === key)
|
|
66
|
+
return m;
|
|
67
|
+
if ((m.aliases ?? []).some((a) => a.toLowerCase() === key))
|
|
68
|
+
return m;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/** The descriptor flagged `default` in a list, falling back to the first entry. */
|
|
73
|
+
export function defaultModel(list) {
|
|
74
|
+
return list.find((m) => m.default) ?? list[0];
|
|
75
|
+
}
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
* http.ts. Endpoints and payloads match the backend DigitalHumanVideoGenerateDTO.
|
|
10
10
|
*/
|
|
11
11
|
import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
|
|
12
|
+
import { getCapabilities } from '../capabilities.js';
|
|
12
13
|
import { emitProgress } from '../progress.js';
|
|
13
14
|
import { isTrue, toNumber } from './shared.js';
|
|
14
|
-
const VALID_SOURCES = new Set(['jimeng', 'hifly']);
|
|
15
|
-
const DEFAULT_VOICE_ID = 'male-qn-qingse';
|
|
16
15
|
async function listAvatars(ctx, input) {
|
|
17
16
|
const payload = { current: 1, pageSize: 100 };
|
|
18
17
|
if (input.source)
|
|
@@ -51,10 +50,10 @@ async function fetchAvatarInfo(ctx, avatarId) {
|
|
|
51
50
|
}
|
|
52
51
|
throw new SkillError(`❌ avatar_id=${avatarId} not found; use --list-avatars [--mine] to see what is available`);
|
|
53
52
|
}
|
|
54
|
-
function resolveSource(explicit, avatar) {
|
|
53
|
+
function resolveSource(explicit, avatar, validSources) {
|
|
55
54
|
const src = (explicit ?? '').trim() || (avatar.source ?? '').trim();
|
|
56
|
-
if (!
|
|
57
|
-
throw new SkillError(
|
|
55
|
+
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.`);
|
|
58
57
|
}
|
|
59
58
|
return src;
|
|
60
59
|
}
|
|
@@ -117,9 +116,10 @@ export async function genDigitalHuman(input, ctxIn) {
|
|
|
117
116
|
throw new SkillError('❌ in TTS mode provide --text, or use --audio-url for audio-driven mode');
|
|
118
117
|
}
|
|
119
118
|
}
|
|
119
|
+
const dh = (await getCapabilities(input.api_base_url)).digitalHuman;
|
|
120
120
|
emitProgress({ phase: 'gen-digital-human:resolve-avatar', avatarId });
|
|
121
121
|
const avatar = await fetchAvatarInfo(ctx, avatarId);
|
|
122
|
-
const source = resolveSource(input.source, avatar);
|
|
122
|
+
const source = resolveSource(input.source, avatar, dh.sources);
|
|
123
123
|
const payload = { avatarId, source };
|
|
124
124
|
if (audioUrl) {
|
|
125
125
|
payload.audioUrl = audioUrl;
|
|
@@ -128,7 +128,7 @@ export async function genDigitalHuman(input, ctxIn) {
|
|
|
128
128
|
}
|
|
129
129
|
else {
|
|
130
130
|
payload.text = text;
|
|
131
|
-
payload.voiceId = input.voice_id ??
|
|
131
|
+
payload.voiceId = input.voice_id ?? dh.defaultVoiceId;
|
|
132
132
|
if (input.voice_name)
|
|
133
133
|
payload.voiceName = String(input.voice_name);
|
|
134
134
|
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gen-image handler — POST ab-api /model/genImg.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
4
|
+
* Thin forwarder: the model roster, defaults, size presets and reference-image
|
|
5
|
+
* caps all come from ab-api's capabilities catalog (see capabilities.ts), not
|
|
6
|
+
* from this package. Behavior:
|
|
7
|
+
* - --model accepts a friendly alias or a full id; resolved against the catalog
|
|
8
|
+
* (ab-api also resolves aliases server-side, so an unknown value still passes
|
|
9
|
+
* through and is validated by the backend).
|
|
10
|
+
* - --size accepts a WxH string (→ width/height) or an aspect-ratio preset key
|
|
11
|
+
* (→ aspectRatio; for Seedream models ab-api maps the preset to pixels).
|
|
12
|
+
* - --resolution maps to the Gemini `size` tier (1K/2K/4K); ignored by others.
|
|
13
|
+
* - Reference images: HTTPS URLs and data: URIs pass through; local paths are
|
|
14
|
+
* read and base64-encoded into a data: URI.
|
|
9
15
|
* - Async response (generationId + status:'generating'/'pending') is polled
|
|
10
|
-
* against /model/getImgStatus until completed/failed/timeout
|
|
16
|
+
* against /model/getImgStatus until completed/failed/timeout.
|
|
11
17
|
*/
|
|
12
18
|
import type { HandlerContext, HandlerInput } from './index.js';
|
|
13
19
|
export declare function genImage(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
|
|
@@ -1,46 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gen-image handler — POST ab-api /model/genImg.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
4
|
+
* Thin forwarder: the model roster, defaults, size presets and reference-image
|
|
5
|
+
* caps all come from ab-api's capabilities catalog (see capabilities.ts), not
|
|
6
|
+
* from this package. Behavior:
|
|
7
|
+
* - --model accepts a friendly alias or a full id; resolved against the catalog
|
|
8
|
+
* (ab-api also resolves aliases server-side, so an unknown value still passes
|
|
9
|
+
* through and is validated by the backend).
|
|
10
|
+
* - --size accepts a WxH string (→ width/height) or an aspect-ratio preset key
|
|
11
|
+
* (→ aspectRatio; for Seedream models ab-api maps the preset to pixels).
|
|
12
|
+
* - --resolution maps to the Gemini `size` tier (1K/2K/4K); ignored by others.
|
|
13
|
+
* - Reference images: HTTPS URLs and data: URIs pass through; local paths are
|
|
14
|
+
* read and base64-encoded into a data: URI.
|
|
9
15
|
* - Async response (generationId + status:'generating'/'pending') is polled
|
|
10
|
-
* against /model/getImgStatus until completed/failed/timeout
|
|
16
|
+
* against /model/getImgStatus until completed/failed/timeout.
|
|
11
17
|
*/
|
|
12
18
|
import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
|
|
19
|
+
import { getCapabilities, matchModel, defaultModel } from '../capabilities.js';
|
|
13
20
|
import { emitProgress } from '../progress.js';
|
|
14
21
|
import { isTrue, resolveImageInput, toNumber, toStringArray } from './shared.js';
|
|
15
|
-
const DEFAULT_IMAGE_MODEL = 'doubao/doubao-seedream-5-0-260128';
|
|
16
|
-
// Maps aspect-ratio shorthand → (width, height) for Seedream models.
|
|
17
|
-
const SIZE_PRESETS = {
|
|
18
|
-
'1:1': [2048, 2048],
|
|
19
|
-
'16:9': [2848, 1600],
|
|
20
|
-
'9:16': [1600, 2848],
|
|
21
|
-
'4:3': [2304, 1728],
|
|
22
|
-
'3:4': [1728, 2304],
|
|
23
|
-
'2:3': [1664, 2496],
|
|
24
|
-
};
|
|
25
|
-
// Per-model reference-image caps (matches ab-api validation).
|
|
26
|
-
const SEEDREAM_REF_LIMIT = 14;
|
|
27
|
-
const GEMINI_REF_LIMIT = 4;
|
|
28
|
-
function isSeedreamModel(model) {
|
|
29
|
-
const lower = model.toLowerCase();
|
|
30
|
-
return lower.includes('seedream') || lower.includes('seedance');
|
|
31
|
-
}
|
|
32
|
-
/** Resolve {width, height} for Seedream from --size (preset key or WxH). */
|
|
33
|
-
function resolveSeedreamSize(size) {
|
|
34
|
-
if (SIZE_PRESETS[size]) {
|
|
35
|
-
const [w, h] = SIZE_PRESETS[size];
|
|
36
|
-
return { width: w, height: h };
|
|
37
|
-
}
|
|
38
|
-
if (/^\d+x\d+$/i.test(size)) {
|
|
39
|
-
const [w, h] = size.toLowerCase().split('x').map((v) => parseInt(v, 10));
|
|
40
|
-
return { width: w, height: h };
|
|
41
|
-
}
|
|
42
|
-
throw new SkillError(`❌ Seedream model does not support size format: ${size} (use 1:1 / 9:16 / 16:9 ... or WxH like 1600x2848)`);
|
|
43
|
-
}
|
|
44
22
|
async function pollImageStatus(ctx, generationId) {
|
|
45
23
|
return pollUntil(async () => {
|
|
46
24
|
const data = await mmPost(ctx, '/model/getImgStatus', { generationId }, { timeoutMs: 30_000 });
|
|
@@ -61,11 +39,13 @@ export async function genImage(input, ctxIn) {
|
|
|
61
39
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
62
40
|
throw new SkillError('❌ Please provide --prompt to describe the image');
|
|
63
41
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
42
|
+
const apiBaseUrl = input.api_base_url;
|
|
43
|
+
const caps = await getCapabilities(apiBaseUrl);
|
|
44
|
+
// Resolve the model against the catalog: explicit alias/id, env override, or default.
|
|
45
|
+
const requested = input.model ?? process.env.MM_IMAGE_MODEL;
|
|
46
|
+
const descriptor = requested ? matchModel(caps.image, requested) : defaultModel(caps.image);
|
|
47
|
+
// Send the resolved id when known; otherwise forward the raw value for the backend to handle.
|
|
48
|
+
const model = descriptor?.id ?? requested ?? defaultModel(caps.image)?.id ?? '';
|
|
69
49
|
const size = input.size ?? '1:1';
|
|
70
50
|
const n = toNumber(input.n, 'n') ?? 1;
|
|
71
51
|
const resolution = input.resolution ?? '1K';
|
|
@@ -77,11 +57,10 @@ export async function genImage(input, ctxIn) {
|
|
|
77
57
|
// when ab-agent's buildCliArgs passes booleans, only the truthy flag appears.
|
|
78
58
|
// Treat presence as true; explicit `--no-watermark` isn't supported here.
|
|
79
59
|
const watermark = input.watermark === true ? true : undefined;
|
|
80
|
-
const seedream = isSeedreamModel(model);
|
|
81
60
|
const references = toStringArray(input.reference).map(resolveImageInput).filter(Boolean);
|
|
82
|
-
const refLimit =
|
|
83
|
-
if (references.length > refLimit) {
|
|
84
|
-
throw new SkillError(`❌ ${
|
|
61
|
+
const refLimit = descriptor?.constraints.refImageMax;
|
|
62
|
+
if (refLimit !== undefined && references.length > refLimit) {
|
|
63
|
+
throw new SkillError(`❌ ${descriptor?.label ?? model} accepts at most ${refLimit} reference images, got ${references.length}`);
|
|
85
64
|
}
|
|
86
65
|
const payload = { model, prompt, imageCount: n };
|
|
87
66
|
if (guidanceScale !== undefined)
|
|
@@ -96,13 +75,22 @@ export async function genImage(input, ctxIn) {
|
|
|
96
75
|
payload.seed = seed;
|
|
97
76
|
if (watermark !== undefined)
|
|
98
77
|
payload.watermark = watermark;
|
|
99
|
-
|
|
100
|
-
|
|
78
|
+
// --size as WxH → explicit pixels; otherwise treat as an aspect-ratio preset.
|
|
79
|
+
// ab-api maps an aspect-ratio preset to pixels for Seedream and uses it directly
|
|
80
|
+
// for Gemini, so the CLI carries no per-model size table.
|
|
81
|
+
if (/^\d+x\d+$/i.test(size)) {
|
|
82
|
+
const [w, h] = size.toLowerCase().split('x').map((v) => parseInt(v, 10));
|
|
83
|
+
payload.width = w;
|
|
84
|
+
payload.height = h;
|
|
101
85
|
}
|
|
102
86
|
else {
|
|
103
87
|
payload.aspectRatio = size;
|
|
104
|
-
payload.size = resolution;
|
|
105
88
|
}
|
|
89
|
+
payload.size = resolution;
|
|
90
|
+
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
91
|
+
apiBaseUrl,
|
|
92
|
+
privateToken: input.priv_token,
|
|
93
|
+
});
|
|
106
94
|
emitProgress({ phase: 'gen-image:request', model, size, n });
|
|
107
95
|
const data = await mmPost(ctx, '/model/genImg', payload, { timeoutMs: 120_000 });
|
|
108
96
|
let imageUrls = (data.imageUrls ?? []).filter((u) => !!u);
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gen-video handler — POST ab-api /model/genVideo, then poll /model/getVideoStatus.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Thin forwarder: the model roster, friendly aliases, defaults and per-model
|
|
5
|
+
* constraints (aspect ratios / resolutions / durations / reference-image caps)
|
|
6
|
+
* come from ab-api's capabilities catalog (see capabilities.ts), not from this
|
|
7
|
+
* package. Pre-flight validation here is purely data-driven from that catalog —
|
|
8
|
+
* the CLI carries no model matrix — and ab-api re-validates authoritatively.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
10
|
+
* --model accepts a friendly alias or a full id; ab-api also resolves aliases
|
|
11
|
+
* server-side, so an unknown value still passes through to the backend.
|
|
13
12
|
*
|
|
14
13
|
* Local image paths for first/last/reference frames are read and base64-encoded
|
|
15
14
|
* into a data: URI; https / data: URIs pass through unchanged.
|
|
@@ -1,79 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gen-video handler — POST ab-api /model/genVideo, then poll /model/getVideoStatus.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Thin forwarder: the model roster, friendly aliases, defaults and per-model
|
|
5
|
+
* constraints (aspect ratios / resolutions / durations / reference-image caps)
|
|
6
|
+
* come from ab-api's capabilities catalog (see capabilities.ts), not from this
|
|
7
|
+
* package. Pre-flight validation here is purely data-driven from that catalog —
|
|
8
|
+
* the CLI carries no model matrix — and ab-api re-validates authoritatively.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
10
|
+
* --model accepts a friendly alias or a full id; ab-api also resolves aliases
|
|
11
|
+
* server-side, so an unknown value still passes through to the backend.
|
|
13
12
|
*
|
|
14
13
|
* Local image paths for first/last/reference frames are read and base64-encoded
|
|
15
14
|
* into a data: URI; https / data: URIs pass through unchanged.
|
|
16
15
|
*/
|
|
17
16
|
import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
|
|
17
|
+
import { getCapabilities, matchModel, defaultModel } from '../capabilities.js';
|
|
18
18
|
import { emitProgress } from '../progress.js';
|
|
19
19
|
import { isTrue, resolveImageInput, toNumber, toStringArray } from './shared.js';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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');
|
|
20
|
+
/** Generic, catalog-driven validation. No model names live here. */
|
|
21
|
+
function validateAgainstConstraints(descriptor, duration, ratio, resolution, refCount) {
|
|
22
|
+
const c = descriptor.constraints;
|
|
23
|
+
const label = descriptor.label || descriptor.id;
|
|
24
|
+
if (c.durations && c.durations.length > 0) {
|
|
25
|
+
if (!c.durations.includes(duration)) {
|
|
26
|
+
throw new SkillError(`❌ ${label} duration must be one of: ${c.durations.join(', ')} s; got: ${duration}`);
|
|
62
27
|
}
|
|
63
28
|
}
|
|
64
|
-
else if (
|
|
65
|
-
if (
|
|
66
|
-
throw new SkillError(`❌
|
|
29
|
+
else if (c.durationMin !== undefined && c.durationMax !== undefined) {
|
|
30
|
+
if (duration < c.durationMin || duration > c.durationMax) {
|
|
31
|
+
throw new SkillError(`❌ ${label} duration must be ${c.durationMin}–${c.durationMax} s, got: ${duration}`);
|
|
67
32
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
33
|
+
}
|
|
34
|
+
if (c.aspectRatios && c.aspectRatios.length > 0 && !c.aspectRatios.includes(ratio)) {
|
|
35
|
+
throw new SkillError(`❌ ${label} aspect ratio must be one of: ${c.aspectRatios.join(', ')}; got: ${ratio}`);
|
|
36
|
+
}
|
|
37
|
+
if (c.resolutions && c.resolutions.length > 0 && !c.resolutions.includes(resolution)) {
|
|
38
|
+
throw new SkillError(`❌ ${label} resolution must be one of: ${c.resolutions.join(', ')}; got: ${resolution}`);
|
|
39
|
+
}
|
|
40
|
+
if (c.refImageMax !== undefined && refCount > c.refImageMax) {
|
|
41
|
+
if (c.refImageMax === 0) {
|
|
42
|
+
throw new SkillError(`❌ ${label} does not support a reference-image array; use --first-frame / --last-frame instead`);
|
|
76
43
|
}
|
|
44
|
+
throw new SkillError(`❌ ${label} accepts at most ${c.refImageMax} reference images; got: ${refCount}`);
|
|
77
45
|
}
|
|
78
46
|
}
|
|
79
47
|
async function pollVideoStatus(ctx, taskId) {
|
|
@@ -92,7 +60,12 @@ async function pollVideoStatus(ctx, taskId) {
|
|
|
92
60
|
}, { intervalMs: 10_000, timeoutMs: 300_000 });
|
|
93
61
|
}
|
|
94
62
|
export async function genVideo(input, ctxIn) {
|
|
95
|
-
const
|
|
63
|
+
const apiBaseUrl = input.api_base_url;
|
|
64
|
+
const caps = await getCapabilities(apiBaseUrl);
|
|
65
|
+
// Resolve the model against the catalog: explicit alias/id, env override, or default.
|
|
66
|
+
const requested = input.model ?? process.env.MM_VIDEO_MODEL;
|
|
67
|
+
const descriptor = requested ? matchModel(caps.video, requested) : defaultModel(caps.video);
|
|
68
|
+
const model = descriptor?.id ?? requested ?? defaultModel(caps.video)?.id ?? '';
|
|
96
69
|
const firstFrame = input.first_frame ? resolveImageInput(String(input.first_frame)) : '';
|
|
97
70
|
const lastFrame = input.last_frame ? resolveImageInput(String(input.last_frame)) : '';
|
|
98
71
|
const references = toStringArray(input.reference).map(resolveImageInput).filter(Boolean);
|
|
@@ -103,11 +76,14 @@ export async function genVideo(input, ctxIn) {
|
|
|
103
76
|
}
|
|
104
77
|
const ratio = input.ratio ?? '16:9';
|
|
105
78
|
const resolution = input.resolution ?? '720p';
|
|
106
|
-
const duration = toNumber(input.duration, 'duration') ??
|
|
79
|
+
const duration = toNumber(input.duration, 'duration') ?? descriptor?.constraints.durationDefault ?? 5;
|
|
107
80
|
const seed = toNumber(input.seed, 'seed');
|
|
108
|
-
|
|
81
|
+
// Pre-flight validation only when we recognize the model (have its constraints).
|
|
82
|
+
if (descriptor) {
|
|
83
|
+
validateAgainstConstraints(descriptor, duration, ratio, resolution, references.length);
|
|
84
|
+
}
|
|
109
85
|
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
110
|
-
apiBaseUrl
|
|
86
|
+
apiBaseUrl,
|
|
111
87
|
privateToken: input.priv_token,
|
|
112
88
|
});
|
|
113
89
|
const payload = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* gen-voice handler — Minimax TTS via ab-api /tool/
|
|
2
|
+
* gen-voice handler — Minimax TTS via ab-api unified /tool/tts (provider=minimax).
|
|
3
3
|
*
|
|
4
4
|
* Operations:
|
|
5
5
|
* --list-voices true → GET available voice IDs from /voice/page (live)
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* handler stateless.
|
|
14
14
|
*/
|
|
15
15
|
import { mmPost, resolveHttpContext, SkillError } from '../http.js';
|
|
16
|
+
import { getCapabilities } from '../capabilities.js';
|
|
16
17
|
import { emitProgress } from '../progress.js';
|
|
17
18
|
import { isTrue, toNumber } from './shared.js';
|
|
18
|
-
const DEFAULT_VOICE_ID = 'Chinese (Mandarin)_Male_Announcer';
|
|
19
19
|
/**
|
|
20
20
|
* Fallback voice catalog printed when `--list-voices --local` is passed.
|
|
21
21
|
* Mirrors the language-keyed defaults that gen_script.py uses, kept in sync
|
|
@@ -64,17 +64,19 @@ export async function genVoice(input, ctxIn) {
|
|
|
64
64
|
if (!text || typeof text !== 'string' || !text.trim()) {
|
|
65
65
|
throw new SkillError('❌ Please provide --text to specify the text to synthesize');
|
|
66
66
|
}
|
|
67
|
-
const
|
|
67
|
+
const apiBaseUrl = input.api_base_url;
|
|
68
|
+
const voice = (await getCapabilities(apiBaseUrl)).voice;
|
|
69
|
+
const voiceId = input.voice_id ?? voice.defaultVoiceId;
|
|
68
70
|
const speed = toNumber(input.speed, 'speed') ?? 1.0;
|
|
69
|
-
if (speed <
|
|
70
|
-
throw new SkillError(
|
|
71
|
+
if (speed < voice.speedMin || speed > voice.speedMax) {
|
|
72
|
+
throw new SkillError(`❌ speed must be in the range ${voice.speedMin}~${voice.speedMax}`);
|
|
71
73
|
}
|
|
72
74
|
const ctx = await resolveHttpContext(ctxIn.skillName, {
|
|
73
|
-
apiBaseUrl
|
|
75
|
+
apiBaseUrl,
|
|
74
76
|
privateToken: input.priv_token,
|
|
75
77
|
});
|
|
76
78
|
emitProgress({ phase: 'gen-voice:request', voiceId, speed });
|
|
77
|
-
const data = await mmPost(ctx, '/tool/
|
|
79
|
+
const data = await mmPost(ctx, '/tool/tts', { provider: voice.provider, text, voiceSetting: { voiceId, speed }, outputFormat: 'url' }, { timeoutMs: 120_000 });
|
|
78
80
|
const audioUrl = data.audio;
|
|
79
81
|
if (!audioUrl) {
|
|
80
82
|
throw new SkillError(`❌ API response did not contain an audio URL: ${JSON.stringify(data)}`);
|
package/dist/http.d.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Base URL resolution:
|
|
16
16
|
* 1. opts.apiBaseUrl — caller override
|
|
17
|
-
* 2. process.env.MM_API_BASE_URL — process env
|
|
18
|
-
* 3.
|
|
17
|
+
* 2. process.env.MM_API_BASE_URL — process env (point at local/staging here)
|
|
18
|
+
* 3. https://api.remixmate.com/api — production default (zero-config)
|
|
19
19
|
*/
|
|
20
20
|
export declare class SkillError extends Error {
|
|
21
21
|
readonly exitCode: number;
|
package/dist/http.js
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Base URL resolution:
|
|
16
16
|
* 1. opts.apiBaseUrl — caller override
|
|
17
|
-
* 2. process.env.MM_API_BASE_URL — process env
|
|
18
|
-
* 3.
|
|
17
|
+
* 2. process.env.MM_API_BASE_URL — process env (point at local/staging here)
|
|
18
|
+
* 3. https://api.remixmate.com/api — production default (zero-config)
|
|
19
19
|
*/
|
|
20
20
|
import { resolvePrivToken, NOT_AUTHENTICATED_HINT } from './auth/resolve.js';
|
|
21
21
|
import { attemptAutoLogin } from './auth/auto-login.js';
|
|
22
|
-
const DEFAULT_API_BASE_URL = '
|
|
22
|
+
const DEFAULT_API_BASE_URL = 'https://api.remixmate.com/api';
|
|
23
23
|
export class SkillError extends Error {
|
|
24
24
|
exitCode;
|
|
25
25
|
constructor(message, exitCode = 1) {
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "AI media generation skills for Claude Code / Codex — 11 skills covering image, video, voice, digital human, web capture, script, template registry, rendering, Jianying export, and video deconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
"test:cli": "npm run build && node --test test/*.test.mjs",
|
|
23
23
|
"test:validators": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-validators.py",
|
|
24
24
|
"test:template-pipeline": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-template-pipeline.py",
|
|
25
|
+
"test:props-contract": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-props-contract.py",
|
|
26
|
+
"test:render-plan": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-render-plan-snapshot.py",
|
|
27
|
+
"test:render-plan:update": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-render-plan-snapshot.py --update",
|
|
25
28
|
"test:contracts": "PYTHONDONTWRITEBYTECODE=1 python3 skills/template-registry/scripts/check_contracts.py",
|
|
29
|
+
"regress": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/regress.py",
|
|
30
|
+
"regress:update": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/regress.py --update",
|
|
31
|
+
"check:render": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/render-template-check.py",
|
|
26
32
|
"clean": "node scripts/clean.mjs"
|
|
27
33
|
},
|
|
28
34
|
"keywords": [
|