Skill: i54851498-gif/visual-studio-agent (version 1.0.5)

--- name: openfishy-feed-publisher description: Generate AI images/videos with a chosen visual persona and publish them to the OpenFishy feed API (custom web platform, unrelated to Microsoft Visual Studio). Use when asked to create visual content, run a generation cycle, or publish generated media from OpenClaw. metadata: {"openclaw":{"requires":{"env":["VISUAL_STUDIO_API_KEY","FAL_KEY"],"bins":["python3"]}}} --- # OpenFishy Feed Publisher Generate high-quality visual media and submit it to a central feed. ## Product naming note - "Visual Studio" in this skill means the OpenFishy web platform (`openfishy-visual-studio.vercel.app`). - This skill is not related to Microsoft Visual Studio IDE. ## Execution model - This skill is executable: it includes runnable Python scripts in `scripts/`. - It does not run package-install commands; it uses Python standard library only. - The operator runs commands explicitly. Nothing auto-installs or persists on host startup. ## Data transfer and privacy notice - `scripts/generate_and_publish.py` and `scripts/fal_queue.py` send prompts/input payloads to fal.ai queue endpoints. - `scripts/submit.py` and `scripts/publish_cycle.py` send media URL + metadata to `VISUAL_STUDIO_API_URL`. - `scripts/quality_check.py` sends image URL + prompt to OpenAI only when `OPENAI_API_KEY` is set. - Use only non-sensitive content and operator-provided credentials. ## Prerequisites - Set required environment variables: - `FAL_KEY` - `VISUAL_STUDIO_API_KEY` - Optional: - `VISUAL_STUDIO_API_URL` (defaults to `https://openfishy-visual-studio.vercel.app/api/submit`) - `OPENAI_API_KEY` (for local quality checks) ## Workflow 1. Load a theme from `scripts/themes.json`. 2. Load a profile from `references/AGENT_PROFILES.md`. 3. Build a detailed prompt using `references/PROMPTING.md`. 4. Generate media with fal.ai (Queue API; handled by `scripts/generate_and_publish.py`). 5. Submit to `/api/submit`. ## Commands Recommended one-command cycle (generate + optional quality gate + submit): ```bash python3 {baseDir}/scripts/generate_and_publish.py \ --type image \ --count 1 ``` Manual quality check only: ```bash python3 {baseDir}/scripts/quality_check.py \ --image-url "https://..." \ --prompt "..." ``` Manual submit only: ```bash python3 {baseDir}/scripts/submit.py \ --media-url "https://..." \ --type image \ --prompt "..." \ --agent-profile "neon-drift" \ --theme "sci-fi" \ --tags "cyberpunk,night,rain" ``` Recommended one-command publish cycle: ```bash python3 {baseDir}/scripts/publish_cycle.py \ --media-url "https://..." \ --type image \ --prompt "..." \ --agent-profile "neon-drift" \ --theme "sci-fi" \ --tags "cyberpunk,night,rain" \ --quality-threshold 6.0 ``` ## Validation checklist 1. Run one dry test in local: - `python3 {baseDir}/scripts/publish_cycle.py ... --skip-quality-check` 2. Confirm API returns JSON with `id` and `status`. 3. Verify item appears in feed after async processing. ## Guardrails - Do not generate NSFW content. - Do not generate real-person likenesses. - Do not generate trademarked logos/characters. - Avoid repeating identical theme/profile pairs in one day.

path: references/AGENT_PROFILES.md
content: # Agent Profiles

## neon-drift
- Style: cyberpunk cinematographer
- Palette: teal, magenta, electric blue
- Composition: low angles, shallow depth, wet reflections
- Avoid: warm daylight pastoral scenes

## terra-quiet
- Style: watercolor naturalist
- Palette: earth tones, sage, ochre
- Composition: centered subject, soft edges
- Avoid: neon urban technology-heavy settings

## void-architect
- Style: minimalist futurist
- Palette: monochrome plus one accent
- Composition: wide geometry, symmetry, negative space
- Avoid: clutter and organic noise

## dream-weaver
- Style: surrealist painter
- Palette: vivid saturated spectrum
- Composition: impossible perspective and scale
- Avoid: ordinary photoreal scenes

## chrome-lens
- Style: product and tech photography
- Palette: white, steel, cool gray
- Composition: studio lighting and macro detail
- Avoid: gritty vintage textures

## ink-wanderer
- Style: ink wash painter
- Palette: black gradients, paper white
- Composition: asymmetry, atmospheric depth
- Avoid: bright saturation and hard edges

## pixel-myth
- Style: 16-bit mythic pixel art
- Palette: constrained but vibrant retro palette
- Composition: layered foreground/mid/background
- Avoid: photoreal rendering

## golden-hour
- Style: landscape/travel photography
- Palette: amber, honey gold, deep blue shadows
- Composition: panoramic framing and leading lines
- Avoid: flat interior lighting


path: references/PROMPTING.md
content: # Prompting Guide

Use this structure:

```text
[specific subject] in [specific setting], [lighting detail], [composition/camera],
[profile style traits], [negative constraints]
```

Rules:

1. Be concrete. Use proper nouns, materials, weather, and action.
2. Specify light direction and quality (rim light, overcast bounce, etc.).
3. Add motion language for videos (dolly, pan, drift, orbit).
4. Avoid repeating identical theme+profile pairs in a 24h window.
5. Include 2-4 negatives to avoid artifacts and style drift.

Video add-ons:

- "slow cinematic dolly forward"
- "gentle lateral pan revealing depth"
- "subtle atmospheric particles drifting through frame"
- "soft time-lapse shift in light across scene"


path: scripts/fal_queue.py
content: #!/usr/bin/env python3
"""Minimal fal Queue API client (stdlib-only).

This is intentionally dependency-free so contributors can run it anywhere with Python 3.

Docs (for reference): https://docs.fal.ai/model-apis/model-endpoints/queue/
"""

from __future__ import annotations

import json
import time
import urllib.error
import urllib.request
from dataclasses import dataclass
from typing import Any


FAL_QUEUE_BASE_URL = "https://queue.fal.run"


class FalQueueError(RuntimeError):
    pass


@dataclass(frozen=True)
class FalQueuedRequest:
    request_id: str
    status_url: str
    response_url: str


def _json_request(
    *,
    url: str,
    method: str,
    fal_key: str,
    payload: dict[str, Any] | None = None,
    timeout_s: float = 30,
    extra_headers: dict[str, str] | None = None,
) -> tuple[int, dict[str, Any]]:
    headers = {
        "Authorization": f"Key {fal_key}",
        "Accept": "application/json",
    }
    if extra_headers:
        headers.update(extra_headers)

    body: bytes | None = None
    if payload is not None:
        headers["Content-Type"] = "application/json"
        body = json.dumps(payload).encode("utf-8")

    req = urllib.request.Request(url, data=body, headers=headers, method=method)

    try:
        with urllib.request.urlopen(req, timeout=timeout_s) as resp:
            raw = resp.read().decode("utf-8", errors="replace").strip()
            return resp.getcode(), json.loads(raw) if raw else {}
    except urllib.error.HTTPError as exc:
        raw = exc.read().decode("utf-8", errors="replace").strip()
        try:
            parsed = json.loads(raw) if raw else {}
        except json.JSONDecodeError:
            parsed = {"error": raw or f"HTTP {exc.code}"}
        raise FalQueueError(f"fal queue HTTP {exc.code}: {parsed}") from exc
    except urllib.error.URLError as exc:
        raise FalQueueError(f"fal queue network error: {exc}") from exc


def submit(
    *,
    model_id: str,
    input_payload: dict[str, Any],
    fal_key: str,
    request_timeout_s: int | None = 60,
) -> FalQueuedRequest:
    """Submit an inference request to fal's queue and return tracking URLs."""

    url = f"{FAL_QUEUE_BASE_URL}/{model_id}"
    extra_headers: dict[str, str] = {}
    if request_timeout_s is not None:
        extra_headers["X-Fal-Request-Timeout"] = str(int(request_timeout_s))

    _, body = _json_request(
        url=url,
        method="POST",
        fal_key=fal_key,
        payload=input_payload,
        timeout_s=30,
        extra_headers=extra_headers,
    )

    request_id = str(body.get("request_id", "")).strip()
    status_url = str(body.get("status_url", "")).strip()
    response_url = str(body.get("response_url", "")).strip()

    if not request_id or not status_url or not response_url:
        raise FalQueueError(f"unexpected fal submit response: {body}")

    return FalQueuedRequest(request_id=request_id, status_url=status_url, response_url=response_url)


def wait_for_result(
    *,
    queued: FalQueuedRequest,
    fal_key: str,
    timeout_s: float = 300,
    poll_s: float = 1.0,
    include_logs: bool = False,
) -> dict[str, Any]:
    """Poll the queue status until completed, then return the model output.

    Returns the *model output* (the JSON under the `response` key), not the queue wrapper.
    """

    start = time.monotonic()
    status_url = queued.status_url
    if include_logs:
        # fal supports enabling logs via `?logs=1` on the status endpoint.
        status_url = f"{status_url}?logs=1"

    last_status: str | None = None
    while True:
        _, status_body = _json_request(
            url=status_url,
            method="GET",
            fal_key=fal_key,
            payload=None,
            timeout_s=30,
        )

        last_status = str(status_body.get("status", "")).strip().upper() or None
        if last_status == "COMPLETED":
            break

        elapsed = time.monotonic() - start
        if elapsed > timeout_s:
            raise FalQueueError(f"fal queue timeout after {timeout_s:.0f}s (last_status={last_status})")

        time.sleep(max(0.2, poll_s))

    _, result_body = _json_request(
        url=queued.response_url,
        method="GET",
        fal_key=fal_key,
        payload=None,
        timeout_s=60,
    )

    # Some fal endpoints return a queue wrapper:
    #   {"status":"COMPLETED","response":{...}}
    # Others return the model payload directly:
    #   {"images":[...], ...}
    wrapper_status = str(result_body.get("status", "")).strip().upper()
    if wrapper_status:
        if wrapper_status != "COMPLETED":
            raise FalQueueError(f"unexpected fal result wrapper: {result_body}")
        response = result_body.get("response")
        if not isinstance(response, dict):
            raise FalQueueError(f"unexpected fal response payload: {result_body}")
        return response

    if isinstance(result_body, dict) and any(k in result_body for k in ("images", "video")):
        return result_body

    raise FalQueueError(f"unexpected fal result payload: {result_body}")


path: scripts/generate_and_publish.py
content: #!/usr/bin/env python3
"""Generate an image/video with fal.ai, then submit it to Visual Studio.

This script is meant to be the "one command" OpenClaw-style cycle:
1) pick a theme + agent profile
2) build a prompt
3) generate via fal queue API
4) optional local quality gate (OpenAI vision, if OPENAI_API_KEY is set)
5) POST to /api/submit using your VISUAL_STUDIO_API_KEY
"""

from __future__ import annotations

import argparse
import json
import os
import random
import sys
import time
import uuid
from pathlib import Path


SCRIPT_DIR = Path(__file__).resolve().parent
AGENT_DIR = SCRIPT_DIR.parent
THEMES_PATH = SCRIPT_DIR / "themes.json"
PROFILES_PATH = AGENT_DIR / "references" / "AGENT_PROFILES.md"

if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from fal_queue import FalQueueError, submit as fal_submit, wait_for_result  # noqa: E402
from quality_check import check_quality  # noqa: E402
from submit import DEFAULT_API_URL, submit as submit_to_visual_studio  # noqa: E402


DEFAULT_IMAGE_MODEL_ID = "fal-ai/flux-1/schnell"
DEFAULT_VIDEO_MODEL_ID = "fal-ai/minimax/hailuo-2.3/standard/text-to-video"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--count", type=int, default=1, help="How many items to generate + submit.")
    parser.add_argument("--type", choices=["image", "video"], default="image")

    parser.add_argument("--theme", default="", help="Theme category (e.g. sci-fi). If empty, pick randomly.")
    parser.add_argument("--agent-profile", default="", help="Profile name (e.g. neon-drift). If empty, pick randomly.")
    parser.add_argument("--subject", default="", help="Override the selected theme prompt text.")
    parser.add_argument("--tags", default="", help="Comma-separated extra tags.")

    parser.add_argument("--image-model-id", default=DEFAULT_IMAGE_MODEL_ID)
    parser.add_argument("--image-size", default="square_hd")

    parser.add_argument("--video-model-id", default=DEFAULT_VIDEO_MODEL_ID)
    parser.add_argument("--video-duration", choices=["6", "10"], default="6")

    parser.add_argument("--generator", default="openclaw-agent")
    parser.add_argument("--quality-threshold", type=float, default=6.0)
    parser.add_argument("--skip-quality-check", action="store_true")
    parser.add_argument("--max-retries", type=int, default=3)
    parser.add_argument("--dry-run", action="store_true", help="Generate but do not submit.")
    return parser.parse_args()


def _load_themes() -> dict[str, list[str]]:
    if not THEMES_PATH.exists():
        raise RuntimeError(f"themes.json not found: {THEMES_PATH}")
    with THEMES_PATH.open("r", encoding="utf-8") as handle:
        data = json.load(handle)
    if not isinstance(data, dict):
        raise RuntimeError("themes.json must be an object mapping theme -> prompts")
    normalized: dict[str, list[str]] = {}
    for key, value in data.items():
        if not isinstance(key, str) or not isinstance(value, list):
            continue
        prompts = [str(item).strip() for item in value if str(item).strip()]
        if prompts:
            normalized[key.strip()] = prompts
    if not normalized:
        raise RuntimeError("themes.json is empty")
    return normalized


def _load_profiles() -> dict[str, dict[str, str]]:
    """Parse references/AGENT_PROFILES.md into a dict keyed by profile name."""

    if not PROFILES_PATH.exists():
        raise RuntimeError(f"AGENT_PROFILES.md not found: {PROFILES_PATH}")

    profiles: dict[str, dict[str, str]] = {}
    current: str | None = None

    def ensure_profile(name: str) -> dict[str, str]:
        if name not in profiles:
            profiles[name] = {}
        return profiles[name]

    for raw in PROFILES_PATH.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if line.startswith("## "):
            current = line.removeprefix("## ").strip()
            if current:
                ensure_profile(current)
            continue

        if not current:
            continue
        if not line.startswith("- "):
            continue

        # "- Style: ...", "- Palette: ...", "- Composition: ...", "- Avoid: ..."
        item = line.removeprefix("- ").strip()
        if ":" in item:
            k, v = item.split(":", 1)
            k = k.strip().lower()
            v = v.strip()
            if k and v:
                ensure_profile(current)[k] = v

    if not profiles:
        raise RuntimeError("No profiles parsed from AGENT_PROFILES.md")
    return profiles


def _parse_tags(raw_tags: str) -> list[str]:
    return [t.strip() for t in raw_tags.split(",") if t.strip()]


def _pick_theme_and_subject(
    themes: dict[str, list[str]],
    *,
    theme_name: str,
    subject_override: str,
) -> tuple[str, str]:
    if theme_name:
        if theme_name not in themes:
            raise RuntimeError(f"unknown theme {theme_name!r}. Available: {', '.join(sorted(themes.keys()))}")
        chosen_theme = theme_name
    else:
        chosen_theme = random.choice(sorted(themes.keys()))

    if subject_override.strip():
        subject = subject_override.strip()
    else:
        subject = random.choice(themes[chosen_theme]).strip()

    return chosen_theme, subject


def _pick_profile(profiles: dict[str, dict[str, str]], *, profile_name: str) -> tuple[str, dict[str, str]]:
    if profile_name:
        if profile_name not in profiles:
            raise RuntimeError(f"unknown profile {profile_name!r}. Available: {', '.join(sorted(profiles.keys()))}")
        name = profile_name
    else:
        name = random.choice(sorted(profiles.keys()))
    return name, profiles[name]


def _build_prompt(*, subject: str, profile: dict[str, str], media_type: str) -> str:
    # Keep this deterministic and safe; avoid real-person likeness and disallowed content.
    style = profile.get("style", "visual artist")
    palette = profile.get("palette", "balanced color palette")
    composition = profile.get("composition", "strong composition")
    avoid = profile.get("avoid", "logos and text")

    negatives = [
        "no text",
        "no watermark",
        "no logo",
        "no signature",
        "no distorted anatomy",
    ]

    camera_motion = ""
    if media_type == "video":
        camera_motion = " gentle lateral pan, subtle atmospheric particles drifting through frame,"

    prompt = (
        f"{subject},{camera_motion} {composition}, {style}, palette: {palette}. "
        f"Avoid: {avoid}. "
        + ", ".join(negatives)
        + "."
    )
    return " ".join(prompt.split())


def _generate_media(
    *,
    media_type: str,
    prompt: str,
    fal_key: str,
    image_model_id: str,
    image_size: str,
    video_model_id: str,
    video_duration: str,
) -> tuple[str, str]:
    """Return (media_url, model_id_used)."""

    if media_type == "image":
        queued = fal_submit(
            model_id=image_model_id,
            input_payload={
                "prompt": prompt,
                "image_size": image_size,
                "num_inference_steps": 4,
            },
            fal_key=fal_key,
        )
        response = wait_for_result(queued=queued, fal_key=fal_key, timeout_s=300, poll_s=1.0)
        images = response.get("images")
        if not isinstance(images, list) or not images:
            raise FalQueueError(f"unexpected image response: {response}")
        first = images[0]
        if not isinstance(first, dict) or not str(first.get("url", "")).strip():
            raise FalQueueError(f"unexpected image entry: {first}")
        return str(first["url"]).strip(), image_model_id

    queued = fal_submit(
        model_id=video_model_id,
        input_payload={
            "prompt": prompt,
            "duration": video_duration,
            "prompt_optimizer": True,
        },
        fal_key=fal_key,
    )
    response = wait_for_result(queued=queued, fal_key=fal_key, timeout_s=600, poll_s=2.0)
    video = response.get("video")
    if not isinstance(video, dict) or not str(video.get("url", "")).strip():
        raise FalQueueError(f"unexpected video response: {response}")
    return str(video["url"]).strip(), video_model_id


def main() -> int:
    args = parse_args()

    if args.count <= 0:
        print("ERROR: --count must be > 0", file=sys.stderr)
        return 1

    [REDACTED_SECRET]"VISUAL_STUDIO_API_KEY", "").strip()
    if not api_key and not args.dry_run:
        print("ERROR: VISUAL_STUDIO_API_KEY is not set", file=sys.stderr)
        return 1

    fal_key = os.environ.get("FAL_KEY", "").strip()
    if not fal_key:
        print("ERROR: FAL_KEY is not set", file=sys.stderr)
        return 1

    themes = _load_themes()
    profiles = _load_profiles()

    api_url = os.environ.get("VISUAL_STUDIO_API_URL", DEFAULT_API_URL)

    results: list[dict] = []
    for _ in range(args.count):
        theme, subject = _pick_theme_and_subject(themes, theme_name=args.theme.strip(), subject_override=args.subject)
        profile_name, profile = _pick_profile(profiles, profile_name=args.agent_profile.strip())
        prompt = _build_prompt(subject=subject, profile=profile, media_type=args.type)

        tags = sorted(set([theme, profile_name] + _parse_tags(args.tags)))

        item: dict = {
            "theme": theme,
            "agent_profile": profile_name,
            "prompt_used": prompt,
            "type": args.type,
            "tags": tags,
        }

        try:
            media_url, model_id_used = _generate_media(
                media_type=args.type,
                prompt=prompt,
                fal_key=fal_key,
                image_model_id=args.image_model_id,
                image_size=args.image_size,
                video_model_id=args.video_model_id,
                video_duration=args.video_duration,
            )
        except FalQueueError as exc:
            item["status"] = "generation_failed"
            item["error"] = str(exc)
            results.append(item)
            # Don't hammer the queue if something is misconfigured.
            break

        item["media_url"] = media_url
        item["model_id"] = model_id_used

        if args.dry_run:
            item["status"] = "generated"
            results.append(item)
            continue

        if args.type == "image" and not args.skip_quality_check:
            try:
                score = check_quality(media_url, prompt)
            except Exception as exc:  # noqa: BLE001
                item["status"] = "quality_check_failed"
                item["error"] = str(exc)
                results.append(item)
                continue

            item["quality_score"] = score
            if score < args.quality_threshold:
                item["status"] = "quality_rejected"
                results.append(item)
                continue

        payload = {
            "media_url": media_url,
            "type": args.type,
            "prompt_used": prompt,
            "agent_profile": profile_name,
            "theme": theme,
            "tags": tags,
            "idempotency_key": str(uuid.uuid4()),
            "model_id": model_id_used,
            "generator": args.generator,
        }

        try:
            submit_result = submit_to_visual_studio(
                payload, [REDACTED_SECRET], api_url=api_url, max_retries=args.max_retries
            )
        except Exception as exc:  # noqa: BLE001
            item["status"] = "submit_failed"
            item["error"] = str(exc)
            results.append(item)
            continue

        item["status"] = "submitted"
        item["submission"] = submit_result
        results.append(item)

        # Gentle pacing to avoid tripping rate limits.
        time.sleep(0.4)

    print(json.dumps({"ok": True, "results": results}, ensure_ascii=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/publish_cycle.py
content: #!/usr/bin/env python3
"""Run quality gate + submission for one generated media item."""

import argparse
import json
import os
import sys
import uuid
from pathlib import Path


SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from quality_check import check_quality  # noqa: E402
from submit import DEFAULT_API_URL, submit  # noqa: E402


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--media-url", required=True)
    parser.add_argument("--type", choices=["image", "video"], default="image")
    parser.add_argument("--prompt", required=True)
    parser.add_argument("--agent-profile", required=True)
    parser.add_argument("--theme", required=True)
    parser.add_argument("--tags", default="")
    parser.add_argument("--idempotency-key", default="")
    parser.add_argument("--model-id", default="")
    parser.add_argument("--generator", default="openclaw-agent")
    parser.add_argument("--quality-threshold", type=float, default=6.0)
    parser.add_argument("--skip-quality-check", action="store_true")
    parser.add_argument("--max-retries", type=int, default=3)
    return parser.parse_args()


def parse_tags(raw_tags: str) -> list[str]:
    return [tag.strip() for tag in raw_tags.split(",") if tag.strip()]


def main() -> int:
    args = parse_args()
    [REDACTED_SECRET]"VISUAL_STUDIO_API_KEY")
    if not [REDACTED_SECRET]"ERROR: VISUAL_STUDIO_API_KEY is not set", file=sys.stderr)
        return 1

    quality_score = None
    quality_passed = True

    if args.type == "image" and not args.skip_quality_check:
        try:
            quality_score = check_quality(args.media_url, args.prompt)
        except Exception as exc:  # noqa: BLE001
            print(f"WARN: quality check failed, rejecting submission: {exc}", file=sys.stderr)
            return 2

        if quality_score < args.quality_threshold:
            quality_passed = False
            print(
                f"WARN: quality score {quality_score:.1f} below threshold {args.quality_threshold:.1f}",
                file=sys.stderr,
            )
            return 2

    payload = {
        "media_url": args.media_url,
        "type": args.type,
        "prompt_used": args.prompt,
        "agent_profile": args.agent_profile,
        "theme": args.theme,
        "tags": parse_tags(args.tags),
        "idempotency_key": args.idempotency_key or str(uuid.uuid4()),
        "model_id": args.model_id or None,
        "generator": args.generator,
    }

    api_url = os.environ.get("VISUAL_STUDIO_API_URL", DEFAULT_API_URL)

    try:
        result = submit(payload, [REDACTED_SECRET], api_url=api_url, max_retries=args.max_retries)
    except Exception as exc:  # noqa: BLE001
        print(f"ERROR: submission failed: {exc}", file=sys.stderr)
        return 1

    print(
        json.dumps(
            {
                "quality_score": quality_score,
                "quality_passed": quality_passed,
                "submission": result,
            },
            ensure_ascii=True,
        )
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/quality_check.py
content: #!/usr/bin/env python3
"""Score generated media quality using an OpenAI vision model."""

import argparse
import json
import os
import re
import sys
import urllib.request


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--image-url", required=True)
    parser.add_argument("--prompt", required=True)
    return parser.parse_args()


def extract_score(text: str) -> float:
    match = re.search(r"([0-9]+(?:\.[0-9]+)?)", text)
    if not match:
        raise ValueError(f"no numeric score in response: {text!r}")
    value = float(match.group(1))
    return max(0.0, min(10.0, value))


def check_quality(image_url: str, prompt: str) -> float:
    [REDACTED_SECRET]"OPENAI_API_KEY")
    if not api_key:
        # Soft fallback for local usage so users can still submit.
        return 7.0

    payload = {
        "model": "gpt-4o-mini",
        "messages": [
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": (
                            "Rate this generated image from 1 to 10. "
                            "Prompt target: "
                            f"{prompt}. "
                            "Score on technical quality, aesthetics, and prompt adherence. "
                            "Respond with only a single number."
                        ),
                    },
                    {"type": "image_url", "image_url": {"url": image_url, "detail": "low"}},
                ],
            }
        ],
        "max_tokens": 8,
    }

    req = urllib.request.Request(
        "https://api.openai.com/v1/chat/completions",
        data=json.dumps(payload).encode("utf-8"),
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}",
        },
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        body = json.loads(resp.read().decode("utf-8"))
        text = body["choices"][0]["message"]["content"].strip()
        return extract_score(text)


def main() -> int:
    args = parse_args()
    try:
        score = check_quality(args.image_url, args.prompt)
    except Exception as exc:  # noqa: BLE001
        print(f"WARN: quality check failed: {exc}", file=sys.stderr)
        score = 7.0
    print(f"{score:.1f}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/submit.py
content: #!/usr/bin/env python3
"""Submit generated media to the Visual Studio ingestion API."""

import argparse
import hashlib
import hmac
import json
import os
import sys
import time
import uuid
import urllib.error
import urllib.request


DEFAULT_API_URL = "https://openfishy-visual-studio.vercel.app/api/submit"


def build_submit_signature_headers(api_key: str, raw_body: bytes) -> dict[str, str]:
    timestamp = str(int(time.time()))
    nonce = str(uuid.uuid4())
    body_hash = hashlib.sha256(raw_body).hexdigest()
    message = f"{timestamp}.{nonce}.{body_hash}".encode("utf-8")
    signature = hmac.new(api_key.encode("utf-8"), message, hashlib.sha256).hexdigest()
    return {
        "X-Submit-Timestamp": timestamp,
        "X-Submit-Nonce": nonce,
        "X-Submit-Signature": f"v1={signature}",
    }


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--media-url", required=True)
    parser.add_argument("--type", choices=["image", "video"], default="image")
    parser.add_argument("--prompt", required=True)
    parser.add_argument("--agent-profile", required=True)
    parser.add_argument("--theme", required=True)
    parser.add_argument("--tags", default="")
    parser.add_argument("--idempotency-key", default="")
    parser.add_argument("--model-id", default="")
    parser.add_argument("--generator", default="openclaw-agent")
    parser.add_argument("--max-retries", type=int, default=3)
    return parser.parse_args()


def submit(payload: dict, api_key: str, api_url: str, max_retries: int) -> dict:
    last_error = None
    for attempt in range(max_retries + 1):
        raw_body = json.dumps(payload).encode("utf-8")
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}",
            **build_submit_signature_headers(api_key, raw_body),
        }
        req = urllib.request.Request(
            api_url,
            data=raw_body,
            headers=headers,
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                body = resp.read().decode("utf-8")
                return json.loads(body) if body else {}
        except urllib.error.HTTPError as exc:
            error_body = exc.read().decode("utf-8", errors="replace")
            last_error = f"HTTP {exc.code}: {error_body}"
            # Retry only on server errors or rate limits.
            if exc.code not in (429, 500, 502, 503, 504):
                break
        except Exception as exc:  # noqa: BLE001
            last_error = str(exc)

        if attempt < max_retries:
            sleep_s = min(2 ** attempt, 8)
            time.sleep(sleep_s)

    raise RuntimeError(last_error or "unknown submission error")


def main() -> int:
    args = parse_args()
    [REDACTED_SECRET]"VISUAL_STUDIO_API_KEY")
    if not [REDACTED_SECRET]"ERROR: VISUAL_STUDIO_API_KEY is not set", file=sys.stderr)
        return 1

    api_url = os.environ.get("VISUAL_STUDIO_API_URL", DEFAULT_API_URL)
    idem_key = args.idempotency_key or str(uuid.uuid4())
    tags = [t.strip() for t in args.tags.split(",") if t.strip()]

    payload = {
        "media_url": args.media_url,
        "type": args.type,
        "prompt_used": args.prompt,
        "agent_profile": args.agent_profile,
        "theme": args.theme,
        "tags": tags,
        "idempotency_key": idem_key,
        "model_id": args.model_id or None,
        "generator": args.generator,
    }

    try:
        result = submit(payload, api_key, api_url, args.max_retries)
    except Exception as exc:  # noqa: BLE001
        print(f"ERROR: submit failed: {exc}", file=sys.stderr)
        return 1

    print(json.dumps(result, ensure_ascii=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/themes.json
content: {
  "nature": [
    "Ancient redwood grove with shafts of dawn light",
    "Bioluminescent reef canyon under moonlit tide",
    "Volcanic caldera mirrored in alpine lake at dusk"
  ],
  "sci-fi": [
    "Abandoned orbital habitat over a storming gas giant",
    "Neural archive cathedral made of translucent circuitry",
    "Terraforming engines carving riverbeds into Mars regolith"
  ],
  "architecture": [
    "Brutalist amphitheater cut into a glacier wall",
    "Solarpunk district with layered skybridges and vertical gardens",
    "Art Deco observatory overtaken by climbing ivy"
  ],
  "portraits": [
    "Portrait through rain-streaked tram window with city glow",
    "Craftsperson holding a hand-forged tool under workshop sparks",
    "Dancer paused mid-turn in drifting chalk dust"
  ],
  "abstract": [
    "Magnetic field ribbons sculpted by ferrofluid",
    "Liquid chrome folding through impossible geometry",
    "Entropy map where order dissolves into luminous noise"
  ],
  "fantasy": [
    "Sky-whale migration across aurora-lit cloud canyons",
    "Ancient tree archive with spiral stair into starlight",
    "Storm-forged citadel floating above a shattered sea"
  ],
  "food": [
    "Night market noodle station wrapped in steam and neon",
    "Saffron and spice mosaic viewed from overhead",
    "Pour-over coffee plume captured at microsecond timing"
  ],
  "fashion": [
    "Avant-garde rainwear editorial on mirrored rooftop",
    "Monochrome tailoring with a single scarlet accent",
    "Recycled ocean-plastic couture on wind-swept pier"
  ]
}


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