@wji.ai/cli 0.5.0 → 0.6.0

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 (32) hide show
  1. package/README.md +22 -2
  2. package/dist/commands/canvas.js +52 -5
  3. package/dist/commands/fsnode.js +13 -6
  4. package/dist/commands/gen.js +3 -2
  5. package/dist/commands/model.js +4 -2
  6. package/dist/commands/workspace.js +6 -3
  7. package/dist/core/output.js +29 -0
  8. package/dist/index.js +5 -2
  9. package/package.json +1 -1
  10. package/skill/ads/SKILL.md +176 -0
  11. package/skill/ads/reference/ai-consistency.md +109 -0
  12. package/skill/ads/reference/brief-template.md +103 -0
  13. package/skill/ads/reference/compliance.md +66 -0
  14. package/skill/ads/reference/formats-image.md +108 -0
  15. package/skill/ads/reference/formats-video.md +121 -0
  16. package/skill/ads/reference/models.md +95 -0
  17. package/skill/ads/reference/product-photography.md +119 -0
  18. package/skill/ads/reference/styles.md +70 -0
  19. package/skill/short-drama/SKILL.md +4 -0
  20. package/skill/short-drama/reference/canvas-recipes.md +18 -0
  21. package/skill/short-drama/reference/operations-cli.md +1 -0
  22. package/skill/short-drama/reference/operations-wji.md +27 -4
  23. package/skill/short-drama/reference/stage-storyboard.md +6 -0
  24. package/skill/social-media/SKILL.md +152 -0
  25. package/skill/social-media/reference/content-craft.md +124 -0
  26. package/skill/social-media/reference/plan-template.md +171 -0
  27. package/skill/social-media/reference/platforms-china.md +94 -0
  28. package/skill/social-media/reference/platforms-global.md +122 -0
  29. package/skill/social-media/reference/ratio-map.md +100 -0
  30. package/skill/social-media/reference/repurpose.md +119 -0
  31. package/skill/wji/SKILL.md +30 -2
  32. package/skill/wji/reference/commands.md +27 -12
package/README.md CHANGED
@@ -117,8 +117,12 @@ Run `wji <group> --help` for the full option list.
117
117
 
118
118
  ## Conventions
119
119
 
120
- - **`--json`** on any command prints the raw API response and nothing else, so
121
- everything composes with `jq`.
120
+ - **`--json`** on any command prints JSON and nothing else, so everything
121
+ composes with `jq`. List and get commands print the **`standard`** level by
122
+ default (ids, names, status, media urls, long text clipped to 200 chars,
123
+ `content` summarised with `truncated: true`). Add **`--detail full`** (or set
124
+ `WJI_DETAIL=full`) for the raw API response, `--detail brief` for id lists.
125
+ Human table output is never affected.
122
126
  - **`@file` and `-`** work anywhere a long text or JSON value is expected:
123
127
  `-p @prompt.txt`, `--content @node.json`, `-p -` to read stdin.
124
128
  - **`--set key=value`** sends arbitrary fields; dotted keys nest
@@ -244,6 +248,7 @@ Skill refs are `system:<key>` or `user:<skillId>` (a bare id is treated as `user
244
248
  | Generation submissions | image 30 / video 10 / audio 20 per minute, per user or key |
245
249
  | Generation queue | queued + running jobs: FREE 3 / VIP 10 / SVIP 25 → HTTP 429 |
246
250
  | Pagination | `--limit` ≤100; `-1` returns at most 500 except on opt-in endpoints |
251
+ | Detail levels | `?detail=brief\|standard\|full` on workspace, fsnode, model and history reads; the API defaults to `full`, the CLI's `--json` to `standard` |
247
252
  | Batches | `asset status` is sent 100 ids at a time; `asset summary` ≤100 ids |
248
253
  | Skills | zip ≤5MB, ≤200 files, ≤2MB per `.md`, ≤20MB total, 50 per account |
249
254
  | Creation caps | 50 workspaces, 3 organizations per account |
@@ -290,6 +295,21 @@ upgrading the CLI so the skill matches the commands.
290
295
  pipeline (outline → characters → scenes/props → script → storyboard) whose
291
296
  stages are checked by `skill/short-drama/scripts/short-drama.mjs validate`,
292
297
  with the data stored as workspace datasets and the work laid out on canvases.
298
+
299
+ `skill/ads/` and `skill/social-media/` are two more: `ads` makes commercials
300
+ and ad creative (TVC / bumper / social ad formats and durations, product
301
+ photography and studio lighting, visual styles, product identity across
302
+ generations, banner sizes, AI-disclosure compliance); `social-media` holds
303
+ every platform's image / video specs (Instagram, TikTok, YouTube, Facebook, X,
304
+ LinkedIn, Pinterest, Threads, Snapchat, 小红书, 抖音, 快手, 视频号, B站, 微博,
305
+ 公众号) and the repurposing recipes — an episode into 9:16 teasers and posters,
306
+ an ad master into every placement. Install them the same way:
307
+
308
+ ```bash
309
+ cp -r "$(npm root -g)/@wji.ai/cli/skill/short-drama" ~/.claude/skills/short-drama
310
+ cp -r "$(npm root -g)/@wji.ai/cli/skill/ads" ~/.claude/skills/ads
311
+ cp -r "$(npm root -g)/@wji.ai/cli/skill/social-media" ~/.claude/skills/social-media
312
+ ```
293
313
  Install it the same way (`cp -r skill/short-drama ~/.claude/skills/short-drama`)
294
314
  next to `wji`; `node skill/short-drama/scripts/selftest.mjs` runs its self-test.
295
315
 
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { WjiClient } from '../core/client.js';
3
3
  import { confirm, parseKeyValues, readValue } from '../core/prompt.js';
4
- import { color, detail, info, isJsonMode, printJson, relativeTime, render, success, table, truncate, warn } from '../core/output.js';
4
+ import { clipText, color, detail, detailLevel, info, isJsonMode, printJson, relativeTime, render, success, table, truncate, warn } from '../core/output.js';
5
5
  import { pollHistory, reportHistory, resolveAssets } from './gen.js';
6
6
  import { loadMembers, memberLabel, resolveMember } from '../core/members.js';
7
7
  import { fetchModels } from './model.js';
@@ -315,6 +315,53 @@ export function describe(canvas, node) {
315
315
  outgoing: canvas.edges.filter((e) => e.fromNodeId === node.id).map((e) => e.toNodeId),
316
316
  };
317
317
  }
318
+ /**
319
+ * The canvas commands always load the full canvas (edits and generation need
320
+ * every field), so --detail is applied here, to the JSON they print. Mirrors
321
+ * the backend's canvas_list_nodes levels.
322
+ */
323
+ export function trimDescribed(row, level = detailLevel()) {
324
+ if (!level || level === 'full')
325
+ return row;
326
+ if (level === 'brief') {
327
+ return compactRow({
328
+ id: row.id,
329
+ kind: row.kind,
330
+ name: row.name,
331
+ tag: row.tag,
332
+ mode: row.mode,
333
+ status: row.status,
334
+ assetId: row.assetId,
335
+ position: row.position,
336
+ references: row.references.map((r) => ({ nodeId: r.nodeId, label: r.label })),
337
+ outgoing: row.outgoing,
338
+ });
339
+ }
340
+ const g = row.generate;
341
+ return compactRow({
342
+ ...row,
343
+ text: row.text === undefined ? undefined : clipText(row.text) ?? '',
344
+ error: typeof row.error === 'string' ? clipText(row.error) : row.error,
345
+ generate: g
346
+ ? compactRow({
347
+ prompt: clipText(g.prompt),
348
+ model: g.model,
349
+ ratio: g.ratio,
350
+ resolution: g.resolution,
351
+ duration: g.duration,
352
+ historyId: g.historyId,
353
+ pendingAssetId: g.pendingAssetId,
354
+ })
355
+ : undefined,
356
+ });
357
+ }
358
+ function compactRow(value) {
359
+ const out = {};
360
+ for (const key of Object.keys(value))
361
+ if (value[key] !== undefined)
362
+ out[key] = value[key];
363
+ return out;
364
+ }
318
365
  // ── models ──────────────────────────────────────────────────────────────────
319
366
  /** An audio node is fed by either an `audio` or a `tts` model — the backend tags voice models `tts`. */
320
367
  const KIND_FEATURES = { audio: ['audio', 'tts'] };
@@ -1022,7 +1069,7 @@ export function canvasCommand(globals) {
1022
1069
  .filter((id) => loaded.byId.has(id))
1023
1070
  .map((id) => describe(loaded, loaded.byId.get(id)));
1024
1071
  }
1025
- const out = { ...res, state: { ...state, canvasName }, selected };
1072
+ const out = { ...res, state: { ...state, canvasName }, selected: selected.map((n) => trimDescribed(n)) };
1026
1073
  render(out, () => {
1027
1074
  const seconds = Math.max(0, Math.round((new Date(state.expiresAt).getTime() - Date.now()) / 1000));
1028
1075
  detail([
@@ -1249,7 +1296,7 @@ export function canvasCommand(globals) {
1249
1296
  .filter((n) => !nameRe || nameRe.test(n.name))
1250
1297
  .map((n) => describe(cv, n))
1251
1298
  .filter((n) => (colors ? !!n.tag && colors.has(n.tag.color) : opts.untagged ? !n.tag : true));
1252
- render({ canvas: { id: cv.id, name: cv.name, workspaceId: cv.workspaceId }, nodes: rows }, () => {
1299
+ render({ canvas: { id: cv.id, name: cv.name, workspaceId: cv.workspaceId }, nodes: rows.map((n) => trimDescribed(n)) }, () => {
1253
1300
  info(color.dim(`${cv.name} (${cv.id}) · ${rows.length} node(s) · ${cv.edges.length} edge(s)`));
1254
1301
  nodeTable(rows);
1255
1302
  });
@@ -1283,13 +1330,13 @@ export function canvasCommand(globals) {
1283
1330
  });
1284
1331
  canvas
1285
1332
  .command('get <nodeId>')
1286
- .description('One node in full, with its references')
1333
+ .description('One node with its references (--detail full for the whole generate / text)')
1287
1334
  .option('-c, --canvas <id>', 'canvas id (defaults to the linked canvas)')
1288
1335
  .action(async (id, opts) => {
1289
1336
  const c = client();
1290
1337
  const cv = await loadCanvas(c, await resolveCanvasId(c, opts.canvas));
1291
1338
  const n = describe(cv, requireNode(cv, id));
1292
- render(n, () => printNode(n));
1339
+ render(trimDescribed(n), () => printNode(n));
1293
1340
  });
1294
1341
  canvas
1295
1342
  .command('edges')
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { WjiClient } from '../core/client.js';
3
3
  import { confirm, parseKeyValues, readJson, readValue } from '../core/prompt.js';
4
- import { color, detail, info, relativeTime, render, success, table, truncate } from '../core/output.js';
4
+ import { color, detail, detailLevel, info, relativeTime, render, success, table, truncate } from '../core/output.js';
5
5
  import { printTree } from './workspace.js';
6
6
  /** Nested plain objects merge; arrays and scalars are replaced. */
7
7
  function deepMerge(base, patch) {
@@ -22,7 +22,10 @@ export function fsCommand(globals) {
22
22
  .action(async (key, opts) => {
23
23
  const client = new WjiClient(globals());
24
24
  const path = key ? `/fsnode/root/${key}` : '/fsnode/root';
25
- const node = await client.get(path, opts.depth ? { depth: Number(opts.depth) } : undefined);
25
+ const node = await client.get(path, {
26
+ depth: opts.depth ? Number(opts.depth) : undefined,
27
+ detail: detailLevel(),
28
+ });
26
29
  render(node, (n) => (opts.depth ? printTree(n) : printNode(n)));
27
30
  });
28
31
  fs.command('ls <id>')
@@ -34,6 +37,7 @@ export function fsCommand(globals) {
34
37
  const result = await client.get(`/fsnode/list/${id}/children`, {
35
38
  limit: opts.limit ? Number(opts.limit) : undefined,
36
39
  scope: opts.scope,
40
+ detail: detailLevel(),
37
41
  });
38
42
  const rows = Array.isArray(result) ? result : result?.data || [];
39
43
  render(result, () => table(rows, [
@@ -50,15 +54,18 @@ export function fsCommand(globals) {
50
54
  .option('-d, --depth <n>', 'levels to expand')
51
55
  .action(async (id, opts) => {
52
56
  const client = new WjiClient(globals());
53
- const node = await client.get(`/fsnode/list/${id}/tree`, opts.depth ? { depth: Number(opts.depth) } : undefined);
57
+ const node = await client.get(`/fsnode/list/${id}/tree`, {
58
+ depth: opts.depth ? Number(opts.depth) : undefined,
59
+ detail: detailLevel(),
60
+ });
54
61
  render(node, (n) => printTree(n));
55
62
  });
56
63
  fs.command('get <id>')
57
- .description('Show one node (comma-separate ids for a batch)')
64
+ .description('Show one node (comma-separate ids for a batch; --detail full for the whole content)')
58
65
  .option('--scope <scope>', 'server-side scope filter')
59
66
  .action(async (id, opts) => {
60
67
  const client = new WjiClient(globals());
61
- const node = await client.get(`/fsnode/list/${id}`, { scope: opts.scope });
68
+ const node = await client.get(`/fsnode/list/${id}`, { scope: opts.scope, detail: detailLevel() });
62
69
  render(node, (n) => (Array.isArray(n) ? n.forEach(printNode) : printNode(n)));
63
70
  });
64
71
  fs.command('path <id>')
@@ -232,7 +239,7 @@ export function datasetCommand(globals) {
232
239
  .description('List dataset nodes in a folder')
233
240
  .action(async (folderId) => {
234
241
  const client = new WjiClient(globals());
235
- const result = await client.get(`/fsnode/list/${folderId}/children`);
242
+ const result = await client.get(`/fsnode/list/${folderId}/children`, { detail: detailLevel() });
236
243
  const rows = (Array.isArray(result) ? result : result?.data || []).filter((n) => n.mimetype === 'workspace/dataset' || n.type === 'file');
237
244
  render(rows, () => table(rows, [
238
245
  { header: 'id', value: (n) => n.id },
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { WjiClient } from '../core/client.js';
3
3
  import { confirm, parseKeyValues, readJson, readValue } from '../core/prompt.js';
4
- import { color, detail, info, isJsonMode, printJson, render, success, table, truncate, warn } from '../core/output.js';
4
+ import { color, detail, detailLevel, info, isJsonMode, printJson, render, success, table, truncate, warn } from '../core/output.js';
5
5
  const READY = 1;
6
6
  const CANCELLED = -1;
7
7
  /** `--ref abc123`, `--ref abc123:hero`, `--ref abc123:hero:0.6` */
@@ -254,6 +254,7 @@ export function historyCommand(globals) {
254
254
  bookmarked: opts.bookmarked ? true : undefined,
255
255
  page: Number(opts.page),
256
256
  limit: Number(opts.limit),
257
+ detail: detailLevel(),
257
258
  });
258
259
  render(result, (r) => {
259
260
  table(r.data || [], [
@@ -284,7 +285,7 @@ export function historyCommand(globals) {
284
285
  }));
285
286
  }
286
287
  else {
287
- history = await client.get(`/ai/assets/history/list/${id}`);
288
+ history = await client.get(`/ai/assets/history/list/${id}`, { detail: detailLevel() });
288
289
  assets = await resolveAssets(client, history);
289
290
  }
290
291
  render({ ...history, images: assets }, () => reportHistory(history, assets));
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { WjiClient } from '../core/client.js';
3
- import { color, detail, info, render, table, truncate, warn } from '../core/output.js';
3
+ import { color, detail, detailLevel, info, render, table, truncate, warn } from '../core/output.js';
4
4
  const FEATURES = [
5
5
  'text',
6
6
  'image',
@@ -24,6 +24,7 @@ export async function fetchModels(client, opts) {
24
24
  const features = opts.features?.length ? opts.features : FEATURES;
25
25
  const list = await client.get(`/ai/model/list/${features.join(',')}`, {
26
26
  workspaceId: client.workspace(opts.workspace),
27
+ detail: opts.detail,
27
28
  });
28
29
  return Array.isArray(list) ? list : list?.data || [];
29
30
  }
@@ -41,6 +42,7 @@ export function modelCommand(globals) {
41
42
  let rows = await fetchModels(client, {
42
43
  features: opts.feature,
43
44
  workspace: opts.workspace,
45
+ detail: detailLevel(),
44
46
  });
45
47
  if (!opts.all)
46
48
  rows = rows.filter((m) => m.enabled !== false && !m.hidden);
@@ -70,7 +72,7 @@ export function modelCommand(globals) {
70
72
  .option('-w, --workspace <id>', 'resolve pricing/access for a workspace')
71
73
  .action(async (name, opts) => {
72
74
  const client = new WjiClient(globals());
73
- const rows = await fetchModels(client, { workspace: opts.workspace });
75
+ const rows = await fetchModels(client, { workspace: opts.workspace, detail: detailLevel() });
74
76
  // `name` is not unique across providers; `nickname` is, so prefer it.
75
77
  const matches = rows.filter((m) => m.nickname === name || m.name === name);
76
78
  if (!matches.length)
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { WjiClient } from '../core/client.js';
3
3
  import { updateProfile } from '../core/config.js';
4
4
  import { confirm, parseKeyValues } from '../core/prompt.js';
5
- import { color, detail, info, relativeTime, render, success, table, truncate } from '../core/output.js';
5
+ import { color, detail, detailLevel, info, relativeTime, render, success, table, truncate } from '../core/output.js';
6
6
  import { loadMembers, memberLabel, resolveMember } from '../core/members.js';
7
7
  /** App tokens accepted by `--app`; mirrors WorkspaceApplication on the backend. */
8
8
  const APPS = {
@@ -54,6 +54,7 @@ export function workspaceCommand(globals) {
54
54
  scope: opts.scope,
55
55
  page: Number(opts.page),
56
56
  limit: Number(opts.limit),
57
+ detail: detailLevel(),
57
58
  });
58
59
  const current = client.workspace();
59
60
  render(result, (r) => {
@@ -77,7 +78,9 @@ export function workspaceCommand(globals) {
77
78
  .description('Show a workspace (defaults to the selected one)')
78
79
  .action(async (id) => {
79
80
  const client = new WjiClient(globals());
80
- const w = await client.get(`/workspace/list/${client.requireWorkspace(id)}`);
81
+ const w = await client.get(`/workspace/list/${client.requireWorkspace(id)}`, {
82
+ detail: detailLevel(),
83
+ });
81
84
  render(w, () => detail([
82
85
  ['id', w.id],
83
86
  ['name', w.name],
@@ -173,7 +176,7 @@ export function workspaceCommand(globals) {
173
176
  const path = opts.app
174
177
  ? `/workspace/list/${wsId}/fs/${opts.app}`
175
178
  : `/workspace/list/${wsId}/fs`;
176
- const root = await client.get(path);
179
+ const root = await client.get(path, { detail: detailLevel() });
177
180
  render(root, (node) => printTree(node));
178
181
  });
179
182
  // ── apps ───────────────────────────────────────────────────────────────────
@@ -17,6 +17,35 @@ export function setJsonMode(on) {
17
17
  export function isJsonMode() {
18
18
  return jsonMode;
19
19
  }
20
+ const DETAIL_LEVELS = ['brief', 'standard', 'full'];
21
+ /**
22
+ * Global --detail (or WJI_DETAIL). Unset means "standard when printing JSON,
23
+ * nothing (server default = full) for human output", so tables are unchanged.
24
+ */
25
+ let detailMode;
26
+ export function setDetailMode(raw) {
27
+ if (raw === undefined || raw === '') {
28
+ detailMode = undefined;
29
+ return;
30
+ }
31
+ const level = String(raw).toLowerCase();
32
+ if (!DETAIL_LEVELS.includes(level)) {
33
+ throw new Error(`--detail must be one of ${DETAIL_LEVELS.join(', ')} (got "${raw}")`);
34
+ }
35
+ detailMode = level;
36
+ }
37
+ export function detailLevel() {
38
+ if (detailMode)
39
+ return detailMode;
40
+ return jsonMode ? 'standard' : undefined;
41
+ }
42
+ /** Clip long text the way the server does at `standard` (200 chars + …). */
43
+ export function clipText(value, max = 200) {
44
+ if (value === undefined || value === null || value === '')
45
+ return undefined;
46
+ const s = String(value);
47
+ return s.length <= max ? s : `${s.slice(0, max)}…`;
48
+ }
20
49
  export function printJson(value) {
21
50
  process.stdout.write(JSON.stringify(value, null, 2) + '\n');
22
51
  }
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'node:module';
3
3
  import { Command } from 'commander';
4
4
  import { ApiError } from './core/client.js';
5
- import { color, setJsonMode } from './core/output.js';
5
+ import { color, setDetailMode, setJsonMode } from './core/output.js';
6
6
  import { authCommand } from './commands/auth.js';
7
7
  import { configCommand } from './commands/config.js';
8
8
  import { orgCommand } from './commands/org.js';
@@ -34,7 +34,8 @@ program
34
34
  .option('--org <orgId>', 'organization context (X-Org-Id)')
35
35
  .option('--no-org', 'force personal context')
36
36
  .option('--locale <locale>', 'response language (cht, chs, en)')
37
- .option('--json', 'print raw JSON instead of tables')
37
+ .option('--json', 'print JSON instead of tables (standard detail unless --detail is given)')
38
+ .option('--detail <level>', 'how much to return: brief | standard | full (default standard for --json; env WJI_DETAIL)')
38
39
  .configureHelp({ sortSubcommands: true });
39
40
  /**
40
41
  * Global flags live on the root command, but subcommand actions run before
@@ -51,10 +52,12 @@ const globals = () => {
51
52
  // commander turns `--no-org` into org === false
52
53
  personal: opts.org === false,
53
54
  locale: opts.locale,
55
+ detail: opts.detail,
54
56
  };
55
57
  };
56
58
  program.hook('preAction', () => {
57
59
  setJsonMode(!!program.opts().json);
60
+ setDetailMode(program.opts().detail ?? process.env.WJI_DETAIL);
58
61
  });
59
62
  program.addCommand(authCommand(globals));
60
63
  program.addCommand(configCommand(globals));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wji.ai/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Command line interface for the WJI AI platform",
5
5
  "license": "UNLICENSED",
6
6
  "author": "WJI AI",
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: ads
3
+ description: Make commercials and ad creative with AI on WJI through the `wji` CLI — video ads (TVC 15 / 30 / 60 s, 6-second bumpers, skippable and non-skippable pre-roll, 9–34 s social ads, product demos, UGC-style, testimonials, explainers) and ad images (product shots / 产品图, key visuals / 主视觉 KV, display banners), with studio lighting and product-photography vocabulary, visual styles by industry, second-by-second ad structure (hook in 3 s, brand by 5 s, CTA card), product identity locked across every generation, deliverables per placement and AI-disclosure compliance. Use whenever a task mentions an ad / 广告 / 廣告, a commercial, TVC, product shot, packshot, banner, key visual, promo / 宣传片 or brand video. Hands per-platform sizes to the social-media skill. Works together with the `wji` skill.
4
+ ---
5
+
6
+ # Ads on WJI
7
+
8
+ One brief, one canvas, one master. **You write the brief, the concept and every prompt**; the
9
+ `wji` CLI keeps the brief as a dataset, lays the work out on a canvas and generates the stills
10
+ and clips. Nothing is generated until the brief is complete and the user has said yes to the
11
+ quote (or the message carried `-auto`, see the `wji` skill).
12
+
13
+ ```
14
+ brief ──▶ concept & hooks ──▶ key visual / storyboard ──▶ stills ──▶ clips ──▶ master + deliverables
15
+ │ │ │ │ │ │
16
+ brief.md 3–5 hooks, shot list with product- image-to- ratio variants,
17
+ + story/ framework, timecodes, one locked video, overlays, end card
18
+ dataset CTA & end card master ratio KV + keys 3–8 s each → social-media skill
19
+ ```
20
+
21
+ `{baseDir}` = this skill. Read `{baseDir}/reference/brief-template.md` before asking the user
22
+ anything and `{baseDir}/reference/formats-video.md` or `formats-image.md` before proposing a format.
23
+ The `wji` skill governs every CLI call (auth, confirmations, spending policy, model choice).
24
+
25
+ | Read | When |
26
+ | --- | --- |
27
+ | `reference/brief-template.md` | before the first question — the fields, and what a complete brief looks like |
28
+ | `reference/formats-video.md` | choosing a video format, its length and second-by-second structure; cutting the ad into clips |
29
+ | `reference/formats-image.md` | a KV, packshot or banner: IAB / Google / Meta sizes, text limits, generate-then-crop |
30
+ | `reference/product-photography.md` | writing any product still: lighting setups, shot types, backgrounds, lens, grade, the prompt template |
31
+ | `reference/styles.md` | picking the look — 13 styles with signifiers and prompt phrases, by industry |
32
+ | `reference/ai-consistency.md` | before generating: how the product stays the same across stills and clips, what never to generate |
33
+ | `reference/models.md` | which WJI model for which need (ratios, resolutions, references, text in image, transparent background) |
34
+ | `reference/compliance.md` | before delivery: AI-content labels per platform / jurisdiction, misleading-depiction rules |
35
+
36
+ ## Operations
37
+
38
+ ### Where the brief lives
39
+
40
+ Locally: an `ads/<product>/` folder with `brief.md` (the brief, concept, shot list and
41
+ deliverables — one file, sections in that order) and `prompts/` (one text file per still and
42
+ clip, so `--prompt-file` can send them verbatim). On WJI: the same brief as a dataset in the
43
+ workspace's **story** folder, so the canvas and the chat see it.
44
+
45
+ ```bash
46
+ wji auth whoami --json && wji profile wallet # preflight; an ad is a handful of stills + 4–8 clips
47
+ wji ws use <workspaceId> # or wji ws create "<product> ads" -r 16:9
48
+ wji ws app list # the Dataset app id → DS_APP
49
+ wji --json ws fs --app DS_APP | jq -r '.. | objects | select(.content.semanticKey? == "story") | .id' # → STORY
50
+ wji dataset new STORY "AD <product>" -t other -d "<one line>" --json | jq -r .id # → BRIEF
51
+ wji fs set BRIEF --merge content=@ads/<product>/brief.md --merge viewMode=content # the brief text
52
+ wji asset upload ads/<product>/packshot.png # the user's product photo → PACK asset id
53
+ wji models get <model> # confirm ratios and the reference cap before promising a size
54
+ ```
55
+
56
+ ### The canvas
57
+
58
+ One canvas per ad, `AD · <product>`, rows top to bottom:
59
+
60
+ | Row | Nodes | Command |
61
+ | --- | --- | --- |
62
+ | 1 references | the packshots and brand assets | `wji canvas add image -c CV -n "packshot" --asset PACK --at 0,0` (display nodes) |
63
+ | 2 key visuals | one image node per style / hook variant | `wji canvas add image -c CV -n "KV minimal" --from P1 -m <image model> -r 1:1 --resolution 2K --prompt-file prompts/kv-minimal.txt --at 0,400` — the packshot node is always **first** in `--from` (`@image1`) |
64
+ | 3 storyboard stills | one per clip, in order | same, `--from P1 KV1` so light and grade carry over |
65
+ | 4 clips | one video node per still | `wji canvas add video -c CV -n "clip 01" --from S1 P1 -m <video model> -r 16:9 -d 5 --prompt-file prompts/clip-01.txt` |
66
+ | 5 copy | headline / CTA / legal lines | `wji canvas add text -c CV --text-file ads/<product>/overlays.md --plain` beside the clip they belong to — overlays are added in the edit, **never generated into the picture** |
67
+
68
+ `--from` order sets `@imageN`; `wji canvas connect <from> <to>` adds a reference later. Quote
69
+ first — `wji canvas gen <nodeIds…>` prints the cost and refuses — tell the user, then
70
+ `wji canvas gen <nodeIds…> --yes --wait` after the yes. `wji canvas set <node> -p … / -r … / -d …`
71
+ changes a node without rebuilding it; `wji canvas get <node> --json | jq -r .assetId` then
72
+ `wji asset download <asset> -o /tmp/ad` to look at a result before approving it.
73
+
74
+ For a one-off still with no canvas: `wji gen image -m <model> -p "…" -r 1:1 --ref PACK --wait`
75
+ — it still needs the user's yes first unless the message carried `-auto`.
76
+
77
+ Take ids from the command's own JSON (`wji --json canvas add … | jq -r .id`), never by searching
78
+ names. Ratio, resolution and duration are validated against the model — copy the spelling from
79
+ `wji models get`.
80
+
81
+ ## Before anything else
82
+
83
+ 1. **Read the brief template**, then **ask for what is missing in one message**: product (and a
84
+ packshot or product photo — without one the product cannot be kept consistent), audience,
85
+ objective (awareness / conversion / retargeting), format and length, placements and their
86
+ ratios, style, brand colours (hex), must-show / must-say, CTA, legal lines, whether the ad
87
+ will run where AI content must be labelled. Offer defaults where the user has none
88
+ (`reference/formats-video.md` §"Formats at a glance", `reference/styles.md` §"Choosing by industry"), but
89
+ **do not generate anything without the product reference and the placement**.
90
+ 2. **Write `brief.md`** and push it as the `AD <product>` dataset. Everything later reads from it.
91
+ 3. **Confirm the master ratio**: 16:9 when the primary placement is YouTube / TV / display,
92
+ 9:16 when it is Reels / TikTok / Shorts / 抖音, 1:1 or 4:5 for feed-only. One master; the
93
+ other ratios are made later (stills regenerated or cropped, video regenerated — see
94
+ `reference/formats-image.md` §"Generate, then crop" and the social-media skill).
95
+ 4. **Check the wallet and quote** (`wji gen cost`) before every generation; say what the whole
96
+ plan will cost before starting row 2.
97
+
98
+ ## Stage 1 — Concept
99
+
100
+ - Pick the framework (`reference/formats-video.md`): AIDA for cold audiences and launches, PAS
101
+ for retargeting, ABCD as the checklist for anything on YouTube. Write **3–5 hooks** (first
102
+ 3 seconds), pick one with the user, keep the others as A/B variants.
103
+ - Write the **shot list with timecodes**: hook 0–3 s, product in frame by 3 s, brand by 5 s,
104
+ proof / demo, CTA card in the last 2–5 s. Sound-off first: every beat must read with captions
105
+ and no audio.
106
+ - Cut the ad into **clips of 3–8 s**, each one generation, sized to the video model
107
+ (`reference/models.md`: h3 5–15 s fixed, seedance-2.5 4–30 s auto, wan 2–30 s, veo 4–8 s,
108
+ kling 3–15 s). A 15-s vertical ad is typically 3 clips + end card; a 30-s TVC 5–7 clips.
109
+ - For an **image-only** job (KV, banner set, packshot variants) the concept is the layout: where
110
+ the product sits, where the headline zone stays clear, which sizes are needed
111
+ (`reference/formats-image.md`).
112
+ - Put the concept into `brief.md` (`## Concept`, `## Shot list`, `## Deliverables`) and
113
+ `wji fs set BRIEF --merge content=@ads/<product>/brief.md`.
114
+
115
+ ## Stage 2 — Key visual and stills
116
+
117
+ - Build row 1 (packshots, brand refs) and row 2 (one KV per style variant, 2–3 at most). Every
118
+ prompt follows `reference/product-photography.md` §"The prompt template": subject → shot → lighting
119
+ → background → camera → grade → constraints, and ends with the identity line from
120
+ `reference/ai-consistency.md` ("keep the label, shape and colour of @image1 exactly …").
121
+ - Pick the model by need (`reference/models.md`): legible text in the picture → nano-banana-pro /
122
+ gpt-image-2.5 / seedream-5-pro; transparent background for a logo or cut-out → gpt-image-2.5
123
+ with `--set background=transparent`; 4:5 or 2:3 placements → nano-banana / flux-2-flex /
124
+ seedream-5; a marketing look out of the box → marketing-studio-image. Default
125
+ `--resolution 2K` for a KV, `1K` while exploring.
126
+ - **Never generate** legal text, small print, prices that must be exact, or a real logo — those
127
+ are text nodes and overlays. Headline text may be generated only to preview a layout.
128
+ - Download and look at the KVs, show the user, take the pick, then generate row 3 (storyboard
129
+ stills) `--from P1 KV1` so light direction, grade and lens stay the same across the ad.
130
+ - Quality pass before any clip: hands on the product, label text, logo drift, product proportions,
131
+ impossible physics (`reference/ai-consistency.md` §"Artefact checklist"). Regenerate a still
132
+ rather than trying to fix it in the clip.
133
+
134
+ ## Stage 3 — Clips
135
+
136
+ - One video node per approved still, `--from S1 P1`, `-d` from the shot list (whole seconds),
137
+ master `-r`. Prompts describe **motion and camera only** — the look is already in the still;
138
+ repeat the product-identity line.
139
+ - Product in frame for the whole clip unless the shot list says otherwise; no text in the video
140
+ prompt (captions and CTA are overlays).
141
+ - Voice-over and music: `wji canvas add audio -c CV -m seed-audio-1.0 --prompt-file prompts/vo.txt`
142
+ when the user wants a VO bed; the script lines come from the brief's must-say.
143
+ - Quote the whole row, generate after the yes, download and review every clip against the still
144
+ it came from (same product, same light), regenerate the ones that drift.
145
+
146
+ ## Stage 4 — Master and deliverables
147
+
148
+ - The master is the clip sequence + overlay text in order; assembling the cut, captions and
149
+ overlays happens in the user's editor (or locally with ffmpeg when it is installed and the user
150
+ asks) — list the timeline explicitly (clip → seconds → overlay text) in `## Deliverables`.
151
+ - **Other ratios**: stills are regenerated at the placement ratio (or cropped when the ratio is
152
+ not native, `reference/formats-image.md`), video is **regenerated** at the placement ratio
153
+ from the same stills — never cropped. When the social-media skill is installed, hand it the
154
+ brief and the canvas; it owns the per-platform spec sheet. Otherwise list the sizes yourself
155
+ from `reference/formats-image.md`.
156
+ - **Compliance** (`reference/compliance.md`): say whether the ad needs an AI-content label on
157
+ each placement, and never depict a result the product cannot deliver.
158
+
159
+ ## Reporting
160
+
161
+ After each stage: what is on the canvas (row, node names), what was spent, what the user must
162
+ decide next (pick a KV, approve the shot list, approve a clip). After delivery: the deliverables
163
+ table (file, ratio, duration, overlay text, label needed) and the A/B variants still unbuilt.
164
+
165
+ ## Rules specific to this skill
166
+
167
+ - **Ask before every generation**, even one image (the `wji` skill's spending policy is the
168
+ floor; this skill asks for images too, because an ad's stills are its most expensive mistakes).
169
+ - **The packshot is `@image1` in every generation** — stills and clips. No packshot, no
170
+ generation; ask for one or generate a packshot first and have the user approve it.
171
+ - One master ratio per ad; variants are separate rows, never reframes of the master.
172
+ - Text, prices, legal lines and logos are overlays, never generated pixels.
173
+ - Do not invent platform limits or sizes — cite the row in `reference/formats-video.md` /
174
+ `reference/formats-image.md`; rows marked *(verify)* are said as such.
175
+ - All-audiences content only, no real people's likeness without the user's confirmation that they have the
176
+ rights, no competitor products in frame.