@remixmate/cli 0.9.15 → 0.9.17

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 (48) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/capabilities.d.ts +5 -1
  4. package/dist/cli.js +12 -0
  5. package/dist/doctor.js +21 -0
  6. package/dist/manifest.json +13 -13
  7. package/dist/project/commands.d.ts +19 -0
  8. package/dist/project/commands.js +156 -0
  9. package/dist/project/host.d.ts +77 -0
  10. package/dist/project/host.js +106 -0
  11. package/dist/project/resolve.d.ts +43 -0
  12. package/dist/project/resolve.js +65 -0
  13. package/dist/project/store.d.ts +31 -0
  14. package/dist/project/store.js +97 -0
  15. package/dist/project/take.d.ts +46 -0
  16. package/dist/project/take.js +102 -0
  17. package/dist/registry.d.ts +4 -0
  18. package/dist/registry.js +2 -0
  19. package/dist/runner.js +59 -0
  20. package/dist/skill-schema.d.ts +14 -0
  21. package/dist/skill-schema.js +15 -0
  22. package/package.json +1 -1
  23. package/skills/export-jianying/version.json +1 -1
  24. package/skills/gen-digital-human/skill.json +78 -11
  25. package/skills/gen-digital-human/version.json +1 -1
  26. package/skills/gen-image/SKILL.md +40 -18
  27. package/skills/gen-image/skill.json +71 -12
  28. package/skills/gen-image/version.json +1 -1
  29. package/skills/gen-script/SKILL.md +17 -21
  30. package/skills/gen-script/version.json +1 -1
  31. package/skills/gen-video/SKILL.md +5 -3
  32. package/skills/gen-video/skill.json +77 -13
  33. package/skills/gen-video/version.json +1 -1
  34. package/skills/gen-voice/SKILL.md +1 -1
  35. package/skills/gen-voice/skill.json +34 -8
  36. package/skills/gen-voice/version.json +1 -1
  37. package/skills/prepare-video-assets/skill.json +43 -9
  38. package/skills/prepare-video-assets/version.json +1 -1
  39. package/skills/render-video/skill.json +43 -8
  40. package/skills/render-video/version.json +1 -1
  41. package/skills/template-registry/version.json +1 -1
  42. package/skills/video-parser/skill.json +29 -7
  43. package/skills/video-parser/version.json +1 -1
  44. package/skills/web-record/SKILL.md +131 -133
  45. package/skills/web-record/skill.json +137 -33
  46. package/skills/web-screenshot/SKILL.md +93 -96
  47. package/skills/web-screenshot/skill.json +66 -16
  48. package/skills/web-screenshot/version.json +1 -1
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Persistent project binding — which Project should this machine's CLI output
3
+ * belong to?
4
+ *
5
+ * Stored at ~/.config/remixmate/projects.json, NOT in credentials.json: that
6
+ * file is a 0600 secret store keyed by API base URL, with different lifetime
7
+ * and different blast radius on corruption.
8
+ *
9
+ * Two layers, because host cwd semantics are not uniform:
10
+ * - `byPath` — keyed by absolute cwd. Hosts that run inside the user's real
11
+ * repo (Codex, Claude Code) get per-repo bindings.
12
+ * - `default` — a single fallback. Hosts that spawn a throwaway working
13
+ * directory per conversation (WorkBuddy uses
14
+ * ~/WorkBuddy/<timestamp>) would never hit a byPath entry, so a
15
+ * cwd-only design silently degrades to nothing for them.
16
+ *
17
+ * See docs/cli-project-binding-design.md §3.
18
+ */
19
+ export declare const CONFIG_DIR: string;
20
+ export declare const PROJECTS_FILE: string;
21
+ export interface StoredBinding {
22
+ projectId: string;
23
+ /** Which layer matched — surfaced by `project show` / `doctor`. */
24
+ scope: 'path' | 'default';
25
+ }
26
+ /** Look up the binding for a cwd: exact byPath match first, then the default. */
27
+ export declare function readBinding(apiBaseUrl: string, cwd?: string): Promise<StoredBinding | null>;
28
+ /** Bind a project to this cwd (`scope: 'path'`) or as the fallback (`'default'`). */
29
+ export declare function writeBinding(apiBaseUrl: string, projectId: string, scope: 'path' | 'default', cwd?: string): Promise<void>;
30
+ /** Drop a binding. Returns true when something was actually removed. */
31
+ export declare function clearBinding(apiBaseUrl: string, scope: 'path' | 'default', cwd?: string): Promise<boolean>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Persistent project binding — which Project should this machine's CLI output
3
+ * belong to?
4
+ *
5
+ * Stored at ~/.config/remixmate/projects.json, NOT in credentials.json: that
6
+ * file is a 0600 secret store keyed by API base URL, with different lifetime
7
+ * and different blast radius on corruption.
8
+ *
9
+ * Two layers, because host cwd semantics are not uniform:
10
+ * - `byPath` — keyed by absolute cwd. Hosts that run inside the user's real
11
+ * repo (Codex, Claude Code) get per-repo bindings.
12
+ * - `default` — a single fallback. Hosts that spawn a throwaway working
13
+ * directory per conversation (WorkBuddy uses
14
+ * ~/WorkBuddy/<timestamp>) would never hit a byPath entry, so a
15
+ * cwd-only design silently degrades to nothing for them.
16
+ *
17
+ * See docs/cli-project-binding-design.md §3.
18
+ */
19
+ import { homedir } from 'node:os';
20
+ import { dirname, join, resolve } from 'node:path';
21
+ import { promises as fs } from 'node:fs';
22
+ export const CONFIG_DIR = join(homedir(), '.config', 'remixmate');
23
+ export const PROJECTS_FILE = join(CONFIG_DIR, 'projects.json');
24
+ /**
25
+ * Must build a NEW object every call, not spread a shared constant: `backends`
26
+ * would then be one object aliased by every "empty" read, and writeBinding
27
+ * mutates it — so a later read of a missing/corrupt file would hand back
28
+ * bindings accumulated earlier in the process instead of nothing.
29
+ */
30
+ function emptyFile() {
31
+ return { version: 1, backends: {} };
32
+ }
33
+ async function readFile() {
34
+ try {
35
+ const raw = await fs.readFile(PROJECTS_FILE, 'utf-8');
36
+ const parsed = JSON.parse(raw);
37
+ if (!parsed || typeof parsed !== 'object' || !parsed.backends)
38
+ return emptyFile();
39
+ return parsed;
40
+ }
41
+ catch {
42
+ // Missing or corrupt → behave as unbound. A broken binding file must never
43
+ // block a render; the resolution ladder just falls through to the default project.
44
+ return emptyFile();
45
+ }
46
+ }
47
+ async function writeFile(data) {
48
+ await fs.mkdir(dirname(PROJECTS_FILE), { recursive: true, mode: 0o700 });
49
+ await fs.writeFile(PROJECTS_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
50
+ }
51
+ /** Look up the binding for a cwd: exact byPath match first, then the default. */
52
+ export async function readBinding(apiBaseUrl, cwd = process.cwd()) {
53
+ const file = await readFile();
54
+ const entry = file.backends[apiBaseUrl];
55
+ if (!entry)
56
+ return null;
57
+ const byPath = entry.byPath?.[resolve(cwd)];
58
+ if (byPath)
59
+ return { projectId: byPath, scope: 'path' };
60
+ if (entry.default)
61
+ return { projectId: entry.default, scope: 'default' };
62
+ return null;
63
+ }
64
+ /** Bind a project to this cwd (`scope: 'path'`) or as the fallback (`'default'`). */
65
+ export async function writeBinding(apiBaseUrl, projectId, scope, cwd = process.cwd()) {
66
+ const file = await readFile();
67
+ const entry = file.backends[apiBaseUrl] ?? {};
68
+ if (scope === 'default') {
69
+ entry.default = projectId;
70
+ }
71
+ else {
72
+ entry.byPath = { ...(entry.byPath ?? {}), [resolve(cwd)]: projectId };
73
+ }
74
+ file.backends[apiBaseUrl] = entry;
75
+ await writeFile(file);
76
+ }
77
+ /** Drop a binding. Returns true when something was actually removed. */
78
+ export async function clearBinding(apiBaseUrl, scope, cwd = process.cwd()) {
79
+ const file = await readFile();
80
+ const entry = file.backends[apiBaseUrl];
81
+ if (!entry)
82
+ return false;
83
+ let removed = false;
84
+ if (scope === 'default') {
85
+ removed = entry.default != null;
86
+ delete entry.default;
87
+ }
88
+ else {
89
+ const key = resolve(cwd);
90
+ removed = entry.byPath?.[key] != null;
91
+ if (entry.byPath)
92
+ delete entry.byPath[key];
93
+ }
94
+ if (removed)
95
+ await writeFile(file);
96
+ return removed;
97
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Take creation — turn a render invocation into a VideoProject the web app can
3
+ * show, and hand its conversation id down to the skill.
4
+ *
5
+ * Why this exists at all: ab-web derives a video's project from
6
+ * `file.conversation_id → media_video_project → media_project`. A render whose
7
+ * upload carries no conversation id is stored correctly but belongs to nothing,
8
+ * so it shows up only in the raw asset list. The cloud agent injects
9
+ * CONVERSATION_ID for its own runs; locally, nobody did — that is the bug this
10
+ * module fixes.
11
+ *
12
+ * Idempotency lives on the server (`/video-project/ensure` is get-or-create on
13
+ * `(user_id, client_session_key)`), not in a local state file: a pipeline is N
14
+ * separate CLI processes, sometimes concurrent, and a local cache would have to
15
+ * solve staleness and write races that a unique index solves for free.
16
+ *
17
+ * See docs/cli-project-binding-design.md §5.
18
+ */
19
+ import { type HttpContext } from '../http.js';
20
+ export interface EnsuredTake {
21
+ conversationId: string;
22
+ videoProjectId: string;
23
+ projectId: string;
24
+ created: boolean;
25
+ attempt: number;
26
+ }
27
+ /**
28
+ * Look up the take for the current grouping key without creating anything.
29
+ *
30
+ * Used by pipeline skills that should attach to a take when one exists but must
31
+ * never bring one into being: someone running `remixmate gen-image` on its own
32
+ * isn't making a video, and an empty VideoProject is worse clutter than an
33
+ * unattributed image.
34
+ *
35
+ * Deliberately does NOT resolve a project — resolveProject()'s last rung
36
+ * lazily *creates* 「未分类」, which a read-only lookup has no business doing.
37
+ */
38
+ export declare function findTake(ctx: HttpContext): Promise<EnsuredTake | null>;
39
+ /**
40
+ * Resolve project + grouping key, then get-or-create the take.
41
+ * Returns null when this invocation shouldn't own a take.
42
+ */
43
+ export declare function ensureTake(ctx: HttpContext, opts: {
44
+ jobId?: number;
45
+ flagProjectId?: string;
46
+ }): Promise<EnsuredTake | null>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Take creation — turn a render invocation into a VideoProject the web app can
3
+ * show, and hand its conversation id down to the skill.
4
+ *
5
+ * Why this exists at all: ab-web derives a video's project from
6
+ * `file.conversation_id → media_video_project → media_project`. A render whose
7
+ * upload carries no conversation id is stored correctly but belongs to nothing,
8
+ * so it shows up only in the raw asset list. The cloud agent injects
9
+ * CONVERSATION_ID for its own runs; locally, nobody did — that is the bug this
10
+ * module fixes.
11
+ *
12
+ * Idempotency lives on the server (`/video-project/ensure` is get-or-create on
13
+ * `(user_id, client_session_key)`), not in a local state file: a pipeline is N
14
+ * separate CLI processes, sometimes concurrent, and a local cache would have to
15
+ * solve staleness and write races that a unique index solves for free.
16
+ *
17
+ * See docs/cli-project-binding-design.md §5.
18
+ */
19
+ import { mmPost } from '../http.js';
20
+ import { resolveTakeKey } from './host.js';
21
+ import { resolveProject } from './resolve.js';
22
+ /**
23
+ * Pull title / aspectRatio / templateId off the persisted render job.
24
+ *
25
+ * The VideoProject row needs them at creation time, but this runs in Node
26
+ * before the Python renderer is spawned, so the DSL itself isn't in reach —
27
+ * `render_jobs.dsl_meta` is the summary of exactly those fields. Best-effort:
28
+ * a take with a placeholder title beats no take at all.
29
+ */
30
+ async function fetchJobMeta(ctx, jobId) {
31
+ try {
32
+ const detail = await mmPost(ctx, '/renderJob/get', { jobId });
33
+ return detail?.dslMeta ?? {};
34
+ }
35
+ catch {
36
+ return {};
37
+ }
38
+ }
39
+ /**
40
+ * Look up the take for the current grouping key without creating anything.
41
+ *
42
+ * Used by pipeline skills that should attach to a take when one exists but must
43
+ * never bring one into being: someone running `remixmate gen-image` on its own
44
+ * isn't making a video, and an empty VideoProject is worse clutter than an
45
+ * unattributed image.
46
+ *
47
+ * Deliberately does NOT resolve a project — resolveProject()'s last rung
48
+ * lazily *creates* 「未分类」, which a read-only lookup has no business doing.
49
+ */
50
+ export async function findTake(ctx) {
51
+ const takeKey = resolveTakeKey(undefined);
52
+ if (!takeKey.key)
53
+ return null;
54
+ const res = await mmPost(ctx, '/video-project/find-by-session', {
55
+ clientSessionKey: takeKey.key,
56
+ });
57
+ if (!res?.conversationId)
58
+ return null;
59
+ return {
60
+ conversationId: res.conversationId,
61
+ videoProjectId: res.id,
62
+ projectId: res.projectId ?? '',
63
+ created: false,
64
+ attempt: res.attempt ?? 0,
65
+ };
66
+ }
67
+ /**
68
+ * Resolve project + grouping key, then get-or-create the take.
69
+ * Returns null when this invocation shouldn't own a take.
70
+ */
71
+ export async function ensureTake(ctx, opts) {
72
+ const jobId = opts.jobId && opts.jobId > 0 ? opts.jobId : undefined;
73
+ const takeKey = resolveTakeKey(jobId);
74
+ // No grouping key at all → don't create. Only reachable for a take-creating
75
+ // skill that has no jobId (prepare-video-assets) on a host that exposes no
76
+ // session id. Creating here would produce an ungrouped take that the later
77
+ // render-video call — which *does* have a jobId, hence the `job:N` key —
78
+ // could not join, leaving two takes for one video.
79
+ if (!takeKey.key)
80
+ return null;
81
+ const { projectId } = await resolveProject(ctx, { flagProjectId: opts.flagProjectId });
82
+ const meta = jobId ? await fetchJobMeta(ctx, jobId) : {};
83
+ const res = await mmPost(ctx, '/video-project/ensure', {
84
+ projectId,
85
+ clientSessionKey: takeKey.key,
86
+ clientHost: takeKey.host,
87
+ jobId: jobId ?? 0,
88
+ title: meta?.title ?? '',
89
+ archetype: meta?.archetype ?? '',
90
+ aspectRatio: meta?.aspectRatio ?? '',
91
+ templateId: meta?.templateId ?? '',
92
+ });
93
+ if (!res?.conversationId)
94
+ return null;
95
+ return {
96
+ conversationId: res.conversationId,
97
+ videoProjectId: res.id,
98
+ projectId,
99
+ created: res.created,
100
+ attempt: res.attempt,
101
+ };
102
+ }
@@ -16,6 +16,10 @@ export interface SkillDef {
16
16
  entry: SkillEntry;
17
17
  /** Authorization requirement enforced by the dispatcher before invocation. */
18
18
  auth: SkillAuthMode;
19
+ /** Ensure a take (VideoProject + conversation) before running — see skill-schema.ts. */
20
+ createsTake: boolean;
21
+ /** Attach to an existing take, never create one — see skill-schema.ts. */
22
+ joinsTake: boolean;
19
23
  skillDir: string;
20
24
  /** Absolute path to the python entry script (only when entry.type === 'python'). */
21
25
  scriptAbsolutePath?: string;
package/dist/registry.js CHANGED
@@ -38,6 +38,8 @@ function loadOne(skillDir) {
38
38
  parameters: raw.parameters ?? {},
39
39
  entry,
40
40
  auth: resolveAuthMode(raw),
41
+ createsTake: raw.createsTake === true,
42
+ joinsTake: raw.joinsTake === true,
41
43
  skillDir,
42
44
  };
43
45
  if (entry.type === 'python') {
package/dist/runner.js CHANGED
@@ -16,6 +16,8 @@ import { HANDLERS } from './handlers/index.js';
16
16
  import { EXIT, SkillError } from './errors.js';
17
17
  import { authChildEnv, ensureAuth } from './auth/ensure.js';
18
18
  import { emitBillingFooter } from './billing.js';
19
+ import { resolveHttpContext } from './http.js';
20
+ import { ensureTake, findTake } from './project/take.js';
19
21
  /** Read the token override accepted by both the TS handlers and the Python skills. */
20
22
  function tokenFlag(args) {
21
23
  const value = args.token ?? args.priv_token;
@@ -33,6 +35,7 @@ export async function runSkill(skillName, opts) {
33
35
  apiBaseUrl: typeof opts.parsedArgs.api_base_url === 'string' ? opts.parsedArgs.api_base_url : undefined,
34
36
  flagToken: tokenFlag(opts.parsedArgs),
35
37
  });
38
+ await applyTakeContext(skill, opts, auth);
36
39
  switch (skill.entry.type) {
37
40
  case 'python':
38
41
  // Python children talk to ab-api themselves and print their own billing
@@ -58,6 +61,62 @@ export async function runSkill(skillName, opts) {
58
61
  emitBillingFooter();
59
62
  }
60
63
  }
64
+ /**
65
+ * Attach this invocation to a take, publishing the result as `CONVERSATION_ID`.
66
+ * That variable is the only thing that makes an uploaded asset reachable from
67
+ * the web app — it rides the upload as `x-conversation-id` and lands in
68
+ * `file.conversation_id`.
69
+ *
70
+ * Writes `process.env` rather than returning a child-env patch, because the two
71
+ * dispatch paths consume it differently: spawned Python inherits the process
72
+ * environment, while http/builtin handlers run in-process and read it via
73
+ * resolveHttpContext(). Four of the seven take-joining skills are in-process
74
+ * handlers, so a child-env-only patch would silently do nothing for them.
75
+ * Safe here because the CLI runs exactly one skill per process.
76
+ *
77
+ * Two deliberate non-behaviours:
78
+ * - An already-set CONVERSATION_ID is left alone. The cloud agent injects its
79
+ * own, and so does our own parent when prepare-video-assets shells out to
80
+ * gen-image / gen-voice. Re-deriving would fork the conversation.
81
+ * - Any failure is downgraded to a warning. Attribution is a convenience;
82
+ * losing it must never cost the user a render they already paid for. The
83
+ * asset still uploads, just unattributed — exactly the old behaviour.
84
+ */
85
+ async function applyTakeContext(skill, opts, auth) {
86
+ if (!skill.createsTake && !skill.joinsTake)
87
+ return;
88
+ if ((process.env.CONVERSATION_ID ?? '').trim())
89
+ return;
90
+ if (!auth.token)
91
+ return;
92
+ try {
93
+ const ctx = await resolveHttpContext(skill.name, {
94
+ apiBaseUrl: auth.apiBaseUrl,
95
+ preflightToken: auth.token,
96
+ });
97
+ let take;
98
+ if (skill.createsTake) {
99
+ const jobIdArg = opts.parsedArgs.job_id;
100
+ const jobId = typeof jobIdArg === 'number' ? jobIdArg : Number(jobIdArg);
101
+ const flagProject = opts.parsedArgs.project;
102
+ take = await ensureTake(ctx, {
103
+ jobId: Number.isFinite(jobId) ? jobId : undefined,
104
+ flagProjectId: typeof flagProject === 'string' ? flagProject : undefined,
105
+ });
106
+ }
107
+ else {
108
+ take = await findTake(ctx);
109
+ }
110
+ if (!take)
111
+ return;
112
+ process.env.CONVERSATION_ID = take.conversationId;
113
+ process.stdout.write(`📁 take ${take.created ? 'created' : 'reused'}: ${take.videoProjectId} ` +
114
+ `(project ${take.projectId}${take.attempt ? `, attempt ${take.attempt}` : ''})\n`);
115
+ }
116
+ catch (err) {
117
+ process.stderr.write(`⚠️ 无法归属本次产物(内容仍会正常生成,但不会出现在项目里): ${err.message}\n`);
118
+ }
119
+ }
61
120
  async function runPython(skill, rawArgs, auth) {
62
121
  if (!skill.scriptAbsolutePath) {
63
122
  throw new SkillError(`skill ${skill.name} has entry.type=python but no scriptAbsolutePath`);
@@ -58,6 +58,20 @@ export interface RawSkillJson {
58
58
  scriptPath?: string;
59
59
  entry?: SkillEntry;
60
60
  envVars?: string[];
61
+ /**
62
+ * Declares that this skill produces a finished video, so the dispatcher should
63
+ * ensure a "take" (VideoProject + conversation) before running it and inject the
64
+ * resulting CONVERSATION_ID. Declarative rather than a hardcoded skill name in
65
+ * runner.ts, so the pipeline's shape stays visible in the skill's own manifest.
66
+ */
67
+ createsTake?: boolean;
68
+ /**
69
+ * Declares that this skill contributes to a video but must never *start* one:
70
+ * attach to an existing take if there is one, otherwise carry on unattributed.
71
+ * A standalone `gen-image` run isn't a video, and an empty VideoProject is
72
+ * worse clutter than an unattributed asset.
73
+ */
74
+ joinsTake?: boolean;
61
75
  }
62
76
  /**
63
77
  * Resolve a skill's auth mode, defaulting for records that predate the field.
@@ -61,6 +61,21 @@ export function validateSkillJson(raw, skillId) {
61
61
  if (raw.auth != null && !AUTH_VALUES.includes(raw.auth)) {
62
62
  errors.push(`skill.json.auth must be one of ${AUTH_VALUES.join(' | ')}, got '${raw.auth}'`);
63
63
  }
64
+ if (raw.createsTake != null && typeof raw.createsTake !== 'boolean') {
65
+ errors.push(`skill.json.createsTake must be a boolean, got '${String(raw.createsTake)}'`);
66
+ }
67
+ if (raw.joinsTake != null && typeof raw.joinsTake !== 'boolean') {
68
+ errors.push(`skill.json.joinsTake must be a boolean, got '${String(raw.joinsTake)}'`);
69
+ }
70
+ if (raw.createsTake === true && raw.joinsTake === true) {
71
+ errors.push("skill.json cannot set both 'createsTake' and 'joinsTake' — createsTake already implies joining");
72
+ }
73
+ // Attribution means calling ab-api as a specific user. An `auth: none` skill
74
+ // never touches the backend and produces no server-side file, so there is
75
+ // nothing to attribute — the flag would be dead config that reads as working.
76
+ if ((raw.createsTake === true || raw.joinsTake === true) && resolveAuthMode(raw) === 'none') {
77
+ errors.push("skill.json declares createsTake/joinsTake but auth='none' — a purely local skill has nothing to attribute");
78
+ }
64
79
  if (raw.name && raw.name !== skillId) {
65
80
  errors.push(`skill.json.name='${raw.name}' does not match directory name '${skillId}'`);
66
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
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",
@@ -3,5 +3,5 @@
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "554",
5
5
  "version": "V3",
6
- "skillDescription": "剪映草稿生成技能,将素材URL打包为剪映可导入的草稿ZIP,支持从 RenderPlan 自动转换(调用 ab-api /file/generateJianYing)。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- 导出剪映、剪映草稿、打包剪映、导入剪映\n- 将素材导出为剪映格式、生成剪映工程\n- 把视频/图片/音频打包成剪映草稿\n- RenderPlan 导出剪映草稿\n\n即使用户没有明确说「剪映」,只要他们想要将素材打包为可在剪映中编辑的草稿格式,也要使用本 skill"
6
+ "skillDescription": "Jianying (CapCut) draft-generation skill. Packages asset URLs into a draft ZIP that Jianying can import; supports automatic conversion from a RenderPlan (calls ab-api /file/generateJianYing).\n\nUse this skill as soon as the user mentions any of these intents:\n- Export to Jianying, Jianying draft, package for Jianying, import into Jianying\n- Export materials to the Jianying format, generate a Jianying project\n- Bundle video / image / audio into a Jianying draft\n- Export a Jianying draft from a RenderPlan\n\nEven when the user does not say \"Jianying\" explicitly, use this skill whenever they want to package materials into a draft that can be edited in Jianying."
7
7
  }
@@ -6,20 +6,87 @@
6
6
  "title": "Digital-Human Talking-Head",
7
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.",
8
8
  "auth": "required",
9
- "envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME"],
10
- "entry": { "type": "http", "handler": "gen-digital-human" },
9
+ "joinsTake": true,
10
+ "envVars": [
11
+ "PRIV_TOKEN",
12
+ "MM_API_BASE_URL",
13
+ "AGENT_NAME"
14
+ ],
15
+ "entry": {
16
+ "type": "http",
17
+ "handler": "gen-digital-human"
18
+ },
11
19
  "parameters": {
12
20
  "type": "object",
13
21
  "properties": {
14
- "list_avatars": { "type": "boolean", "description": "List available digital-human avatars" },
15
- "source": { "type": "string", "enum": ["jimeng", "hifly"], "description": "Filter by source" },
16
- "gender": { "type": "string", "enum": ["male", "female"], "description": "Filter by gender" },
17
- "avatar_id": { "type": "number", "description": "Avatar id" },
18
- "text": { "type": "string", "description": "Narration text (TTS mode)" },
19
- "audio_url": { "type": "string", "description": "Audio URL (audio-driven mode)" },
20
- "voice_id": { "type": "string", "description": "Voice id" },
21
- "aspect_ratio": { "type": "string", "description": "Aspect ratio" },
22
- "json_output": { "type": "boolean", "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }) instead of human-readable output" }
22
+ "list_avatars": {
23
+ "type": "boolean",
24
+ "description": "List available digital-human avatars"
25
+ },
26
+ "mine": {
27
+ "type": "boolean",
28
+ "description": "With list_avatars=true: list the caller's own custom avatars instead of the public catalog. Custom avatars do not appear without this."
29
+ },
30
+ "name": {
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."
41
+ },
42
+ "gender": {
43
+ "type": "string",
44
+ "enum": [
45
+ "male",
46
+ "female"
47
+ ],
48
+ "description": "Filter by gender"
49
+ },
50
+ "avatar_id": {
51
+ "type": "number",
52
+ "description": "Avatar id"
53
+ },
54
+ "text": {
55
+ "type": "string",
56
+ "description": "Narration text (TTS mode)"
57
+ },
58
+ "audio_url": {
59
+ "type": "string",
60
+ "description": "Audio URL (audio-driven mode)"
61
+ },
62
+ "voice_id": {
63
+ "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."
65
+ },
66
+ "voice_name": {
67
+ "type": "string",
68
+ "description": "Voice display name, recorded alongside voice_id for bookkeeping. Does not affect synthesis."
69
+ },
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
+ "check_status": {
79
+ "type": "boolean",
80
+ "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."
81
+ },
82
+ "generation_id": {
83
+ "type": "number",
84
+ "description": "Job id to poll (required when check_status=true)"
85
+ },
86
+ "json_output": {
87
+ "type": "boolean",
88
+ "description": "Emit a JSON result (generate: { url, generationId }; list: { avatars }; check-status: { status, url }) instead of human-readable output"
89
+ }
23
90
  },
24
91
  "required": []
25
92
  }
@@ -3,5 +3,5 @@
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "336",
5
5
  "version": "V8",
6
- "skillDescription": "数字人口播视频技能,支持查询形象、TTS 口播、音频驱动口播和查询生成状态(调用 ab-api 数字人接口,即梦 / 飞影)。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- 数字人、数字人视频、数字人口播、生成数字人视频\n- 用户提供已有音频 URL(MP3 等)、用外链音频驱动数字人口型\n- AI 主播、虚拟主播、虚拟人物视频、口播视频\n- 让数字人说话、让虚拟人说一段话、让 AI 人物播报\n- 查看/列出数字人形象、有哪些数字人可以用\n- 使用即梦/飞影数字人\n\n即使用户没有明确说「使用 AI」,只要他们想要让一个虚拟人物朗读/播报一段文字并生成视频,也要使用本 skill。"
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."
7
7
  }
@@ -23,21 +23,28 @@ Wraps ab-api's `POST /model/genImg` (the same endpoint the web studio uses), aut
23
23
 
24
24
  ## Models and sizes
25
25
 
26
- Aligned with the gen-image handler's model presets and the backend `ModelGenImgDTO`:
27
-
28
- | LiteLLM `model` | Display name | Provider |
29
- |-----------------|--------------|----------|
30
- | `doubao/doubao-seedream-4-5-251128` | Seedream 4.5 | Volcano |
31
- | `doubao/doubao-seedream-5-0-260128` | Seedream 5.0 Lite | Volcano |
32
- | `gemini-3-pro-image` | Gemini 3 Pro | Google |
33
- | `gemini-3.1-flash-image-preview` | Gemini 3.1 Flash | Google |
34
-
35
- - **Seedream**: `--size` is an aspect ratio (e.g. `1:1`, `9:16`) or `WxH`; the backend may auto-upscale the 4.5 model to meet a minimum pixel count.
36
- - **Gemini**: `--size` is a backend-allowed aspect ratio (e.g. `1:1`, `16:9`); add `--resolution`: `1K` / `2K` / `4K` (default `1K`).
26
+ Pass `--model` a short name. The authoritative roster ids, aliases, per-model limits —
27
+ lives in the backend catalog (`/model/capabilities`), which the CLI fetches at runtime;
28
+ the names below are the stable aliases to use.
29
+
30
+ | `--model` | What it is | Reference images |
31
+ |-----------|------------|------------------|
32
+ | `seedream` | Default. General-purpose, highest output resolution. | up to 14 |
33
+ | `seedream-pro` | High-fidelity variant: better placement/element control, more faithful text rendering. Costs more per image. | up to 10 |
34
+ | `gemini` | Gemini 3 Pro. | up to 4 |
35
+
36
+ - **Seedream**: `--size` is an aspect ratio (e.g. `1:1`, `9:16`) or `WxH`. The backend maps the
37
+ ratio to that model's own pixel preset and rescales out-of-range sizes, so prefer a ratio
38
+ over explicit pixels.
39
+ - **`seedream-pro`** additionally supports `3:2` / `2:3` / `21:9`, and caps output at ~2K
40
+ (about 4.6 MP). Asking it for 4K pixels gets scaled down, not rejected — use `seedream`
41
+ when you need a genuinely larger image.
42
+ - **Gemini**: `--size` is a backend-allowed aspect ratio (e.g. `1:1`, `16:9`); add
43
+ `--resolution`: `1K` / `2K` / `4K` (default `1K`). `--resolution` is ignored by Seedream.
37
44
 
38
45
  ## Auth & environment
39
46
 
40
- No skill-local env file — the executing process inherits the system environment. Examples say `python`; on macOS you may need `python3`.
47
+ No skill-local env file — the executing process inherits the system environment.
41
48
 
42
49
  - **Enterprise OpenClaw**: auth is already injected, **no need** for `PRIV_TOKEN` / `--priv-token`.
43
50
  - **Other environments**: configure the token. Without a token, non-interactive runs fail; interactive ones may prompt.
@@ -45,7 +52,7 @@ No skill-local env file — the executing process inherits the system environmen
45
52
  | Env var | Description | Default |
46
53
  |---------|-------------|---------|
47
54
  | `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
48
- | `MM_IMAGE_MODEL` | Default model id | `doubao/doubao-seedream-4-5-251128` |
55
+ | `MM_IMAGE_MODEL` | Default model, as a `--model` value | backend default (`seedream`) |
49
56
  | `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.com/api` |
50
57
  | `AGENT_NAME` | Optional `x-invoke-agent` header | none |
51
58
 
@@ -67,17 +74,30 @@ remixmate gen-image \
67
74
  ```bash
68
75
  remixmate gen-image \
69
76
  --prompt "<image description>" \
70
- --model gemini-3-pro-image \
77
+ --model gemini \
71
78
  --size "16:9" \
72
79
  --resolution "2K"
73
80
  ```
74
81
 
82
+ ```bash
83
+ # High-fidelity: precise placement, legible on-image text
84
+ remixmate gen-image \
85
+ --prompt "<image description>" \
86
+ --model seedream-pro \
87
+ --size "16:9"
88
+ ```
89
+
75
90
  ### Image-to-image (reference image)
76
91
 
77
92
  Reference images accept local file paths, HTTPS URLs, or data URIs. Pass `--reference` multiple times for multiple references.
78
93
 
79
- - **Seedream**: up to **14** reference images, `--image-strength` controls reference influence.
80
- - **Gemini**: up to **4** reference images.
94
+ - **`seedream`**: up to **14** reference images, `--image-strength` controls reference influence.
95
+ - **`seedream-pro`**: up to **10** reference images. Best choice when the edit has to land in a
96
+ specific spot — describe the target region in the prompt (e.g. "in the marked area at the
97
+ bottom left") and it holds position far better than `seedream`.
98
+ - **`gemini`**: up to **4** reference images.
99
+
100
+ Over-the-limit runs fail fast in the CLI, before spending credits.
81
101
 
82
102
  ```bash
83
103
  # URL reference
@@ -109,7 +129,7 @@ remixmate gen-image \
109
129
  | Flag | Description | Default |
110
130
  |------|-------------|---------|
111
131
  | `-p` / `--prompt` | Description (required) | — |
112
- | `-m` / `--model` | Model id | see `MM_IMAGE_MODEL` |
132
+ | `-m` / `--model` | `seedream` / `seedream-pro` / `gemini` | see `MM_IMAGE_MODEL` |
113
133
  | `-s` / `--size` | Seedream: ratio or WxH; Gemini: ratio | `1:1` |
114
134
  | `--resolution` | Gemini only: `1K` / `2K` / `4K` | `1K` |
115
135
  | `-n` | Number of images, 1–4 | `1` |
@@ -124,7 +144,9 @@ remixmate gen-image \
124
144
 
125
145
  ## Credits
126
146
 
127
- Every run charges credits. The CLI prints a footer on stdout when it does:
147
+ Every run charges credits, per image and **per model** `seedream-pro` costs noticeably more
148
+ per image than `seedream`, so don't reach for it by default. The CLI prints a footer on stdout
149
+ when it charges:
128
150
 
129
151
  ```
130
152
  💳 Charged 31 credits · balance 1,240