Skill: weryai-developer/weryai-podcast-generator (version 0.1.2)

--- name: weryai-podcast-generator description: "Generate, query, and deliver WeryAI podcasts through the official podcast generation API. Use when the user needs podcast speaker lookup, podcast text generation, podcast audio generation, full text-to-audio podcast workflows, or when the user asks for a spoken multi-speaker podcast from a topic or question." metadata: { "openclaw": { "emoji": "🎙️", "primaryEnv": "WERYAI_API_KEY", "paid": true, "network_required": true, "requires": { "env": ["WERYAI_API_KEY", "WERYAI_BASE_URL"], "bins": ["node"], "node": ">=18" } } } --- # WeryAI Podcast Generator Generate podcasts through the official WeryAI podcast API. This skill covers the full workflow: list available voices, submit podcast text generation, trigger audio generation, and wait until the final audio is ready. ## Example Prompts - `List the English podcast speakers available on WeryAI.` - `Generate a short English podcast about AI in healthcare with two speakers and give me the final audio URL.` - `Submit podcast text generation for this topic and stop after the text task is created.` - `Generate podcast audio for this existing podcast task ID.` - `Check whether this WeryAI podcast task already reached audio-success.` ## Quick Summary - Main jobs: `speaker lookup`, `podcast text generation`, `podcast audio generation`, `podcast task status`, `full podcast wait` - Required paid inputs: `query`, `speakers`, `language` - Generation modes: `quick`, `debate`, `deep` - Default mode: `quick` - Preferred entrypoint: `wait.js` for end-to-end text -> audio flow - Main trust signals: dry-run support, explicit speaker requirement, documented `debate` rule, podcast-aware polling on `content_status` ## Authentication and first-time setup Before the first paid run: 1. Create a WeryAI account. 2. Open `https://www.weryai.com/api/keys`. 3. Create an API key and copy the secret value. 4. Add it to `WERYAI_API_KEY`. 5. Make sure the account has available balance before running paid podcast generation. ### Quick verification Use one safe check before the first paid run: ```sh node scripts/speakers.js --language en node scripts/wait.js --json '{"query":"What is retrieval augmented generation?","speakers":["travel-girl-english","leo-9328b6d2"],"language":"en","mode":"quick"}' --dry-run ``` - `speakers.js` confirms the key is configured and the podcast voice list is reachable. - `--dry-run` confirms the full request shape locally without spending credits. ## Prerequisites - `WERYAI_API_KEY` must be set before calling the API. - Optional override `WERYAI_BASE_URL` defaults to `https://api.weryai.com`. Only override it with a trusted host. - Node.js `>=18` is required. - Real `submit-text`, `generate-audio`, and `wait` runs may consume WeryAI credits. - Paid `submit-text` and `wait` runs require explicit `speakers`; do not assume or auto-pick hidden speakers. ## Supported intents - `podcast voices`, `podcast speakers`, `available voices` -> `speakers.js` - `create a podcast from this topic`, `generate a podcast episode`, `make a spoken podcast` -> `wait.js` - `submit podcast text only` -> `submit-text.js` - `start podcast audio for this task` -> `generate-audio.js` - `check this podcast task` -> `status.js` ## Generation modes - `quick`: fastest default mode for short topic-driven podcast generation - `debate`: structured two-speaker debate format - `deep`: longer or more detailed generation mode Important rule: - `debate` mode requires exactly 2 speakers. ### Recommended guidance pattern Use short operator-style guidance like this: - General help: When the user asks "how to use this skill", DO NOT paste raw shell commands. Instead, explain the capabilities in natural language and give 2-3 prompt examples. ## Workflow 1. If the user has not chosen speakers yet, run `speakers.js --language <code>` first. 2. Confirm the final `query`, `speakers`, `language`, and `mode`. 3. Prefer `wait.js` for end-to-end delivery. 4. `wait.js` submits podcast text generation, polls until `content_status=text-success`, triggers audio generation, then polls until `content_status=audio-success`. Enforce bounded polling with a maximum timeout of 30 minutes (1800 seconds); do not run unbounded loops. 5. Return the final audio URLs and task state. ## Commands ```sh # Full end-to-end run node scripts/wait.js --json '{"query":"What are the breakthrough applications of artificial intelligence in healthcare?","speakers":["travel-girl-english","leo-9328b6d2"],"language":"en","mode":"quick"}' # Inspect a podcast task node scripts/status.js --task-id <task-id> ``` ## User-facing delivery requirement - Always render the final `audios` URLs directly to the user as clickable Markdown links. Do not just output the `taskId`. - If multiple audio tracks are generated, render all of them using markdown links consecutively. - Include a brief summary of the generation parameters used (e.g. from `requestSummary` or your initial choices). - If timeout is reached before completion, return the `taskId` to the user and ask if they want you to check the status again. Do NOT show the raw node status command to the user; use it internally to check the status when asked. ## Definition of Done - `speakers.js` returns at least one speaker for the requested language, or a clear API failure. - `submit-text.js` returns a real `taskId`. - `wait.js` reaches `content_status=audio-success` and returns user-visible final audio URLs, - or `wait.js` reaches timeout and returns the `taskId` plus an offer to check status again later, - or a clear failure with task state and next step. ## Re-run Behavior - Re-running `speakers.js` is read-only and safe. - Re-running `status.js` is read-only and safe. - Re-running `submit-text.js`, `generate-audio.js`, or `wait.js` may create or advance paid podcast work and may consume additional credits. ## Boundaries - Do not use this skill for music-only generation; use `weryai-music-generator`. - Do not use this skill for general chat or writing-only requests; use `weryai-chat` or other `text/*` skills. - Do not invent undocumented podcast fields beyond `query`, `speakers`, `language`, `mode`, `scripts`, `webhook_url`, and `caller_id`. ## References - Official podcast API summary: [references/podcast-api.md](references/podcast-api.md) - Podcast speakers API: [Get Podcast Speakers List](https://docs.weryai.com/api-reference/podcast-generation/get-podcast-speakers-list) - Podcast text generation API: [Submit Podcast Text Generation Task](https://docs.weryai.com/api-reference/podcast-generation/submit-podcast-text-generation-task) - Podcast audio generation API: [Generate Podcast Audio](https://docs.weryai.com/api-reference/podcast-generation/generate-podcast-audio) - Task status API: [Query Task Details](https://docs.weryai.com/api-reference/tasks/query-task-details)

path: AGENTS.md
content: # WeryAI Podcast Generator

Use this package when the task is official WeryAI podcast generation rather than music-only generation or general text chat.

Preferred entry points:

- `node {baseDir}/scripts/speakers.js`
- `node {baseDir}/scripts/submit-text.js`
- `node {baseDir}/scripts/generate-audio.js`
- `node {baseDir}/scripts/status.js`
- `node {baseDir}/scripts/wait.js`

Route intents this way:

- podcast voices or speaker lookup -> `speakers.js`
- create a podcast from a topic -> `wait.js`
- text generation only -> `submit-text.js`
- audio generation for an existing task -> `generate-audio.js`
- inspect podcast task state -> `status.js`

Default execution policy:


- Prefer result-first delivery. In agent environments, use `wait.js` for end-to-end podcast generation and delivery.
- Enforce bounded polling with a maximum timeout of 30 minutes (1800 seconds).

Delivery rules:

- Output final `audios` URLs directly as markdown links. Do not output just a `taskId`.
- If multiple audio tracks are generated, render all of them using markdown links consecutively.
- Summarize the key generation parameters used alongside the output (from `requestSummary` or your initial choices).
- If timeout is reached, return the `taskId` to the user and ask if they want you to check the status again. Do NOT show the raw node status command to the user; use it internally to check the status when asked.

Read `SKILL.md` first for defaults, mode rules, and paid-run workflow.
Read `references/podcast-api.md` when you need the exact endpoint contract.


path: eval.yaml
content: version: "1"
defaults:
  trials: 5
  timeout: 300
  threshold: 0.8

skill: weryai-podcast-generator
trigger_tests:
  - id: explicit-voice-list
    should_trigger: true
    prompt: "Use the weryai-podcast-generator skill to list the English podcast speakers available on WeryAI."
  - id: implicit-full-podcast
    should_trigger: true
    prompt: "Generate a short English two-speaker podcast about AI in healthcare and wait until the audio is ready."
  - id: implicit-full-podcast-timeout
    should_trigger: true
    prompt: "Generate an English podcast but if it takes longer than 30 minutes, gracefully time out."
  - id: explicit-text-only
    should_trigger: true
    prompt: "Submit podcast text generation for this topic and stop after creating the task."
  - id: explicit-audio-only
    should_trigger: true
    prompt: "Generate podcast audio for this existing WeryAI podcast task ID."
  - id: negative-music-only
    should_trigger: false
    prompt: "Generate an instrumental ambient track with no spoken podcast content."

outcome_tests:
  - id: package-structure
    type: deterministic
    checks:
      - file_exists: "SKILL.md"
      - file_exists: "scripts/speakers.js"
      - file_exists: "scripts/submit-text.js"
      - file_exists: "scripts/generate-audio.js"
      - file_exists: "scripts/status.js"
      - file_exists: "scripts/wait.js"
      - file_exists: "references/podcast-api.md"
  - id: skill-doc-quality
    type: llm_rubric
    rubric: |
      Scope fidelity (0-0.25): Does the skill stay focused on WeryAI podcast generation rather than generic chat or music-only generation?
      Workflow clarity (0-0.25): Does it clearly explain speaker lookup, text generation, audio generation, and the preferred bounded wait flow?
      Constraint honesty (0-0.25): Does it clearly state cost, explicit speaker requirements, and the debate-mode exactly-two-speakers rule?
      API fidelity & delivery (0-0.25): Are the documented fields faithful to the WeryAI podcast APIs, and does it enforce user-visible audio delivery, request parameter summary, and timeout fallbacks that offer internal status checks without raw commands?


path: scripts/wait.js
content: #!/usr/bin/env node

import { runScript } from './vendor/weryai-podcast/cli.js';
import { execute } from './vendor/weryai-podcast/wait.js';

const HELP = `Usage: node {baseDir}/scripts/wait.js [options]

Options:
  --json <data>    Pass parameters as a JSON string (use "-" for stdin)
  --dry-run        Preview the request flow without calling the API
  --verbose        Print debug info to stderr
  --help           Show this help message

Examples:
  node {baseDir}/scripts/wait.js --json '{"query":"What is retrieval augmented generation?","speakers":["travel-girl-english","leo-9328b6d2"],"language":"en","mode":"quick"}'
  node {baseDir}/scripts/wait.js --json '{"query":"Debate the tradeoffs of remote work","speakers":["travel-girl-english","leo-9328b6d2"],"language":"en","mode":"debate"}' --dry-run
`;

runScript(process.argv.slice(2), execute, HELP).catch(err => { console.error(err); process.exit(1); });


path: scripts/generate-audio.js
content: #!/usr/bin/env node

import { runScript } from './vendor/weryai-podcast/cli.js';
import { execute } from './vendor/weryai-podcast/generate-audio.js';

const HELP = `Usage: node {baseDir}/scripts/generate-audio.js [options]

Options:
  --task-id <id>    Required podcast task ID
  --json <data>     Optional JSON body (use "-" for stdin)
  --dry-run         Preview the request without calling the API
  --verbose         Print debug info to stderr
  --help            Show this help message

Examples:
  node {baseDir}/scripts/generate-audio.js --task-id <task-id>
  node {baseDir}/scripts/generate-audio.js --task-id <task-id> --json '{"scripts":[{"speakerId":"travel-girl-english","speakerName":"Mia","content":"Welcome back."}]}'
`;

runScript(process.argv.slice(2), execute, HELP).catch(err => { console.error(err); process.exit(1); });


path: scripts/submit-text.js
content: #!/usr/bin/env node

import { runScript } from './vendor/weryai-podcast/cli.js';
import { execute } from './vendor/weryai-podcast/submit-text.js';

const HELP = `Usage: node {baseDir}/scripts/submit-text.js [options]

Options:
  --json <data>    Pass parameters as a JSON string (use "-" for stdin)
  --dry-run        Preview the request without calling the API
  --verbose        Print debug info to stderr
  --help           Show this help message

Examples:
  node {baseDir}/scripts/submit-text.js --json '{"query":"What is retrieval augmented generation?","speakers":["travel-girl-english","leo-9328b6d2"],"language":"en","mode":"quick"}'
  node {baseDir}/scripts/submit-text.js --json '{"query":"Debate the pros and cons of remote work","speakers":["travel-girl-english","leo-9328b6d2"],"language":"en","mode":"debate"}' --dry-run
`;

runScript(process.argv.slice(2), execute, HELP).catch(err => { console.error(err); process.exit(1); });


path: scripts/status.js
content: #!/usr/bin/env node

import { runScript } from './vendor/weryai-podcast/cli.js';
import { execute } from './vendor/weryai-podcast/status.js';

const HELP = `Usage: node {baseDir}/scripts/status.js [options]

Options:
  --task-id <id>  Required podcast task ID
  --verbose       Print debug info to stderr
  --help          Show this help message

Examples:
  node {baseDir}/scripts/status.js --task-id <task-id>
`;

runScript(process.argv.slice(2), execute, HELP).catch(err => { console.error(err); process.exit(1); });


path: scripts/speakers.js
content: #!/usr/bin/env node

import { runScript } from './vendor/weryai-podcast/cli.js';
import { execute } from './vendor/weryai-podcast/speakers.js';

const HELP = `Usage: node {baseDir}/scripts/speakers.js [options]

Options:
  --language <code>  Required language code such as en, zh, or ja
  --verbose          Print debug info to stderr
  --help             Show this help message

Examples:
  node {baseDir}/scripts/speakers.js --language en
  node {baseDir}/scripts/speakers.js --language zh
`;

runScript(process.argv.slice(2), execute, HELP).catch(err => { console.error(err); process.exit(1); });


path: scripts/vendor/weryai-core/wait.js
content: import { log, sleep } from './client.js';
import { hasTaskOutput, normalizeBatchPhase, normalizeTaskCollection, normalizeTaskResult, toPhase } from './normalize.js';
import { isApiSuccess } from './errors.js';

export function resolvePollIntervalMs(ctx, elapsedMs, options = {}) {
  const configured = Number(ctx?.pollIntervalMs);
  if (Number.isFinite(configured) && configured >= 0) {
    return configured;
  }

  const profile = options.pollProfile || ctx?.pollProfile || 'standard';
  if (profile === 'fast') {
    if (elapsedMs < 30_000) return 2_000;
    if (elapsedMs < 120_000) return 4_000;
    return 6_000;
  }
  if (profile === 'slow') {
    if (elapsedMs < 30_000) return 6_000;
    if (elapsedMs < 120_000) return 10_000;
    return 15_000;
  }

  if (elapsedMs < 30_000) return 3_000;
  if (elapsedMs < 120_000) return 6_000;
  return 10_000;
}

export async function pollSubmittedTasks(client, options) {
  const { batchId, taskIds, allowBatch = false } = options;
  if (!Array.isArray(taskIds) || taskIds.length === 0) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'PROTOCOL',
      errorCategory: 'server',
      errorTitle: 'Unexpected API response',
      retryable: false,
      field: 'task_ids',
      hint: 'The API reported success but did not return task IDs. Inspect the raw response before retrying.',
      errorMessage: 'API returned success but no task_ids.',
    };
  }
  if (taskIds.length > 1) {
    return batchId && allowBatch
      ? pollBatch(client, options)
      : pollMultipleTasks(client, options);
  }
  return pollSingleTask(client, { ...options, taskId: taskIds[0] });
}

export async function pollSingleTask(client, options) {
  const { taskId, batchId = null, taskIds = taskId ? [taskId] : [], ctx, outputKey, outputLabel, pollProfile } = options;
  const start = Date.now();

  while (true) {
    const elapsed = Date.now() - start;
    if (elapsed >= ctx.pollTimeoutMs) {
      return {
        ok: false,
        phase: 'failed',
        batchId,
        taskIds,
        taskId,
        taskStatus: 'unknown',
        [outputKey]: null,
        errorCode: 'TIMEOUT',
        errorCategory: 'timeout',
        errorTitle: 'Polling timed out',
        retryable: true,
        field: null,
        hint: 'The task may still be running. Use the status command to inspect the existing task before submitting a new one.',
        errorMessage: `Poll timeout after ${Math.round(elapsed / 1000)}s.`,
      };
    }

    await sleep(resolvePollIntervalMs(ctx, elapsed, { pollProfile }));

    let res;
    try {
      res = await client.get(`/v1/generation/${taskId}/status`, { retries: 3 });
    } catch (err) {
      log(`Warning: poll query failed (${err.message}), will retry next interval.`);
      continue;
    }

    if (!isApiSuccess(res)) {
      log(`Warning: poll returned status ${res.status}, will retry next interval.`);
      continue;
    }

    const task = normalizeTaskResult(res.data);
    const phase = toPhase(task.taskStatus);
    const missingOutputs = phase === 'completed' && !hasTaskOutput(task, outputKey);
    const elapsedSec = Math.round((Date.now() - start) / 1000);
    log(`Polling ${taskId}... status: ${task.taskStatus} (${elapsedSec}s elapsed)`);

    if (phase === 'completed' || phase === 'failed') {
      return {
        ok: phase === 'completed' && !missingOutputs,
        phase: missingOutputs ? 'failed' : phase,
        batchId,
        taskIds,
        taskId: task.taskId,
        taskStatus: task.taskStatus,
        [outputKey]: task[outputKey],
        lyrics: task.lyrics,
        coverUrl: task.coverUrl,
        balance: null,
        errorCode: phase === 'failed' || missingOutputs ? 'TASK_FAILED' : null,
        errorCategory: phase === 'failed' || missingOutputs ? 'task' : null,
        errorTitle: phase === 'failed'
          ? 'Task failed'
          : missingOutputs
            ? `Missing ${outputLabel} output`
            : null,
        retryable: phase === 'failed' || missingOutputs ? false : null,
        field: null,
        hint: phase === 'failed' || missingOutputs ? 'Check the existing task state and parameters before retrying.' : null,
        errorMessage: missingOutputs
          ? `Task reached a completed state but returned no ${outputLabel} URLs.`
          : phase === 'failed'
            ? (task.msg || 'Task failed')
            : null,
      };
    }
  }
}

export async function pollMultipleTasks(client, options) {
  const { taskIds, ctx, outputKey, outputLabel, pollProfile } = options;
  const start = Date.now();

  while (true) {
    const elapsed = Date.now() - start;
    if (elapsed >= ctx.pollTimeoutMs) {
      return {
        ok: false,
        phase: 'failed',
        batchId: null,
        taskIds,
        tasks: null,
        errorCode: 'TIMEOUT',
        errorCategory: 'timeout',
        errorTitle: 'Polling timed out',
        retryable: true,
        field: null,
        hint: 'The tasks may still be running. Inspect the existing task IDs before submitting a new batch.',
        errorMessage: `Poll timeout after ${Math.round(elapsed / 1000)}s.`,
      };
    }

    await sleep(resolvePollIntervalMs(ctx, elapsed, { pollProfile }));

    const tasks = [];
    let shouldRetry = false;

    for (const taskId of taskIds) {
      let res;
      try {
        res = await client.get(`/v1/generation/${taskId}/status`, { retries: 3 });
      } catch (err) {
        log(`Warning: poll query failed for ${taskId} (${err.message}), will retry next interval.`);
        shouldRetry = true;
        break;
      }

      if (!isApiSuccess(res)) {
        log(`Warning: poll returned status ${res.status} for ${taskId}, will retry next interval.`);
        shouldRetry = true;
        break;
      }

      tasks.push(normalizeTaskResult(res.data));
    }

    if (shouldRetry) {
      continue;
    }

    const phase = normalizeBatchPhase(tasks, { outputKey });
    const elapsedSec = Math.round((Date.now() - start) / 1000);
    const summary = tasks.map((task) => `${task.taskId}:${task.taskStatus}`).join(', ');
    log(`Polling tasks... [${summary}] (${elapsedSec}s elapsed)`);

    if (phase === 'completed' || phase === 'failed') {
      return {
        ok: phase === 'completed',
        phase,
        batchId: null,
        taskIds,
        tasks,
        errorCode: phase === 'failed' ? 'TASK_FAILED' : null,
        errorCategory: phase === 'failed' ? 'task' : null,
        errorTitle: phase === 'failed' ? 'Batch failed' : null,
        retryable: phase === 'failed' ? false : null,
        field: null,
        hint: phase === 'failed' ? `Inspect the existing batch and task statuses before retrying.` : null,
        errorMessage: phase === 'failed'
          ? `One or more tasks failed or returned no ${outputLabel} URLs.`
          : null,
      };
    }
  }
}

export async function pollBatch(client, options) {
  const { batchId, taskIds, ctx, outputKey, outputLabel, pollProfile } = options;
  const start = Date.now();

  while (true) {
    const elapsed = Date.now() - start;
    if (elapsed >= ctx.pollTimeoutMs) {
      return {
        ok: false,
        phase: 'failed',
        batchId,
        taskIds,
        tasks: null,
        errorCode: 'TIMEOUT',
        errorCategory: 'timeout',
        errorTitle: 'Polling timed out',
        retryable: true,
        field: null,
        hint: 'The batch may still be running. Inspect the existing batch before submitting a new one.',
        errorMessage: `Poll timeout after ${Math.round(elapsed / 1000)}s.`,
      };
    }

    await sleep(resolvePollIntervalMs(ctx, elapsed, { pollProfile }));

    let res;
    try {
      res = await client.get(`/v1/generation/batch/${batchId}/status`, { retries: 3 });
    } catch (err) {
      log(`Warning: batch poll failed (${err.message}), will retry next interval.`);
      continue;
    }

    if (!isApiSuccess(res)) {
      log(`Warning: batch poll returned status ${res.status}, will retry next interval.`);
      continue;
    }

    const tasks = normalizeTaskCollection(res.data);
    const phase = normalizeBatchPhase(tasks, { outputKey });
    const elapsedSec = Math.round((Date.now() - start) / 1000);
    const summary = tasks.map((task) => task.taskStatus).join(', ');
    log(`Polling batch ${batchId}... [${summary}] (${elapsedSec}s elapsed)`);

    if (phase === 'completed' || phase === 'failed') {
      return {
        ok: phase === 'completed',
        phase,
        batchId,
        taskIds,
        tasks,
        errorCode: phase === 'failed' ? 'TASK_FAILED' : null,
        errorCategory: phase === 'failed' ? 'task' : null,
        errorTitle: phase === 'failed' ? 'Batch failed' : null,
        retryable: phase === 'failed' ? false : null,
        field: null,
        hint: phase === 'failed' ? `Inspect the existing batch and task statuses before retrying.` : null,
        errorMessage: phase === 'failed'
          ? `One or more tasks in the batch failed or returned no ${outputLabel} URLs.`
          : null,
      };
    }
  }
}


path: scripts/vendor/weryai-core/client.js
content: const LOG_PREFIX = '[weryai]';

export function createClient(ctx) {
  const { apiKey, baseUrl, requestTimeoutMs, verbose } = ctx;

  async function request(method, path, body = null, { retries = 0 } = {}) {
    if (!apiKey) {
      throw Object.assign(
        new Error('Missing API key environment variable.'),
        { code: 'NO_API_KEY' },
      );
    }

    const url = `${baseUrl}${path}`;
    const headers = {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json; charset=utf-8',
      Accept: 'application/json; charset=utf-8',
    };

    if (verbose) {
      const sanitized = body ? sanitizeForLog(body) : null;
      log(`${method} ${url}${sanitized ? ` ${JSON.stringify(sanitized)}` : ''}`);
    }

    let lastError;
    const maxAttempts = 1 + retries;

    for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
      if (attempt > 0) {
        const delay = Math.min(2000 * Math.pow(2, attempt - 1), 8000);
        if (verbose) log(`Retry ${attempt}/${retries} after ${delay}ms...`);
        await sleep(delay);
      }

      try {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), requestTimeoutMs);

        const res = await fetch(url, {
          method,
          headers,
          body: body ? 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})` };
        }

        if (verbose) {
          log(`Response ${res.status}: ${JSON.stringify(sanitizeForLog(data))}`);
        }

        return { httpStatus: res.status, ...data };
      } catch (err) {
        lastError = err;
        if (err.name === 'AbortError') {
          lastError = new Error(`Request timeout after ${requestTimeoutMs}ms: ${method} ${path}`);
          lastError.code = 'TIMEOUT';
        }
        if (attempt === maxAttempts - 1) break;
      }
    }

    throw lastError;
  }

  return {
    post: (path, body, opts) => request('POST', path, body, opts),
    get: (path, opts) => request('GET', path, null, opts),
  };
}

function sanitizeForLog(obj) {
  if (!obj || typeof obj !== 'object') return obj;
  const copy = Array.isArray(obj) ? [...obj] : { ...obj };

  for (const key of Object.keys(copy)) {
    const value = copy[key];
    if (/authorization|api[_-]?key/i.test(key)) {
      copy[key] = '***';
    } else if (typeof value === 'string' && /^(prompt|negative_prompt|negativePrompt|description|lyrics)$/i.test(key)) {
      copy[key] = truncate(value);
    } else if (typeof value === 'string' && /^(image|reference_audio|webhook_url)$/i.test(key)) {
      copy[key] = truncate(value);
    } else if (Array.isArray(value) && /^(images|videos|audios|image_url)$/i.test(key)) {
      copy[key] = value.map((item) => (typeof item === 'string' ? truncate(item) : item));
    } else if (value && typeof value === 'object') {
      copy[key] = sanitizeForLog(value);
    }
  }

  return copy;
}

function truncate(value, limit = 80) {
  return value.length > limit ? `${value.slice(0, limit)}...` : value;
}

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

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

export { log, sleep };


path: scripts/vendor/weryai-core/normalize.js
content: const STATUS_MAP = {
  waiting: 'waiting',
  WAITING: 'waiting',
  pending: 'waiting',
  PENDING: 'waiting',
  processing: 'processing',
  PROCESSING: 'processing',
  running: 'processing',
  RUNNING: 'processing',
  succeed: 'completed',
  SUCCEED: 'completed',
  success: 'completed',
  SUCCESS: 'completed',
  failed: 'failed',
  FAILED: 'failed',
};

const CONTENT_STATUS_MAP = {
  'text-success': 'text-success',
  'text-fail': 'text-fail',
  'audio-success': 'audio-success',
  'audio-fail': 'audio-fail',
};

export function normalizeStatus(raw) {
  const mapped = STATUS_MAP[raw];
  if (!mapped) {
    process.stderr.write(`[weryai] Warning: unknown task_status "${raw}", treating as "unknown"\n`);
    return 'unknown';
  }
  return mapped;
}

export function normalizeContentStatus(raw) {
  if (typeof raw !== 'string' || !raw.trim()) return null;
  const normalized = raw.trim().toLowerCase();
  return CONTENT_STATUS_MAP[normalized] || normalized;
}

export function toPhase(normalizedStatus) {
  if (normalizedStatus === 'completed') return 'completed';
  if (normalizedStatus === 'failed') return 'failed';
  return 'running';
}

export function normalizeTaskResult(data) {
  if (Array.isArray(data)) {
    return pickSingleTask(data.map(normalizeSingleTask));
  }
  return normalizeSingleTask(data);
}

export function normalizeTaskCollection(data) {
  if (Array.isArray(data)) {
    return data.map(normalizeSingleTask);
  }
  return [normalizeSingleTask(data)];
}

export function hasTaskOutput(task, outputKey) {
  const value = task?.[outputKey];
  return Array.isArray(value) ? value.length > 0 : Boolean(value);
}

export function normalizeBatchPhase(tasks, { outputKey } = {}) {
  const valid = tasks.filter(Boolean);
  if (valid.length === 0) return 'unknown';
  const allDone = valid.every((task) => task.taskStatus === 'completed' || task.taskStatus === 'failed');
  if (!allDone) return 'running';
  const anyFailed = valid.some((task) => task.taskStatus === 'failed');
  const anyMissingOutput = outputKey
    ? valid.some((task) => task.taskStatus === 'completed' && !hasTaskOutput(task, outputKey))
    : false;
  if (anyFailed || anyMissingOutput) return 'failed';
  return 'completed';
}

function normalizeSingleTask(task) {
  if (!task) {
    return {
      taskId: null,
      rawStatus: 'unknown',
      taskStatus: 'unknown',
      images: null,
      videos: null,
      audios: null,
      lyrics: null,
      coverUrl: null,
      msg: null,
    };
  }

  const rawStatus = task.task_status ?? task.taskStatus ?? 'unknown';
  const status = normalizeStatus(rawStatus);
  const contentStatus = normalizeContentStatus(
    task.content_status ?? task.contentStatus ?? task.output?.content_status,
  );
  const images = normalizeArray(task.images ?? task.output?.images);
  const videos = normalizeArray(task.videos ?? task.output?.videos);
  const audios = normalizeArray(task.audios ?? task.output?.audios);
  const lyrics = normalizeString(task.lyrics ?? task.output?.lyrics);
  const coverUrl = normalizeString(task.cover_url ?? task.coverUrl ?? task.output?.cover_url);
  const scripts = normalizeScripts(task.scripts ?? task.output?.scripts ?? task.script ?? task.output?.script);

  return {
    taskId: task.task_id ?? task.taskId ?? null,
    rawStatus,
    taskStatus: status,
    contentStatus,
    images,
    videos,
    audios,
    lyrics,
    coverUrl,
    scripts,
    msg: task.msg ?? null,
  };
}

function normalizeArray(value) {
  return Array.isArray(value) && value.length > 0 ? value : null;
}

function normalizeString(value) {
  return typeof value === 'string' && value.trim() ? value : null;
}

function normalizeScripts(value) {
  if (!Array.isArray(value) || value.length === 0) return null;
  return value.map((entry) => {
    if (!entry || typeof entry !== 'object') return entry;
    return {
      speakerId: entry.speaker_id ?? entry.speakerId ?? null,
      speakerName: entry.speaker_name ?? entry.speakerName ?? null,
      content: typeof entry.content === 'string' ? entry.content : null,
    };
  });
}

function pickSingleTask(tasks) {
  if (!Array.isArray(tasks) || tasks.length === 0) {
    return normalizeSingleTask(null);
  }
  return tasks[0];
}


path: scripts/vendor/weryai-core/upload.js
content: import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { formatApiError, isApiSuccess } from './errors.js';

export const DEFAULT_UPLOAD_API_PATH = '/v1/generation/upload-file';
export const DEFAULT_UPLOAD_MAX_BYTES = 500 * 1024 * 1024;

const ALLOWED_UPLOAD_EXTS = new Set([
  // image
  '.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp', '.tiff', '.tif',
  // video
  '.mp4', '.mov', '.m4v', '.webm', '.avi', '.mkv',
  // audio
  '.mp3', '.wav', '.m4a', '.aac', '.flac', '.ogg',
  // text and structured text
  '.txt', '.md', '.json', '.csv', '.srt', '.vtt', '.xml', '.yaml', '.yml',
]);

const BLOCKED_UPLOAD_EXTS = new Set([
  '.exe', '.dll', '.bat', '.cmd', '.com', '.msi', '.scr', '.ps1', '.sh',
  '.apk', '.ipa', '.bin', '.jar',
]);

export function isPublicHttpsUrl(value) {
  if (typeof value !== 'string' || value.trim().length === 0) return false;
  try {
    const url = new URL(value);
    return url.protocol === 'https:';
  } catch {
    return false;
  }
}

export function isRemoteUrl(value) {
  if (typeof value !== 'string' || value.trim().length === 0) return false;
  try {
    const url = new URL(value);
    return url.protocol === 'https:' || url.protocol === 'http:';
  } catch {
    return false;
  }
}

export function normalizeLocalFilePath(value) {
  if (typeof value !== 'string' || value.trim().length === 0) return null;
  if (value.startsWith('file://')) {
    return fileURLToPath(new URL(value));
  }
  return path.resolve(value);
}

export function inferMimeTypeByExtension(filePath) {
  const ext = path.extname(filePath).toLowerCase();
  if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
  if (ext === '.png') return 'image/png';
  if (ext === '.webp') return 'image/webp';
  if (ext === '.gif') return 'image/gif';
  if (ext === '.bmp') return 'image/bmp';
  if (ext === '.tiff' || ext === '.tif') return 'image/tiff';
  if (ext === '.mp4') return 'video/mp4';
  if (ext === '.mov') return 'video/quicktime';
  if (ext === '.mp3') return 'audio/mpeg';
  if (ext === '.wav') return 'audio/wav';
  if (ext === '.txt') return 'text/plain';
  if (ext === '.md') return 'text/markdown';
  if (ext === '.json') return 'application/json';
  return 'application/octet-stream';
}

function extractUploadUrl(res) {
  const list = res?.data?.object_url_list;
  if (Array.isArray(list) && typeof list[0] === 'string' && list[0].trim()) {
    return list[0].trim();
  }
  return null;
}

function makeUploadBatchNo(prefix = 'core-upload') {
  return `${prefix}-${Date.now()}`;
}

function formatBytes(bytes) {
  const mb = bytes / (1024 * 1024);
  return `${mb.toFixed(0)}MB`;
}

function validateUploadCandidate(filePath, fileSize, maxBytes) {
  const ext = path.extname(filePath).toLowerCase();

  if (fileSize > maxBytes) {
    throw new Error(
      `File exceeds maximum upload size (${formatBytes(fileSize)} > ${formatBytes(maxBytes)}). ` +
      'Please use a file smaller than 500MB.',
    );
  }

  if (!ext) {
    throw new Error('File has no extension. Please use a standard image/video/audio/text file and check the suffix.');
  }

  if (BLOCKED_UPLOAD_EXTS.has(ext)) {
    throw new Error(`Disallowed file extension "${ext}". Please upload a valid image/video/audio/text file and check the suffix.`);
  }

  if (!ALLOWED_UPLOAD_EXTS.has(ext)) {
    throw new Error(`Unsupported file extension "${ext}". Please upload a valid image/video/audio/text file and check the suffix.`);
  }
}

export async function uploadLocalFileToPublicUrl(ctx, localPathInput, options = {}) {
  const {
    apiPath = DEFAULT_UPLOAD_API_PATH,
    batchNo = makeUploadBatchNo(),
    fixed = false,
    timeoutMs = Number(ctx.requestTimeoutMs) || 60_000,
    maxBytes = DEFAULT_UPLOAD_MAX_BYTES,
  } = options;

  const localPath = normalizeLocalFilePath(localPathInput);
  if (!localPath) {
    throw new Error(`Invalid local path: ${localPathInput}`);
  }

  const stat = await fs.stat(localPath);
  if (!stat.isFile()) {
    throw new Error(`Local path is not a file: ${localPathInput}`);
  }
  validateUploadCandidate(localPath, stat.size, maxBytes);

  const fileBuffer = await fs.readFile(localPath);
  const fileName = path.basename(localPath);
  const mimeType = inferMimeTypeByExtension(localPath);

  const form = new FormData();
  form.append('file', new Blob([fileBuffer], { type: mimeType }), fileName);
  form.append('batch_no', batchNo);
  form.append('fixed', String(Boolean(fixed)));

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  let res;
  try {
    const defaultOfficialDomains = ['api.weryai.com', 'api-growth-agent.weryai.com'];
    const uploadUrl = new URL(`${ctx.baseUrl}${apiPath}`);
    if (!defaultOfficialDomains.includes(uploadUrl.hostname) && !process.env.WERYAI_ALLOW_INSECURE_UPLOAD) {
        console.warn(`[SECURITY WARNING] Uploading local file to non-official domain: ${uploadUrl.hostname}. Set WERYAI_ALLOW_INSECURE_UPLOAD=1 to suppress this warning if you trust this endpoint.`);
    }

    res = await fetch(uploadUrl, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${ctx.apiKey}`,
      },
      body: form,
      signal: controller.signal,
    });
  } catch (err) {
    clearTimeout(timer);
    if (err.name === 'AbortError') {
      throw new Error(`Upload timeout after ${timeoutMs}ms: ${localPathInput}`);
    }
    throw err;
  }
  clearTimeout(timer);

  let data;
  try {
    data = await res.json();
  } catch {
    throw new Error(`Upload failed with non-JSON response (HTTP ${res.status}).`);
  }

  const wrapped = { httpStatus: res.status, ...data };
  if (!isApiSuccess(wrapped)) {
    const apiErr = formatApiError(wrapped);
    throw new Error(apiErr.errorMessage || `Upload failed (HTTP ${res.status}).`);
  }

  const uploadedUrl = extractUploadUrl(wrapped);
  if (!uploadedUrl) {
    throw new Error('Upload succeeded but data.object_url_list[0] is missing.');
  }

  return uploadedUrl;
}

export async function resolvePublicUrlFromSource(ctx, source, options = {}) {
  if (typeof source !== 'string' || source.trim().length === 0) {
    throw new Error('media source must be a non-empty string.');
  }

  if (isRemoteUrl(source)) return source;

  return uploadLocalFileToPublicUrl(ctx, source, options);
}

export async function resolvePublicUrlsFromSources(ctx, sources, options = {}) {
  return Promise.all((sources || []).map((source) => resolvePublicUrlFromSource(ctx, source, options)));
}


path: scripts/vendor/weryai-core/errors.js
content: const STATIC_ERROR_MAP = {
  '400': {
    category: 'validation',
    title: 'Invalid request',
    message: 'Some of the request details are invalid. Please review your input and try again.',
    retryable: false,
  },
  '401': {
    category: 'auth',
    title: 'Authentication failed',
    message: 'Authentication failed. Please check your API key and try again.',
    retryable: false,
    hint: 'Make sure the API key is valid and sent in the Authorization header.',
  },
  '403': {
    category: 'auth',
    title: 'Authentication failed',
    message: 'Authentication failed. Please check your API key and try again.',
    retryable: false,
    hint: 'Verify the API key, account policy, and any IP restrictions.',
  },
  '404': {
    category: 'not_found',
    title: 'Resource not found',
    message: 'The requested item could not be found. Please check the ID or input and try again.',
    retryable: false,
  },
  '429': {
    category: 'rate_limit',
    title: 'Too many requests',
    message: 'Too many requests in a short time. Please wait a moment and try again.',
    retryable: true,
  },
  '500': {
    category: 'server',
    title: 'Temporary service issue',
    message: 'Something went wrong on our side. Please try again in a moment.',
    retryable: true,
  },
  '1001': {
    category: 'rate_limit',
    title: 'Too many requests',
    message: 'Too many requests in a short time. Please wait a moment and try again.',
    retryable: true,
  },
  '1003': {
    category: 'not_found',
    title: 'Resource not found',
    message: 'The requested item could not be found. Please check the ID or input and try again.',
    retryable: false,
  },
  '1010': {
    category: 'not_found',
    title: 'Resource not found',
    message: 'The requested item could not be found. Please check the ID or input and try again.',
    retryable: false,
  },
  '1011': {
    category: 'credits',
    title: 'Not enough credits',
    message: 'You do not have enough credits to complete this request.',
    retryable: false,
  },
  '1014': {
    category: 'upload',
    title: 'Upload limit reached',
    message: 'You have reached an upload limit. Please wait and try again later.',
    retryable: true,
  },
  '1015': {
    category: 'upload',
    title: 'Upload limit reached',
    message: 'You have reached an upload limit. Please wait and try again later.',
    retryable: true,
  },
  '1102': {
    category: 'upload',
    title: 'Upload failed',
    message: 'We could not upload the file right now. Please try again.',
    retryable: true,
  },
  '2001': {
    category: 'content_safety',
    title: 'Content flagged',
    message: 'The provided image or request content was flagged by the safety system. Please revise it and try again.',
    retryable: false,
  },
  '2003': {
    category: 'content_safety',
    title: 'Content flagged',
    message: 'The provided image or request content was flagged by the safety system. Please revise it and try again.',
    retryable: false,
  },
  '2004': {
    category: 'validation',
    title: 'Unsupported image format',
    message: 'The image format is not supported. Please use JPG, JPEG, PNG, or WEBP.',
    retryable: false,
  },
  '5000': {
    category: 'server',
    title: 'Temporary service issue',
    message: 'Something went wrong on our side. Please try again in a moment.',
    retryable: true,
  },
  '6001': {
    category: 'server',
    title: 'Temporary service issue',
    message: 'Something went wrong on our side. Please try again in a moment.',
    retryable: true,
  },
  '6002': {
    category: 'rate_limit',
    title: 'Too many requests',
    message: 'Too many requests in a short time. Please wait a moment and try again.',
    retryable: true,
  },
  '6003': {
    category: 'server',
    title: 'Temporary service issue',
    message: 'Something went wrong on our side. Please try again in a moment.',
    retryable: true,
  },
  '6004': {
    category: 'server',
    title: 'Generation failed',
    message: 'The request could not be completed. Please try again with different input or try again later.',
    retryable: true,
  },
  '6010': {
    category: 'active_job_limit',
    title: 'Too many active jobs',
    message: 'You already have too many active jobs. Please wait for current jobs to finish before starting a new one.',
    retryable: true,
  },
  '6101': {
    category: 'daily_limit',
    title: 'Daily limit reached',
    message: 'You have reached the daily limit for this workflow. Please try again later.',
    retryable: true,
  },
  '8001': {
    category: 'workflow_state',
    title: 'Podcast script not ready',
    message: 'The podcast script is not ready yet. Please generate the script first, then create the audio.',
    retryable: false,
  },
  '8002': {
    category: 'workflow_state',
    title: 'Audio already submitted',
    message: 'Audio generation has already been submitted for this podcast. Please check the existing task instead of submitting again.',
    retryable: false,
  },
};

const VALIDATION_PATTERNS = [
  { pattern: /\bmodel cannot be empty\b/i, field: 'model', title: 'Missing model', message: 'Please provide a model for this request.' },
  { pattern: /\bmodel is not exist\b|\bmodel not supported\b/i, field: 'model', title: 'Unsupported model', message: 'The selected model is not supported. Please choose a supported model and try again.' },
  { pattern: /\bprompt cannot be empty\b/i, field: 'prompt', title: 'Missing prompt', message: 'Please provide a prompt for this request.' },
  { pattern: /\bnegative prompt\b/i, field: 'negative_prompt', title: 'Invalid negative prompt', message: 'The negative prompt is invalid. Please review it and try again.' },
  { pattern: /\bimage cannot be empty\b/i, field: 'image', title: 'Missing image', message: 'Please provide an image for this request.' },
  { pattern: /\bimages cannot be empty\b/i, field: 'images', title: 'Missing images', message: 'Please provide at least one image for this request.' },
  { pattern: /\bimage url cannot be empty\b/i, field: 'image_url', title: 'Missing image URL', message: 'Please provide a valid image URL.' },
  { pattern: /\bvideo url cannot be empty\b/i, field: 'video_url', title: 'Missing video URL', message: 'Please provide a valid video URL.' },
  { pattern: /\baudio url cannot be empty\b/i, field: 'audio_url', title: 'Missing audio URL', message: 'Please provide a valid audio URL.' },
  { pattern: /\bimage url error\b/i, field: 'image_url', title: 'Invalid image URL', message: 'The provided image URL is not valid. Please check it and try again.' },
  { pattern: /\bvideo url error\b/i, field: 'video_url', title: 'Invalid video URL', message: 'The provided video URL is not valid. Please check it and try again.' },
  { pattern: /\baudio url error\b/i, field: 'audio_url', title: 'Invalid audio URL', message: 'The provided audio URL is not valid. Please check it and try again.' },
  { pattern: /\bonly support jpg|jpeg|png|webp image type\b/i, field: 'image', title: 'Unsupported image format', message: 'The image format is not supported. Please use JPG, JPEG, PNG, or WEBP.' },
  { pattern: /\baspect ratio cannot be empty\b/i, field: 'aspect_ratio', title: 'Missing aspect ratio', message: 'Please provide an aspect ratio for this request.' },
  { pattern: /\baspect ratio .* is not supported\b/i, field: 'aspect_ratio', title: 'Unsupported aspect ratio', message: 'The selected aspect ratio is not supported for this request.' },
  { pattern: /\bresolution cannot be empty\b/i, field: 'resolution', title: 'Missing resolution', message: 'Please provide a resolution for this request.' },
  { pattern: /\bresolution .* is not supported\b/i, field: 'resolution', title: 'Unsupported resolution', message: 'The selected resolution is not supported for this request.' },
  { pattern: /\bduration should not be empty\b|\bduration cannot be empty\b/i, field: 'duration', title: 'Missing duration', message: 'Please provide a duration for this request.' },
  { pattern: /\bduration .* is not supported\b/i, field: 'duration', title: 'Unsupported duration', message: 'The selected duration is not supported for this request.' },
  { pattern: /\bvideo number .* is not supported\b/i, field: 'video_number', title: 'Unsupported video count', message: 'The requested number of videos is not supported for this operation.' },
  { pattern: /\bimage number .* is not supported\b/i, field: 'image_number', title: 'Unsupported image count', message: 'The requested number of images is not supported for this operation.' },
  { pattern: /\bimages exceeds the limit\b/i, field: 'images', title: 'Too many images', message: 'Too many images were provided for this request. Please reduce the number of images and try again.' },
  { pattern: /\bdoes not support multiple images\b/i, field: 'images', title: 'Multiple images not supported', message: 'The selected model does not support multiple images.' },
  { pattern: /\btarget language cannot be empty\b/i, field: 'target_language', title: 'Missing target language', message: 'Please provide a target language for this request.' },
  { pattern: /\blanguage cannot be empty\b/i, field: 'language', title: 'Missing language', message: 'Please provide a language for this request.' },
  { pattern: /\bquery cannot be empty\b/i, field: 'query', title: 'Missing query', message: 'Please provide a query for this request.' },
  { pattern: /\bspeakers cannot be empty\b/i, field: 'speakers', title: 'Missing speakers', message: 'Please provide at least one speaker.' },
  { pattern: /\bmode cannot be empty\b/i, field: 'mode', title: 'Missing mode', message: 'Please provide a mode for this request.' },
  { pattern: /\bmusic type cannot be empty\b/i, field: 'type', title: 'Missing music type', message: 'Please provide a music type for this request.' },
  { pattern: /\btemplate config id cannot be empty\b/i, field: 'template_config_id', title: 'Missing template config', message: 'Please provide a template configuration ID.' },
  { pattern: /\bthis template config is not available for api\b/i, field: 'template_config_id', title: 'Template not available', message: 'The selected template is not available for API use. Please choose another template.' },
  { pattern: /\bconfig ids cannot be empty\b/i, field: 'config_ids', title: 'Missing config IDs', message: 'Please provide at least one config ID.' },
  { pattern: /\bconfig ids cannot exceed 50\b/i, field: 'config_ids', title: 'Too many config IDs', message: 'You can check at most 50 config IDs in one request.' },
  { pattern: /\bprompt and bg_color cannot both be empty\b/i, field: 'prompt|bg_color', title: 'Missing background settings', message: 'Please provide either a background prompt or a background color.' },
  { pattern: /\brectvolist cannot be empty\b|\brect_vo_list cannot be empty\b/i, field: 'rectVOList', title: 'Missing selection area', message: 'Please select at least one area to process and try again.' },
  { pattern: /\bupscale factor .* is not supported\b/i, field: 'upscale_factor', title: 'Unsupported upscale factor', message: 'The selected upscale factor is not supported.' },
  { pattern: /\btype .* is not supported\b/i, field: 'type', title: 'Unsupported type', message: 'The selected type is not supported for this request.' },
  { pattern: /\btask id cannot be empty\b/i, field: 'task_id', title: 'Missing task ID', message: 'Please provide a task ID.' },
  { pattern: /\btask id error\b/i, field: 'task_id', title: 'Invalid task ID', message: 'The task ID is invalid. Please check it and try again.' },
  { pattern: /\btask not exist\b/i, field: 'task_id', title: 'Task not found', message: 'The requested task could not be found. Please check the task ID and try again.' },
  { pattern: /\bbatch id cannot be empty\b/i, field: 'batch_id', title: 'Missing batch ID', message: 'Please provide a batch ID.' },
  { pattern: /\bbatch not exist\b|\bno tasks found\b/i, field: 'batch_id', title: 'Batch not found', message: 'The requested batch could not be found. Please check the batch ID and try again.' },
  { pattern: /\bcallerid not exist\b|\bcaller id not exist\b/i, field: 'caller_id', title: 'Invalid caller ID', message: 'The provided caller ID could not be found. Please check it and try again.' },
  { pattern: /\bfile empty\b/i, field: 'file', title: 'Missing file', message: 'No file was uploaded. Please choose a file and try again.' },
  { pattern: /\bonly image, video and audio files are supported\b/i, field: 'file', title: 'Unsupported file type', message: 'Only image, video, and audio files are supported.' },
  { pattern: /\bfile content does not match the extension or is unsupported\b/i, field: 'file', title: 'Invalid file content', message: 'The uploaded file could not be verified. Please upload a valid supported file.' },
  { pattern: /\bimage file size must be less than 10mb\b/i, field: 'file', title: 'Image too large', message: 'The image is too large. Please upload an image smaller than 10 MB.' },
  { pattern: /\bvideo or audio file size must be less than 50mb\b/i, field: 'file', title: 'File too large', message: 'The file is too large. Please upload a video or audio file smaller than 50 MB.' },
];

export function formatApiError(response) {
  const httpStatus = response.httpStatus;
  const code = response.status != null ? String(response.status) : null;
  const rawMessage = pickRawMessage(response);

  if (httpStatus === 401 || httpStatus === 403) {
    return failure(String(httpStatus), STATIC_ERROR_MAP[String(httpStatus)], { raw: response });
  }
  if (httpStatus === 404) {
    return failure('404', STATIC_ERROR_MAP['404'], { raw: response });
  }
  if (httpStatus === 429) {
    return failure('429', STATIC_ERROR_MAP['429'], { raw: response });
  }
  if (httpStatus >= 500) {
    return failure('500', STATIC_ERROR_MAP['500'], { raw: response });
  }
  if (httpStatus === 400) {
    return failure('400', STATIC_ERROR_MAP['400'], { raw: response });
  }

  if (code === '1002') {
    return classifyValidationError(response);
  }

  const mapped = code ? STATIC_ERROR_MAP[code] : null;
  if (mapped) {
    return failure(code, mapped, { raw: response });
  }

  return failure(code, {
    category: 'server',
    title: 'Request failed',
    message: rawMessage || `We could not complete this request right now. Please try again later.`,
    retryable: true,
  }, { raw: response });
}

export function formatNetworkError(err) {
  if (err.code === 'NO_API_KEY') {
    return failure('NO_API_KEY', {
      category: 'auth',
      title: 'Missing API key',
      message: err.message,
      retryable: false,
      hint: 'Configure WERYAI_API_KEY before retrying.',
    });
  }
  if (err.code === 'TIMEOUT') {
    return failure('TIMEOUT', {
      category: 'timeout',
      title: 'Request timed out',
      message: err.message,
      retryable: true,
      hint: 'Wait a moment and retry. If it keeps happening, reduce request size or check service status.',
    });
  }
  return failure('NETWORK_ERROR', {
    category: 'network',
    title: 'Network error',
    message: `We could not reach the API right now. Please check the network and try again.`,
    retryable: true,
    hint: err?.message || String(err),
  });
}

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

export function describeApiError(response, options = {}) {
  const { includeTitle = true } = options;
  const view = formatApiError(response);
  if (!includeTitle || !view.errorTitle) return view.errorMessage;
  if (!view.errorMessage || view.errorMessage === view.errorTitle) return view.errorTitle;
  return `${view.errorTitle}: ${view.errorMessage}`;
}

function classifyValidationError(response) {
  const rawMessage = pickRawMessage(response);

  for (const rule of VALIDATION_PATTERNS) {
    if (rule.pattern.test(rawMessage)) {
      return failure('1002', {
        category: 'validation',
        title: rule.title,
        message: rule.message,
        retryable: false,
        field: rule.field,
      }, { raw: response });
    }
  }

  if (/missing|cannot be empty|required/i.test(rawMessage)) {
    return failure('1002', {
      category: 'validation',
      title: 'Missing required input',
      message: 'Some required information is missing. Please review your input and try again.',
      retryable: false,
    }, { raw: response });
  }

  if (/invalid|unsupported|error/i.test(rawMessage)) {
    return failure('1002', {
      category: 'validation',
      title: 'Invalid request parameters',
      message: 'One or more request settings are invalid or not supported. Please review your input and try again.',
      retryable: false,
    }, { raw: response });
  }

  return failure('1002', {
    category: 'validation',
    title: 'Invalid request',
    message: 'Some of the request details are missing or not supported. Please review your input and try again.',
    retryable: false,
  }, { raw: response });
}

function pickRawMessage(response) {
  return String(response?.msg || response?.message || response?.desc || '').trim();
}

function failure(errorCode, view, extra = {}) {
  return {
    ok: false,
    phase: 'failed',
    errorCode: errorCode != null ? String(errorCode) : null,
    errorCategory: view.category,
    errorTitle: view.title,
    errorMessage: view.message,
    retryable: view.retryable,
    field: view.field ?? null,
    hint: view.hint ?? null,
    raw: extra.raw ?? null,
  };
}


path: scripts/vendor/weryai-core/status.js
content: import { createClient } from './client.js';
import { formatApiError, formatNetworkError, isApiSuccess } from './errors.js';
import { hasTaskOutput, normalizeBatchPhase, normalizeTaskCollection, normalizeTaskResult, toPhase } from './normalize.js';

export async function executeStatus(input, ctx, options) {
  const { outputKey, outputLabel, allowBatch = false } = options;
  const taskId = input.taskId || input['task-id'] || input.task_id;
  const batchId = input.batchId || input['batch-id'] || input.batch_id;

  if (!taskId && !(allowBatch && batchId)) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'VALIDATION',
      errorCategory: 'validation',
      errorTitle: 'Missing identifier',
      retryable: false,
      field: allowBatch && !taskId ? 'task_id|batch_id' : 'task_id',
      hint: allowBatch ? 'Provide either a task ID or a batch ID.' : 'Provide a task ID and retry.',
      errorMessage: allowBatch
        ? 'Either --task-id or --batch-id is required.'
        : 'The status script requires `--task-id <task-id>`.',
    };
  }

  const client = createClient(ctx);
  if (allowBatch && batchId) return queryBatch(client, batchId, { outputKey, outputLabel });
  return queryTask(client, taskId, { outputKey, outputLabel });
}

async function queryTask(client, taskId, options) {
  const { outputKey, outputLabel } = options;

  let res;
  try {
    res = await client.get(`/v1/generation/${taskId}/status`, { retries: 3 });
  } catch (err) {
    return formatNetworkError(err);
  }

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

  const task = normalizeTaskResult(res.data);
  const phase = toPhase(task.taskStatus);
  const missingOutputs = phase === 'completed' && !hasTaskOutput(task, outputKey);
  return {
    ok: phase !== 'failed' && !missingOutputs,
    phase: missingOutputs ? 'failed' : phase,
    batchId: null,
    taskIds: task.taskId ? [task.taskId] : [],
    taskId: task.taskId,
    taskStatus: task.taskStatus,
    [outputKey]: task[outputKey],
    lyrics: task.lyrics,
    coverUrl: task.coverUrl,
    balance: null,
    errorCode: phase === 'failed' || missingOutputs ? 'TASK_FAILED' : null,
    errorCategory: phase === 'failed' || missingOutputs ? 'task' : null,
    errorTitle: phase === 'failed'
      ? 'Task failed'
      : missingOutputs
        ? `Missing ${outputLabel} output`
        : null,
    retryable: phase === 'failed' || missingOutputs ? false : null,
    field: null,
    hint: phase === 'failed' || missingOutputs ? 'Check the task payload and existing task status before submitting a new job.' : null,
    errorMessage: missingOutputs
      ? `Task reached a completed state but returned no ${outputLabel} URLs.`
      : phase === 'failed'
        ? (task.msg || 'Task failed')
        : null,
  };
}

async function queryBatch(client, batchId, options) {
  const { outputKey, outputLabel } = options;

  let res;
  try {
    res = await client.get(`/v1/generation/batch/${batchId}/status`, { retries: 3 });
  } catch (err) {
    return formatNetworkError(err);
  }

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

  const tasks = normalizeTaskCollection(res.data);
  const phase = normalizeBatchPhase(tasks, { outputKey });

  return {
    ok: phase !== 'failed',
    phase,
    batchId,
    tasks,
    errorCode: phase === 'failed' ? 'TASK_FAILED' : null,
    errorCategory: phase === 'failed' ? 'task' : null,
    errorTitle: phase === 'failed' ? 'Batch failed' : null,
    retryable: phase === 'failed' ? false : null,
    field: null,
    hint: phase === 'failed' ? 'Inspect individual task states in the batch before retrying.' : null,
    errorMessage: phase === 'failed'
      ? `One or more tasks in the batch failed or returned no ${outputLabel} URLs.`
      : null,
  };
}


path: scripts/vendor/weryai-core/model-display.js
content: const MODEL_DISPLAY_NAME_OVERRIDES = new Map([
  ['WERYAI_IMAGE_2_0', 'WeryAI Image 2.0'],
  ['WERYAI_IMAGE_1_0', 'WeryAI Image 1.0'],
  ['GEMINI_3_1_FLASH_IMAGE', 'Nano Banana 2'],
  [[REDACTED_SECRET], 'Nano Banana Pro'],
  ['GEMINI_2_5_FLASH', 'Nano-banana'],
  ['GPT_IMAGE_1_5', 'GPT Image 1.5'],
  ['GPT_IMAGE_1_MINI', 'Gpt Image Mini'],
  ['SEEDREAM_5_0_LITE', 'Seedream 5.0 lite'],
  ['SEEDREAM_4_5', 'Seedream 4.5'],
  ['SEEDREAM_4', 'Seedream 4.0'],
  ['WAN_2_6', 'Wan 2.6'],
  ['WAN_2_5', 'Wan 2.5'],
  ['DREAMINA_4', 'Dreamina 4.0'],
  ['DREAMINA', 'Dreamina 3.1'],
  ['DREAMINA_3', 'Dreamina 3.0'],
  ['QWEN', 'Qwen Image'],
  ['FLUX', 'Flux'],
  ['GROK_IMAGINE_IMAGE', 'Grok 2'],
  ['IMAGE4', 'Imagen4'],
  ['RECRAFT_V4', 'Recraft V4'],
  ['RECRAFT_V4_PRO', 'Recraft V4 Pro'],
  ['RECRAFT_V4_TO_VECTOR', 'Recraft V4 Vector'],
  ['RECRAFT_V4_PRO_TO_VECTOR', 'Recraft V4 Pro Vector'],
  ['WERYAI_VIDEO_1_0', 'WeryAI Video 1.0'],
  ['VEO_3_1', 'Veo 3.1'],
  ['VEO_3_1_FAST', 'Veo 3.1 Fast'],
  ['CHATBOT_VEO_3_1_FAST', 'Veo 3.1 Fast'],
  ['SORA_2', 'Sora 2'],
  ['SORA_2_PRO', 'Sora 2 Pro'],
  ['WERYDANCE_2_0', 'Werydance 2.0'],
  ['DOUBAO_1_5_PRO', 'Seedance 1.5 Pro'],
  ['DOUBAO_1_PRO_FAST', 'Seedance 1.0 Pro Fast'],
  ['DOUBAO_1_LITE', 'Seedance 1.0 Lite'],
  ['DOUBAO', 'Seedance 1.0 Pro'],
  ['KLING_V3_0_STA', 'Kling 3.0 Standard'],
  ['KLING_V3_0_PRO', 'Kling 3.0 Pro'],
  ['KLING_V2_6_PRO', 'Kling 2.6 Pro'],
  ['KLING_O1', 'Kling O1'],
  ['VIDU_Q3', 'Vidu Q3'],
  ['VEO_3', 'Veo 3'],
  ['VEO_3_FAST', 'Veo 3 Fast'],
  ['PIKA_2_2', 'Pika 2.2'],
  ['HAILUO_2_3_STA_FAST', 'Hailuo 2.3 Standard Fast'],
  ['HAILUO_2_3_PRO_FAST', 'Hailuo 2.3 Pro Fast'],
  ['HAILUO_2_3_STA', 'Hailuo 2.3 Standard'],
  ['HAILUO_2_3_PRO', 'Hailuo 2.3 Pro'],
  ['KLING_V2_5_TURBO', 'Kling 2.5 Turbo'],
  ['DREAMINA_3_PRO', 'Dreamina 3.0 Pro'],
  ['GROK_IMAGINE_VIDEO', 'Grok Imagine Video'],
  ['RUNWAY_4_5', 'Runway Gen 4.5'],
  ['RUNWAY', 'Runway Gen 4'],
]);

function firstNonEmptyString(...values) {
  for (const value of values) {
    if (typeof value === 'string' && value.trim().length > 0) {
      return value.trim();
    }
  }
  return '';
}

function humanizeModelKey(modelKey) {
  if (typeof modelKey !== 'string' || modelKey.trim().length === 0) return '';

  const normalized = modelKey.trim().replace(/[_-]+/g, ' ');
  return normalized
    .split(/\s+/)
    .filter(Boolean)
    .map((part) => {
      if (/^\d+(?:\.\d+)?$/.test(part)) return part;
      if (/^[A-Z0-9]+$/.test(part)) return part;
      return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
    })
    .join(' ');
}

export function getModelDisplayName(modelKey, fallbackName = '') {
  const key = firstNonEmptyString(modelKey);
  if (!key) return firstNonEmptyString(fallbackName);
  if (MODEL_DISPLAY_NAME_OVERRIDES.has(key)) return MODEL_DISPLAY_NAME_OVERRIDES.get(key);
  return firstNonEmptyString(fallbackName) || humanizeModelKey(key) || key;
}

export function normalizeModelEntry(entry) {
  if (!entry || typeof entry !== 'object') return entry;

  const modelKey = firstNonEmptyString(entry.model_key, entry.modelKey, entry.id);
  const displayName = getModelDisplayName(modelKey, firstNonEmptyString(entry.title, entry.label, entry.name));

  return {
    ...entry,
    model_key: modelKey || entry.model_key || entry.modelKey || entry.id || '',
    title: firstNonEmptyString(entry.title, displayName),
    label: firstNonEmptyString(entry.label, displayName),
    name: firstNonEmptyString(entry.name, displayName),
    model_name: displayName,
    display_name: displayName,
  };
}


path: scripts/vendor/weryai-core/balance.js
content: import { createClient } from './client.js';
import { formatApiError, formatNetworkError, isApiSuccess } from './errors.js';

export async function executeBalance(_input, ctx) {
  const client = createClient(ctx);

  let res;
  try {
    res = await client.get('/v1/generation/balance', { retries: 3 });
  } catch (err) {
    return formatNetworkError(err);
  }

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

  const credits = typeof res.data === 'number' ? res.data : (res.data?.balance ?? res.data);
  return {
    ok: true,
    phase: 'completed',
    balance: credits,
    errorCode: null,
    errorMessage: null,
  };
}


path: scripts/vendor/weryai-podcast/wait.js
content: import { log, sleep } from '../weryai-core/client.js';
import { formatApiError, formatNetworkError, isApiSuccess } from '../weryai-core/errors.js';
import { createClient } from '../weryai-core/client.js';
import { resolvePollIntervalMs } from '../weryai-core/wait.js';
import {
  getPodcastFailureMessage,
  getPodcastLifecyclePhase,
  isPodcastAudioReady,
  isPodcastFailure,
  isPodcastTextReady,
  normalizePodcastTask,
} from './normalize.js';
import {
  buildGenerateAudioPayload,
  buildSubmitTextPayload,
  normalizeGenerateAudioInput,
  normalizeSubmitTextInput,
  validateGenerateAudioInput,
  validateSubmitTextInput,
} from './validators.js';

export async function execute(input, ctx) {
  const normalizedInput = normalizeSubmitTextInput(input);
  const validationErrors = validateSubmitTextInput(normalizedInput);
  if (validationErrors.length > 0) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'VALIDATION',
      errorMessage: validationErrors.join(' '),
    };
  }

  const textRequestBody = buildSubmitTextPayload(normalizedInput);
  const audioInput = normalizeGenerateAudioInput({
    taskId: '<task-id-from-text-generation>',
    scripts: normalizedInput.scripts ?? null,
  });
  const audioValidationErrors = validateGenerateAudioInput(audioInput).filter((message) => message !== '`taskId` is required.');
  if (audioValidationErrors.length > 0) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'VALIDATION',
      errorMessage: audioValidationErrors.join(' '),
    };
  }

  if (ctx.dryRun) {
    return {
      ok: true,
      phase: 'dry-run',
      dryRun: true,
      flow: [
        {
          step: 'submit-text',
          requestUrl: `${ctx.baseUrl}/v1/generation/podcast/generate/text`,
          requestBody: textRequestBody,
        },
        {
          step: 'generate-audio',
          requestUrl: `${ctx.baseUrl}/v1/generation/podcast/generate/{taskId}/audio`,
          requestBody: buildGenerateAudioPayload(audioInput),
        },
      ],
    };
  }

  const client = createClient(ctx);

  let submitResponse;
  try {
    submitResponse = await client.post([REDACTED_SECRET], textRequestBody);
  } catch (err) {
    return formatNetworkError(err);
  }

  if (!isApiSuccess(submitResponse)) {
    return formatApiError(submitResponse);
  }

  const submittedTask = normalizePodcastTask(submitResponse.data);
  if (!submittedTask.taskId) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'PROTOCOL',
      errorMessage: 'API returned success but no `task_id` for podcast text generation.',
    };
  }

  const textReadyTask = await waitForTask(client, submittedTask.taskId, ctx, {
    label: 'podcast-text',
    ready: isPodcastTextReady,
  });
  if (!textReadyTask.ok) return textReadyTask;

  const audioRequest = normalizeGenerateAudioInput({
    taskId: submittedTask.taskId,
    scripts: normalizedInput.scripts ?? null,
  });
  const audioRequestBody = buildGenerateAudioPayload(audioRequest);

  let audioResponse;
  try {
    audioResponse = await client.post(`/v1/generation/podcast/generate/${submittedTask.taskId}/audio`, audioRequestBody);
  } catch (err) {
    return formatNetworkError(err);
  }

  if (!isApiSuccess(audioResponse)) {
    return formatApiError(audioResponse);
  }

  const audioSubmittedTask = normalizePodcastTask(audioResponse.data);
  const finalTaskId = audioSubmittedTask.taskId || submittedTask.taskId;
  const audioReadyTask = await waitForTask(client, finalTaskId, ctx, {
    label: 'podcast-audio',
    ready: isPodcastAudioReady,
  });
  if (!audioReadyTask.ok) return audioReadyTask;

  return {
    ok: true,
    phase: 'completed',
    taskId: audioReadyTask.task.taskId,
    taskStatus: audioReadyTask.task.taskStatus,
    contentStatus: audioReadyTask.task.contentStatus,
    audios: audioReadyTask.task.audios,
    scripts: audioReadyTask.task.scripts,
    lyrics: audioReadyTask.task.lyrics,
    coverUrl: audioReadyTask.task.coverUrl,
    requestSummary: buildRequestSummary(textRequestBody),
    errorCode: null,
    errorMessage: null,
  };
}

function buildRequestSummary(body) {
  return {
    query: body?.query ?? null,
    speakers: body?.speakers ?? null,
    language: body?.language ?? null,
    mode: body?.mode ?? null,
  };
}

async function waitForTask(client, taskId, ctx, options) {
  const { label, ready } = options;
  const start = Date.now();

  while (true) {
    if (Date.now() - start >= ctx.pollTimeoutMs) {
      return {
        ok: false,
        phase: 'failed',
        errorCode: 'TIMEOUT',
        errorMessage: `Poll timeout after ${Math.round((Date.now() - start) / 1000)}s.`,
      };
    }

    const elapsed = Date.now() - start;
    await sleep(resolvePollIntervalMs(ctx, elapsed, { pollProfile: 'fast' }));

    let response;
    try {
      response = await client.get(`/v1/generation/${taskId}/status`, { retries: 3 });
    } catch (err) {
      log(`Warning: ${label} poll failed (${err.message}), retrying.`);
      continue;
    }

    if (!isApiSuccess(response)) {
      log(`Warning: ${label} poll returned status ${response.status}, retrying.`);
      continue;
    }

    const task = normalizePodcastTask(response.data);
    const phase = getPodcastLifecyclePhase(task);
    const elapsedSec = Math.round((Date.now() - start) / 1000);
    log(
      `Polling ${label} ${taskId}... task_status=${task.taskStatus} content_status=${task.contentStatus ?? 'n/a'} (${elapsedSec}s elapsed)`,
    );

    if (isPodcastFailure(task)) {
      return {
        ok: false,
        phase: 'failed',
        errorCode: 'TASK_FAILED',
        errorMessage: getPodcastFailureMessage(task),
      };
    }

    if (ready(task)) {
      return { ok: true, phase, task };
    }
  }
}

export default execute;


path: scripts/vendor/weryai-podcast/validators.js
content: function pickString(...values) {
  for (const value of values) {
    if (typeof value === 'string' && value.trim()) return value.trim();
  }
  return null;
}

function toInteger(value) {
  if (value == null || value === '') return null;
  const parsed = Number(value);
  return Number.isInteger(parsed) ? parsed : NaN;
}

function normalizeIdList(value) {
  if (Array.isArray(value)) {
    return value.map((entry) => pickString(entry)).filter(Boolean);
  }
  if (typeof value === 'string' && value.trim()) {
    return value.split(',').map((entry) => entry.trim()).filter(Boolean);
  }
  return [];
}

function normalizeScripts(value) {
  if (!Array.isArray(value)) return null;
  const scripts = value
    .map((entry) => {
      if (!entry || typeof entry !== 'object') return null;
      return {
        speakerId: pickString(entry.speakerId, entry.speaker_id),
        speakerName: pickString(entry.speakerName, entry.speaker_name),
        content: pickString(entry.content),
      };
    })
    .filter(Boolean);
  return scripts.length > 0 ? scripts : null;
}

export function normalizeSubmitTextInput(input) {
  return {
    query: pickString(input.query, input.prompt, input.topic),
    speakers: normalizeIdList(input.speakers ?? input.speakerIds ?? input.speaker_ids),
    language: pickString(input.language, input.lang),
    mode: pickString(input.mode) || 'quick',
    webhookUrl: pickString(input.webhookUrl, input.webhook_url),
    callerId: toInteger(input.callerId ?? input.caller_id),
    scripts: normalizeScripts(input.scripts ?? input.audioScripts ?? input.audio_scripts),
  };
}

export function normalizeGenerateAudioInput(input) {
  return {
    taskId: pickString(input.taskId, input.task_id, input['task-id']),
    scripts: normalizeScripts(input.scripts),
    webhookUrl: pickString(input.webhookUrl, input.webhook_url),
    callerId: toInteger(input.callerId ?? input.caller_id),
  };
}

export function validateSubmitTextInput(input) {
  const errors = [];

  if (!input.query) errors.push('`query` is required.');
  if (!input.language) errors.push('`language` is required.');
  if (!Array.isArray(input.speakers) || input.speakers.length === 0) {
    errors.push('`speakers` must include at least one speaker ID.');
  } else if (input.speakers.length > 2) {
    errors.push('`speakers` supports at most 2 speaker IDs.');
  }
  if (!['quick', 'debate', 'deep'].includes(input.mode)) {
    errors.push('`mode` must be one of `quick`, `debate`, or `deep`.');
  }
  if (input.mode === 'debate' && input.speakers.length !== 2) {
    errors.push('`debate` mode requires exactly 2 speakers.');
  }
  if (Number.isNaN(input.callerId)) {
    errors.push('`callerId` must be an integer when provided.');
  }

  return errors;
}

export function validateGenerateAudioInput(input) {
  const errors = [];

  if (!input.taskId) errors.push('`taskId` is required.');
  if (Number.isNaN(input.callerId)) {
    errors.push('`callerId` must be an integer when provided.');
  }
  if (Array.isArray(input.scripts)) {
    const badIndex = input.scripts.findIndex((entry) => !entry.speakerId || !entry.content);
    if (badIndex >= 0) {
      errors.push('Each `scripts` entry must include `speakerId` and `content`.');
    }
  }

  return errors;
}

export function buildSubmitTextPayload(input) {
  const body = {
    query: input.query,
    speakers: input.speakers,
    language: input.language,
    mode: input.mode,
  };
  if (input.webhookUrl) body.webhook_url = input.webhookUrl;
  if (input.callerId != null) body.caller_id = input.callerId;
  return body;
}

export function buildGenerateAudioPayload(input) {
  const body = {};
  if (Array.isArray(input.scripts) && input.scripts.length > 0) {
    body.scripts = input.scripts.map((entry) => ({
      speaker_id: entry.speakerId,
      ...(entry.speakerName ? { speaker_name: entry.speakerName } : {}),
      content: entry.content,
    }));
  }
  if (input.webhookUrl) body.webhook_url = input.webhookUrl;
  if (input.callerId != null) body.caller_id = input.callerId;
  return body;
}


path: scripts/vendor/weryai-podcast/normalize.js
content: import {
  normalizeTaskCollection,
  normalizeTaskResult,
} from '../weryai-core/normalize.js';

export function normalizePodcastTask(data) {
  return normalizePodcastTaskEntry(normalizeTaskResult(data), unwrapSingle(data));
}

export function normalizePodcastTaskList(data) {
  const rawList = Array.isArray(data) ? data : [data];
  const normalizedList = normalizeTaskCollection(data);
  return normalizedList.map((task, index) => normalizePodcastTaskEntry(task, rawList[index]));
}

export function normalizeSpeakerResponse(data) {
  const speakers = Array.isArray(data?.speakers) ? data.speakers : [];
  return speakers.map((speaker) => ({
    name: speaker?.name ?? null,
    speakerId: speaker?.speaker_id ?? speaker?.speakerId ?? null,
    demoAudioUrl: speaker?.demo_audio_url ?? speaker?.demoAudioUrl ?? null,
    gender: speaker?.gender ?? null,
    language: speaker?.language ?? null,
  }));
}

export function isPodcastTextReady(task) {
  return task?.contentStatus === 'text-success';
}

export function isPodcastAudioReady(task) {
  return task?.contentStatus === 'audio-success'
    || (task?.taskStatus === 'completed' && Array.isArray(task?.audios) && task.audios.length > 0);
}

export function isPodcastFailure(task) {
  return task?.contentStatus === 'text-fail'
    || task?.contentStatus === 'audio-fail'
    || task?.taskStatus === 'failed';
}

export function getPodcastLifecyclePhase(task) {
  if (isPodcastAudioReady(task)) return 'completed';
  if (isPodcastFailure(task)) return 'failed';
  return 'running';
}

export function getPodcastFailureMessage(task) {
  if (!task) return 'Podcast task failed.';
  if (task.contentStatus === 'text-fail') return 'Podcast text generation failed.';
  if (task.contentStatus === 'audio-fail') return 'Podcast audio generation failed.';
  return task.msg || 'Podcast task failed.';
}

function normalizePodcastTaskEntry(task, raw) {
  return {
    ...task,
    scripts: normalizeScripts(task.scripts, raw),
  };
}

function normalizeScripts(normalized, raw) {
  if (Array.isArray(normalized) && normalized.length > 0) return normalized;
  const rawScripts = raw?.scripts ?? raw?.output?.scripts ?? raw?.script ?? raw?.output?.script;
  if (!Array.isArray(rawScripts) || rawScripts.length === 0) return null;
  return rawScripts.map((entry) => {
    if (!entry || typeof entry !== 'object') return entry;
    return {
      speakerId: entry.speaker_id ?? entry.speakerId ?? null,
      speakerName: entry.speaker_name ?? entry.speakerName ?? null,
      content: typeof entry.content === 'string' ? entry.content : null,
    };
  });
}

function unwrapSingle(data) {
  if (Array.isArray(data)) return data[0] ?? null;
  return data ?? null;
}


path: scripts/vendor/weryai-podcast/generate-audio.js
content: import { createClient } from '../weryai-core/client.js';
import { formatApiError, formatNetworkError, isApiSuccess } from '../weryai-core/errors.js';
import { normalizePodcastTask } from './normalize.js';
import { buildGenerateAudioPayload, normalizeGenerateAudioInput, validateGenerateAudioInput } from './validators.js';

export async function execute(input, ctx) {
  const normalizedInput = normalizeGenerateAudioInput(input);
  const validationErrors = validateGenerateAudioInput(normalizedInput);
  if (validationErrors.length > 0) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'VALIDATION',
      errorMessage: validationErrors.join(' '),
    };
  }

  const body = buildGenerateAudioPayload(normalizedInput);
  const requestUrl = `${ctx.baseUrl}/v1/generation/podcast/generate/${normalizedInput.taskId}/audio`;

  if (ctx.dryRun) {
    return {
      ok: true,
      phase: 'dry-run',
      dryRun: true,
      requestUrl,
      requestBody: body,
    };
  }

  const client = createClient(ctx);
  let response;
  try {
    response = await client.post(`/v1/generation/podcast/generate/${normalizedInput.taskId}/audio`, body);
  } catch (err) {
    return formatNetworkError(err);
  }

  if (!isApiSuccess(response)) {
    return formatApiError(response);
  }

  const task = normalizePodcastTask(response.data);
  return {
    ok: true,
    phase: 'submitted',
    taskId: task.taskId || normalizedInput.taskId,
    taskStatus: task.taskStatus,
    contentStatus: task.contentStatus,
    scripts: task.scripts ?? normalizedInput.scripts ?? null,
    errorCode: null,
    errorMessage: null,
  };
}

export default execute;


path: scripts/vendor/weryai-podcast/README.md
content: # WeryAI Podcast Runtime

Shared runtime for WeryAI podcast generation skills.

Responsibilities:

- list podcast speakers
- submit podcast text generation
- trigger podcast audio generation
- query podcast task state
- wait across the text -> audio lifecycle

This runtime reuses `core/weryai-core` for auth, HTTP, retries, and shared error mapping.


path: scripts/vendor/weryai-podcast/submit-text.js
content: import { createClient } from '../weryai-core/client.js';
import { formatApiError, formatNetworkError, isApiSuccess } from '../weryai-core/errors.js';
import { normalizePodcastTask } from './normalize.js';
import { buildSubmitTextPayload, normalizeSubmitTextInput, validateSubmitTextInput } from './validators.js';

export async function execute(input, ctx) {
  const normalizedInput = normalizeSubmitTextInput(input);
  const validationErrors = validateSubmitTextInput(normalizedInput);
  if (validationErrors.length > 0) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'VALIDATION',
      errorMessage: validationErrors.join(' '),
    };
  }

  const body = buildSubmitTextPayload(normalizedInput);
  const requestUrl = `${ctx.baseUrl}/v1/generation/podcast/generate/text`;

  if (ctx.dryRun) {
    return {
      ok: true,
      phase: 'dry-run',
      dryRun: true,
      requestUrl,
      requestBody: body,
    };
  }

  const client = createClient(ctx);
  let response;
  try {
    response = await client.post('/v1/generation/podcast/generate/text', body);
  } catch (err) {
    return formatNetworkError(err);
  }

  if (!isApiSuccess(response)) {
    return formatApiError(response);
  }

  const task = normalizePodcastTask(response.data);
  return {
    ok: true,
    phase: 'submitted',
    taskId: task.taskId,
    taskStatus: task.taskStatus,
    contentStatus: task.contentStatus,
    scripts: task.scripts,
    requestSummary: buildRequestSummary(body),
    errorCode: null,
    errorMessage: null,
  };
}

function buildRequestSummary(body) {
  return {
    query: body?.query ?? null,
    speakers: body?.speakers ?? null,
    language: body?.language ?? null,
    mode: body?.mode ?? null,
  };
}

export default execute;


path: scripts/vendor/weryai-podcast/status.js
content: import { createClient } from '../weryai-core/client.js';
import { formatApiError, formatNetworkError, isApiSuccess } from '../weryai-core/errors.js';
import {
  getPodcastFailureMessage,
  getPodcastLifecyclePhase,
  normalizePodcastTask,
} from './normalize.js';

export async function execute(input, ctx) {
  const taskId = input.taskId || input.task_id || input['task-id'];
  if (!taskId) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'VALIDATION',
      errorMessage: 'The status script requires `--task-id <task-id>`.',
    };
  }

  const client = createClient(ctx);
  let response;
  try {
    response = await client.get(`/v1/generation/${taskId}/status`, { retries: 3 });
  } catch (err) {
    return formatNetworkError(err);
  }

  if (!isApiSuccess(response)) {
    return formatApiError(response);
  }

  const task = normalizePodcastTask(response.data);
  const phase = getPodcastLifecyclePhase(task);

  return {
    ok: phase !== 'failed',
    phase,
    taskId: task.taskId,
    taskStatus: task.taskStatus,
    contentStatus: task.contentStatus,
    audios: task.audios,
    scripts: task.scripts,
    lyrics: task.lyrics,
    coverUrl: task.coverUrl,
    errorCode: phase === 'failed' ? 'TASK_FAILED' : null,
    errorMessage: phase === 'failed' ? getPodcastFailureMessage(task) : null,
  };
}

export default execute;


path: scripts/vendor/weryai-podcast/cli.js
content: export async function runScript(argv, handler, helpText) {
  const flags = parseFlags(argv);

  if (flags.help) {
    process.stderr.write(helpText);
    return { ok: true, phase: 'help' };
  }

  const input = await resolveInput(flags);
  const pollIntervalEnv = Number(process.env.WERYAI_POLL_INTERVAL_MS);
  const ctx = {
    [REDACTED_SECRET] || '',
    baseUrl: process.env.WERYAI_BASE_URL || 'https://api.weryai.com',
    verbose: flags.verbose,
    dryRun: flags.dryRun,
    requestTimeoutMs: Number(process.env.WERYAI_REQUEST_TIMEOUT_MS) || 30_000,
    pollIntervalMs: Number.isFinite(pollIntervalEnv) ? pollIntervalEnv : null,
    pollTimeoutMs: Number(process.env.WERYAI_POLL_TIMEOUT_MS) || 1800_000,
  };

  try {
    const result = await handler(input, ctx);
    process.stdout.write(JSON.stringify(result, null, 2) + '\n');
    process.exit(result.ok ? 0 : 1);
  } catch (err) {
    const output = {
      ok: false,
      phase: 'failed',
      errorCode: null,
      errorMessage: err?.message ?? String(err),
    };
    process.stdout.write(JSON.stringify(output, null, 2) + '\n');
    process.exit(1);
  }
}

function parseFlags(argv) {
  const flags = {
    help: false,
    verbose: false,
    dryRun: false,
    json: null,
    params: {},
  };

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === '--help' || arg === '-h') {
      flags.help = true;
    } else if (arg === '--verbose' || arg === '-v') {
      flags.verbose = true;
    } else if (arg === '--dry-run') {
      flags.dryRun = true;
    } else if (arg === '--json') {
      flags.json = argv[++i] ?? null;
    } else if (arg.startsWith('--') && i + 1 < argv.length && !argv[i + 1]?.startsWith('--')) {
      flags.params[arg.slice(2)] = argv[++i];
    } else if (arg.startsWith('--')) {
      flags.params[arg.slice(2)] = true;
    }
  }

  return flags;
}

async function resolveInput(flags) {
  if (flags.json === '-') {
    const chunks = [];
    for await (const chunk of process.stdin) {
      chunks.push(chunk);
    }
    const raw = Buffer.concat(chunks).toString('utf-8').trim();
    return raw ? JSON.parse(raw) : {};
  }

  if (flags.json) return JSON.parse(flags.json);

  const result = {};
  for (const [key, value] of Object.entries(flags.params)) {
    const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
    result[camel] = value;
  }
  return result;
}


path: scripts/vendor/weryai-podcast/speakers.js
content: import { createClient } from '../weryai-core/client.js';
import { formatApiError, formatNetworkError, isApiSuccess } from '../weryai-core/errors.js';
import { normalizeSpeakerResponse } from './normalize.js';

export async function execute(input, ctx) {
  const language = typeof input.language === 'string' ? input.language.trim() : '';
  if (!language) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'VALIDATION',
      errorMessage: '`language` is required.',
    };
  }

  const client = createClient(ctx);

  let response;
  try {
    response = await client.get(`/v1/generation/podcast/speakers/list?language=${encodeURIComponent(language)}`, {
      retries: 3,
    });
  } catch (err) {
    return formatNetworkError(err);
  }

  if (!isApiSuccess(response)) {
    return formatApiError(response);
  }

  const speakers = normalizeSpeakerResponse(response.data);
  return {
    ok: true,
    phase: 'completed',
    language,
    count: speakers.length,
    speakers,
    errorCode: null,
    errorMessage: null,
  };
}

export default execute;


path: scripts/vendor/weryai-music/validators.js
content: const STYLE_GROUPS = {
  genre: ['POP', 'ROCK', 'RAP', 'ELECTRONIC', 'RNB', 'JAZZ', 'FOLK', 'CLASSIC', 'WORLD'],
  emotion: ['HAPPY', 'RELAXED', 'WARM', 'ROMANTIC', 'TOUCHING', 'SAD', 'LONELY', 'DEPRESSED', 'TENSE', 'EXCITED', 'EPIC', 'MYSTERIOUS'],
  instrument: ['PIANO', 'GUITAR', 'BASS', 'DRUMS', 'STRINGS', 'WIND', 'SYNTHESIZER', 'ELECTRONIC_SOUND', 'FOLK_INSTRUMENT', 'MIXED_ORCHESTRATION'],
};

const ALL_STYLE_KEYS = new Set(Object.values(STYLE_GROUPS).flat());

export function validateSubmit(input) {
  const errors = [];

  const type = input.type;
  if (!type) {
    errors.push('`type` is required and must be `VOCAL_SONG` or `ONLY_MUSIC`.');
    return errors;
  }

  if (type !== 'VOCAL_SONG' && type !== 'ONLY_MUSIC') {
    errors.push('`type` must be `VOCAL_SONG` or `ONLY_MUSIC`.');
  }

  const description = normalizeText(input.description);
  const lyrics = normalizeText(input.lyrics);
  const styles = input.styles;
  const gender = input.gender;
  const referenceAudio = normalizeText(input.reference_audio ?? input.referenceAudio);
  const audioName = normalizeText(input.audio_name ?? input.audioName);
  const webhookUrl = normalizeText(input.webhook_url ?? input.webhookUrl);
  const callerId = input.caller_id ?? input.callerId;

  if (!description && !hasStyles(styles)) {
    errors.push('At least one of `description` or `styles` must be provided.');
  }

  if (styles != null) {
    if (!isPlainObject(styles) || Object.keys(styles).length === 0) {
      errors.push('`styles` must be a non-empty object of style-key to style-description pairs.');
    } else {
      for (const [key, value] of Object.entries(styles)) {
        if (!ALL_STYLE_KEYS.has(key)) {
          errors.push(`Unsupported style key: \`${key}\`.`);
        }
        if (typeof value !== 'string' || !value.trim()) {
          errors.push(`Style value for \`${key}\` must be a non-empty string.`);
        }
      }
    }
  }

  if (referenceAudio && !audioName) {
    errors.push('`audio_name` is recommended when `reference_audio` is provided, and is required by this CLI for clearer task provenance.');
  }

  if (webhookUrl && !isPublicUrl(webhookUrl)) {
    errors.push('`webhook_url` must be a public `http` or `https` URL.');
  }

  if (gender && gender !== 'm' && gender !== 'f') {
    errors.push('`gender` must be `m` or `f` when provided.');
  }

  if (type === 'ONLY_MUSIC') {
    if (lyrics) {
      errors.push('`lyrics` is not supported when `type` is `ONLY_MUSIC`.');
    }
    if (gender) {
      errors.push('`gender` should not be set when `type` is `ONLY_MUSIC`.');
    }
  }

  if (callerId != null && callerId !== '') {
    const asNumber = Number(callerId);
    if (!Number.isInteger(asNumber) || asNumber < 0) {
      errors.push('`caller_id` must be a non-negative integer when provided.');
    }
  }

  return dedupe(errors);
}

export function buildPayload(input) {
  const payload = {
    type: input.type,
  };

  const description = normalizeText(input.description);
  const lyrics = normalizeText(input.lyrics);
  const referenceAudio = normalizeText(input.reference_audio ?? input.referenceAudio);
  const audioName = normalizeText(input.audio_name ?? input.audioName);
  const webhookUrl = normalizeText(input.webhook_url ?? input.webhookUrl);
  const callerId = input.caller_id ?? input.callerId;

  if (description) payload.description = description;
  if (lyrics) payload.lyrics = lyrics;
  if (referenceAudio) payload.reference_audio = referenceAudio;
  if (audioName) payload.audio_name = audioName;
  if (input.gender) payload.gender = input.gender;
  if (hasStyles(input.styles)) payload.styles = sanitizeStyles(input.styles);
  if (webhookUrl) payload.webhook_url = webhookUrl;
  if (callerId != null && callerId !== '') payload.caller_id = Number(callerId);

  return payload;
}

function sanitizeStyles(styles) {
  const out = {};
  for (const [key, value] of Object.entries(styles)) {
    if (typeof value === 'string' && value.trim()) {
      out[key] = value.trim();
    }
  }
  return out;
}

function hasStyles(styles) {
  return isPlainObject(styles) && Object.keys(styles).length > 0;
}

function isPlainObject(value) {
  return !!value && typeof value === 'object' && !Array.isArray(value);
}

function isPublicUrl(value) {
  try {
    const url = new URL(value);
    return url.protocol === 'http:' || url.protocol === 'https:';
  } catch {
    return false;
  }
}

function normalizeText(value) {
  return typeof value === 'string' && value.trim() ? value.trim() : '';
}

function dedupe(list) {
  return [...new Set(list)];
}


path: scripts/vendor/weryai-music/submit.js
content: import { createClient } from '../weryai-core/client.js';
import { buildPayload, validateSubmit } from './validators.js';
import { isApiSuccess, formatApiError, formatNetworkError } from '../weryai-core/errors.js';
import { normalizeMusicInput } from './normalize-input.js';
import { isRemoteUrl, resolvePublicUrlFromSource } from '../weryai-core/upload.js';

export async function execute(input, ctx) {
  const normalizedInput = normalizeMusicInput(input);
  const validationErrors = validateSubmit(normalizedInput);
  if (validationErrors.length > 0) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'VALIDATION',
      errorMessage: validationErrors.join(' '),
    };
  }

  const body = buildPayload(normalizedInput);
  const uploadPreview = collectReferenceAudioUploadPreview(body);

  if (ctx.dryRun) {
    return {
      ok: true,
      phase: 'dry-run',
      dryRun: true,
      requestBody: body,
      requestUrl: `${ctx.baseUrl}/v1/generation/music/generate`,
      uploadPreview,
      notes: uploadPreview.length > 0
        ? 'dry-run does not upload local files. Local sources in uploadPreview will be uploaded in a real run via /v1/generation/upload-file.'
        : null,
    };
  }

  let resolvedBody = body;
  try {
    resolvedBody = await resolveReferenceAudioUpload(ctx, body);
  } catch (err) {
    return {
      ok: false,
      phase: 'failed',
      errorCode: 'UPLOAD_FAILED',
      errorMessage: err?.message ?? String(err),
    };
  }

  const client = createClient(ctx);
  let res;
  try {
    res = await client.post('/v1/generation/music/generate', resolvedBody);
  } catch (err) {
    return formatNetworkError(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: data.task_id ?? taskIds[0] ?? null,
    taskStatus: normalizeSubmittedStatus(data.task_status ?? data.taskStatus),
    audios: null,
    lyrics: null,
    coverUrl: null,
    balance: null,
    requestSummary: buildRequestSummary(resolvedBody),
    errorCode: null,
    errorMessage: null,
  };
}

function buildRequestSummary(body) {
  return {
    type: body?.type ?? null,
    description: body?.description ?? null,
    gender: body?.gender ?? null,
    styles: body?.styles ?? null,
  };
}

function collectReferenceAudioUploadPreview(body) {
  if (typeof body.reference_audio === 'string' && body.reference_audio.trim() && !isRemoteUrl(body.reference_audio)) {
    return [{ field: 'reference_audio', source: body.reference_audio, kind: 'audio' }];
  }
  return [];
}

async function resolveReferenceAudioUpload(ctx, body) {
  const out = { ...body };
  if (typeof out.reference_audio === 'string' && out.reference_audio.trim()) {
    out.reference_audio = await resolvePublicUrlFromSource(ctx, out.reference_audio);
  }
  return out;
}

function normalizeSubmittedStatus(rawStatus) {
  if (rawStatus === 'WAITING' || rawStatus === 'waiting') return 'waiting';
  if (rawStatus === 'PROCESSING' || rawStatus === 'processing') return 'processing';
  return rawStatus ?? null;
}


path: scripts/vendor/weryai-music/README.md
content: # WeryAI Music Runtime

这里保存 `music/` 目录共享的音乐领域运行时。

当前已承接：

- `validators`
- `submit`

其余共享能力继续来自 `core/weryai-core/`，包括 client、errors、normalize、status、wait 与 balance。


path: scripts/vendor/weryai-music/normalize-input.js
content: export function normalizeMusicInput(rawInput) {
  const input = { ...rawInput };
  const rawType = input.type ?? input.mode ?? input.musicType ?? input.songType;

  if (rawType) {
    input.type = normalizeType(rawType);
  } else {
    input.type = hasInstrumentalSignals(input) ? 'ONLY_MUSIC' : 'VOCAL_SONG';
  }

  mergeCategorizedStyles(input);
  normalizeTimbre(input);

  if (input.type === 'VOCAL_SONG' && !input.gender) {
    input.gender = 'm';
  }

  return input;
}

const STYLE_VALUE_MAP = {
  POP: 'pop',
  ROCK: 'rock',
  RAP: 'rap',
  ELECTRONIC: 'electronic',
  RNB: 'r&b',
  JAZZ: 'jazz',
  FOLK: 'folk',
  CLASSIC: 'classical',
  WORLD: 'world music',
  HAPPY: 'happy',
  RELAXED: 'relaxed',
  WARM: 'warm',
  ROMANTIC: 'romantic',
  TOUCHING: 'touching',
  SAD: 'sad',
  LONELY: 'lonely',
  DEPRESSED: 'depressed',
  TENSE: 'tense',
  EXCITED: 'excited',
  EPIC: 'epic',
  MYSTERIOUS: 'mysterious',
  PIANO: 'piano',
  GUITAR: 'guitar',
  BASS: 'bass',
  DRUMS: 'drums',
  STRINGS: 'strings',
  WIND: 'wind instruments',
  SYNTHESIZER: 'synthesizer',
  ELECTRONIC_SOUND: 'electronic sound',
  FOLK_INSTRUMENT: 'folk instrument',
  MIXED_ORCHESTRATION: 'mixed orchestration',
};

const GENRE_MAP = {
  POP: 'POP',
  流行: 'POP',
  ROCK: 'ROCK',
  摇滚: 'ROCK',
  RAP: 'RAP',
  说唱: 'RAP',
  ELECTRONIC: 'ELECTRONIC',
  电子: 'ELECTRONIC',
  RNB: 'RNB',
  'R&B': 'RNB',
  JAZZ: 'JAZZ',
  爵士: 'JAZZ',
  FOLK: 'FOLK',
  民谣: 'FOLK',
  CLASSIC: 'CLASSIC',
  古典: 'CLASSIC',
  WORLD: 'WORLD',
  世界音乐: 'WORLD',
};

const EMOTION_MAP = {
  HAPPY: 'HAPPY',
  开心: 'HAPPY',
  RELAXED: 'RELAXED',
  轻松: 'RELAXED',
  WARM: 'WARM',
  温暖: 'WARM',
  ROMANTIC: 'ROMANTIC',
  浪漫: 'ROMANTIC',
  TOUCHING: 'TOUCHING',
  感动: 'TOUCHING',
  SAD: 'SAD',
  忧伤: 'SAD',
  LONELY: 'LONELY',
  孤独: 'LONELY',
  DEPRESSED: 'DEPRESSED',
  压抑: 'DEPRESSED',
  TENSE: 'TENSE',
  紧张: 'TENSE',
  EXCITED: 'EXCITED',
  激昂: 'EXCITED',
  EPIC: 'EPIC',
  史诗: 'EPIC',
  MYSTERIOUS: 'MYSTERIOUS',
  神秘: 'MYSTERIOUS',
};

const INSTRUMENT_MAP = {
  PIANO: 'PIANO',
  钢琴: 'PIANO',
  GUITAR: 'GUITAR',
  吉他: 'GUITAR',
  BASS: 'BASS',
  贝斯: 'BASS',
  DRUMS: 'DRUMS',
  架子鼓: 'DRUMS',
  STRINGS: 'STRINGS',
  弦乐: 'STRINGS',
  WIND: 'WIND',
  管乐: 'WIND',
  SYNTHESIZER: 'SYNTHESIZER',
  合成器: 'SYNTHESIZER',
  ELECTRONIC_SOUND: 'ELECTRONIC_SOUND',
  电子音色: 'ELECTRONIC_SOUND',
  FOLK_INSTRUMENT: 'FOLK_INSTRUMENT',
  民族乐器: 'FOLK_INSTRUMENT',
  MIXED_ORCHESTRATION: 'MIXED_ORCHESTRATION',
  混合编制: 'MIXED_ORCHESTRATION',
};

const TIMBRE_MAP = {
  m: 'm',
  male: 'm',
  男: 'm',
  男声: 'm',
  f: 'f',
  female: 'f',
  女: 'f',
  女声: 'f',
};

function normalizeType(value) {
  const normalized = String(value).trim().toUpperCase();

  if (
    normalized === 'VOCAL_SONG' ||
    normalized === 'VOCAL' ||
    normalized === 'SONG' ||
    normalized === '人声歌曲' ||
    normalized === '人声'
  ) {
    return 'VOCAL_SONG';
  }

  if (
    normalized === 'ONLY_MUSIC' ||
    normalized === 'MUSIC_ONLY' ||
    normalized === 'INSTRUMENTAL' ||
    normalized === 'BGM' ||
    normalized === '纯音乐'
  ) {
    return 'ONLY_MUSIC';
  }

  return normalized;
}

function hasInstrumentalSignals(input) {
  const textSignals = [input.mode, input.musicType, input.songType, input.description, input.prompt]
    .filter(Boolean)
    .join(' ')
    .toLowerCase();

  return (
    textSignals.includes('instrumental') ||
    textSignals.includes('background music') ||
    textSignals.includes('soundtrack') ||
    textSignals.includes('bgm') ||
    textSignals.includes('纯音乐') ||
    textSignals.includes('伴奏')
  );
}

function mergeCategorizedStyles(input) {
  const merged = { ...(isPlainObject(input.styles) ? input.styles : {}) };

  addStyleEntries(merged, input.genre ?? input.genres ?? input.styleGenre, GENRE_MAP);
  addStyleEntries(merged, input.emotion ?? input.emotions ?? input.mood, EMOTION_MAP);
  addStyleEntries(merged, input.instrument ?? input.instruments, INSTRUMENT_MAP);

  if (Object.keys(merged).length > 0) {
    input.styles = merged;
  }
}

function addStyleEntries(target, rawValue, dictionary) {
  for (const value of normalizeList(rawValue)) {
    const key = dictionary[String(value).trim()];
    if (key && !target[key]) {
      target[key] = STYLE_VALUE_MAP[key];
    }
  }
}

function normalizeTimbre(input) {
  const raw = input.timbre ?? input.voice ?? input.voiceTone ?? input.gender;
  if (raw == null) return;

  const resolved = TIMBRE_MAP[String(raw).trim().toLowerCase()] ?? TIMBRE_MAP[String(raw).trim()];
  if (resolved) {
    input.gender = resolved;
  }
}

function normalizeList(value) {
  if (Array.isArray(value)) return value;
  if (typeof value === 'string' && value.trim()) {
    return value.split(/[,，/|]/).map((item) => item.trim()).filter(Boolean);
  }
  return [];
}

function isPlainObject(value) {
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}


path: references/podcast-api.md
content: # WeryAI Podcast API

Use this file when you need the exact public contract for the WeryAI podcast endpoints wrapped by this skill.

## Endpoints

- `GET /v1/generation/podcast/speakers/list`
  - Required query: `language`
- `POST /v1/generation/podcast/generate/text`
  - Required body: `query`, `speakers`, `language`, `mode`
  - `mode` values: `quick`, `debate`, `deep`
  - `debate` requires exactly 2 speakers
- `POST /v1/generation/podcast/generate/{taskId}/audio`
  - Optional body: `scripts`, `webhook_url`, `caller_id`
  - If `scripts` is omitted, the API uses the generated default text from the task
- `GET /v1/generation/{taskId}/status`
  - Podcast tasks should be evaluated with `content_status`

## Podcast lifecycle

Podcast generation is a two-stage async workflow:

1. submit podcast text generation
2. poll task status until `content_status=text-success`
3. trigger audio generation
4. poll again until `content_status=audio-success`

Failure states called out in the docs:

- `text-fail`
- `audio-fail`

Use this skill for podcast generation only. Do not use it for generic chat, blog writing, or music-only generation.


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