sanzaru
A stateless, lightweight MCP server and agent CLI that wraps OpenAI's Sora Video API, Whisper, GPT-4o Audio, and TTS APIs via the OpenAI Python SDK.
Features
Video Generation (Sora)
- Create videos with
sora-2orsora-2-promodels - Use reference images to guide generation
- Remix and refine existing videos
- Download variants (video, thumbnail, spritesheet)
Image Generation
- Generate images with gpt-image-2.5 (sunburst/flare, recommended), gpt-image-2, gpt-image-1.5, or GPT-5
- Edit and compose images with up to 16 inputs
- Iterative refinement via Responses API
- Automatic resizing for Sora compatibility
Audio Processing
- Transcription: Whisper and GPT-4o models
- Audio Chat: Interactive analysis with GPT-4o
- Text-to-Speech: Multi-voice TTS generation
- Processing: Format conversion, compression, file management
Podcast Generation
- Multi-voice podcasts with up to 4 speakers and 10 TTS voices
- Parallel segment generation with configurable pacing
- MP3/WAV output with loudness normalization
- Recommended engine for scripted shows: ElevenLabs
eleven_v4(the ElevenLabs default) indialoguerender mode with--verify - ElevenLabs
dialoguerender mode: consecutive turns go out together so the model paces them --verifytranscribes the rendered audio and re-renders segments the TTS silently dropped
Simulated Podcasts
- The conversation is generated, not read — N
gpt-realtimeagents actually talk to each other - Each host hears the others' audio, so they respond to delivery and timing, not to a transcript
- Pre-production plans acts that record in parallel: a 30-minute episode in ~1 minute
- Checkpointed per act and resumable; cost ceiling, dry-run projection, per-host stems
- QC transcribes the rendered audio and judges it against the plan
- See
docs/audio/simulated-podcasts.md
Agent CLI
- Every capability as a shell command:
sanzaru video create,sanzaru image generate, ... - One-shot async workflows:
create ... -o out.mp4submits, polls, downloads in one command - JSON envelopes on stdout, progress on stderr, deterministic exit codes, resumable waits
- Concurrent fan-out (multi-prompt image batches, multi-job
wait) and arbitrary-ooutput paths - See
docs/cli.md— baresanzarustill runs the MCP server (nothing breaks)
Note: Content guardrails are enforced by OpenAI. This server does not run local moderation.
Requirements
- Python 3.10+
OPENAI_API_KEYenvironment variable
Media storage (choose one):
# Recommended: unified path (auto-creates videos/, images/, audio/ subdirs)
SANZARU_MEDIA_PATH="/path/to/media"
# Or individual paths (legacy, still supported)
VIDEO_PATH="/path/to/videos"
IMAGE_PATH="/path/to/images"
AUDIO_PATH="/path/to/audio"
Features are auto-detected based on configured paths. Set only what you need.
Quick Start
-
Clone the repository:
git clone https://github.com/TJC-LP/sanzaru.git cd sanzaru
-
Run the setup script:
./setup.sh
The script will:
- Prompt for your OpenAI API key
- Create directories and
.envconfiguration - Install dependencies with
uv sync --all-extras --dev
-
Start using:
claude
That's it! Claude Code will automatically connect and you can start generating videos, images, and processing audio.
Or skip MCP entirely — the agent CLI
uv tool install sanzaru && export OPENAI_API_KEY=sk-...
# One command: submit Sora job → poll → download → print the file path
sanzaru video create "a tabby cat stretches on a windowsill" --seconds 4 -o ./cat.mp4 | jq -r .result.file.path
# Synchronous image generation (gpt-image-2.5), batch fan-out, JSONL output
sanzaru image generate "app icon" "hero banner" --quality high -o ./art/
sanzaru capabilities # machine-readable: what's enabled here
JSON envelopes on stdout, progress on stderr, exit 4 = still-running-and-resumable. Full
reference: docs/cli.md.
Installation
Claude Code Plugin (Recommended)
Install as a plugin — auto-configures the MCP server and ships three skills: sanzaru-mcp (the tool surface, incl. wait_for), sanzaru-cli (the shell surface), prompt-guidance (how to write Sora/image prompts):
/plugin marketplace add TJC-LP/sanzaru
Requires OPENAI_API_KEY and SANZARU_MEDIA_PATH environment variables to be set.
Quick Install
# All features
uv add "sanzaru[all]"
# Specific features
uv add "sanzaru[audio]" # With audio support
uv add "sanzaru[elevenlabs]" # ElevenLabs as a second TTS provider
uv add sanzaru # Base (video + image only)
Alternative Installation Methods
From Source
git clone https://github.com/TJC-LP/sanzaru.git
cd sanzaru
uv sync --all-extras
Claude Desktop
Add to your claude_desktop_config.json:
{
"mcpServers": {
"sanzaru": {
"command": "uvx",
"args": ["sanzaru[all]"],
"env": {
"OPENAI_API_KEY": "your-api-key-here",
"SANZARU_MEDIA_PATH": "/absolute/path/to/media"
}
}
}
}
Or from source:
{
"mcpServers": {
"sanzaru": {
"command": "uv",
"args": ["run", "--directory", "/path/to/sanzaru", "sanzaru"]
}
}
}
Codex MCP
# Using uvx (from PyPI)
codex mcp add sanzaru \
--env OPENAI_API_KEY="sk-..." \
--env SANZARU_MEDIA_PATH="$HOME/sanzaru-media" \
-- uvx "sanzaru[all]"
Manual Setup
uv venv
uv sync
# Set required environment variables
export OPENAI_API_KEY=sk-...
export SANZARU_MEDIA_PATH=~/sanzaru-media
# Run server (stdio for MCP clients)
uv run sanzaru
# Or HTTP mode (for remote access)
uv run sanzaru --transport http --port 8000
Available Tools
| Category | Tools | Description |
|---|---|---|
| Video | create_video, get_video_status, download_video, list_videos, list_local_videos, delete_video, remix_video |
Generate and manage Sora videos with optional reference images |
| Jobs | wait_for |
Block server-side on any mix of video_*/resp_* ids until they finish (progress on every poll, optional download); replaces model-driven polling |
| Image | generate_image, edit_image, create_image, get_image_status, download_image |
Generate with gpt-image-2.5 (default, sync) or GPT-5 (polling) |
| Reference | list_reference_images, prepare_reference_image |
Manage and resize images for Sora compatibility |
| Audio | transcribe_audio, chat_with_audio, create_audio, convert_audio, compress_audio, list_audio_files, get_latest_audio, transcribe_with_enhancement |
Transcription, analysis, TTS (OpenAI or ElevenLabs), and file management |
| Podcast | generate_podcast |
Multi-voice podcast generation with parallel TTS and audio stitching; speakers may mix TTS providers |
| Simulated Podcast | simulate_podcast |
Realtime agents converse from a rundown — parallel acts, checkpointing, cost ceiling, QC |
| Media | view_media |
Interactive media player via MCP App protocol |
Full API documentation: See docs/api-reference.md
Basic Workflows
Generate a Video
# Create video from text
video = create_video(
prompt="A serene mountain landscape at sunrise",
model="sora-2",
seconds="8",
size="1280x720"
)
# Poll for completion
status = get_video_status(video.id)
# Download when ready
download_video(video.id, filename="mountain_sunrise.mp4")
Generate with Reference Image
# 1. Generate reference image (gpt-image-2.5-flare, synchronous)
generate_image(
prompt="futuristic pilot in mech cockpit",
size="1536x1024",
filename="pilot.png"
)
# 2. Prepare for video (resize to Sora dimensions)
prepare_reference_image("pilot.png", "1280x720", resize_mode="crop")
# 3. Animate
video = create_video(
prompt="The pilot looks up and smiles",
size="1280x720",
input_reference_filename="pilot_1280x720.png"
)
Audio Transcription
# List available audio files
files = list_audio_files(format="mp3")
# Transcribe — long files are auto-windowed, so nothing truncates silently
result = transcribe_audio("interview.mp3")
# Or analyze with GPT-4o
analysis = chat_with_audio(
"meeting.mp3",
user_prompt="Summarize key decisions and action items"
)
Generate a Podcast
generate_podcast(script={
"title": "AI Weekly",
"speakers": [
{"id": "host", "name": "Alex", "voice": "nova"},
{"id": "guest", "name": "Sam", "voice": "echo"}
],
"segments": [
{"speaker": "host", "text": "Welcome to AI Weekly!"},
{"speaker": "guest", "text": "Thanks for having me."}
]
})
Simulate a Podcast (no script — the agents talk)
# 1. Plan it. Cheap, and the JSON is yours to edit.
sanzaru podcast rundown "why TTS providers drop sentence tails" \
--acts 3 -m 6 --host "Avery" --host "Rory:cedar:You chased the bug." \
-o rundown.json
# 2. See what it would cost. Records nothing.
sanzaru podcast simulate @rundown.json --dry-run
# 3. Record it. Acts run in parallel and each is checkpointed as it lands.
sanzaru podcast simulate @rundown.json --model gpt-realtime-2.1-mini \
--max-cost 2.00 --stems -o ep1.mp3
# Interrupted? The run id is on stderr from the start.
sanzaru podcast simulate --resume 6f1a9c02
Documentation
- API Reference - Complete tool documentation with parameters and examples
- Reference Images Guide - Working with reference images and resizing
- Image Generation Guide - Generating and editing reference images
- Sora Prompting Guide - Crafting effective video prompts
- Audio Features - Audio transcription, chat, and TTS
- Simulated Podcasts - Realtime agents in conversation: producer model, act chunking, cost, QC
- Performance & Architecture - Technical details and benchmarks
Transport Modes
| Mode | Command | Use Case |
|---|---|---|
| stdio (default) | uv run sanzaru |
Claude Desktop, Claude Code, local MCP clients |
| HTTP | uv run sanzaru --transport http |
Remote access, Databricks Apps, web clients |
Authenticating HTTP mode
HTTP mode exposes the full toolset — paid generation, delete_video, and every
stored media file — to whoever can reach the port. Set a token and send it as
Authorization: Bearer <token> on both /mcp and /media:
export SANZARU_HTTP_TOKEN="$(openssl rand -hex 32)"
uv run sanzaru --transport http --host 0.0.0.0
Binding to anything other than loopback requires the token: sanzaru refuses
to start otherwise (exit 3). Set SANZARU_ALLOW_UNAUTHENTICATED_HTTP=1 only when
something in front of it already authenticates every request — and name that
proxy's hostnames in SANZARU_ALLOWED_HOSTS (comma-separated, host or host:*),
which the hatch requires: it keeps the SDK's DNS-rebinding check on, Origin
included, so the unauthenticated path is never also the least-protected one. With
a token and no allowlist the Host/Origin check is switched off and the token is the
control. On a loopback bind the token is optional but still recommended.
Databricks Apps authenticates in front of the app, so run it with
SANZARU_ALLOW_UNAUTHENTICATED_HTTP=1 and SANZARU_ALLOWED_HOSTS set to the Host
value the platform proxy forwards (include whatever its health probe sends). Include
the loopback names too if the probe uses them, e.g.
SANZARU_ALLOWED_HOSTS=myapp.example.com,localhost:*.
Upgrading from 0.10.x: sanzaru --transport http --host 0.0.0.0 used to start
with no credential. It now refuses unless SANZARU_HTTP_TOKEN or the hatch above
is set — deliberate, and worth a line in your deployment notes.
/media/{type}/{name} requires the same Authorization header, and every response
is Content-Disposition: attachment, so it is for programmatic clients; a browser
media element cannot send the header and should use the viewer's _get_media_data
path (the bundled MCP App already does).
Embedding the server in your own ASGI stack? Use sanzaru.server.build_http_app() —
the same authenticated app the CLI serves — never the bare mcp.streamable_http_app(),
which carries none of the middleware. See CLAUDE.md, Transport Modes.
What a .env file can (and cannot) configure
The sanzaru command — every subcommand, sanzaru serve included — autoloads a
.env for local development. python-dotenv is a runtime dependency, so this
works in any install, not only under uv sync. Two deliberate limits apply,
because a file found on disk must not be able to redirect credentials, relax
transport security, or change what a run is allowed to cost:
- Only
./.envis read — the directory you run sanzaru in. There is no search of parent directories, so a.envat your project root is not found when you run from a subdirectory. - Only sanzaru's documented configuration keys load (API keys, media paths,
storage backend credentials, tuning knobs), matched exactly and
case-sensitively. Everything else in the file is ignored with a warning naming
the keys. Notably ignored on purpose:
DATABRICKS_HOSTand any*_BASE_URL/proxy variable (they decide where credentials are sent); every HTTP-security variable —SANZARU_HTTP_TOKEN,SANZARU_ALLOW_UNAUTHENTICATED_HTTP,SANZARU_ALLOWED_HOSTS,SANZARU_ALLOWED_ORIGINS,SANZARU_IDENTITY_HEADER,SANZARU_REQUIRE_USER_CONTEXT(a planted file must not weaken or satisfy transport auth, nor pick whose identity is trusted);SANZARU_RUN_SECRET(the signing key);SANZARU_REALTIME_PRICE_*(the price table is what--max-costis enforced against — a planted0,0,0,0,0,0would make every turn free); andDATABRICKS_VIDEO_DIR/_IMAGE_DIR/_AUDIO_DIR(joined into the volume path unsanitized, so..in one walks into another tenant's files).
Anything the allowlist skips still works exported in the real environment, or
injected explicitly with npx dotenv-cli -- <command> — both are deliberate
operator actions rather than a file discovered on disk.
Storage Backends
| Backend | Config | Use Case |
|---|---|---|
| Local (default) | SANZARU_MEDIA_PATH=/path/to/media |
Development, local deployments |
| Databricks | STORAGE_BACKEND=databricks |
Databricks Apps with Unity Catalog Volumes |
The Databricks backend supports per-user storage isolation via the user_context module, enabling multi-tenant deployments where each user's media is stored under their own volume prefix (<local-part>-<hash>, injective over email addresses; the prefix format changed after 0.10.0 — see CLAUDE.md for the migration note). In HTTP mode the identity comes from a proxy-injected header, and trusting one is opt-in: set SANZARU_IDENTITY_HEADER (e.g. x-forwarded-email on Databricks Apps) only when a proxy in front of sanzaru both injects that header and strips any client-supplied copy. When it is unset, no header is trusted and every request resolves to the shared volume root. A request carrying the header twice (an appending proxy forwards the client's copy first) or malformed is refused with 400 rather than binding either copy. Set SANZARU_REQUIRE_USER_CONTEXT=1 on a shared deployment so a request with no identity is refused (403 on /media) instead of silently served out of the shared root.
Multi-tenant deployments should also set SANZARU_RUN_SECRET, which signs simulated-podcast run manifests and act checkpoints so --resume refuses bookkeeping this installation did not write.
See CLAUDE.md for full configuration details.
Performance
Fully asynchronous architecture with proven scalability:
- ✅ 32+ concurrent operations verified
- ✅ 8-10x speedup for parallel tasks
- ✅ Non-blocking I/O with
aiofiles+anyio - ✅ Python 3.14 free-threading ready
See docs/async-optimizations.md for technical details.
License
Metadata
Release files for sanzaru 0.12.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sanzaru-0.12.0.tar.gz | 451.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sanzaru-0.12.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 937.8 kB
Release files / sanzaru-0.12.0.tar.gz
| Download URL | sanzaru-0.12.0.tar.gz |
|---|---|
| Size | 451.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
373b06137841051db7d7a1758a2ba0b3a4895b5946e58c89811dc2c153d81099
|
|
BLAKE2b-256 checksum How to use checksums |
a8460499d8ae156bd5c9ad9972e611e0fff0b356290720502ad15039e79c36f3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logRelease files / sanzaru-0.12.0-py3-none-any.whl
| Download URL | sanzaru-0.12.0-py3-none-any.whl |
|---|---|
| Size | 486.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
735ccd0c11c54e0a8f4f794386640b4bedeed964e1d644b94069017781e3c979
|
|
BLAKE2b-256 checksum How to use checksums |
456207654e84aa4401f2ef72035bf2c64db5b5099049417543ad023ec1b737df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency log