# product-photo-studio

Adopt this skill: https://proskills.md/skills/other/beatra-ai-ai-product-photography-skill-skills-product-photo-studio

Page: https://proskills.md/skills/other/beatra-ai-ai-product-photography-skill-skills-product-photo-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/product-photo-studio/SKILL.md
size: 9114
sha: bf64bf0c9ade5a850b6824232709c3fbe12173d2
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-studio/SKILL.md

```text
---
name: "product-photo-studio"
description: "Transform a real product photo into a studio-quality ecommerce image, lifestyle scene, or marketplace-ready hero shot. This AI product photography tool replaces backgrounds, improves lighting, and stages scenes while using the source photo and confirmed product details as the visual anchor. Create clean white-background listings, contextual lifestyle compositions, and premium ad visuals from a single phone snap for Amazon, Taobao, Shopify, and social media. Start from one product photo, combine several references, or refine a selected draft toward a polished listing image."
---

# AI Product Photo Studio

Transform one real product photo into a studio-quality listing image, lifestyle
scene, or marketplace-ready hero shot. Reuse decisions already present in the
conversation and move by the shortest route that completes the requested image.

## Choose the route

- **Clean background:** with one product photo, remove the original background
  and place the product on a clean white, light-gray, or studio-gradient
  background using `beatra.images.transform`. This is the default for
  marketplace main images.
- **Scene and lifestyle:** with one product photo and a scene description,
  place the product in a contextual lifestyle setting—on a kitchen counter, a
  wooden table, a marble shelf, or a seasonal backdrop—using
  `beatra.images.transform`.
- **Refine an accepted draft:** use `beatra.images.edit` with the accepted
  image as `images[0]` to fix a shadow, remove a reflection, adjust color
  temperature, or clean up a small defect without changing the composition.

Follow [product routing](references/product-routing.md) for the precise branch
and [scene craft](references/scene-craft.md) when turning the request into a
visual specification anchored to the source product.

## Shape one product brief

The hard input is a real product photo. Scene and lighting must not rewrite a
confirmed color, accessory, or quantity.

Reuse the user's product type, intended marketplace, background preference,
and any style references. Ask only when a missing decision materially changes
the result. For a standard marketplace main image, propose a clean white
background as the default; for a lifestyle request, propose a scene that
matches the product's category.

Build the brief around:

- the product itself—what it is, its category, and any key visual details
  (label text, brand logo, shape, color);
- one target marketplace or use (Amazon, Taobao, Shopify, social media, ad
  campaign) when it determines format rules;
- one background or scene direction (clean white, studio gradient, lifestyle
  context, seasonal);
- ordered visual references when available (style inspiration, background
  reference, angle reference).

If the user has already stated the target marketplace or background type, reuse
it. If that choice is genuinely missing, propose the best default and include
it in the single paid-call confirmation.

## Prepare the call

Use only this Skill's bundled `scripts/mcp_client.py` for every remote MCP
operation. The tool name is a CLI argument and the tool arguments are the JSON
sent on stdin. Do not configure or call a host Beatra Connector, and do not use
REST/OpenAPI as a fallback. For exact commands and troubleshooting, use
[Bundled MCP Client diagnostics](references/mcp-connection.md).

- Upload the product photo through the bundled client helpers first, then
  call `beatra.images.transform` with the uploaded artifact as the first
  ordered reference. Label the product image's role explicitly in the prompt
  so the model treats it as the visual anchor.
- For a clean background, set an explicit square or marketplace-ratio canvas.
- For a lifestyle scene, describe the scene, lighting direction, and surface
  material in the prompt while identifying the source product details that
  should carry into the result.
- For an accepted draft, call `beatra.images.edit`. Use at most two normalized
  `edit_regions` on `image_index=0` for localized fixes; omit regions for a
  whole-image adjustment.

Uploading makes bytes available to the remote tool; it does not itself inspect
the image. Review only visual facts the host can actually see.

Keep `model=auto` and `count=1` unless the user explicitly chooses otherwise.
Call `beatra.models.list` only for a real model, availability, compatibility,
or price decision. The detailed request shapes and examples are in
[workflow](references/workflow.md).

## Confirm and execute once

Planning and brief preparation are free. Before the paid image call, show and
freeze the final prompt, ordered references, canvas, background or scene
direction, model, controls, and output count. Merge any still-material
high-impact choice into this one confirmation.

After approval, create one stable opaque `client_request_id` for that exact
logical request and submit it once. A changed prompt, reference or order,
canvas, scene direction, model, count, or control is new paid work and needs a
new confirmation and a new ID.

## Track, review, and deliver

After receiving a `task_id`, poll only that task with `beatra.tasks.get`. If the
ID is lost, use `beatra.tasks.list` to find candidates and verify the selected
one with `tasks.get`. Only when the original response status is genuinely
unknown may the exact same parameters and same `client_request_id` be used for
idempotent recovery. Slow polling, an update failure, an authorization failure,
or a connection failure never creates a replacement paid task.

Use `beatra.tasks.cancel` only when the user asks. If cancellation returns
`409`, continue tracking the original task. See [review and
recovery](references/review-and-recovery.md) for the full recovery contract.

When the result is visible, review product fidelity against the source photo,
background quality (clean edges, consistent
lighting, natural shadow), color accuracy (do product colors match the
original?), canvas fit, and the marketplace's current image guidance if
applicable. Deliver the
artifact links, observed dimensions, task ID, and
`billing.net_charged_credits`. Offer at most one focused, unexecuted revision.
Generated assets can also be viewed and managed at
[beatra.ai](https://beatra.ai).

If the tool returns insufficient credits: give the user the tool message in
full. Do not change the URL. You may translate the other words. Do not replace
it with your own top-up copy. Point the user to pay using that message.

When the user asks how many credits remain, 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.

## References by task

- Choosing among clean background, lifestyle scene, and detail edit, or
  planning for a specific marketplace: [product routing](references/product-routing.md)
- Turning a request into a scene specification anchored to the source product:
  [scene craft](references/scene-craft.md)
- Exact request shapes, ordered-reference labeling, and JSON examples for each
  route: [workflow](references/workflow.md)
- Lost task, slow task, cancellation, result review, or planning a revision:
  [review and recovery](references/review-and-recovery.md)
- First install or expired authorization:
  [installation and authentication](references/installation-and-auth.md)
- Bundled MCP Client commands and diagnostics:
  [Bundled MCP Client diagnostics](references/mcp-connection.md)
- Installation registration: [installation registration](references/installation-registration.md)
- Task lookup, polling, and result fields: [tasks and results](references/tasks-and-results.md)
- Balance, validation, and structured errors:
  [billing, errors, and recovery](references/billing-errors-and-recovery.md)
- Disconnecting the installation: [uninstall and disconnect](references/uninstall-and-disconnect.md)
- Updates and security: [updates and security](references/updates-and-security.md)

## Installation and account operations

For first use and shared operations, follow [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).

## 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/product-photo-studio/scripts/authorize.py
size: 62571
sha: 72b2c4ca652d9bcab9ca1c13f553e632f738f22b
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-studio/scripts/authorize.py

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

## File: skills/product-photo-studio/scripts/mcp_client.py
size: 88485
sha: ce9e04eb42b32b6d678ace1996d91a40594c6d45
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-studio/scripts/mcp_client.py

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

## File: skills/product-photo-studio/scripts/uninstall.py
size: 12907
sha: 97f477fade16789dc42619ef9433c12147098ea7
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-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 = "product-photo-studio"
PACKAGE_DISPLAY_NAME = "AI Product Photo Studio"
PACKAGE_VERSION = "0.3.1"
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/product-photo-studio/references/billing-errors-and-recovery.md
size: 4900
sha: 4492c70c65d8053a86491da13f49d164470e5985
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-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/product-photo-studio/references/installation-and-auth.md
size: 9544
sha: 149c77b01f1ad83a83c774ade0cb2a795288422c
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-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/product-photo-studio/references/installation-registration.md
size: 2087
sha: 7da7c1c9b57c161af2500af8f037f7a7c5166293
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-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/product-photo-studio/references/mcp-connection.md
size: 3348
sha: 9365c7be5051522eadf734b5cb971d10a168b220
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-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/product-photo-studio/references/product-routing.md
size: 3987
sha: 9fd3e689139e87ddec64ac2ec18f4f8757b18c74
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-studio/references/product-routing.md

```text
# Product routing

## Start from the seller's actual starting point

A product photo request arrives in one of three shapes. Identify which one
before choosing a tool.

| Starting point | What is enough to start | Route |
| --- | --- | --- |
| One product photo + background or scene goal | The product image plus one target look | `beatra.images.transform` |
| One product photo + multiple style references | Ordered references (product first, style/background after) | `beatra.images.transform` |
| Accepted draft + specific fix | The draft plus the requested change | `beatra.images.edit` |

A single product photo with no explicit background preference is enough to
propose a clean white background and include it in the single paid-call
confirmation.

## Extract the product brief

Collect what is already known and fill gaps only when they materially change
the result.

- **Product identity.** What the product is, its category, and any key visual
  details (brand label, shape, texture, color, packaging). Treat the source
  photo and confirmed details as the visual reference, then review the result
  for fidelity.
- **Target marketplace.** Amazon, Taobao, Shopify, Rakuten, social media, or ad
  campaign—when it determines format or background rules.
- **Background or scene direction.** Clean white, light-gray gradient, studio
  sweep, lifestyle context (kitchen, bathroom, office, outdoor), seasonal
  (holiday, summer).
- **Visual references.** Style inspiration, desired background, angle
  reference, or a competitor listing to match—ordered with the product photo
  first.

## Marketplace image guidance

Marketplace requirements change. When a named platform determines the output,
ask for or consult its current image guidance before the paid call. Use these
common patterns only as starting points:

- **Amazon main image** — typically a pure-white background with the product
  prominent and without added text or watermarks.
- **Taobao main image** — often a clean light background and square composition.
- **Shopify product image** — consistent background across the catalog,
  square or 4:3 ratio preferred.
- **Social media** — lifestyle or contextual scene with the product as hero.
- **Ad campaign** — dramatic lighting, premium composition, may include text
  overlay (added downstream, not in the generated image).

When the user names a marketplace, apply the current supplied guidance to the
prompt. When absent, propose clean white as the default.

## Product category scene suggestions

When the user requests a lifestyle scene but does not specify the setting,
infer from the product category:

- **Food and beverage** — wooden table, marble countertop, restaurant setting,
  natural light.
- **Cosmetics and skincare** — bathroom shelf, vanity table, silk backdrop,
  soft diffused light.
- **Electronics and tech** — desk surface, modern interior, dark dramatic
  background, cool blue accent light.
- **Fashion and accessories** — model or mannequin context, fabric backdrop,
  neutral studio.
- **Home and living** — styled room interior, bookshelf, windowsill, cozy
  warm light.
- **Baby and kids** — nursery, soft pastel background, playful props.

These are starting points, not constraints. Override with the user's stated
preference whenever available.

## Defaults that avoid unnecessary questions

- Canvas: square `1:1` for marketplace main images; `4:3` or `16:9` when the
  user specifies a banner or hero format.
- Count: 1 (product photography requires precision, not exploration).
- Model: `auto`.
- Background: clean white when no preference is stated.

## Visual access

Local files enter the workflow through `beatra.assets.upload`. Upload makes the
bytes available to the remote tool; it does not itself inspect the image.
Review only visual facts the host can actually see. When the host cannot view an
image, state that visual verification was not possible and proceed on the
user's declared intent.
```

## File: skills/product-photo-studio/references/review-and-recovery.md
size: 3815
sha: 41559738962c86dc3eab6f3e6133d8a79304ed04
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-studio/references/review-and-recovery.md

```text
# Review and recovery

## Before the paid boundary

Present one final direction. Freeze the prompt, ordered references, canvas,
background or scene direction, count, model, and controls in a single
confirmation. After approval, create one stable opaque `client_request_id` for
that exact logical request and submit once. Any change—prompt, reference or
order, canvas, scene direction, model, count, or control—is new paid work with
a new confirmation and a new ID.

## Monitor and recover

After receiving a `task_id`, poll only that task with `beatra.tasks.get`. Use
bounded backoff: first poll around 2 seconds, increasing toward 5 and 10
seconds, maximum 15 seconds. Honor `deadline_at`. Stop active polling after 30
minutes and report the task ID and status.

- **Lost create response:** if the response to the original submission is
  genuinely unknown, retry the exact same parameters with the same
  `client_request_id`.
- **Lost task ID:** call `beatra.tasks.list` to find candidates, then verify
  the selected one with `beatra.tasks.get`. Match by capability, input, and
  timing. If the match is ambiguous, do not submit a replacement.
- **Slow task:** a queued or running task never authorizes a replacement.
- **Authorization failure:** run `python3 scripts/authorize.py` and follow its output until it prints `Beatra is ready`.
  The original `client_request_id` remains valid for idempotent recovery.
- **Connection failure:** preserve the credential and the original
  `client_request_id`. Retry only the exact same parameters.

Use `beatra.tasks.cancel` only when the user asks. If cancellation returns
`409`, the task continues. See the shared
[billing, errors, and recovery](billing-errors-and-recovery.md) for the full
structured-failure reference.

## Review only what is visible

When the result is visible, review in this order:

1. **Product fidelity** — is the product itself recognizable, correctly shaped,
   and undistorted? Compare against the original product photo.
2. **Color accuracy** — do the product's colors match the original? Look for
   color casts introduced by the scene lighting.
3. **Edge quality** — are the product's edges clean where they meet the
   background? No halo, fringe, or ghosting.
4. **Shadow and grounding** — does the product cast a natural contact shadow?
   A floating product looks artificial.
5. **Lighting consistency** — does the light direction on the product match
   the scene's implied light source?
6. **Marketplace guidance** — if targeting a specific marketplace, compare the
   image with the platform's current background, size, and framing guidance.

If the host cannot view the artifact, state that visual verification was not
possible and deliver the result links for the user to review.

## Deliver

Present the returned image. Include the artifact links, observed dimensions,
task ID, and `billing.net_charged_credits`. Do not infer credits from elapsed
time.

Offer at most one focused, unexecuted revision. When the user accepts a result
but wants a specific fix, describe the revision as a new `beatra.images.edit`
request with the accepted image as `images[0]`.

## Product image variant set

A complete ecommerce listing typically uses several image variants:

- **main image** — clean white background, product centered (marketplace
  requirement);
- **lifestyle scene** — product in a real-world context;
- **detail or infographic** — close-up of a feature or annotated callouts
  (text added downstream);
- **scale or dimension** — product next to a common object for size reference;
- **seasonal or campaign** — holiday or promotion-themed background.

Each variant is a distinct paid request with its own confirmation and
`client_request_id`. Review each result independently for product fidelity.
```

## File: skills/product-photo-studio/references/scene-craft.md
size: 3290
sha: 876e4f928e98832b461c849bd3c030cb4bbd8c40
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-studio/references/scene-craft.md

```text
# Scene craft

## Anchor the result to the product

The product is the subject. Build the prompt around the source photo and
confirmed product details, then verify fidelity in the result:

- state explicitly that Image 1 is the product reference and ask the model to
  match its shape, color, label, and proportions closely;
- describe only the background, lighting, and scene around it;
- do not request changes to the product itself in a transform call—use
  `beatra.images.edit` for localized product fixes.

## Background quality

A professional product background has three properties:

- **clean edges** — the product's outline must have no halo, fringe, or
  ghosting where it meets the background;
- **consistent lighting** — the light direction on the product must match the
  light direction implied by the background (shadows fall the same way);
- **natural shadow** — the product should cast a subtle contact shadow on the
  surface beneath it; a floating product with no shadow looks artificial.

Include these constraints in the prompt: "clean product edges, consistent
lighting direction, natural contact shadow, no halo or fringe."

## Clean white background

For marketplace main images requiring pure white:

- state "pure white background, RGB 255 255 255" explicitly;
- request even, shadowless lighting for the background while keeping a subtle
  contact shadow under the product for grounding;
- avoid gradients, textures, or patterns unless the user explicitly requests
  them.

## Lifestyle scene composition

For lifestyle and contextual scenes:

- match the surface material to the product category (wood for food, marble
  for cosmetics, metal for tech);
- keep the product as the focal point—larger and more prominent than any prop
  or background element;
- use depth of field to separate the product from the background (sharp
  product, slightly soft background);
- ensure color temperature consistency (warm scene = warm product lighting,
  cool scene = cool product lighting).

## Lighting direction

State the light direction in the prompt to avoid conflicting shadows:

- **front light** — even, flat illumination; good for catalog accuracy;
- **side light** — dimensional, shows texture; good for food and crafts;
- **backlight** — dramatic silhouette rim; good for glass and transparent
  products;
- **top light** — natural overhead; good for flat-lay and top-down shots.

The chosen direction must be consistent between the product lighting and the
scene's implied light source.

## Color accuracy

Use the original photo as the color reference, then review the result for color
fidelity:

- do not describe the product's color in the prompt (let the reference image
  determine it);
- if the original photo has a color cast, note it and advise the user that
  color correction may be needed;
- structured `palette` is optional—use it only when the user provides exact
  brand or background color values.

## Canvas and output

- Default canvas for marketplace main images: square `1:1` at `2K` tier.
- For banner or hero images: `16:9` or `4:3` at `2K`.
- For edit: the default is `source`, which inherits the base image's ratio.
- A different ratio is a separate paid request with its own confirmation and
  `client_request_id`.
```

## File: skills/product-photo-studio/references/tasks-and-results.md
size: 2254
sha: 34977869c667cda680bb242c904b15ec2bccde53
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-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/product-photo-studio/references/uninstall-and-disconnect.md
size: 2739
sha: 5435c8ecc18194916070abac32e41f19d9e8f072
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-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/product-photo-studio/references/updates-and-security.md
size: 6077
sha: 5c0172b541897c6c482d32fd2a1ac7765ec83935
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-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/product-photo-studio/references/workflow.md
size: 3548
sha: 025f662a60b99a7fe8c8c0e5d069c74b483be514
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-studio/references/workflow.md

````text
# Workflow

## Build one product image brief

Translate the product type, target marketplace, background or scene direction,
and any style references into a single coherent prompt. Keep the prompt focused
on the background and scene, not on changing the product.

## Prepare the selected route

### Transform to clean background

Use `beatra.images.transform` with the product photo as the first ordered
reference. Label the product's role explicitly.

```json
{
  "prompt": "Professional ecommerce product photo. Image 1 is the product reference; match its shape, color, label, and proportions closely. Place it on a pure white background, with even studio lighting, a subtle contact shadow, and clean edges with no halo.",
  "model": "auto",
  "count": 1,
  "canvas": {
    "type": "preset",
    "tier": "2K",
    "aspect": "1:1"
  },
  "images": [
    {"type": "artifact", "artifact_id": "<product-photo-artifact-id>"}
  ],
  "client_request_id": "product-clean-bg-001"
}
```

### Transform to lifestyle scene

Use `beatra.images.transform` with the product photo first and optional style
or background references after. Label each image's role.

```json
{
  "prompt": "Professional lifestyle product photo. Image 1 is the product reference; match its shape, color, label, and proportions closely. Image 2 guides only the desired scene style and color palette. Place the product on a warm wooden kitchen counter, soft morning window light from the left, shallow depth of field, natural contact shadow.",
  "model": "auto",
  "count": 1,
  "canvas": {
    "type": "preset",
    "tier": "2K",
    "aspect": "1:1"
  },
  "images": [
    {"type": "artifact", "artifact_id": "<product-photo-artifact-id>"},
    {"type": "artifact", "artifact_id": "<scene-style-artifact-id>"}
  ],
  "client_request_id": "product-lifestyle-001"
}
```

When more than one reference is supplied, their order matters. The product
photo should always be `images[0]`. Later images guide style, background, or
angle only. The last image anchors the output ratio when canvas aspect is
`source`.

### Edit an accepted draft

Use `beatra.images.edit` with the accepted image as `images[0]`. Use at most
two normalized `edit_regions` for targeted fixes; omit regions for a
whole-image adjustment.

```json
{
  "prompt": "Remove the small reflection on the lower left of the product bottle. Keep everything else unchanged.",
  "model": "auto",
  "count": 1,
  "images": [
    {"type": "artifact", "artifact_id": "<accepted-draft-artifact-id>"}
  ],
  "edit_regions": [
    {
      "image_index": 0,
      "x": 0.1,
      "y": 0.6,
      "width": 0.25,
      "height": 0.2
    }
  ],
  "client_request_id": "product-fix-reflection-001"
}
```

## Apply model controls

Keep `model=auto` unless the user explicitly requests a concrete model. Keep
`count=1`—product photography requires precision, not variation. Call
`beatra.models.list` with the relevant capability (`image_to_image` or
`image_edit`) only when the user asks about model availability, compatibility,
or price.

## Confirm, submit once, and monitor

Present one final confirmation card containing the complete prompt, ordered
references, canvas, background or scene direction, count, and model. After
approval, create one stable opaque `client_request_id` and submit once. Record
the returned `task_id` and poll with `beatra.tasks.get`.

A changed prompt, reference set or order, canvas, scene direction, model,
count, or control value is new paid work requiring a new confirmation and a new
`client_request_id`.
````

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

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

## File: skills/product-photo-studio/manifest.json
size: 5256
sha: 5d8fa80676b696ab0e4f81adcc6946f061f07a7a
upstream: https://github.com/beatra-ai/ai-product-photography-skill/blob/main/skills/product-photo-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": "AI Product Photo Studio",
  "entry": "SKILL.md",
  "homepage": "https://beatra.ai/skills/product-photo-studio",
  "icon": "https://beatra.ai/textures/icons/product-photo-studio-0.2.9.png",
  "localized": {
    "en": {
      "description": "Transform a real product photo into a studio-quality ecommerce image, lifestyle scene, or marketplace-ready hero shot. This AI product photography tool replaces backgrounds, improves lighting, and stages scenes while using the source photo and confirmed product details as the visual anchor. Create clean white-background listings, contextual lifestyle compositions, and premium ad visuals from a single phone snap for Amazon, Taobao, Shopify, and social media. Start from one product photo, combine several references, or refine a selected draft toward a polished listing image.",
      "display_name": "AI Product Photo Studio",
      "summary": "Transform a real product photo into a studio-quality ecommerce image, lifestyle scene, or marketplace-ready hero shot with clean backgrounds, professional lighting, and composition guided by confirmed product details.",
      "tags": [
        "AI product photography",
        "product photo editor",
        "product image background",
        "ecommerce product photo",
        "product photo background remover",
        "AI product shoot",
        "white background product photo",
        "lifestyle product photography",
        "product hero image",
        "amazon product image",
        "product photo studio",
        "product listing photo"
      ],
      "title": "AI Product Photo Studio: Ecommerce Product Photography & Background Remover"
    },
    "ja": {
      "description": "実際の製品写真をスタジオ品質のEC画像、ライフスタイルシーン、または販売プラットフォーム向けのヒーローショットに変換します。このAI商品写真ツールは、元写真と確認済みの商品外観を基準に、背景の置換、照明の改善、シーンの演出を行います。スマホで撮った1枚から、白背景のメイン画像、ライフスタイルシーン、上質な広告ビジュアルを作成し、Amazon、楽天、Shopify、SNS向けの商品画像に活用できます。1枚の製品写真、複数の参考画像、または選んだドラフトから、目的に合う商品画像へ仕上げます。",
      "display_name": "AI 商品写真スタジオ",
      "summary": "実際の製品写真をスタジオ品質のEC画像、ライフスタイルシーン、または販売プラットフォーム向けのヒーローショットに変換。背景置換、照明改善、シーン演出に対応し、確認済みの商品外観を基準に構成します。",
      "tags": [
        "AI商品写真",
        "商品画像作成",
        "背景切り抜き",
        "製品画像編集",
        "商品写真 白背景",
        "楽天 商品画像",
        "EC用画像作成",
        "商品写真 レタッチ",
        "メルカリ 写真",
        "商品ライフスタイル写真",
        "製品ヒーロー画像",
        "商品画像 背景"
      ],
      "title": "AI 商品写真スタジオ：EC製品画像作成と背景切り抜き"
    },
    "zh": {
      "description": "将真实产品照片制作成影棚级电商主图、场景图或面向市场平台的高清商品图。这款 AI 商品图工作室用于商品图片处理和电商图片编辑，可替换背景、改善光效、搭建场景。它以原图中的商品外观特征为依据，从一张手机随拍就能做出白底主图、生活场景图和高级广告图。适用于亚马逊白底产品图、淘宝主图、Shopify 产品大图和社交媒体商品图。从一张产品照片出发，组合多张参考图，或在选定草稿上做定向精修，完成适合上架使用的成品；生活场景图和广告图也能直接用作商品详情页图片。",
      "display_name": "商品图",
      "summary": "将真实产品照片转化为影棚级电商主图、场景图或面向市场平台的高清商品图，支持换背景、改善光效和搭建场景，并延续已确认的商品外观特征。",
      "tags": [
        "AI商品图",
        "产品图制作",
        "商品图片处理",
        "电商产品图",
        "商品白底图",
        "产品主图",
        "淘宝主图制作",
        "商品场景图",
        "产品精修图",
        "商品详情页图片",
        "电商图片编辑",
        "产品拍照",
        "产品图"
      ],
      "title": "产品图工作室：AI 商品图、电商主图与背景替换"
    }
  },
  "mcp": {
    "authentication": "device-bearer",
    "credential_file": "~/.beatra/credentials.json",
    "name": "beatra",
    "transport": "streamable-http",
    "url": "https://mcp.beatra.ai/mcp"
  },
  "name": "product-photo-studio",
  "schema_version": 1,
  "version": "0.3.1"
}
```

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

````text
# AI Product Photography Skill

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

> 👉 See also [Ecommerce Product Images Skill](https://github.com/beatra-ai/ecommerce-product-images-skill)

Turn a real product photo into ecommerce studio photos and lifestyle scenes, 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-product-photography-skill) account and uses credits. The skill itself is free to install.

| Question | Answer |
| --- | --- |
| **What it does** | Transform a real product photo into a studio-quality ecommerce image, lifestyle scene, or marketplace-ready hero shot with clean backgrounds, professional lighting, and composition guided by confirmed product details. |
| **Models** | Qwen Image 2.0 Fast, Qwen Image 3.0, Qwen Image 3.0 Pro, Wan Image 2.7 Fast, Qwen Image 2.0 Pro, Wan Image 2.7 Pro, GPT Image 2.5 Flare, GPT Image 2.5 Sunburst, GPT Image 2, MiniMax Image 01 Live |
| **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/hero.webp" width="800" alt="A cluttered kitchen-counter phone snapshot of a fictional rosehip face oil, OLUNEA (itself AI-generated as the demo input), turned into a white-background studio listing image with the label text and bottle shape unchanged. AI-generated with Beatra."></p>

*A cluttered kitchen-counter phone snapshot of a fictional rosehip face oil, OLUNEA (itself AI-generated as the demo input), turned into a white-background studio listing image with the label text and bottle shape unchanged. AI-generated with Beatra.*

| Skill | Entry point | Version |
| --- | --- | --- |
| [`product-photo-studio`](skills/product-photo-studio) | [SKILL.md](skills/product-photo-studio/SKILL.md) | 0.3.1 |

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

## Install

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

```bash
npx skills add beatra-ai/ai-product-photography-skill
```

As a Claude Code plugin:

```bash
claude plugin marketplace add beatra-ai/ai-product-photography-skill
claude plugin install product-photo-studio@ai-product-photography-skill
```

As a Codex plugin:

```bash
codex plugin marketplace add beatra-ai/ai-product-photography-skill
codex plugin add product-photo-studio@ai-product-photography-skill
```

As a GitHub Copilot CLI plugin:

```bash
copilot plugin marketplace add beatra-ai/ai-product-photography-skill
copilot plugin install product-photo-studio@ai-product-photography-skill
```

As a Gemini CLI extension:

```bash
gemini extensions install https://github.com/beatra-ai/ai-product-photography-skill
```

With the GitHub CLI:

```bash
gh skill install beatra-ai/ai-product-photography-skill product-photo-studio
```

Or clone this repository and copy `skills/product-photo-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 product-photo-studio skill from https://github.com/beatra-ai/ai-product-photography-skill (folder skills/product-photo-studio), then follow its SKILL.md to connect my Beatra account.
```

## Examples

<p align="center"><img src="assets/demo-2.webp" width="800" alt="The same OLUNEA bottle from the phone snapshot placed in a marble bathroom-vanity lifestyle scene with soft window light, label and shape kept as in the source. AI-generated with Beatra."></p>

*The same OLUNEA bottle from the phone snapshot placed in a marble bathroom-vanity lifestyle scene with soft window light, label and shape kept as in the source. AI-generated with Beatra.*

Ask your agent:

```text
Using ./olunea-counter.jpg as the product reference, place the same dropper bottle on a marble bathroom vanity with soft window light for a social post. Keep the label text, shape and color exactly as in the photo.
```

Prompt:

```text
Professional lifestyle product photo for social media. Image 1 is the product reference: the dropper bottle standing on the counter. Match its shape, color, label, label text, and proportions closely, and keep the label text exactly as it appears in Image 1. Leave out the kitchen counter and all other objects from Image 1. Place only the bottle upright on a white marble bathroom vanity, facing the camera, slightly right of center, with a folded linen hand towel and a small sprig of dried rose hips softly out of focus in the background, smaller than the bottle. Soft diffused window light from the left, warm neutral color temperature, gentle highlight on the glass, shallow depth of field with the bottle sharp and the background soft, natural contact shadow falling to the right, clean product edges, consistent lighting direction, no halo or fringe. No added text, logos, or watermarks.
```

## 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
- **Image to image** — 10 models (Qwen Image 2.0 Fast, Qwen Image 3.0, Qwen Image 3.0 Pro, Wan Image 2.7 Fast, Qwen Image 2.0 Pro, Wan Image 2.7 Pro, GPT Image 2.5 Flare, GPT Image 2.5 Sunburst, GPT Image 2, MiniMax Image 01 Live); PNG; up to 4 images per request; canvas up to 16.8 MP
  - 16.8 MP (4K tier): GPT Image 2.5 Flare, GPT Image 2.5 Sunburst, GPT Image 2 (1 image per request)
  - Input images: up to 4 per request, 10–20 MB each; BMP, GIF, JPEG, PNG, TIFF, WEBP
- **Image editing** — 6 models (Qwen Image 2.0 Fast, Qwen Image 3.0, Qwen Image 3.0 Pro, Wan Image 2.7 Fast, Qwen Image 2.0 Pro, Wan Image 2.7 Pro); PNG; up to 4 images per request; canvas up to 4.2 MP
  - 4.2 MP (2K tier): Qwen Image 2.0 Fast, Qwen Image 3.0, Qwen Image 3.0 Pro, Wan Image 2.7 Fast, Qwen Image 2.0 Pro, Wan Image 2.7 Pro (up to 4 images per request)
  - Input images: up to 4 per request, 10–20 MB each; BMP, GIF, JPEG, PNG, TIFF, WEBP

## Why use Beatra instead of calling model APIs directly

- **One account for every model this skill uses** — Image to image 10; Image editing 6 (Qwen Image 2.0 Fast, Qwen Image 3.0, Qwen Image 3.0 Pro, Wan Image 2.7 Fast, Qwen Image 2.0 Pro, Wan Image 2.7 Pro, GPT Image 2.5 Flare, GPT Image 2.5 Sunburst, GPT Image 2, MiniMax Image 01 Live) (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.

## What you get

- **The source product as the visual anchor** — Carry the confirmed shape, color, label, proportions, materials, and included pieces from the source photo into each new background or scene.
- **Marketplace-oriented backgrounds** — Create pure-white Amazon main images, clean Taobao listings, consistent Shopify catalogs, and lifestyle social posts from the same product photo.
- **Scene and lifestyle staging** — Place the product in a contextual scene—kitchen counter, wooden table, marble shelf, or seas
````
