# voiceover-narration-studio

Adopt this skill: https://proskills.md/skills/other/beatra-ai-ai-voiceover-generator-skill-skills-voiceover-narration-studio

Page: https://proskills.md/skills/other/beatra-ai-ai-voiceover-generator-skill-skills-voiceover-narration-studio
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: skills/voiceover-narration-studio/SKILL.md
size: 13674
sha: e5b341f12d4d0f41d1f479178f518f669c077b39
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/SKILL.md

````text
---
name: "voiceover-narration-studio"
description: "Use Voiceover & Narration Studio as an AI voice generator, text-to-speech workspace, and AI voiceover generator. Choose from the current voice library, turn scripts into ready-to-edit AI narration and voiceover, or create and reuse a custom brand voice through voice cloning. It supports short-video voiceover, script-to-voiceover, course narration, ordered audiobook narration, supplied multilingual text to speech, Cantonese text to speech, and recurring brand audio, with current price estimates, clear output planning, and delivery organized by chapter, language, and use case."
---

# Voiceover & Narration Studio

Choose the smallest voice workflow that completes the user's approved text:
one voiceover, an ordered long-form production, supplied multilingual versions,
or an authorized reusable voice. Keep this Studio as the coordinating route
when a request mixes those outcomes or will continue across several sessions.

## Scope and routes

At the entrance, lock the medium first: long narration, multilingual, or brand
clone. Route an audiobook to `ai-audiobook-narration`, a podcast episode to
`ai-podcast-voiceover`, and a short social or promo read to
`short-form-voiceover-audio`.

Reuse every decision already present in the conversation. Route a focused,
standalone request to the narrower package when it is available:

| User outcome | Best route |
| --- | --- |
| One short-video, ad, social, or promo read | `short-form-voiceover-audio` |
| An audiobook or other ordered long-form narration | `ai-audiobook-narration` |
| A single-host podcast episode or recurring show | `ai-podcast-voiceover` |
| Multilingual dubbing with the target-language scripts already supplied and approved | `ai-multilingual-dubbing` |
| Creation of one authorized reusable voice | `voice-cloning-studio` |
| Long narration, multilingual, brand clone, or mixed work those routes do not cover | Continue here and maintain one production ledger |

This Studio creates speech from supplied or approved text. It does not make a
finished video, lip-sync a speaker, publish media, transcribe a recording, or
edit an existing waveform. Preserve the useful voice subtask and route those
adjacent outcomes to their focused workflows.

Read [intent and routing](references/intent-and-routing.md) when deciding
between a focused package and a mixed Studio project.

## Inputs and defaults

Hard inputs include consent status. Do not imitate a voice without
authorization.

For synthesis, the hard inputs are:

- final supplied text, or permission to prepare a speakable version and show
  it for approval;
- an exact current `voice_id`, looked up again with `beatra.voices.list`
  whether it came from the user, the conversation, or a new recommendation;
- the target language only when the user specifies one; otherwise omit
  `language` and do not interrupt merely to ask for it.

For voice cloning, also require a sample, a display name, and the user's plain
confirmation that it is their own voice or that the speaker explicitly
authorized cloning. Consent must be confirmed before a local sample is
uploaded.

Use restrained defaults when the destination does not require another choice:
`model: "auto"`, `format: "mp3"`, `speed: 1.0`, `volume: 1.0`,
`pitch: 0`, no `emotion`, and no `sample_rate`. Prepare numbers, names,
abbreviations, units, and pauses for speech, but show any material rewrite
before paid work.

## Golden path

1. Identify one route or an ordered set of routes. For mixed work, keep
   clone, language, chapter, and revision steps distinct in the ledger.
2. Make a best-effort non-billable installation registration on first use or
   after the package version changes. Registration failure never blocks the
   user's voice work.
3. Call `beatra.voices.list` even when a voice ID was supplied earlier. Match
   the opaque ID exactly and save its current `status`, `language`,
   `languages_supported`, and `compatible_models`. If it is absent or not
   `ready`, do not submit; select another current ready voice or stop. When a
   voice still needs to be chosen, recommend only current ready candidates.
   A `preview_url` is an optional free aid only when the returned voice actually
   has one; never synthesize a paid audition.
4. Freeze that exact current `voice_id` and its saved compatibility facts. Call
   `beatra.models.list` for `text_to_speech` before comparing models,
   validating a requested language, or estimating price.
5. Prepare an exact production card: approved text or pilot text, voice ID,
   language when supplied, controls, weighted-character count, current model
   choice, estimate, paid-call count, delivery order, and review target.
6. Obtain approval for that card, create one new opaque local
   `client_request_id` per logical paid request, submit each request once,
   and poll only its returned task.
7. Deliver actual artifacts and facts in ledger order. A changed text, voice,
   model, language, or control is new paid work with a new card, approval, and
   request identity.

Use the bundled `scripts/mcp_client.py` for every Beatra operation:

```text
python3 scripts/mcp_client.py call <tool-name>
```

Pass one JSON object to standard input. Use
`python3 scripts/mcp_client.py upload <path> --mime-type <exact MIME>` for a
local sample. Never configure or call a host Beatra Connector. Never use
REST/OpenAPI as a fallback, and never imitate the upload protocol.

## Voice, language, and price truth

Validate a supplied language as a BCP-47 tag before using it. Lowercase its
primary tag, map the documented aliases `nb` to `no` and `tl` to `fil`, and
apply the same normalization to catalog values; for example, `yue-HK` becomes
`yue`. The current voice supports the target only when either `voice.language`
or one entry in `voice.languages_supported` has the same normalized primary
tag.

For `model: "auto"`, derive the live possible model set as:

```text
models.list.auto.candidate_order ∩ selected_voice.compatible_models
```

preserving candidate order and keeping only live available cards. Separately
build the explicit pool from every live card in the exact current voice's
`compatible_models`; a compatible card outside `candidate_order` is
explicit-only and never enters the auto set. When the user supplied a language,
check its normalized primary tag against `constraints.supported_languages` on
every auto candidate. Keep `auto` only when every candidate supports it. If the
auto set is empty or any candidate fails, propose one language-compatible card
from the explicit pool and obtain approval. Without a supplied language, keep a
nonempty auto set; if it is empty, propose one card from the explicit pool. If
no applicable explicit card exists, offer another current voice, a user-chosen
supported language when relevant, or no submission. Language is not a hidden
auto-selection input.

Speech is estimated from each applicable live card's `estimate_formula`,
`unit_price_credits`, `scale`, and
`billing_basis: "beatra_weighted_characters"`. Each Han ideograph counts as
2 and every other character as 1. Quote one exact estimate when the frozen
explicit model—or every possible auto model—has the same result; otherwise
quote the live range. Never invent an auto-resolved model or use remembered
prices.

Read [voice casting and delivery](references/voice-casting-and-delivery.md)
for exact model, language, pricing, request, and output handling.

## Pilots and paid confirmation

Planning, script preparation, voice listing, preview links, model listing, and
estimates are non-billable. `beatra.speech.synthesize` and
`beatra.voices.clone` are billable.

- A single approved script uses one approved synthesis card.
- Long-form work uses one supplied and approved pilot passage before the
  separately approved remainder.
- Multilingual work uses one supplied and approved pilot for every distinct
  voice/language/model route before a separately approved remainder.
- A clone is one paid operation. Before any `client_request_id` or
  `beatra.voices.clone` call, show the clone admission card: route
  `voice_clone`, tool `beatra.voices.clone`, live `beatra.models.list` price
  for one successful activated voice, the provisional estimate, the fact that
  the 600-credit signup gift usually cannot start this clone, and what
  happens if the balance is short. Do not create `client_request_id` or
  submit until the user confirms they have topped up or already have enough
  credits for this estimate. Do not offer a free clone or a free sample that
  replaces the clone. On `insufficient_balance`, relay the returned public
  message, keep the URL exact, translate the rest, and retry the same frozen
  `client_request_id` only after the user says they have topped up. Any
  spoken proof is a separate synthesis operation with its own approved text,
  card, approval, and request identity.

A current explicit “generate” or “make it” instruction is approval when it
fully covers the frozen card. Do not ask for redundant confirmation in that
case. Price-only, comparison, preparation, general permission to “handle it,”
and an unfrozen batch or variant set are not approval. Cancellation is
requested only when the user asks; a conflict or unconfirmed remote stop means
continue polling the same task.

Read [long-form and multilingual
production](references/long-form-and-multilingual.md) before any ordered batch,
and [voice cloning and review](references/voice-cloning-and-review.md) before
sample upload or clone submission.

## Execution, recovery, and delivery

Store locally, before submission, the route, exact JSON, production-card
approval, `client_request_id`, and later `task_id`. The request identity is a
local recovery key; `tasks.list` and `tasks.get` do not filter by or return it.

If a response or task ID is lost, call `beatra.tasks.list` with the relevant
capability and follow every `next_cursor` needed to cover the time window.
Verify plausible candidates with `beatra.tasks.get` using returned task facts.
Do not submit while a matching task may be queued or running. Reuse the same
request identity only for a field-for-field identical replay after task
creation remains genuinely uncertain; any changed field needs new approval
and a new identity. A slow, failed, canceled, authorization, or update result
does not authorize an automatic replacement.

For successful speech, report only returned facts: `type`, `model`,
`voice_id`, `audio.url`, `audio.artifact_id`,
`audio.duration_seconds`, `audio.mime_type`, `audio.size_bytes`, optional
`audio.sample_rate`, `characters`, optional `subtitles`, task status,
resolved model, and billing when present. For a successful clone, preserve the
exact four-field output: `type`, `voice_id`, `status`, and `display_name`.
Never claim listening, synchronization, publication, accuracy, durability,
deletion, or a refund that the returned result does not establish.

Deliver long-form and multilingual results in the approved route, language,
and segment order. Keep accepted audio unchanged. A correction re-synthesizes
only an explicitly approved affected segment.

## Account balance

When the user asks how many credits remain or whether a live estimate fits,
call `beatra.wallet.get`. When they ask what was charged, call
`beatra.wallet.ledger`. Both are read-only. Do not invent an account-balance or
top-up tool. Do not make `wallet.get` a required step before every paid submit.

When a model card comes back carrying a `top_up` block, relay its tiers as the
card lists them and in that order. Do not rank them, do not talk one down, and
do not pick one for the user. Which tier suits them is their call, made on
the wallet page with the whole list in front of them. Never quote a tier from
memory.

## References by task

- Read [intent and routing](references/intent-and-routing.md) for the Studio
  matrix, minimum inputs, free planning, and paid pilot boundaries.
- Read [voice casting and delivery](references/voice-casting-and-delivery.md)
  for exact voice identity, live model/language checks, price math, synthesis
  JSON, and result review.
- Read [long-form and multilingual
  production](references/long-form-and-multilingual.md) for route ledgers,
  pilot coverage, ordered batches, and interruption recovery.
- Read [voice cloning and review](references/voice-cloning-and-review.md) for
  consent, sample transport, clone JSON, exact output, and optional proof.
- Read [bundled MCP Client diagnostics](references/mcp-connection.md) only for
  bundled-client verification and transport troubleshooting.
- Read [installation and authentication](references/installation-and-auth.md),
  [installation registration](references/installation-registration.md),
  [tasks and results](references/tasks-and-results.md), [billing, errors, and
  recovery](references/billing-errors-and-recovery.md), and [uninstall and
  disconnect](references/uninstall-and-disconnect.md) only when that
  operational branch is active.

## Updates and security

This Skill runs only the files installed with it and leaves them unchanged while
it runs; it never downloads or executes other code. It talks to Beatra's official
services over HTTPS, and a file you choose to upload goes only to the one-time
upload address Beatra issues for it.

Once a day at most, the bundled client reads Beatra's public version list. When
the store or tool you installed from has a newer version, it prints one line with
the command that updates it there. Updating stays your choice, through that store
or tool. See [updates and security](references/updates-and-security.md).
````

## File: skills/voiceover-narration-studio/scripts/authorize.py
size: 62582
sha: 70a5b0a1945d68d58b49c4fb2c79f2ae4cae833d
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/scripts/authorize.py

Content is not inlined in the catalog. Read the upstream file: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/scripts/authorize.py

## File: skills/voiceover-narration-studio/scripts/mcp_client.py
size: 88508
sha: 5316b3895e61a4a86b7ab2fda9ce91c9fbe90e69
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/scripts/mcp_client.py

Content is not inlined in the catalog. Read the upstream file: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/scripts/mcp_client.py

## File: skills/voiceover-narration-studio/scripts/uninstall.py
size: 12918
sha: 6a2afac93cd66c9bad18a293328a4f8ff9d614bb
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/scripts/uninstall.py

```text
#!/usr/bin/env python3
"""Uninstall one Beatra Skill package without stranding the shared connection.

The device connection (~/.beatra) is shared by every installed Beatra skill.
Removing one skill must therefore never touch the shared credential while any
other skill still uses it — even when the user asks for a complete removal.
Only when this device's skill inventory says nothing else is left does the
credential revoke itself and the local state leave with it.

Every uncertain situation resolves to "keep the connection": a wrongly kept
credential idles out server-side after 15 days and can be revoked from the
Console at any time, while a wrongly revoked one breaks every remaining skill.

This script never deletes the package directory it runs from — it reports the
directory so the calling agent can remove it as the final step.
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any, Callable

sys.dont_write_bytecode = True  # never leave __pycache__ inside the installed Skill

REVOKE_PATH = "/oauth/device/revoke"
PACKAGE_SLUG = "voiceover-narration-studio"
PACKAGE_DISPLAY_NAME = "Voiceover & Narration Studio"
PACKAGE_VERSION = "0.2.9"
HTTP_USER_AGENT = f"Beatra-Skill/{PACKAGE_SLUG}/{PACKAGE_VERSION}"

#: Everything the connection owns inside ~/.beatra. Removal unlinks exactly
#: these and the update records below, then removes each directory only if it
#: is empty — the script never recursively deletes a directory it does not
#: fully understand.
_STATE_FILES = (
    "credentials.json",
    "installation.json",
    "host.json",
    "skills.json",
    "registrations.json",
    "network.json",
    "balance-holds.json",
    "pending_authorization.json",
    ".authorize.lock",
)

#: The per-install records in ~/.beatra/updates/<id>/: the version check,
#: install source and registration, and a domestic channel's update lock. Only
#: these names leave; an interrupted update's transaction stays, and so does
#: every directory that still holds something.
_UPDATE_RECORD_FILES = ("state.json", "source.json", "registration.json", "update.lock")
_UPDATE_RECORD_DIGITS = frozenset("0123456789abcdef")

PostRevoke = Callable[[str, str], int]

class _RejectRedirectHandler(urllib.request.HTTPRedirectHandler):
    def redirect_request(
        self,
        _request: urllib.request.Request,
        _file_pointer: Any,
        code: int,
        _message: str,
        _headers: Any,
        _new_url: str,
    ) -> None:
        raise RuntimeError(f"Beatra revocation refused HTTP redirect ({code})")

def _revoke_url(state_dir: Path) -> str:
    """Revoke over this computer's recorded network route. Uninstall never asks
    Beatra which route to use: revocation is best effort, and the record it
    would write is about to be removed."""

    import mcp_client  # the bundled client beside this file owns the route

    return f"{mcp_client.route_api_origin(mcp_client.current_route(state_dir))}{REVOKE_PATH}"

def _default_post_revoke(token: str, url: str) -> int:
    request = urllib.request.Request(
        url,
        data=b"",
        headers={
            "Accept": "application/json",
            "Authorization": f"Bearer {token}",
            "User-Agent": HTTP_USER_AGENT,
        },
        method="POST",
    )
    opener = urllib.request.build_opener(_RejectRedirectHandler())
    try:
        with opener.open(request, timeout=30) as response:
            return int(response.status)
    except urllib.error.HTTPError as exc:
        return int(exc.code)
    except urllib.error.URLError as exc:
        raise RuntimeError("Beatra authorization service is unreachable") from exc

def _own_skill_root() -> Path | None:
    """The installed package root this script runs from, when knowable."""
    try:
        return Path(__file__).resolve().parent.parent
    except NameError:
        return None

def _load_inventory(state_dir: Path) -> tuple[list[dict[str, Any]] | None, str]:
    """Return (entries, state) where state names why entries may be None."""

    path = state_dir / "skills.json"
    if not path.exists():
        return None, "inventory_missing"
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        return None, "inventory_unreadable"
    loaded = value.get("skills") if isinstance(value, dict) else None
    if not isinstance(loaded, list):
        return None, "inventory_unreadable"
    return [entry for entry in loaded if isinstance(entry, dict)], "ok"

def _device_token(state_dir: Path) -> str | None:
    path = state_dir / "credentials.json"
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        return None
    token = value.get("access_token") if isinstance(value, dict) else None
    if not isinstance(token, str) or not token:
        return None
    return token

def _provably_deleted(install_path: str) -> bool:
    """True only when the entry's directory is verifiably gone.

    "Not currently visible" is not "uninstalled": an unmounted volume, a
    permission-restricted parent, or any stat error must count the skill as
    alive — the rule resolves every uncertainty to keeping the connection.
    Only a clean missing entry under a reachable parent is proof of removal.
    """

    try:
        os.stat(install_path)
        return False
    except FileNotFoundError:
        pass
    except OSError:
        return False
    parent = os.path.dirname(install_path.rstrip("/\\")) or os.sep
    try:
        os.stat(parent)
    except OSError:
        return False
    return True

def _remove_update_records(state_dir: Path) -> None:
    updates = state_dir / "updates"
    try:
        if updates.is_symlink() or not updates.is_dir():
            return
        records = [
            child
            for child in updates.iterdir()
            if len(child.name) == 24
            and set(child.name) <= _UPDATE_RECORD_DIGITS
            and not child.is_symlink()
            and child.is_dir()
        ]
    except OSError:
        return
    for record in records:
        for name in _UPDATE_RECORD_FILES:
            try:
                (record / name).unlink(missing_ok=True)
            except OSError:
                pass
        try:
            record.rmdir()
        except OSError:
            pass
    try:
        updates.rmdir()
    except OSError:
        pass

def _remove_local_state(state_dir: Path) -> None:
    for name in _STATE_FILES:
        try:
            (state_dir / name).unlink(missing_ok=True)
        except OSError:
            pass
    _remove_update_records(state_dir)
    try:
        state_dir.rmdir()
    except OSError:
        pass

def uninstall(
    *,
    state_dir: Path | None = None,
    skill_root: Path | None = None,
    post_revoke: PostRevoke = _default_post_revoke,
    dry_run: bool = False,
) -> dict[str, Any]:
    """Apply the last-skill rule and return a machine-readable decision."""

    state_dir = (state_dir or Path.home() / ".beatra").expanduser()
    root = skill_root or _own_skill_root()
    resolved_root = str(Path(root).expanduser().resolve()) if root is not None else None

    result: dict[str, Any] = {
        "package": PACKAGE_SLUG,
        "dry_run": dry_run,
        "delete_next": resolved_root,
    }

    if not state_dir.is_dir():
        # Nothing local to retire; the package files are all that is left.
        result.update({"decision": "disconnected", "revoked": False, "reason": "no_local_state"})
        return result

    if resolved_root is None:
        result.update({"decision": "keep_connection", "reason": "unknown_install_path"})
        return result

    try:
        entries, inventory_state = _load_inventory(state_dir)
        if entries is None:
            # No trustworthy inventory means no proof this is the last skill —
            # the connection stays, and the 15-day idle expiry backstops it.
            result.update({"decision": "keep_connection", "reason": inventory_state})
            return result

        survivors: list[dict[str, Any]] = []
        for entry in entries:
            if (
                entry.get("slug") == PACKAGE_SLUG
                and entry.get("install_path") == resolved_root
            ):
                continue  # the package being uninstalled
            install_path = entry.get("install_path")
            if isinstance(install_path, str) and install_path and _provably_deleted(install_path):
                continue  # verifiably removed outside this flow — self-heal
            survivors.append(entry)
    except Exception:  # noqa: BLE001 -- any surprise resolves to keeping the
        # connection; a crash here must never read as permission to revoke.
        result.update({"decision": "keep_connection", "reason": "inventory_error"})
        return result

    if survivors:
        # The inventory is deliberately not rewritten here: this package's own
        # entry disappears through the provably-deleted check once the agent
        # finishes removing the directory, so an interrupted uninstall leaves
        # a still-installed skill visible rather than silently forgotten.
        result.update(
            {
                "decision": "keep_connection",
                "reason": "other_skills_still_installed",
                "remaining_skills": [
                    {"slug": entry.get("slug"), "platform": entry.get("platform")}
                    for entry in survivors
                ],
            }
        )
        return result

    if dry_run:
        result.update({"decision": "disconnected", "revoked": False, "reason": "dry_run"})
        return result
    token = _device_token(state_dir)
    revoked = False
    revoke_state = "no_credential"
    if token is not None:
        try:
            status = post_revoke(token, _revoke_url(state_dir))
            if status == 200:
                revoked = True
                revoke_state = "revoked"
            elif status == 401:
                # The server no longer recognises this token; the local copy
                # is worthless either way, but nothing was confirmed revoked.
                revoke_state = "not_recognized"
            else:
                # Reached but refused (rate limit, server error): keep every
                # local file so a retry — or a Console revoke followed by a
                # rerun — can still finish the job cleanly.
                result.update(
                    {
                        "decision": "revoke_retry",
                        "revoked": False,
                        "reason": f"http_{status}",
                    }
                )
                return result
        except RuntimeError:
            revoke_state = "unreachable"
    token = None
    _remove_local_state(state_dir)
    result.update({"decision": "disconnected", "revoked": revoked, "reason": revoke_state})
    return result

def main() -> int:
    parser = argparse.ArgumentParser(
        description=f"Uninstall the {PACKAGE_DISPLAY_NAME} Skill from this device"
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Report the decision without revoking or changing any file.",
    )
    args = parser.parse_args()
    result = uninstall(dry_run=args.dry_run)
    print(json.dumps(result, ensure_ascii=False, indent=2))
    if args.dry_run:
        print("Dry run: nothing was changed. Run without --dry-run to uninstall.")
        return 0
    if result["decision"] == "keep_connection":
        print(
            "The shared Beatra connection stays in place"
            + (
                " because other installed skills still use it."
                if result.get("reason") == "other_skills_still_installed"
                else " because this device's skill inventory could not prove it is unused."
            )
        )
    elif result["decision"] == "revoke_retry":
        print(
            "Nothing was changed: the Beatra server refused the revocation just now. "
            "Run this script again shortly, or revoke the device in the Beatra "
            "Console and rerun to finish removing local state."
        )
        return 0
    elif result.get("revoked"):
        print("The Beatra device authorization is revoked and local state is removed.")
    else:
        print(
            "Local state is removed. The server-side authorization could not be "
            "confirmed revoked from here; it expires after 15 days of inactivity "
            "and can be revoked in the Beatra Console at any time."
        )
    if result.get("delete_next"):
        print(f"Finish by deleting the package directory: {result['delete_next']}")
    return 0

if __name__ == "__main__":
    sys.exit(main())
```

## File: skills/voiceover-narration-studio/references/billing-errors-and-recovery.md
size: 4900
sha: 4492c70c65d8053a86491da13f49d164470e5985
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/billing-errors-and-recovery.md

```text
# Billing, errors, and recovery

Every generation tool consumes Beatra credits and creates an asynchronous task.
Use one logical request identity across transport uncertainty so recovery cannot
accidentally create or charge a duplicate task.

## Submit a billable request

1. Assemble the final generation arguments.
2. Generate one opaque `client_request_id`.
3. Submit exactly once.
4. Store both `client_request_id` and the returned `task_id`.
5. Poll with `beatra.tasks.get`; do not resubmit while the task is queued or
   running.

The prepaid amount accepted with the request can be an estimate. A successful
task is settled against authoritative measured usage. Read the terminal
`usage` and `billing` fields, and report net credits as
`billing.net_charged_credits`. Do not promise that the initial estimate
is the final charge.

## Run a batch

When one request becomes many paid calls (a list of items, a loop, or a
script):

1. Before the first paid call, price the whole batch from the live prices
   (`beatra.models.list`, or `beatra.social.tools.get` for social data) and
   tell the user the estimated total.
2. Let the first `insufficient_balance` stop the whole batch. Do not go on to
   the next item; report which items finished and which did not.
3. Resume only the unfinished items, and only after the user says they have
   topped up. A refused item keeps its `client_request_id` and arguments.

## Recover an uncertain response

- If no response arrived, make an identical retry with the same
  `client_request_id` and exactly the same generation arguments.
- If any prompt, input artifact, model, output setting, or other generation
  argument changes, treat it as new work and use a new `client_request_id`.
- If the task ID was lost, use `beatra.tasks.list` before considering a retry.
- If remote execution failed, report the terminal error. Start changed or newly
  requested work only with a new request ID.

## Handle structured failures

- HTTP 401 authentication failure: do not replay the paid call automatically.
  Run `python3 scripts/authorize.py` and follow its output
  until it prints `Beatra is ready`, then recover with the original request
  identity and unchanged arguments.
- Timeout, DNS/TLS, HTTP 429, HTTP 5xx, or another transport failure: preserve
  the credential. If delivery of a paid call is uncertain, retry only with the
  same `client_request_id` and exactly the same arguments after connectivity
  recovers.
- Insufficient scope: explain that the active connection lacks required access;
  run `python3 scripts/authorize.py --force` only after the
  user explicitly chooses to reconnect the full Beatra authorization. Never ask
  for a media-specific grant.
- Insufficient balance (error code `insufficient_balance`): relay the returned
  message to the user — it already carries the facts that matter: nothing was
  charged, the top-up link, credits take effect immediately, and the same
  request can be retried as-is. Translate into the user's language but keep
  the URL exactly as returned. Stop all remaining paid work, including the
  rest of a batch, and do not retry until the user says they have topped up.
  A successful task can also leave a negative balance when actual measured
  usage exceeded the estimate; the output remains valid, but new billable
  work must wait for a top-up.
- After that refusal, `scripts/mcp_client.py` holds further calls to the same
  tool on this computer: it answers them with the same message and sends
  nothing (`details.local_hold`). Its `details.agent_note` names the balance
  check that lifts the hold once the user says they have topped up. If the user
  chose a cheaper request instead, run that cheaper call once with
  `--recheck-balance`. Never put `--recheck-balance` in a loop or script.
- Idempotency conflict: the request identity was reused with different
  arguments. Do not retry it. Keep the original task if it exists, or use a new
  `client_request_id` only for user-approved changed work.
- Rate or concurrency limit: honor the returned retry delay, then retry the
  exact same arguments with the same request identity. Do not create a
  replacement task.
- Validation failure or model incompatibility: call `beatra.models.list` for current
  constraints, correct the invalid arguments, and use a new
  `client_request_id` because the generation inputs changed.
- Upload expiration, MIME mismatch, or byte-length mismatch: request a fresh
  grant and upload the unchanged file again. Never reuse a consumed or expired
  grant.
- Remote execution failure: preserve the Beatra error code and task ID;
  report only the failure classification and billing/refund fields returned by
  Beatra. Do not infer a different cause or promise a refund that is not shown.

Never expose authentication tokens, complete private prompts, or sensitive
input content while explaining recovery.
```

## File: skills/voiceover-narration-studio/references/installation-and-auth.md
size: 9544
sha: 149c77b01f1ad83a83c774ade0cb2a795288422c
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/installation-and-auth.md

````text
# Installation and authentication

Use this reference when Beatra is newly installed, unavailable, or needs its
authorization recovered. This flow is owned by the Skill and works the same way
in any agent that can run Python 3.10 or newer and make HTTPS requests. The
helpers use only the Python standard library; do not install packages for them.

## Keep one private local identity

The Skill uses two user-only files:

- `~/.beatra/installation.json` contains one stable, non-secret installation
  reference.
- `~/.beatra/credentials.json` contains the single Device Token.

On POSIX systems the directory must be mode `0700` and both files mode `0600`.
On Windows the current user must be the only principal granted access through
the file ACL. Never copy the Device Token into conversation context, stdout,
stderr, a command argument, an environment variable, a log, a backup, a diff,
or another file. A completed authorization leaves the token only in
`~/.beatra/credentials.json`.

## Ensure Beatra is ready

From the installed `beatra` Skill directory, run:

```text
python3 scripts/authorize.py
```

Use `python` instead when that is the local Python 3.10+ command. Always run this
idempotent helper after installing or replacing a Beatra Skill and when
recovering its connection. It validates the candidate credential with one
non-billable live MCP request. A valid credential becomes Ready without opening
the browser, so installing another Beatra package does not require a second
approval.

When no valid credential exists, or the live MCP endpoint definitively returns
HTTP 401, the helper:

1. reuses or creates `~/.beatra/installation.json`;
2. continues an approval that is already open on this computer, or requests a
   new Device Authorization from Beatra. When the user already selected Allow
   on this computer and nothing collected it, Beatra connects at once — no
   second approval;
3. prints the HTTPS approval link at once and opens it in the user's browser
   (only once for each approval). The URL fragment carries the approval code,
   so the page verifies the code by itself;
4. waits while it checks every few seconds whether the user selected Allow,
   and keeps checking through brief network failures. One run waits up to
   about 15 minutes; an approval page stays open for up to an hour;
5. atomically saves the returned Device Token to
   `~/.beatra/credentials.json` without printing an HTTP response body;
6. validates the new credential with the same non-billable MCP request and
   prints `Beatra is ready` only after it succeeds.

### Wait for Allow

The user usually needs a minute or more to sign in or create an account before
selecting Allow. The helper waits for it: leave it running. If your host moves
the command to the background, let it keep running and do not start a second
one.

Act on how each run ends:

- `Beatra is ready` — the connection works; continue with the user's request.
- `BEATRA_AUTH_PENDING` (exit status 3) — the run ended before the user selected
  Allow: the approval page had not been opened yet, the run's wait ran out, or
  the run was stopped. Show the approval link to the user as described below if
  you have not already, and again if the link is different. Run the same command
  at most twice in one turn; it continues the same approval, so the user
  approves only once. When it is still pending after that, deliver what needs
  no connection (below) and stop. When the message says Beatra could not be
  reached, tell the user the connection is being retried instead of asking them
  to approve again.
- `BEATRA_AUTH_IN_PROGRESS` (exit status 3) — another run on this computer is
  starting an approval right now. Run the same command again in a moment; it
  continues that approval.
- `Beatra authorization stopped (access_denied)` — the user declined on the
  approval page. A decline is often a mis-click: if the user still wants to
  connect, run the same command again and it opens a new approval page.
- `Beatra authorization stopped (expired_token)` — the approval page expired
  before the user selected Allow. Tell the user; if they still want to
  connect, run the same command again and share the new approval link.

A run stopped by a timeout or an interrupted tool call counts as one of the
two runs in the turn: run the same command again once, since it continues the
approval that is already open, and after a second stop deliver what needs no
connection and stop.

An approval does not depend on a run being active when the user selects Allow.
Beatra keeps it, and every Beatra command collects it first: when the user
writes again after approving, run the helper once (or go on with their
request) and the connection completes without a second approval. So if the
user interrupts to ask something else, stop the helper and answer them; and if
they do not want to connect now, stop waiting — the link stays valid for a
while.

#### Deliver while the approval is pending

When the approval is still pending at the end of a turn, finish and deliver
everything in the request that needs no connection, in the format the user
asked for — the Skill's root names it for each kind of work: the prepared
text, prompt, lyrics, shot list, or selection criteria. Say in one line that
the paid step continues on their next message after Allow, because the next
Beatra command collects the approval. Do not fill the gap with another tool or
service, and do not name voices, models, prices, or results that no live call
returned.

When telling the user what to do:

- present the approval page as a link and say the only decision on it is
  selecting Allow;
- never quote, display, or read out the approval code. It travels inside the
  link fragment, the approval page does not show it, and the user never types
  or compares it — a code surfaced in conversation is noise, not a step;
- say prominently that a signed-out browser lands on Beatra sign-in first:
  the user signs in or creates their account there, and the approval page
  continues automatically after sign-in;
- say once that the connection continues automatically after Allow — never
  ask the user to confirm in chat that they approved.
- on the mainland China network, follow the sign-in and console-link guidance
  in [MCP connection](mcp-connection.md#network-route).

The helper opens no local listener and requires no inbound connection. One
approval covers image, video, music, speech, upload, model, and task tools.

HTTP 403 or insufficient scope does not prove the credential is invalid. Keep
the credential and run `python3 scripts/authorize.py --force`
only when the user explicitly wants to reconnect or switch accounts. A timeout,
DNS/TLS failure, HTTP 429, or HTTP 5xx is transient: keep the credential, do
not start Device Authorization, and retry after connectivity recovers.

### Record where this Skill was installed from

After this Skill first connects, if this conversation shows how this
Skill was installed — for example an install command or link that names a
store or site — record it once:

```text
python3 scripts/mcp_client.py install-source --guess <source>
```

Use one of `website`, `github`, `skillhub`, `skillatlas`, `modelscope`,
`workbuddy`, `clawhub`, or `other`. If the conversation does not show it,
skip this and do not ask the user. If the command is not recognized, skip it.

## Use the bundled MCP client

Every Beatra Skill tool runs through the bundled `scripts/mcp_client.py`, which
connects to `https://mcp.beatra.ai/mcp`. Do not add, enable, trust, or configure
a host Beatra Connector. The bundled client reads the shared credential file
and never accepts the token through argv or the environment:

```text
python3 scripts/mcp_client.py verify
python3 scripts/mcp_client.py tools
python3 scripts/mcp_client.py call beatra.tasks.list
```

The host follows this computer's network route; see [MCP connection](mcp-connection.md).

For `call`, provide the tool arguments as one JSON object on stdin. Do not put
user content or credentials in the command arguments. The authorization helper
already performs this verification. Run the explicit `verify` command only for
diagnostics; it invokes `beatra.tasks.list` with `{}` and is non-billable.

The direct client follows the Streamable HTTP sequence:

1. send `initialize` with the MCP protocol version and client information;
2. retain an `MCP-Session-Id` response header when the server provides one;
3. send the `notifications/initialized` notification;
4. use `tools/list` for discovery and `tools/call` for execution;
5. include the protocol version, optional session ID, and Device Token bearer
   header on every request.

## Recover or revoke access

The single Device Token has a sliding 15-day idle lifetime. Every accepted MCP
use extends that idle deadline without user action. If Beatra returns HTTP 401
after the token was idle too long or was revoked, run
`python3 scripts/authorize.py` and act on its output as
described above; the helper replaces and validates the credential. Do not start another
authorization for a media-type change and do not delete a credential because of
a transient connection failure.

The user can revoke a connected agent from the Beatra Console at any time.
Revocation immediately invalidates the credential. Rerun Device Authorization
only when the user wants to reconnect that installation.

After first installing or replacing the Skill, start a new agent session when
the host discovers Skills only at session startup.
````

## File: skills/voiceover-narration-studio/references/installation-registration.md
size: 2087
sha: 7da7c1c9b57c161af2500af8f037f7a7c5166293
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/installation-registration.md

```text
# Installation registration

On first use, and again when the version, agent environment, channel, network
route, install source or installation reference changes or 24 hours pass, the
bundled client makes one best-effort, non-billable
`beatra.installations.register` call for this package. It is the only reporting call
the Skill makes, it does not replace Device Authorization, it never asks for
narrower package scopes, and its failure never blocks the user's requested
task. It records:

- the package, its version, the agent environment (`platform`), and the
  stable installation reference;
- the distribution channel and locale fixed when the package was built;
- the network route this computer uses and why (see the MCP connection
  reference);
- where this copy was installed from, when known: a source file Beatra adds to
  copies it hands to a store or its website, a store client's own record in
  the Skill directory, or the agent's `install-source --guess`;
- coarse runtime facts: operating system and version, processor architecture,
  Python version, interface language, and time-zone offset.

It never sends user names, file paths, environment variables, prompts, or tool
details. Beatra uses these fields to understand which stores and networks
people install from, as its privacy policy describes, and clears the runtime
facts when the account is deleted.

A per-installation cache in `~/.beatra/updates/` suppresses another
registration until the version, agent environment, channel, network route, install
source or installation reference changes, or 24 hours pass. An unknown but well-formed package
slug is accepted and recorded as unrecognized, so package release never
depends on a synchronized backend registry deployment.

The `platform` field is the real agent environment, resolved once per process
from environment signatures (or `~/.beatra/host.json`, written at
authorization time) — never detected per request. When authorizing manually,
pass `--platform <environment-name>` to `scripts/authorize.py` if automatic
detection cannot identify the host.
```

## File: skills/voiceover-narration-studio/references/intent-and-routing.md
size: 4196
sha: 51d2828f275cdf49b932796b4291c1f9a711bb4c
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/intent-and-routing.md

```text
# Intent and routing

## Keep the Studio central only when it adds coordination

Choose one primary route, reuse known facts, and ask only for a missing hard
input or a choice that changes paid work.

| Request shape | Route | Minimum before a paid call |
| --- | --- | --- |
| One short-video, advertisement, social, or promo voiceover | `short-form-voiceover-audio` | Final supplied or approved text and an exact current voice |
| Audiobook, course, serialized narration, or other ordered long-form work | `ai-audiobook-narration` | Approved segment plan, pilot text, and narrator |
| Supplied target-language scripts for a multilingual production | `ai-multilingual-dubbing` | Approved text per language and one valid voice route per language |
| One authorized reusable voice | `voice-cloning-studio` | Explicit speaker authorization, valid sample, and display name |
| Mixed, ongoing, or cross-route production | This Studio | An ordered route ledger and the first exact paid card |

Do not move a mixed request wholesale into a focused package. Sequence its
parts. For example, create and finish an authorized clone before resolving its
returned `voice_id` for later synthesis; pilot each distinct multilingual
route before approving its remainder.

## Minimum questions and positive defaults

For synthesis, ask for final text or permission to prepare a speakable version,
and ask for a target language only when the user intends a language not already
clear from the supplied text or context. Do not ask for language merely because
the request omitted it. Ask for voice preferences only when no voice has been
selected and “choose for me” is not already an answer.

Use `model: "auto"`, MP3, speed 1.0, volume 1.0, pitch 0, no emotion, and no
sample rate unless the user's destination changes one of those choices. Treat
duration as a review target, not a guarantee. Preserve supplied claims,
pronunciations, names, ordering, and target-language text.

For cloning, the one non-negotiable first fact is consent. A clear statement
that it is the user's own voice is enough; otherwise obtain a clear statement
that the speaker authorized cloning. Do not turn that confirmation into
paperwork. Stop before upload while it is missing.

## Free planning and paid units

Free planning includes:

- routing and ledger preparation;
- speakable edits shown for approval;
- `beatra.voices.list`, returned preview links, and casting comparison;
- `beatra.models.list`, compatibility and language checks, and price estimates;
- search and reconciliation of already-created tasks.

Each `beatra.speech.synthesize` and `beatra.voices.clone` submission is a
separate billable request. A card must state the exact approved text or sample,
voice and model decision, controls, count, current estimate, delivery position,
and what the user will review.

## Pilot coverage

- For one supplied and approved short script, the one synthesis is the complete
  approved unit; do not add alternate takes.
- For long-form work, select one representative supplied passage and obtain
  approval for that pilot alone. Review it, then present the remaining ordered
  segments and total estimate as a separate approval.
- For multilingual work, define a distinct route by its voice, BCP-47 language,
  and possible or explicit model set. Run one supplied and approved pilot for
  every distinct route. Only after all required pilots are accepted may the
  remaining language-by-segment matrix receive a separate approval.
- A clone does not include a spoken proof. If the user wants one, it begins
  after clone success as a separate text-to-speech card.

Never use invented text for a paid sample. General permission, earlier pilot
approval, or clone approval does not approve a remainder, another language,
an alternate take, or a spoken proof.

## Adjacent outcomes

Keep this package's result at ready-to-edit speech audio or a returned reusable
voice. A request for transcription needs text first. Existing-audio repair,
mixing, synchronization, video, avatar, lip-sync, music, and publication use
their own workflows. Preserve and complete the voice component rather than
claiming those adjacent deliverables.
```

## File: skills/voiceover-narration-studio/references/long-form-and-multilingual.md
size: 4051
sha: 2b96d0c334356a38d2de063ad1000218773c96fc
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/long-form-and-multilingual.md

````text
# Long-form and multilingual production

## Build one ordered ledger

Split only at natural chapter, lesson, scene, or paragraph boundaries. One
request accepts at most 50,000 characters, but the approved editorial
boundaries and review cost normally justify smaller segments. Record:

- route, language, segment number, title, and exact approved text;
- exact `voice_id`, possible auto set or explicit model, format, and controls;
- weighted-character count, current formula, price or range, and approval;
- local `client_request_id`, returned `task_id`, terminal state, artifact,
  and billing facts when they exist.

Every segment is one logical paid request. Generate IDs only after approval.
Do not submit the next batch merely because the ledger has been prepared.

## Pilot a long-form route

Choose one representative passage from text the user supplied and approved.
The pilot card covers only that passage. After the returned audio is reviewed
for narrator fit, readings, pacing, tone, and destination, freeze any accepted
settings.

Then present the remaining ordered segment count, exact or ranged total
estimate, paid-call count, and delivery order for a separate approval. A pilot
approval never authorizes the remainder. Keep a single accepted narrator and
settings unless the user explicitly approves a route change.

## Pilot every distinct multilingual route

The target-language texts must already be supplied or shown and approved.
This workflow does not silently translate, invent, or merge scripts.

Define a route by:

- exact voice ID;
- supplied BCP-47 language;
- ordered possible auto set or explicit model;
- controls and output format.

Validate both layers: the selected voice must list the language, and the live
model decision must follow `constraints.supported_languages`. One voice may
serve several languages only when each route passes independently.

Run one approved pilot for every distinct route. A pilot in one language or
with one voice/model route does not authorize another. After all required
pilots are accepted, present the complete language-by-segment remainder,
weighted totals, price per route, total range or upper bound, paid-call count,
and delivery order for separate approval.

## Submit in controlled order

Submit each approved JSON once and poll its task to terminal before moving
beyond the concurrency and order the user approved. Keep results grouped by
route and language, then ordered by segment:

```text
<language-or-route>/01-<segment>
<language-or-route>/02-<segment>
```

Do not represent the audio as synchronized, mixed, merged, or published.
Deliver ready-to-edit artifacts with their actual media facts.

## Continue after interruption

Start from the ledger. Poll every known `task_id` before looking for more work.
For a lost task ID, call `beatra.tasks.list` with
`capability: "text_to_speech"` and paginate through every `next_cursor` needed
to cover the relevant window. Verify candidates with `beatra.tasks.get`,
comparing capability, time, returned input, voice, settings, and task facts.

The local `client_request_id` is not a remote filter or returned task field.
Do not resubmit while the original may exist. Reuse it only for an identical
JSON replay when task creation remains genuinely uncertain after
reconciliation. Any changed text, language, voice, model, or control is a new
paid request with a new card, approval, and ID.

Resume only ledger rows with no completed artifact and no known queued or
running task. Accepted rows remain final. If the user requests cancellation,
call `beatra.tasks.cancel` for the exact task; a conflict or unconfirmed stop
means continue polling that same task rather than create a replacement.

## Correct only the approved segment

For a misread word, correct the supplied reading and show the changed segment.
For pace or tone, adjust the smallest relevant control. Present the affected
text, new settings, incremental estimate, and one new paid request. After
approval, use a new identity and preserve every accepted segment.
````

## File: skills/voiceover-narration-studio/references/mcp-connection.md
size: 3348
sha: 9365c7be5051522eadf734b5cb971d10a168b220
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/mcp-connection.md

````text
# MCP connection

All Beatra Skill packages use one connection path:

```text
scripts/mcp_client.py -> https://mcp.beatra.ai/mcp
```

The host follows this computer's network route, described below.

They share the one full-scope Device Token stored in
`~/.beatra/credentials.json`. Installing or switching packages must not trigger
another authorization when that credential is already valid. Do not add,
enable, trust, or configure a host Beatra Connector. Never print or move the
token into command arguments, environment variables, logs, chat, or another
package directory.

The bundled client performs MCP `initialize`, retains `MCP-Session-Id` when the
server returns one, sends `notifications/initialized`, and then uses
`tools/list` or `tools/call`. Its `verify` command runs the non-billable
`beatra.tasks.list` operation. It supplies the private `Authorization: Bearer`
header on every request without exposing its value.

Source attribution: every `tools/call` may carry two optional string
arguments, `source_package_slug` (this package's slug) and `source_platform`
(the agent environment, for example `claude-code` or `codex`). The bundled
client adds them automatically. They are telemetry only: missing values never
block a call, and the server treats them as `unknown`.

## Network route

Beatra serves two networks: the international hosts, and the mainland China
hosts `mcp.beatra.cn`, `api.beatra.cn`, `www.beatra.cn`, `cdn.beatra.cn` and
`console.beatra.cn`. The saved credential works on both. Packages of the
mainland China (`cn`) channel always use the mainland hosts and cannot change
network (`network` shows `"policy":"cn"`). Other packages choose once a day,
before their first command: the client asks Beatra which network suits this computer,
in a small request that carries no credential and no installation identity,
and records the answer in `~/.beatra/network.json`. Approval pages, console and
top-up links, uploads, and result downloads then follow the chosen network.

```text
python3 scripts/mcp_client.py network
python3 scripts/mcp_client.py network --use auto
```

`network` shows the current choice. Only when the user asks for a specific
network, run `network --use cn` (mainland China) or `network --use ai`
(international): it first checks that Beatra answers on that network and
refuses otherwise; packages of the `cn` channel refuse `--use`. If commands start failing to connect after a network was
chosen this way, run `network --use auto`. A request that failed is never
resent on the other network; the next command decides again unless a network was chosen with
`network --use`. A chosen network also keeps the version-list check on it.

On the mainland China network the approval page offers WeChat, phone and email
sign-in. If the user cannot find their sign-in method, or wants to sign in with
Google, run `python3 scripts/authorize.py --approval-link ai`: it shows the same
approval on Beatra's international site, and the network does not change;
`--approval-link auto` goes back. When you give the user a console or top-up
link on the mainland network, remind them to sign in the way they did before
(phone, email, or the same WeChat): scanning with a different WeChat creates a
separate account. If credits do not appear after a top-up, first check that
they signed in to the same account.
````

## File: skills/voiceover-narration-studio/references/tasks-and-results.md
size: 2254
sha: 34977869c667cda680bb242c904b15ec2bccde53
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/tasks-and-results.md

```text
# Tasks and results

Generation returns an asynchronous task. Keep its `task_id` and use
`beatra.tasks.get` to poll it with bounded backoff: wait 2 seconds before the
first poll, then increase the interval toward 5, 10, and at most 15 seconds.
Honor `deadline_at` when the task response provides it. Otherwise stop active
polling after 30 minutes, report the task ID and current status, and explain
that the user or a later agent session can resume polling the same task.

## Interpret state

- `queued` and `running` are nonterminal. Continue polling; do not submit a
  replacement generation.
- `succeeded`, `failed`, and `canceled` are terminal. Stop polling.
- On success, present returned artifacts, resolved model, actual usage, and
  billing truth. For prepaid work, report `billing.net_charged_credits`; the gross charged value
  may include a supplemental actual-usage debit and the refunded value may
  return an unused request-time estimate.
- For a settled video task, `usage.video_seconds` is the authoritative
  billable total. When present, `usage.input_video_seconds` and
  `usage.output_video_seconds` are the authoritative components of that total.
  This distinction matters for reference-video generation and video editing,
  where both input and output can be billable. Do not infer either component
  when Beatra returns it as `null`.
- On failure, preserve the structured error and apply the recovery reference.

Use `beatra.tasks.list` to recover a lost task ID or inspect recent work from
this connection. Filter only when doing so helps identify the task.

Call `beatra.tasks.cancel` only when the user requests cancellation. Before
remote execution starts, cancellation stops the task. After remote execution
starts, Beatra returns `canceled` only when the stop is confirmed. A `409`
means the task continues: do not promise a stop or refund, do not submit a
replacement, and continue polling the same task.

Do not infer an artifact URL, completion, usage, or charged credits from
elapsed time. Return only fields present in Beatra's task response.
For MCP task reads, prefer the direct `structuredContent.usage` and
`structuredContent.billing` objects; the complete envelope remains available
under `structuredContent.task`.
```

## File: skills/voiceover-narration-studio/references/uninstall-and-disconnect.md
size: 2739
sha: 5435c8ecc18194916070abac32e41f19d9e8f072
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/uninstall-and-disconnect.md

````text
# Uninstall and disconnect

Use this workflow when the user asks to remove, uninstall, or delete this
package, or to disconnect this device from Beatra entirely.

## The one rule

The device connection (`~/.beatra`) is shared by every installed Beatra skill.
It may only be retired when no other Beatra skill remains on this device — and
`scripts/uninstall.py` is the only component that can decide that. Never delete
`~/.beatra` or any file inside it yourself, and never skip the script, even
when the user asks for a complete removal: if another installed skill still
uses the connection, the connection must survive this uninstall.

## Steps

1. Run the bundled decision script first, while the package files still exist:

   ```
   python3 scripts/uninstall.py
   ```

   `--dry-run` prints the same JSON decision without changing anything or
   contacting the server. Never act on a dry-run report — rerun without the
   flag to actually uninstall.

2. Read the JSON decision it prints:

   - `"decision": "keep_connection"` — another installed skill still uses the
     shared connection (`remaining_skills` lists them), or the inventory could
     not prove this is the last one. The credential and `~/.beatra` stay
     untouched. Tell the user which skills keep the connection alive.
   - `"decision": "disconnected"` — this was the last Beatra skill. The script
     has removed the local connection state. `"revoked": true` means the
     server confirmed the revocation; `"revoked": false` means it could not be
     confirmed from here (server unreachable, or the token was no longer
     recognised) — the authorization then expires on its own after 15 days of
     inactivity, and the user can revoke it immediately in the Beatra Console
     under Agents.
   - `"decision": "revoke_retry"` — the server was reached but refused just
     now (for example rate limiting). Nothing was changed. Run the script
     again shortly, or have the user revoke the device in the Beatra Console
     and then rerun it to finish removing local state.

3. After a `keep_connection` or `disconnected` decision, finish by deleting
   only the package directory the script names in `delete_next`. Do not delete
   anything else. (After `keep_connection`, the inventory notices the deleted
   directory by itself — no further bookkeeping is needed.)

## Disconnecting everything

There is no separate "disconnect all" action. Uninstall each installed Beatra
skill with its own bundled script; the last one automatically revokes the
authorization and clears `~/.beatra`. The user can always revoke the device
from the Beatra Console as well — that invalidates the credential server-side
immediately, whatever remains on disk.
````

## File: skills/voiceover-narration-studio/references/updates-and-security.md
size: 6077
sha: 5c0172b541897c6c482d32fd2a1ac7765ec83935
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/updates-and-security.md

```text
# Updates and security

Since the October 2026 releases, Beatra Skill packages contain no self-updater.
Earlier releases could replace their own files from Beatra's CDN; that code has
been removed. Updates now come only from the store or tool you installed the
Skill from.

## What runs

Every operation runs one of the bundled scripts: `scripts/mcp_client.py`,
`scripts/authorize.py`, or `scripts/uninstall.py`. They use only the Python
standard library. `scripts/authorize.py` opens the Beatra approval page in
this computer's web browser when approval is still needed. They never
download, write, or execute other code, and the files of this installed Skill
stay exactly as installed.

## Where requests go

Requests use HTTPS and follow this computer's network route, described in
[MCP connection](mcp-connection.md): the international hosts, or the mainland
China hosts `mcp.beatra.cn`, `api.beatra.cn`, and `www.beatra.cn`. When you
choose to upload a local file, it goes only to the one-time upload address
Beatra issues for that upload. Choosing the network may send a small request
to the other network, and so may retrying the version list once, unless a
network was chosen with `network --use`.

## What is stored on this computer

These files are written under `~/.beatra/`. On POSIX that directory is mode
0700. Nothing is written inside the installed Skill directory.

- `credentials.json` (POSIX mode 0600), written by `scripts/authorize.py`: the device credential.
- `installation.json` (POSIX mode 0600), written by `scripts/authorize.py`: the stable installation reference.
- `host.json` (POSIX mode 0600), written by `scripts/authorize.py`: the agent environment, and a device name when one is known.
- `network.json` (POSIX mode 0600), written by `scripts/mcp_client.py`: this computer's network route.
- `skills.json` (POSIX mode 0600), written by `scripts/authorize.py` and `scripts/mcp_client.py`: which Beatra skills are installed here, with the slug, agent environment, install path, and time.
- `pending_authorization.json` (POSIX mode 0600), written by `scripts/authorize.py`: an approval that is still open. Removed when that approval is collected or discarded.
- `.authorize.lock` (POSIX mode 0600), written by `scripts/authorize.py` while authorization is in progress, then removed.
- `balance-holds.json` (POSIX mode 0600), written by `scripts/mcp_client.py`: a local record after a tool is refused for insufficient credits. Removed when no such record remains.

`scripts/mcp_client.py` also keeps one directory per installation under
`~/.beatra/updates/`. Its name is the first 24 hexadecimal characters of the
SHA-256 of the resolved installation path, and on POSIX the directory is mode
0700. The files inside it are POSIX mode 0600:

- `state.json`: the time of the last version check.
- `registration.json`: the cache of the last installation registration.
- `source.json`: where this copy was installed from, when that is known.

Replacing one of these files writes a temporary file in the same directory and
then renames it over the old file.

When `scripts/uninstall.py` decides this is the last Beatra skill on the
computer, it first asks Beatra to revoke the device credential when one is
stored. If Beatra refuses, it changes nothing so the run can be repeated.
Otherwise it unlinks exactly these names in `~/.beatra/`: `credentials.json`,
`installation.json`, `host.json`, `skills.json`, `registrations.json`,
`network.json`, `balance-holds.json`, `pending_authorization.json`, and
`.authorize.lock`. It also unlinks `state.json`, `registration.json`, and
`source.json` in each installation directory under `~/.beatra/updates/`. It
then removes each of those directories, `~/.beatra/updates/`, and `~/.beatra/`
only when that directory is empty. These
scripts do not create `registrations.json`; uninstall still unlinks that name
if it is present. The script never deletes the installed Skill directory. It
names that directory in `delete_next` for the caller to delete. If another
skill remains, or the inventory cannot prove this is the last one, it leaves
`~/.beatra/` unchanged.

## What is reported

Beyond the requests a command makes, the only report is the installation
registration described in [installation registration](installation-registration.md).
No prompts, file contents, file paths, or tool-call details are sent with it.
Each tool call itself names this package and the agent environment (for example
`claude-code`), as source attribution in [MCP connection](mcp-connection.md)
describes, so a task can be traced to the Skill that made it.

## Version notice

At most once every 24 hours, before an ordinary command, the bundled client
reads Beatra's public version list for this package
(`/skills/<package>/sources.json` on the site host of the current network). It
works out from local records which store or tool installed this copy. When that
place offers a newer version, it prints one line on standard error with the
command that updates it there. It never installs anything. If the check fails,
nothing is printed and the command runs as usual. A copy with no such record
gets no notice.

| Installed with | Update with |
| --- | --- |
| ClawHub CLI | `clawhub update <package> --workdir <dir> --dir <skills-dir>` (add `--force` if it reports local changes) |
| SkillHub CLI | `skillhub install <package> --force --dir <skills-dir>` |
| GitHub CLI | `gh skill update <package>`, run in the project directory for a project install |
| skills CLI (GitHub or ModelScope) | `npx skills update <package> -p -y`, run in the project directory; or `npx skills update <package> -g -y` for a global install |
| WorkBuddy | Update the Skill in WorkBuddy |
| Beatra website (English packages) | Reinstall from the package's install page |

## Check by hand

    python3 scripts/mcp_client.py update --check

It reports what the store or tool you installed from offers now. It installs
nothing and leaves the Skill's files unchanged. If `python3` is not the local
Python command, use the available Python 3.10+ command.
```

## File: skills/voiceover-narration-studio/references/voice-casting-and-delivery.md
size: 6047
sha: c2d42f34b4da3a94cd5f55804e4f3467ded566b4
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/voice-casting-and-delivery.md

````text
# Voice casting and delivery

## Resolve an exact current voice

Always call `beatra.voices.list`, even when the user or conversation already
contains a `voice_id`. Match that opaque ID exactly and save its current
`status`, `language`, `languages_supported`, and `compatible_models`. If the
ID is absent or not `ready`, do not submit; offer another returned current ready
voice or stop. Use `language`, `gender`, `category`, or `query` only when the
user's preferences justify the filter. A returned voice may
contain `display_name`, `category`, `language`, `languages_supported`,
`gender`, `age`, `accent`, `use_case`, `preview_url`,
`compatible_models`, `status`, and `created_at`.

Recommend only ready candidates. Keep the opaque `voice_id` exactly; a display
name, description, or previously remembered voice fact is not a substitute for
the fresh exact-ID lookup. Offer
`preview_url` only when it is returned, and never synthesize a paid audition.

## Build the possible auto set

Call `beatra.models.list` with:

```json
{"capability":"text_to_speech"}
```

Define the possible auto set as the ordered intersection of
`auto.candidate_order` and the exact current voice's `compatible_models`, using
only currently available model cards. Separately build an explicit pool from
every live available card in `compatible_models`. A compatible card outside
`candidate_order` is explicit-only and must never be mixed into the auto set.
Do not infer an auto result before admission.

Validate a supplied language against the BCP-47 shape used by the current
contract: a 2-to-8-letter primary tag followed by optional 1-to-8-character
alphanumeric subtags. Lowercase the primary tag and normalize the documented
aliases `nb` to `no` and `tl` to `fil`; normalize catalog values the same way,
so `yue-HK` compares as `yue`.

When the user supplied a valid BCP-47 language:

1. Confirm either the returned `voice.language` or one entry in
   `voice.languages_supported` has the same normalized primary tag.
2. Read `constraints.supported_languages` from every model in the possible
   auto set and compare their normalized primary tags.
3. Keep `model: "auto"` only when every candidate supports the target.
4. If the auto set is empty or any candidate fails, select one qualifying card
   from the full live voice-compatible explicit pool and obtain confirmation.
5. If the explicit pool has no qualifying card, propose another current ready
   voice, let the user voluntarily choose another supported target, or make no
   submission.

If no language was supplied, omit `language`. Do not ask for one only to make
the catalog decision. Keep a nonempty voice-first auto set; if it is empty,
propose one live card from the explicit pool. If that pool is also empty, offer
another current ready voice or no submission. Do not treat language as an
undocumented auto selection input.

## Controls and approved text

The text must be supplied or shown and approved. Preserve names, claims,
pronunciations, and meaning. Convert written constructions into speakable
phrasing only with approval when the edit is material. Use punctuation to
express breaths and pauses.

Current synthesis controls are `format` (`mp3`, `wav`, `flac`, `opus`, or
`pcm`), optional `sample_rate`, `speed` from 0.5 to 2.0, `volume` greater
than 0 and at most 10, `pitch` from -1 to 1 (normalized: 0 keeps the
voice's original pitch; use decimals like 0.25 for subtle tuning), and
optional `emotion` from the live supported values. Keep defaults unless the
destination or user requires a change.

## Calculate the current estimate

Count `beatra_weighted_characters` exactly: each Han ideograph weighs 2 and
every other character weighs 1. For every applicable model, use the card's
`estimate_formula`:

```text
(unit_price_credits * billable_quantity) / scale
```

Preserve its current `meter`, `unit`, `billing_basis`,
`unit_price_credits`, and `scale`. An explicit model produces one estimate.
Auto produces one exact estimate only if all possible candidates have the same
result; otherwise show the minimum and maximum. Never quote a remembered price
or raw character count.

## Freeze and submit one request

The production card contains the exact approved text, exact current `voice_id`,
language if supplied, model or honest auto set, format and controls, weighted
quantity, live formula and estimate, one paid call, and delivery position.
An explicit current “generate” or “make it” that fully covers this frozen card
is approval; do not ask again. A price question, comparison, preparation
request, or unfrozen batch is not. After approval, create one new local
`client_request_id` and send exactly:

```json
{
  "voice": "voice_...",
  "input": "The exact approved text.",
  "model": "auto",
  "format": "mp3",
  "speed": 1.0,
  "volume": 1.0,
  "pitch": 0,
  "client_request_id": "local-opaque-id"
}
```

Include `language`, `emotion`, or `sample_rate` only when frozen in the card.
Pass this JSON on standard input to:

```text
python3 scripts/mcp_client.py call beatra.speech.synthesize
```

Submit once and poll the returned `task_id` with `beatra.tasks.get` until a
terminal state. Do not create an alternate take or automatic retry.

## Review and deliver returned facts

A successful `SpeechOutput` contains:

- `type: "text_to_speech"`, concrete `model`, and `voice_id`;
- `audio.url`, `audio.artifact_id`, `audio.duration_seconds`,
  `audio.mime_type`, `audio.size_bytes`, and optional `audio.sample_rate`;
- `characters` and optional `subtitles`.

Return those facts with the task's actual status, resolved model, usage, and
billing when present. Compare returned duration with a target without claiming
exact timing. Ask the user to listen for readings, pauses, pace, tone, and
destination fit; do not claim to have heard audio unless it was actually
available for review.

A revision changes the production card, needs a new approval and request
identity, and re-synthesizes only the approved affected segment. Keep every
accepted artifact unchanged.
````

## File: skills/voiceover-narration-studio/references/voice-cloning-and-review.md
size: 5493
sha: d33d2f760e1b4fb82c5f9fcc77d13c082ffc8d92
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/references/voice-cloning-and-review.md

````text
# Voice cloning and review

## Confirm consent before transport

Accept a plain statement that the sample is the user's own voice. For another
speaker, require a plain statement that the speaker explicitly authorized
cloning. A file, contract access, publication, public-figure recording, or
suggestive filename is not that confirmation. Do not upload the sample and do
not set `consent_attested: true` while the fact is missing.

## Prepare one supported sample

Read the current `voice_clone` model card before judging technical readiness.
The current card accepts `artifact`, `data_uri`, and `https_url` samples,
requires 10 to 300 seconds, limits the sample to 20 MiB (20,971,520 bytes),
requires consent attestation, and activates a successful voice automatically.
Treat these as live card facts, not permanent prose promises.

Prefer one speaker talking naturally without music, other voices, heavy noise,
or pronounced echo. For a local file, preserve its exact filename, MIME type,
and byte count, then run only after consent:

```text
python3 scripts/mcp_client.py upload <path> --mime-type <exact MIME>
```

Record the returned `artifact_id`. Do not call `beatra.assets.upload` directly
for the file body and do not teach a raw PUT. The shared data-URI transport cap
is 256 KiB encoded; use the bundled upload helper or an HTTPS URL for a larger
otherwise-valid sample.

The three exact `MediaInputRef` shapes are:

```json
{"type":"artifact","artifact_id":"artifact_..."}
{"type":"data_uri","data":"data:audio/...;base64,..."}
{"type":"url","url":"https://example.com/authorized-sample.wav"}
```

## Freeze one clone card

Call `beatra.models.list` for `voice_clone`. The card must include the exact
sample reference and observed readiness, display name, optional BCP-47 language
hint, model decision, current task price, `meter: "task"`, `unit: "task"`,
the live `unit_price_credits`, `scale: 1`,
`billing_basis: "successful_activated_voice"`, quantity 1, the live estimate
formula, one paid clone, and the review plan.

`language` is only a clone hint; the clone card does not publish supported
languages. Do not claim language coverage from it. Resolve `auto` according to
the current card. If it is deterministic, quote the one current default price;
otherwise preserve every possible live price as a range.

Before creating `client_request_id` or submitting `beatra.voices.clone`, show
the clone admission card: route `voice_clone`, tool `beatra.voices.clone`,
live `beatra.models.list` price for one successful activated voice, the
provisional estimate, the fact that the 600-credit signup gift usually cannot
start this clone, and what happens if the balance is short. Do not create
`client_request_id` or submit until the user confirms they have topped up or
already have enough credits for this estimate. Do not offer a free clone or a
free sample that replaces the clone. On `insufficient_balance`, relay the
returned public message, keep the URL exact, and retry the same frozen
`client_request_id` only after the user says they have topped up.

After that top-up or balance confirmation, create one local request identity
and pass:

```json
{
  "sample": {"type":"artifact","artifact_id":"artifact_..."},
  "display_name": "Approved display name",
  "model": "auto",
  "consent_attested": true,
  "client_request_id": "local-opaque-id"
}
```

Include `language` only when the user supplied the hint. Send the object on
standard input to:

```text
python3 scripts/mcp_client.py call beatra.voices.clone
```

Submit once and poll only its returned task.

## Preserve the exact clone result

Successful `VoiceCloneOutput` has exactly:

```json
{
  "type": "voice_clone",
  "voice_id": "voice_...",
  "status": "ready",
  "display_name": "Approved display name"
}
```

Lead with the named ready voice and preserve the opaque `voice_id` for later
selection. Do not invent similarity, language coverage, sample retention,
deletion, duration, artifact, or client-request fields that the output does
not contain.

## Make a spoken proof only by separate choice

Clone approval does not authorize speech synthesis. If the user requests a
proof after success:

1. Call `beatra.voices.list` and locate the exact returned `voice_id`.
2. Use its current `compatible_models` and the live text-to-speech cards.
3. If a language was supplied, validate both the voice and every possible auto
   model; use auto only when all candidates support it.
4. Use text the user supplied or approved, count weighted characters, and
   prepare a separate synthesis card and estimate.
5. Obtain separate approval, create a new request identity, submit
   `beatra.speech.synthesize` once, and review the actual returned audio.

## Recover without duplicating either paid step

Record clone and optional proof as distinct ledger rows. If a clone task ID is
lost, paginate `beatra.tasks.list` with `capability: "voice_clone"` through
every relevant `next_cursor` and verify candidates with `beatra.tasks.get`.
The request identity remains local-only. Replay the same identity only when
creation remains genuinely uncertain and the JSON is field-for-field
identical.

For a lost proof, use the same process with
`capability: "text_to_speech"`. A new display name, sample, text, voice, model,
language, or control is changed paid work requiring a new card, approval, and
identity. Never automatically retry a failed or canceled task, and never
replace missing charged, refunded, or net values with zero.
````

## File: skills/voiceover-narration-studio/install-source.json
size: 39
sha: 71bdc5669a3b743f3a99e0c93243eefebd355e06
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/install-source.json

```text
{"schema_version":1,"source":"github"}
```

## File: skills/voiceover-narration-studio/manifest.json
size: 4879
sha: 82ac383408ff4b9ab9ead9756eb1c302abe0d3bb
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/skills/voiceover-narration-studio/manifest.json

```text
{
  "changelog": "The helper shared by all Beatra Skills now runs its connection step at most twice in one turn. While your approval is still pending, the Skill delivers the part of your request that needs no connection and continues the paid step on your next message after Allow.",
  "display_name": "Voiceover & Narration Studio",
  "entry": "SKILL.md",
  "homepage": "https://beatra.ai/skills/voiceover-narration-studio",
  "icon": "https://beatra.ai/textures/icons/voiceover-narration-studio-0.2.8.png",
  "localized": {
    "en": {
      "description": "Use Voiceover & Narration Studio as an AI voice generator, text-to-speech workspace, and AI voiceover generator. Choose from the current voice library, turn scripts into ready-to-edit AI narration and voiceover, or create and reuse a custom brand voice through voice cloning. It supports short-video voiceover, script-to-voiceover, course narration, ordered audiobook narration, supplied multilingual text to speech, Cantonese text to speech, and recurring brand audio, with current price estimates, clear output planning, and delivery organized by chapter, language, and use case.",
      "display_name": "Voiceover & Narration Studio",
      "summary": "Choose or reuse a voice, turn scripts into ready-to-edit narration, plan ordered long-form or multilingual audio, and create a reusable custom voice.",
      "tags": [
        "AI voice studio",
        "AI voice generator",
        "text to speech",
        "AI voiceover generator",
        "AI narration",
        "voice cloning",
        "multilingual text to speech",
        "voice library",
        "script to voiceover",
        "short video voiceover",
        "audiobook narration",
        "course narration",
        "Cantonese text to speech",
        "reusable brand voice"
      ],
      "title": "Voiceover & Narration Studio – Text to Speech and Voice Cloning"
    },
    "ja": {
      "description": "ナレーション制作スタジオは、AI音声生成、テキスト読み上げ、AIナレーション、音声クローンを一つにまとめた制作入口です。現在の音声ライブラリから用途に合う声を選び、原稿から動画ナレーション、教材ナレーション、順番を保ったオーディオブック朗読、外国語ナレーション、多言語音声、広東語の読み上げを制作。カスタム音声やブランド音声も再利用でき、出力計画を確認しながら、用途・言語・章ごとに整理された音声を受け取れます。",
      "display_name": "ナレーション制作スタジオ",
      "summary": "用途に合う声を選び、原稿から編集に使えるナレーションを作成。長編や多言語の読み上げ、カスタム音声の作成と再利用も一つの入口で進めます。",
      "tags": [
        "AI音声スタジオ",
        "AIナレーション",
        "テキスト読み上げ",
        "AI音声合成",
        "ナレーション作成",
        "多言語音声",
        "音声クローン",
        "音声作成",
        "動画ナレーション",
        "オーディオブック朗読",
        "教材ナレーション",
        "読み上げ音声",
        "外国語ナレーション"
      ],
      "title": "ナレーション制作スタジオ：テキスト読み上げ・多言語対応・音声クローン"
    },
    "zh": {
      "description": "用配音与旁白工作室统筹 AI配音、文字转语音和 AI语音生成：从当前声音库选择合适音色，把文稿制作成可直接剪辑的语音合成音频，或通过声音克隆创建并复用品牌专属声音。适合文稿转语音、短视频配音、有声书旁白、课程配音、多语言配音和粤语配音，也可作为 AI语音生成器与品牌声音工作台；长篇内容按章节、多语言内容按语言清晰交付。",
      "display_name": "配音与旁白工作室",
      "summary": "从同一个入口选声音、把文稿做成可剪辑配音、安排长篇或多语言音频，并创建可复用的品牌音色。",
      "tags": [
        "AI语音工作室",
        "AI配音",
        "文字转语音",
        "AI语音生成器",
        "语音合成",
        "声音克隆",
        "多语言配音",
        "声音库",
        "短视频配音",
        "有声书旁白",
        "课程配音",
        "粤语配音",
        "品牌声音",
        "文稿转语音"
      ],
      "title": "配音与旁白工作室：AI配音、文字转语音与声音克隆"
    }
  },
  "mcp": {
    "authentication": "device-bearer",
    "credential_file": "~/.beatra/credentials.json",
    "name": "beatra",
    "transport": "streamable-http",
    "url": "https://mcp.beatra.ai/mcp"
  },
  "name": "voiceover-narration-studio",
  "schema_version": 1,
  "version": "0.2.9"
}
```

## File: README.md
upstream: https://github.com/beatra-ai/ai-voiceover-generator-skill/blob/main/README.md

````text
# AI Voiceover Generator Skill

English | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md)

> 👉 See also [AI Voice Cloning Skill](https://github.com/beatra-ai/ai-voice-cloning-skill)

Pick or reuse a voice, turn scripts into narration ready for editing, plan ordered long-form or multilingual audio, and create a reusable custom voice, from inside Claude Code, Cursor, Codex, Windsurf, Cline, or OpenClaw.

> [!IMPORTANT]
> Rendering needs a [Beatra](https://beatra.ai?utm_source=github&utm_medium=flagship&utm_campaign=ai-voiceover-generator-skill) account and uses credits. The skill itself is free to install.

| Question | Answer |
| --- | --- |
| **What it does** | Choose or reuse a voice, turn scripts into ready-to-edit narration, plan ordered long-form or multilingual audio, and create a reusable custom voice. |
| **Models** | MiniMax Voice Clone, MiniMax Speech 2.8 HD, MiniMax Speech 2.8 Turbo |
| **Requirements** | Python 3.10+ and an agent that loads `SKILL.md` |
| **Cost** | Free to install. Each render uses credits on your Beatra account, and paid steps run only when you ask for that exact render or approve its card. |
| **Works with** | Claude Code, Cursor, Codex, Windsurf, Cline, OpenClaw |

<p align="center"><img src="assets/cover.webp" width="800" alt="A narration studio cover with a microphone and three colored waveforms, one for each voice in the sample. AI-generated with Beatra."></p>

*A narration studio cover with a microphone and three colored waveforms, one for each voice in the sample. AI-generated with Beatra.*

| Skill | Entry point | Version |
| --- | --- | --- |
| [`voiceover-narration-studio`](skills/voiceover-narration-studio) | [SKILL.md](skills/voiceover-narration-studio/SKILL.md) | 0.2.9 |

This repository is published automatically from [beatra-ai/beatra-skills](https://github.com/beatra-ai/beatra-skills/tree/main/skills/voiceover-narration-studio). Report issues there.

## Install

With the [`skills`](https://skills.sh) CLI:

```bash
npx skills add beatra-ai/ai-voiceover-generator-skill
```

As a Claude Code plugin:

```bash
claude plugin marketplace add beatra-ai/ai-voiceover-generator-skill
claude plugin install voiceover-narration-studio@ai-voiceover-generator-skill
```

As a Codex plugin:

```bash
codex plugin marketplace add beatra-ai/ai-voiceover-generator-skill
codex plugin add voiceover-narration-studio@ai-voiceover-generator-skill
```

As a GitHub Copilot CLI plugin:

```bash
copilot plugin marketplace add beatra-ai/ai-voiceover-generator-skill
copilot plugin install voiceover-narration-studio@ai-voiceover-generator-skill
```

As a Gemini CLI extension:

```bash
gemini extensions install https://github.com/beatra-ai/ai-voiceover-generator-skill
```

With the GitHub CLI:

```bash
gh skill install beatra-ai/ai-voiceover-generator-skill voiceover-narration-studio
```

Or clone this repository and copy `skills/voiceover-narration-studio` into `~/.claude/skills/` for Claude Code,
`~/.cursor/skills/` for Cursor, `~/.agents/skills/` for Codex, `~/.codeium/windsurf/skills/` for Windsurf,
`~/.cline/skills/` for Cline, or `~/.openclaw/skills/` for OpenClaw.

Or paste this into your agent:

```text
Install the voiceover-narration-studio skill from https://github.com/beatra-ai/ai-voiceover-generator-skill (folder skills/voiceover-narration-studio), then follow its SKILL.md to connect my Beatra account.
```

## Examples

<p align="center"><img src="assets/cover.webp" width="800" alt="A narration studio cover with a microphone and three colored waveforms, one for each voice in the sample. AI-generated with Beatra."></p>

[▶ Listen (MP3)](assets/sample.mp3)

*Three short reads in three preset voices, in order: a calm product explainer, an upbeat short-video ad, and a documentary line, 41 seconds in total. AI-generated with Beatra.*

Ask your agent:

```text
Read these three short scripts in three different preset voices: a calm product explainer for the Tidewell desk lamp, an upbeat short-video ad for Crunchlane chips, and one documentary line. Deliver the three reads in that order.
```

Prompt:

```text
Meet Tidewell, a desk lamp that follows your day. In the morning, it glows cool and bright for focus. After sunset, it turns warmer, so your eyes can rest.

Okay, stop scrolling! Crunchlane spicy mango chips are here: sweet, hot, and seriously crunchy. Grab a bag this Saturday, and tell us how fast it disappeared!

High in the northern mountains, the first snow falls without a sound. A red fox stops and listens. Something small is moving beneath the snow. She waits. Then she leaps.
```

## Technical Specifications & Output Limits

As of 2026-10-18. Live list: https://docs.beatra.ai/en/docs/models

- **Uploads** — up to 100 MB per file; the per-model input limits below also apply
- **Voice cloning** — 1 model (MiniMax Voice Clone); sample 10–300 s, up to 20 MB; consent attestation required
- **Text to speech** — 2 models (MiniMax Speech 2.8 HD, MiniMax Speech 2.8 Turbo); FLAC, MP3, OPUS, PCM, WAV; 40 languages; sample rates 8, 16, 22.05, 24, 32, 44.1 kHz; up to 50000 characters per request
  - Languages (ISO 639 codes): zh, yue, en, es, fr, ru, de, pt, ar, it, ja, ko, id, vi, tr, nl, uk, th, pl, ro, el, cs, fi, hi, bg, da, he, ms, fa, sk, sv, hr, fil, hu, no, sl, ca, nn, ta, af

## Why use Beatra instead of calling model APIs directly

- **One account for every model this skill uses** — Voice cloning 1; Text to speech 2 (MiniMax Voice Clone, MiniMax Speech 2.8 HD, MiniMax Speech 2.8 Turbo) (as of 2026-10-18), behind one Beatra sign-in; no keys from individual model providers to manage.
- **You decide what gets paid for** — Paid renders run only when you ask for that exact render or approve its card, which shows the live credit estimate.
- **Direction before rendering** — the skill plans, directs, and reviews the work, then renders it on Beatra.
- **Files you can verify** — every file under `skills/` matches the release archive on cdn.beatra.ai byte for byte, checked by SHA-256 before each push.
- **Updates you control** — no self-updater; update with the tool you installed it with.
- **Runs in your agent** — Claude Code, Cursor, Codex, Windsurf, Cline, OpenClaw.

## How it compares

| | Beatra (this skill) | [scenario-labs/skills](https://github.com/scenario-labs/skills) | [vargHQ/skills](https://github.com/vargHQ/skills) |
| --- | --- | --- | --- |
| Account or key needed | A Beatra account; no model provider keys | A Scenario account (OAuth) or a Scenario API key ([source](https://github.com/scenario-labs/skills/blob/49dd26e47371163ae0020c7f5627d0993626bfc2/README.md#L105)) | A VARG_API_KEY, required for all skills ([source](https://github.com/vargHQ/skills/blob/862c0297ba2063b10eeb5d4d1058b0f90dc9f92d/README.md#L70)) |
| Models | Voice cloning 1; Text to speech 2 (MiniMax Voice Clone, MiniMax Speech 2.8 HD, MiniMax Speech 2.8 Turbo) | Voice and speech generation through its scenario-audio skill ([source](https://github.com/scenario-labs/skills/blob/49dd26e47371163ae0020c7f5627d0993626bfc2/README.md#L175)) | Kling, Flux, ElevenLabs, Sora and more through the varg platform ([source](https://github.com/vargHQ/skills/blob/862c0297ba2063b10eeb5d4d1058b0f90dc9f92d/README.md#L3)) |
| Before spending | Paid renders run only when you ask for that exact render or approve its card, which shows the live credit estimate | Dry-run price estimates in its quality-gate and model-comparison skills ([source](https://github.com/scenario-labs/skills/blob/49dd26e47371163ae0020c7f5627d0993626bfc2/README.md#L208-L210)) | A free `--preview` render before the full render ([source](https://github.com/vargHQ/skills/blob/862c0297ba2063b10eeb5d4d1058b0f90dc9f92d/README.md#L158-L160)) |
| Agents | Claude Code, Cursor, Codex, Windsurf, Cline, OpenClaw | Claude Code, Cursor, Codex, Copilot and 70+ others ([source](https://github.com/scenario-labs/skills/blob/49dd26e47371163ae0020c7f5627d0993626bfc2/README.md#L30)) | Claude Code, Cursor, Windsurf, OpenCode, 
````
