Skill: zoucdr/hidden-truth-reveal-video-gen (version 1.0.0)

--- name: hidden-truth-reveal-video-gen version: 1.0.0 description: "Create vertical reveal shorts: polished product vs harsh origin, one-line verdict, timed English captions (WeryAI). Use for supply-chain shock, contrast edits, or ‘99% don’t know’ hooks. Use when the user asks where everyday goods come from. SEO: hidden truth video; contrast reveal video." tags: [reveal, contrast, supply-chain, consumer, hooks, subtitles, short-video, video-gen] metadata: { "openclaw": { "emoji": "👁️", "primaryEnv": "WERYAI_API_KEY", "paid": true, "network_required": true, "requires": { "env": ["WERYAI_API_KEY"], "bins": ["node"], "node": ">=18" } } } user-invocable: true --- # Hidden truth reveal (polished → brutal → one line) **Template (seconds below assume `duration` 10; rescale for 5 or 15):** **0–3s** hook: *what you use daily comes from here* (user’s **X**); **3–8s** **stark contrast** montage (sleek product ↔ labor, factory, waste, or source environment—**non-gore**, suggestive not graphic); **8–10s** **one-line summary** that lands the thesis. **English subtitles** with **explicit second windows**. Default **`duration`** is **10** when unspecified. **Dependencies:** `WERYAI_API_KEY` + Node.js 18+. **`{baseDir}/scripts/video_gen.js`** + **[`WERYAI_VIDEO_API.md`](resources/WERYAI_VIDEO_API.md)** under **`{baseDir}/resources/`**. **No other Cursor skills required.** ## Prerequisites - `WERYAI_API_KEY` **must be set**; Node **18+**; `https` images only. - Non-empty `model` in JSON; pick from **Recommended models** or frozen tables. - Each `wait` consumes credits. ## Security, secrets, and API hosts - **`WERYAI_API_KEY`**: Secret; **`requires.env`** / **`primaryEnv`**—never commit values. - **API hosts:** `video_gen.js` uses **`https://api.weryai.com`** (tasks) and **`https://api-growth-agent.weryai.com`** (models list). - Run in an isolated environment when possible. ## Prompt expansion (mandatory) - **Duration:** Default **10**; **5** or **15** when the user asks and the model allows. - **Timeline scaling:** Rescale **0–3 / 3–5 / 5–7 / 7–10** proportionally when **`duration` ≠ 10**; keep the **four-beat order**. **Locked caption rhythm (example for duration 10):** - **0.0–3.0s** — Hook naming the **daily X** (user-supplied): e.g. *YOUR DAILY X COMES FROM HERE* (keep **≤6 words** per line). - **3.0–5.0s** — **Polished** beat label: e.g. *SHELF PERFECT*. - **5.0–7.0s** — **Harsh** beat label: e.g. *HIDDEN COST*. - **7.0–10.0s** — **One-line verdict** (user-themed): e.g. *THAT’S THE TRUTH* (customize to topic). **Visuals:** Hard cuts; **high contrast** lighting; **hands / machines / environments**—avoid **graphic gore** or identifiable real people in exploitative frames. **Motion** every beat. **Typography:** Bold sans, white + black outline, lower third, fades; **English only** on screen unless user opts out. **Audio:** Tension bed / impact whoosh if `generate_audio` true—no named tracks. **Negatives (Kling):** illegible captions, watermark, muddy text. **Exception:** User supplies finished long prompt + no rewrite—still show full text in confirmation. **Confirmation:** Full expanded `prompt` in the table. ### Niche checklist - Contrast must read in **silent skim**—image does half the work. - Middle beats are **two emotional temperatures**, not a lecture. **Examples below** are targets only. ## Workflow 1. **Text** and/or **image-to-video** (multi-image only **SEEDANCE_2_0**, ≤3 URLs). 2. Collect **product or topic X**, optional reference `image`, tier. 3. **Expand** with locked windows + visuals; **translate** user Chinese brief to **English** on-screen lines unless they request another language. 4. Validate fields vs frozen tables. 5. Confirmation table + full `prompt` → user **confirm**. 6. `node {baseDir}/scripts/video_gen.js wait --json '…'`. 7. Return **`[Video](url)`**; no code fences around links. ## CLI reference ~~~sh node {baseDir}/scripts/video_gen.js wait --json '{"model":"KLING_V3_0_PRO","prompt":"…","duration":10,"aspect_ratio":"9:16","generate_audio":true,"negative_prompt":"illegible text, watermark, garbled captions"}' node {baseDir}/scripts/video_gen.js wait --json '{"model":"SEEDANCE_2_0","prompt":"…","duration":10,"aspect_ratio":"9:16","resolution":"720p","generate_audio":true}' ~~~ **Full reference:** **[`WERYAI_VIDEO_API.md`](resources/WERYAI_VIDEO_API.md)**. ## Definition of done **[Video](url)** or clear error. `prompt` contains **timed English** subtitle block matching the **four-beat order**, with **second ranges scaled to `duration`** (or user’s approved variant). ## Boundaries (out of scope) - Not a journalism fact-check; **opinion / hook** tone—user responsible for claims. - No gore; no harassment of real individuals. - Captions only via **prompt**; no NLE export spec. - Use **`resources/WERYAI_VIDEO_API.md`** for CLI; no `negative_prompt` on Seedance. - **Multi-image** only **SEEDANCE_2_0**, max **3**. ### Example prompts - `Coffee cup: shelf glamour vs harvest-labor montage, verdict line about price to growers` - `Smartphone: polished device vs mine/e-waste suggestion, one-line kicker` - `Image URL of sneakers → same four-window subtitle schedule` - `Fast fashion haul vs factory floor suggestion—verdict about true cost` --- ## Model and API constraints (frozen for this skill) ### Text-to-video | model_key | durations | aspect_ratios | resolutions | audio | negative_prompt | prompt limit | |-----------|-----------|---------------|-------------|-------|-----------------|--------------| | KLING_V3_0_PRO | 5, 10, 15 | 9:16, 1:1, 16:9 | *(omit `resolution`)* | yes | yes | 2000 | | KLING_V3_0_STA | 5, 10, 15 | 9:16, 1:1, 16:9 | *(omit `resolution`)* | yes | yes | 2000 | | SEEDANCE_2_0 | 5, 10, 15 | 9:16, 1:1, 16:9 | 480p, 720p | yes | no | 2000 | ### Image-to-video | model_key | durations | aspect_ratios | resolutions | audio | negative_prompt | image slots | |-----------|-----------|---------------|-------------|-------|-----------------|-------------| | KLING_V3_0_PRO | 5, 10, 15 | 9:16, 16:9, 1:1 | *(omit)* | yes | yes | single `image` | | KLING_V3_0_STA | 5, 10, 15 | 9:16, 16:9, 1:1 | *(omit)* | yes | yes | single `image` | | SEEDANCE_2_0 | 5, 10, 15 | 9:16, 1:1, 16:9 | 480p, 720p | yes | no | up to **3** `images` | ## Recommended models | Tier | model_key | Default duration | |------|-----------|------------------| | ⭐ Best | KLING_V3_0_PRO | 10 | | 👍 Good | KLING_V3_0_STA | 10 | | ⚡ Fast | SEEDANCE_2_0 | 10 (`resolution`: **720p**) | ## Default parameters | Field | Value | |-------|--------| | aspect_ratio | 9:16 | | duration | 10 | | generate_audio | true | | resolution | 720p for Seedance only | | negative_prompt | Kling: `illegible text, garbled captions, watermark` | ## Text-to-video (primary) Collect **X** → expand **four-window** captions → confirm → `wait`. ## Image-to-video (optional) Product still → **motion + same schedule**. ## Tips - If claims are sensitive, soften to **“what they don’t show”** rhetoric. - **Verdict line** should fit **one breath**. > Packaged as **`hidden-truth-reveal-video-gen`**.

path: eval.yaml
content: version: "1"
defaults:
  trials: 5
  timeout: 600
  threshold: 0.8
skill: "hidden-truth-reveal-video-gen"
trigger_tests:
  - id: explicit-invoke
    should_trigger: true
    prompt: "Use hidden-truth-reveal-video-gen for a vertical short: polished product vs harsh origin, one-line verdict, English captions on a fixed beat."
  - id: implicit-domain
    should_trigger: true
    prompt: "99% don’t know where their daily coffee really comes from—need brutal contrast edit with timed English subtitles."
  - id: implicit-paraphrase
    should_trigger: true
    prompt: "Create a Reels ‘supply chain shock’ storyboard: shelf glamour then factory realism, last line lands the thesis on screen."
  - id: negative-control
    should_trigger: false
    prompt: "Write a Kubernetes deployment YAML for me."
outcome_tests:
  - id: layout
    type: deterministic
    checks:
      - file_exists: "SKILL.md"
      - file_exists: "eval.yaml"
  - id: prompt-expansion-required
    type: llm_rubric
    rubric: |
      SKILL.md must require prompt expansion, four-beat subtitle order with scaling for duration, and confirmation-table full prompt before submit.


path: resources/WERYAI_VIDEO_API.md
content: # WeryAI video CLI & JSON (`video_gen.js`)

Shipped at **`{baseDir}/resources/WERYAI_VIDEO_API.md`** (next to `SKILL.md`). Run the CLI from **`{baseDir}/scripts/video_gen.js`**. Node.js **18+** required.

## Environment

- **`WERYAI_API_KEY`** (required for `models`, generation, and `status`, except `--dry-run`): bearer token for HTTPS requests. Never commit the value.
- **No other environment variables** are read by this script. API hosts and poll timing are fixed in `video_gen.js` source.

## API hosts (fixed)

- Video tasks: `https://api.weryai.com`
- Models registry: `https://api-growth-agent.weryai.com`

## `models` — fetch model metadata (JSON on stdout)

Run from the skill root (`{baseDir}`):

```sh
node {baseDir}/scripts/video_gen.js models
node {baseDir}/scripts/video_gen.js models --mode text_to_video
node {baseDir}/scripts/video_gen.js models --mode image_to_video
node {baseDir}/scripts/video_gen.js models --mode multi_image_to_video
```

Typical fields per model (names as returned by the API): `model_key`, `title`, `durations`, `aspect_ratios`, `resolutions`, `has_generate_audio`, `has_negative_prompt`, `prompt_length_limit`; image / multi-image modes may include `support_multiple_images`, `support_first_last_frame`, `upload_image_limit`, etc.

Use this output to fill **`## Model and API constraints`** in the skill’s `SKILL.md`. Do not send fields (e.g. `resolution`, `aspect_ratio`) that the chosen model does not support.

## `wait` — submit and block until done (polls status)

```sh
node {baseDir}/scripts/video_gen.js wait --json '{"model":"KLING_V3_0_PRO","prompt":"...","duration":5,"aspect_ratio":"9:16"}'
node {baseDir}/scripts/video_gen.js wait --json '{"model":"...","prompt":"...","image":"https://...","duration":5}'
node {baseDir}/scripts/video_gen.js wait --json '{"model":"...","prompt":"...","images":["https://..."],"duration":5}'
node {baseDir}/scripts/video_gen.js wait --json '...' --dry-run
```

## `submit-*` — submit only (async; no wait)

```sh
node {baseDir}/scripts/video_gen.js submit-text --json '{"model":"…","prompt":"…","duration":5}'
node {baseDir}/scripts/video_gen.js submit-image --json '{"model":"…","prompt":"…","image":"https://…","duration":5}'
node {baseDir}/scripts/video_gen.js submit-multi-image --json '{"model":"…","prompt":"…","images":["https://…"],"duration":5}'
```

## `status` / `batch-status`

```sh
node {baseDir}/scripts/video_gen.js status --task-id <id>
node {baseDir}/scripts/video_gen.js batch-status --batch-id <id>
```

Task id / batch id can also be passed inside JSON for some flows; see `video_gen.js` usage text.

## Common JSON fields

| Field | Notes |
|--------|--------|
| `model` | Model key; **required** for `wait`, `submit-*`, and for `--dry-run` when `prompt` is present—if omitted or empty, the script returns `MISSING_PARAM` (no default model). **The script does not enforce** per-skill model rules from `SKILL.md`—callers must pass the model required by that skill. |
| `prompt` | Required for `wait` and `submit-*` |
| `duration` | Must be allowed for the model |
| `aspect_ratio` | Only if supported |
| `resolution` | Only if supported |
| `generate_audio` | Only if supported |
| `negative_prompt` | Only if supported |
| `image` | Public **https** URL (single image) |
| `images` | Array of public **https** URLs (multi-image) |

## Status response (conventions)

- HTTP OK with business `status === 0`: query succeeded.
- `data` may be a single object or an array of tasks; the script matches `task_id` or uses the first item.
- In progress: `phase` may be `running`, `inProgress: true`.
- Errors: stdout JSON with `ok: false`, `errorCode`, `errorMessage` (prefers API `message` / `desc`).

## Stdout JSON (generation / wait)

Common keys: `ok`, `phase`, `taskId`, `batchId`, `taskIds`, `videos` (`url`, `cover_url`), `tasks` (batch), `inProgress`, `errorCode`, `errorMessage`.

---

**Per-skill rules** in `SKILL.md` (prompt expansion, confirmation table, niche tables) still apply; this file only documents the **CLI and request shape**.


path: scripts/video_gen.js
content: #!/usr/bin/env node
/**
 * WeryAI 视频生成 — 通用 CLI（零 npm 依赖，需 Node.js 18+）
 *
 * 命令:
 *   wait | submit-text | submit-image | submit-multi-image | status | models
 *
 * 示例:
 *   node video_gen.js models
 *   node video_gen.js models --mode text_to_video
 *   node video_gen.js wait --json '{"model":"WERYAI_VIDEO_1_0","prompt":"...","duration":5}'
 *   node video_gen.js wait --json '...' --dry-run
 *
 * 环境变量:
 *   WERYAI_API_KEY（models / 生成 / status 必填；--dry-run 除外）— 敏感凭据，勿写入仓库；registry 元数据声明见同目录 SKILL.md。
 *   本脚本仅读取上述密钥；API 主机与轮询间隔为固定常量，勿用其他 env 覆盖。
 */

const BASE_URL = 'https://api.weryai.com';
const MODELS_BASE_URL = 'https://api-growth-agent.weryai.com';
const MODELS_API_PATH = '/growthai/v1/video/models';
const POLL_INTERVAL_MS = 6000;
const POLL_TIMEOUT_MS = 600000;

const STATUS_MAP = {
  waiting: 'waiting',
  WAITING: 'waiting',
  pending: 'waiting',
  PENDING: 'waiting',
  processing: 'processing',
  PROCESSING: 'processing',
  succeed: 'completed',
  SUCCEED: 'completed',
  success: 'completed',
  SUCCESS: 'completed',
  failed: 'failed',
  FAILED: 'failed',
};

const ERROR_MESSAGES = {
  400: 'Bad request — check your request parameters.',
  403: 'Invalid API key or IP access denied — verify WERYAI_API_KEY is correct.',
  500: 'WeryAI server error. Please try again later.',
  1001: 'Request rate limit exceeded — slow down and retry after a moment.',
  1002: 'Parameter error — check prompt, image URLs, aspect_ratio, model key, and duration.',
  1003: 'Resource not found — the task_id may not exist or has expired.',
  1006: 'Model not supported — check the model key or try a different model.',
  1007: 'Queue full — the service is busy, please try again later.',
  1011: 'Insufficient credits — recharge at weryai.com.',
  2003: 'Content flagged by safety system — revise your prompt or input image.',
  2004: 'Image format not supported — use jpg, png, or webp.',
  6004: 'Generation failed — please try again later.',
  6010: 'Concurrent task limit reached (max 20) — wait for existing tasks to complete.',
};

function log(msg) {
  process.stderr.write(`[weryai] ${msg}\n`);
}

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function httpJson(method, fullUrl, body, apiKey) {
  const headers = {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  };
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 30_000);
  try {
    const res = await fetch(fullUrl, {
      method,
      headers,
      body: body != null ? JSON.stringify(body) : undefined,
      signal: controller.signal,
    });
    clearTimeout(timer);
    let data;
    try {
      data = await res.json();
    } catch {
      data = { status: res.status, msg: `Non-JSON response (HTTP ${res.status})` };
    }
    return { httpStatus: res.status, ...data };
  } catch (err) {
    clearTimeout(timer);
    if (err.name === 'AbortError') {
      throw new Error(`Request timeout: ${method} ${fullUrl}`);
    }
    throw err;
  }
}

async function apiRequest(method, path, body, apiKey) {
  return httpJson(method, BASE_URL + path, body, apiKey);
}

async function fetchModelsRegistry(apiKey) {
  return httpJson('GET', MODELS_BASE_URL + MODELS_API_PATH, null, apiKey);
}

function isApiSuccess(res) {
  const httpOk = res.httpStatus >= 200 && res.httpStatus < 300;
  const bodyOk = res.status === 0 || res.status === 200;
  return httpOk && bodyOk;
}

function formatApiError(res) {
  const httpStatus = res.httpStatus || 0;
  const code = res.status;
  const msg = res.msg || '';

  if (httpStatus === 403) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: '403',
      errorMessage: ERROR_MESSAGES[403] + (msg ? ` (${msg})` : ''),
    };
  }
  if (httpStatus === 429) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'RATE_LIMIT',
      errorMessage: 'Rate limited by WeryAI API. Please wait and try again.',
    };
  }
  if (httpStatus >= 500) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: '500',
      errorMessage: `${ERROR_MESSAGES[500]} (HTTP ${httpStatus})`,
    };
  }
  if (httpStatus === 400) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: '400',
      errorMessage: ERROR_MESSAGES[400] + (msg ? ` (${msg})` : ''),
    };
  }

  const friendly = ERROR_MESSAGES[code] || '';
  const message =
    friendly && msg
      ? `${friendly} (${msg})`
      : friendly || msg || `API error (status: ${code}, HTTP ${httpStatus})`;

  return {
    ok: false,
    phase: 'failed',
    errorCode: code != null ? String(code) : null,
    errorMessage: message,
  };
}

function detectMode(params) {
  if (Array.isArray(params.images) && params.images.length > 0) return 'multi_image';
  if (typeof params.image === 'string' && params.image) return 'image';
  return 'text';
}

function coerceBool(v, defaultVal = false) {
  if (v == null) return defaultVal;
  if (typeof v === 'boolean') return v;
  return String(v).toLowerCase() === 'true';
}

function hasNonEmptyModel(params) {
  const m = params.model;
  return m != null && String(m).trim() !== '';
}

function buildRequestBody(params) {
  const model = String(params.model).trim();
  const body = {
    prompt: params.prompt,
    model,
    duration: Number(params.duration) || 5,
  };
  if (params.aspect_ratio) body.aspect_ratio = params.aspect_ratio;
  if (params.resolution) body.resolution = params.resolution;
  if (params.generate_audio != null) body.generate_audio = coerceBool(params.generate_audio, false);
  if (params.negative_prompt) body.negative_prompt = params.negative_prompt;
  if (params.image) body.image = params.image;
  if (params.images) body.images = params.images.slice(0, 3);
  return body;
}

async function submitTask(params, apiKey) {
  if (!hasNonEmptyModel(params)) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'MISSING_PARAM',
      errorMessage: "'model' is required in JSON parameters (no default model).",
    };
  }
  const mode = detectMode(params);
  const pathMap = {
    text: '/v1/generation/text-to-video',
    image: '/v1/generation/image-to-video',
    multi_image: '/v1/generation/multi-image-to-video',
  };
  const body = buildRequestBody(params);
  let res;
  try {
    res = await apiRequest('POST', pathMap[mode], body, apiKey);
  } catch (err) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'NETWORK_ERROR',
      errorMessage: err.message || String(err),
    };
  }

  if (!isApiSuccess(res)) return formatApiError(res);

  const data = res.data || {};
  const taskIds = data.task_ids ?? (data.task_id ? [data.task_id] : []);
  return {
    ok: true,
    phase: 'submitted',
    batchId: data.batch_id ?? null,
    taskIds,
    taskId: taskIds[0] ?? null,
    taskStatus: null,
    videos: null,
    errorCode: null,
    errorMessage: null,
  };
}

function extractVideos(taskData) {
  const tr = taskData.task_result || {};
  const raw = tr.videos || taskData.videos || [];
  return raw.map((v) => {
    if (typeof v === 'string') {
      return { url: v, cover_url: '' };
    }
    return {
      url: v?.url || v?.video_url || '',
      cover_url: v?.cover_image_url || v?.cover_url || '',
    };
  });
}

async function pollUntilDone(taskId, batchId, taskIds, apiKey) {
  const start = Date.now();
  while (true) {
    const elapsed = (Date.now() - start) / 1000;
    if (elapsed * 1000 >= POLL_TIMEOUT_MS) {
      return {
        ok: false,
        phase: 'failed',
        errorCode: 'TIMEOUT',
        errorMessage: `Poll timeout after ${Math.floor(elapsed)}s.`,
        batchId,
        taskIds,
        taskId,
        taskStatus: 'unknown',
        videos: null,
      };
    }

    await sleep(POLL_INTERVAL_MS);

    let res;
    try {
      res = await apiRequest('GET', `/v1/generation/${taskId}/status`, null, apiKey);
    } catch (err) {
      log(`Warning: poll failed (${err.message}), retrying...`);
      continue;
    }

    if (!isApiSuccess(res)) {
      log('Warning: poll returned non-success status, retrying...');
      continue;
    }

    const taskData = res.data || {};
    const rawStatus = taskData.task_status || '';
    const normalized = STATUS_MAP[rawStatus] || 'unknown';
    const elapsedSec = Math.floor((Date.now() - start) / 1000);
    log(`Polling ${taskId}... status: ${rawStatus} (${elapsedSec}s elapsed)`);

    if (normalized === 'completed') {
      const videos = extractVideos(taskData);
      return {
        ok: true,
        phase: 'completed',
        batchId,
        taskIds,
        taskId,
        taskStatus: rawStatus,
        videos: videos.length ? videos : null,
        errorCode: null,
        errorMessage: null,
      };
    }

    if (normalized === 'failed') {
      const tr = taskData.task_result || {};
      const errMsg = tr.message || taskData.msg || 'Task failed';
      return {
        ok: false,
        phase: 'failed',
        batchId,
        taskIds,
        taskId,
        taskStatus: rawStatus,
        videos: null,
        errorCode: 'TASK_FAILED',
        errorMessage: errMsg,
      };
    }
  }
}

async function cmdWait(params, apiKey) {
  const submitResult = await submitTask(params, apiKey);
  if (!submitResult.ok) return submitResult;
  const taskId = submitResult.taskId;
  const batchId = submitResult.batchId;
  const taskIds = submitResult.taskIds || [taskId];
  log(`Submitted task ${taskId}. Polling for completion...`);
  return pollUntilDone(taskId, batchId, taskIds, apiKey);
}

async function cmdStatus(taskId, apiKey) {
  let res;
  try {
    res = await apiRequest('GET', `/v1/generation/${taskId}/status`, null, apiKey);
  } catch (err) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'NETWORK_ERROR',
      errorMessage: err.message || String(err),
    };
  }

  if (!isApiSuccess(res)) return formatApiError(res);

  const taskData = res.data || {};
  const rawStatus = taskData.task_status || '';
  const normalized = STATUS_MAP[rawStatus] || 'unknown';
  const videos = extractVideos(taskData);
  const phase =
    normalized === 'completed' ? 'completed' : normalized === 'failed' ? 'failed' : 'running';
  const tr = taskData.task_result || {};
  return {
    ok: phase !== 'failed',
    phase,
    batchId: null,
    taskIds: [taskId],
    taskId,
    taskStatus: rawStatus,
    videos: videos.length ? videos : null,
    errorCode: phase === 'failed' ? 'TASK_FAILED' : null,
    errorMessage: phase === 'failed' ? tr.message || taskData.msg || null : null,
  };
}

const VALID_MODEL_MODES = ['text_to_video', 'image_to_video', 'multi_image_to_video'];

async function cmdModels(modeFilter, apiKey) {
  let res;
  try {
    res = await fetchModelsRegistry(apiKey);
  } catch (err) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'NETWORK_ERROR',
      errorMessage: err.message || String(err),
    };
  }

  if (!isApiSuccess(res)) return formatApiError(res);

  const data = res.data || {};
  const out = { ok: true, phase: 'completed' };

  if (modeFilter) {
    if (!VALID_MODEL_MODES.includes(modeFilter)) {
      return {
        ok: false,
        phase: 'failed',
        errorCode: 'VALIDATION',
        errorMessage: `Invalid --mode. Use: ${VALID_MODEL_MODES.join(', ')}`,
      };
    }
    out[modeFilter] = data[modeFilter] || [];
  } else {
    out.text_to_video = data.text_to_video || [];
    out.image_to_video = data.image_to_video || [];
    out.multi_image_to_video = data.multi_image_to_video || [];
  }

  return out;
}

function parseArgs(argv) {
  const cmd = argv[0];
  let jsonStr = null;
  let taskId = null;
  let dryRun = false;
  let modelsMode = null;
  for (let i = 1; i < argv.length; i++) {
    if (argv[i] === '--json') jsonStr = argv[++i];
    else if (argv[i] === '--task-id') taskId = argv[++i];
    else if (argv[i] === '--dry-run') dryRun = true;
    else if (argv[i] === '--mode') modelsMode = argv[++i];
  }
  return { command: cmd, jsonStr, taskId, dryRun, modelsMode };
}

function printJson(obj) {
  process.stdout.write(`${JSON.stringify(obj, null, 2)}\n`);
}

async function main() {
  const argv = process.argv.slice(2);
  const valid = new Set([
    'wait',
    'submit-text',
    'submit-image',
    'submit-multi-image',
    'status',
    'models',
  ]);
  const { command, jsonStr, taskId: cliTaskId, dryRun, modelsMode } = parseArgs(argv);

  if (!command || !valid.has(command)) {
    printJson({
      ok: false,
      phase: 'failed',
      errorCode: 'USAGE',
      errorMessage:
        'Usage: node video_gen.js <wait|submit-text|submit-image|submit-multi-image|status|models> [--json \'...\'] [--task-id id] [--dry-run] [--mode text_to_video|image_to_video|multi_image_to_video]',
    });
    process.exit(1);
  }

  let params = {};
  if (jsonStr) {
    try {
      params = JSON.parse(jsonStr);
    } catch (e) {
      printJson({
        ok: false,
        phase: 'failed',
        errorCode: 'INVALID_JSON',
        errorMessage: `Invalid JSON: ${e.message}`,
      });
      process.exit(1);
    }
  }

  if (command === 'models') {
    const [REDACTED_SECRET] || '').trim();
    if (!apiKey) {
      printJson({
        ok: false,
        phase: 'failed',
        errorCode: 'NO_API_KEY',
        errorMessage: 'Missing WERYAI_API_KEY environment variable.',
      });
      process.exit(1);
    }
    const result = await cmdModels(modelsMode, apiKey);
    printJson(result);
    process.exit(result.ok ? 0 : 1);
  }

  if (dryRun) {
    const mode = detectMode(params);
    const pathMap = {
      text: '/v1/generation/text-to-video',
      image: '/v1/generation/image-to-video',
      multi_image: '/v1/generation/multi-image-to-video',
    };
    if ('prompt' in params && !hasNonEmptyModel(params)) {
      printJson({
        ok: false,
        phase: 'failed',
        errorCode: 'MISSING_PARAM',
        errorMessage: "'model' is required in JSON parameters (no default model).",
      });
      process.exit(1);
    }
    const body = 'prompt' in params ? buildRequestBody(params) : {};
    printJson({
      ok: true,
      phase: 'dry-run',
      dryRun: true,
      requestBody: body,
      requestUrl: BASE_URL + (pathMap[mode] || pathMap.text),
    });
    process.exit(0);
  }

  const [REDACTED_SECRET] || '').trim();
  if (!apiKey) {
    printJson({
      ok: false,
      phase: 'failed',
      errorCode: 'NO_API_KEY',
      errorMessage: 'Missing WERYAI_API_KEY environment variable.',
    });
    process.exit(1);
  }

  let result;
  if (command === 'status') {
    const tid = cliTaskId || params.task_id || params.taskId;
    if (!tid) {
      printJson({
        ok: false,
        phase: 'failed',
        errorCode: 'MISSING_PARAM',
        errorMessage: 'status command requires --task-id <id>',
      });
      process.exit(1);
    }
    result = await cmdStatus(tid, apiKey);
  } else {
    if (!params.prompt) {
      printJson({
        ok: false,
        phase: 'failed',
        errorCode: 'MISSING_PARAM',
        errorMessage: "'prompt' is required in JSON parameters.",
      });
      process.exit(1);
    }
    if (!hasNonEmptyModel(params)) {
      printJson({
        ok: false,
        phase: 'failed',
        errorCode: 'MISSING_PARAM',
        errorMessage: "'model' is required in JSON parameters (no default model).",
      });
      process.exit(1);
    }
    result = command === 'wait' ? await cmdWait(params, apiKey) : await submitTask(params, apiKey);
  }

  printJson(result);
  process.exit(result.ok ? 0 : 1);
}

main().catch((err) => {
  printJson({
    ok: false,
    phase: 'failed',
    errorCode: 'FATAL',
    errorMessage: err.message || String(err),
  });
  process.exit(1);
});


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)