to complement your fries and gravy
Generate images from the command line. The default image model is Nano Banana
2.1 (nano-banana-2.1; Replicate google/nano-banana-2.1, OpenRouter
google/gemini-nano-banana-2.1). GPT Image 2.5 Flare stays available with
-model gpt-image-2.5 (direct OpenAI, Replicate, or OpenRouter). Also generates
images/videos/music/speech via Replicate-hosted models such as
Seedance 2.5 and Grok Imagine Video 1.5 (video), Kling 3.0, ElevenLabs Music
(-model music), MiniMax Music 2.6 (-model music-vocal), Stable Audio 2.5
(-model sfx) for sound effects, Gemini 3.1 Flash TTS (-model tts),
MiniMax Speech 2.8 HD (-model tts-minimax) and ElevenLabs v3
(-model tts-elevenlabs) for text-to-speech (OpenAI's gpt-4o-mini-tts via
-model tts-openai; with an ElevenLabs key, -model tts-elevenlabs and
-model music call the ElevenLabs API directly), and the talking-head pair Kling Avatar 2.0
(-model kling-avatar) and Sync Labs lipsync-2-pro (-model lipsync). Also
wraps Replicate's bria/remove-background
for one-shot transparent-PNG cutouts (-model remove-bg) and
prunaai/p-image-upscale / nightmareai/real-esrgan / topazlabs/image-upscale
for super-resolution upscaling (-model upscale / upscale-esrgan /
upscale-pro), and curds run OWNER/MODEL key=value … drives any other
Replicate model with raw inputs.
Logs upstream progress in colorized
logfmt. When prompt or token is missing
curds clears the screen and drops into a Bubble Tea TUI with a CURDS
banner, multiline prompt, spinner, scrolling log panel, and a "generate
another?" loop after each render.
- Go 1.26+ for building (curds uses generics, structured logging, and
recent stdlib helpers; older toolchains will refuse to compile).
Install via go.dev/dl, Homebrew (
brew install go), asdf, or your distro's package manager. - macOS, Linux, or Windows.
-openusesopenon macOS,xdg-openon Linux, andcmd /C starton Windows. - A terminal that supports ANSI colors and Unicode box drawing for the best TUI experience (any modern terminal qualifies — iTerm2, Alacritty, Kitty, Windows Terminal, GNOME Terminal, etc.).
- An API token from at least one provider:
- OpenAI — needs API Organization Verification to call gpt-image-2.5.
- OpenRouter — mapped image, video, and speech models.
- ElevenLabs — direct v4 speech, account voices, and music.
- Replicate — alternative backend, no verification dance.
- xAI — native Grok Imagine Video (recommended for video; cheaper than the Replicate wrapper).
- Network access to
api.openai.com,api.replicate.com, and/orapi.x.ai,openrouter.ai, and/orapi.elevenlabs.io(plusreplicate.delivery/vidgen.x.aifor downloading rendered assets).
./install.sh # builds and installs to /usr/local/bin or ~/.local/binOr build manually:
go build -o curds ./cmd/curdsCurds uses CalVer tags in the form v0.YYYYMMDD.N: the UTC release date
followed by a zero-based same-day release number. For example,
v0.20261008.0 is the first release on October 8, 2026; another release that
day would be v0.20261008.1. The v0 prefix keeps the current Go module path
compatible with Go's version rules.
go install github.com/gersham/curds/cmd/curds@v0.20261010.0
curds -versionEarlier tags such as v0.8.0 remain available. See the
changelog and release process.
curds -prompt "a watercolor fox in a meadow"Or with no flags at all to land in the TUI. On first run curds writes a
default config to ~/.config/curds/config.toml and saves output to
~/Desktop/curds/<unix_milli>.webp for images, or .mp4 for video models.
skill/ ships a standalone Claude Code / agent skill that
teaches an agent to drive the curds CLI: assemble the right flags, run one
command, verify the artifact, and report — without rewriting the prompt or
launching extra jobs. It's self-contained (SKILL.md + reference.md, no
external references), so installing curds and installing the skill go together.
If you install curds for use by a coding agent, also install this skill — copy the directory into your skills location and the agent will know how to call curds for images, edits, video, and background removal:
# Skip this if you already have a `curds` skill installed — don't clobber it.
test -d ~/.claude/skills/curds || cp -R skill ~/.claude/skills/curds~/.claude/skills/curds is the per-user Claude Code location; adjust the
destination for your agent runtime. The guard above leaves any pre-existing
curds skill untouched.
Triggered when prompt or token is missing (suppress with -no-tui).
- Clears the screen and renders the CURDS banner.
- Captures provider+token via a one-shot form when none is configured; optionally writes the token back to the config file.
- Multiline prompt input.
ctrl+dsubmits,ctrl+cquits. - During generation: spinner + a bordered, scrolling log panel showing
every upstream event live (
generation.started,openai.request,image.received, …). - After generation: shows the saved path(s) and elapsed time.
Press
gto clear the prompt and generate again,qto quit.
Resolution priority (first non-empty wins):
-tokenflag[tokens]section of~/.config/curds/config.toml.envin the current working directory- process environment (
OPENAI_API_KEY,REPLICATE_API_TOKEN,XAI_API_KEY,ELEVENLABS_API_KEY,OPENROUTER_API_KEY)
If no token is found at startup, curds opens the TUI and offers to capture one and (optionally) save it to the config.
The ElevenLabs key is optional. Add it to the config:
[tokens]
elevenlabs = "sk_..."or export ELEVENLABS_API_KEY (or put it in .env). With it,
-model tts-elevenlabs and -model music run on the ElevenLabs API instead
of Replicate; see ElevenLabs direct.
Curds selects the provider per model: a native OpenAI, ElevenLabs, or xAI
key first, then Replicate, then OpenRouter for a mapped model.
ElevenLabs speech prefers v4: direct ElevenLabs → OpenRouter → Replicate
v3, so a configured Replicate key never displaces available v4. For example,
with OpenRouter + Replicate + ElevenLabs keys, GPT Image 2.5 and FLUX/video
use Replicate, Gemini 3.8 speech uses OpenRouter, and ElevenLabs speech/music
use ElevenLabs directly. Gemini speech also prefers 3.8 on OpenRouter over
the older 3.1 Replicate fallback. Leave provider = "" to enable this selection.
-provider openai|openrouter|replicate|xai|elevenlabs pins a route. A nonempty
config provider also pins it; the CLI flag wins. An incompatible provider
and model fail locally. When MP4 output has no explicit model, curds uses
default_video_model (seedance-2.5) with Replicate or OpenRouter; native
Grok is the fallback when only an xAI video key is available.
Recommended baseline: OpenRouter + Replicate. Add an ElevenLabs key for custom/account voices, voice discovery, and direct music. V4 speech is already available through OpenRouter; the ElevenLabs key is optional for it.
provider = "" # choose automatically for each model
[tokens]
openrouter = "YOUR_OPENROUTER_KEY"
replicate = "YOUR_REPLICATE_KEY"
# Optional for account voices and direct speech/music:
# elevenlabs = "YOUR_ELEVENLABS_KEY"These are the automatically selected routes with only the column's keys configured and no provider pin. OR = OpenRouter, Rep = Replicate, EL = ElevenLabs; — = unavailable with those keys.
| Generation / method | OpenRouter only | Replicate only | OpenRouter + Replicate | OpenRouter + Replicate + ElevenLabs |
|---|---|---|---|---|
| GPT Image 2.5 Flare / Sunburst | OR | Rep | Rep | Rep |
| GPT Image 2, FLUX.2/3, Nano Banana 2/2.1; reference edits | OR | Rep | Rep | Rep |
| Seedance, Kling, MiniMax H3, Grok 1.5 video | OR | Rep | Rep | Rep |
| Native Grok video model | OR | — | OR | OR |
Gemini speech (tts) |
OR 3.8 | Rep 3.1 | OR 3.8 | OR 3.8 |
| MiniMax speech | OR | Rep | Rep | Rep |
| ElevenLabs speech | OR v4 | Rep v3 | OR v4 | EL v4 |
Music (music) |
— | Rep | Rep | EL |
| Songs with lyrics / sound effects | — | Rep | Rep | Rep |
| Avatars / lip-sync | — | Rep | Rep | Rep |
| Background removal / upscaling | — | Rep | Rep | Rep |
| ElevenLabs account voices / voice lookup | — | — | — | EL |
Raw inputs (curds run) |
— | Rep | Rep | Rep |
OpenRouter + Replicate covers every generation type in the table. OpenAI speech and masked image edits require an additional OpenAI key. An xAI key optionally enables the direct native Grok route. Provider-specific limits still apply; see the complete model mappings and route limits.
| Provider | Default model | Endpoint |
|---|---|---|
| openai | gpt-image-2.5 (gpt-image-2.5-flare; Sunburst via -model gpt-image-2.5-sunburst); speech: gpt-4o-mini-tts (-model tts-openai), tts-1-hd |
/v1/images/generations (or /v1/images/edits with -input-image); /v1/audio/speech for TTS |
| openrouter | image: google/gemini-nano-banana-2.1; mapped image, video, and speech models, including ElevenLabs v4 |
POST /api/v1/images, POST /api/v1/videos + polling/content, POST /api/v1/audio/speech |
| replicate | image: google/nano-banana-2.1; video: bytedance/seedance-2.5 (bytedance/seedance-2.0 via -model seedance-2; talking heads: kwaivgi/kling-avatar-v2, sync/lipsync-2-pro); audio: elevenlabs/music, minimax/music-2.6, stability-ai/stable-audio-2.5; speech: google/gemini-3.1-flash-tts, minimax/speech-2.8-hd, elevenlabs/v3; upscale: prunaai/p-image-upscale, nightmareai/real-esrgan, topazlabs/image-upscale |
/v1/models/<owner>/<name>/predictions (sync via Prefer: wait); curds run posts raw inputs to the same endpoint |
| xai | video: grok-imagine-video |
POST /v1/videos/generations + GET /v1/videos/{request_id} (async polling) |
| elevenlabs | speech: eleven_v4 (-tts-model to change); music: music_v2_5. Picked automatically for -model tts-elevenlabs / -model music when an elevenlabs token resolves |
POST /v1/text-to-speech/{voice_id}, POST /v1/music; curds voices reads GET /v2/voices and GET /v1/shared-voices |
ElevenLabs is never auto-detected as the general provider: it serves only
those two models, and -provider replicate keeps them on Replicate even when
the key is set.
Pass -input-image (repeatable or comma-separated, up to 16). curds
switches to OpenAI's /v1/images/edits endpoint, OpenRouter's
input_references, or the model's Replicate reference field automatically. JPEG inputs are transparently
re-encoded as PNG for the OpenAI edits endpoint — some camera JPEGs
(e.g. straight off an iPhone) are otherwise rejected with
invalid_image_file.
# Compose from two references
curds -input-image body-lotion.png,bath-bomb.png \
-prompt "Relax & Unwind gift basket"
# Mask-driven inpainting (OpenAI only; mask must be PNG with alpha)
curds -input-image lounge.png -mask mask.png \
-prompt "indoor lounge with flamingo in pool"Nano Banana 2.1 is the default (nano-banana-2.1). It routes to Replicate,
then OpenRouter. Select -model gpt-image-2.5 for Flare, or sunburst for
the larger GPT Image 2.5 model. Both GPT Image 2.5 models work through OpenAI,
Replicate, and OpenRouter. -provider openai without -model uses Flare,
because OpenAI does not serve Nano Banana. -model flux-3 is explicit.
The nano-banana-2, flux-2-pro, and gpt-image-2 names keep their original
versions. An existing config file keeps its default_model.
| Model | Resolution flag | Reference images | Notes |
|---|---|---|---|
nano-banana-2.1 |
-image-resolution 1k/2k/4k |
14 | Current Google image/editing model; PNG/JPEG. |
flux-3 |
-image-resolution 768sq/1k/1.5k/2k/4k |
10 | Separate FLUX.3 contract; PNG/JPEG/WebP. |
gpt-image-2.5, sunburst |
-size WxH |
16 | OpenAI supports custom sizes; Replicate accepts its enumerated size set. |
For a generated transparent asset, use GPT Image 2.5 with
-background transparent -output-format png (WebP also supports alpha).
JPEG is rejected for transparent output. Masked edits require direct OpenAI.
For quality-first recommendations, current model gaps, and a live comparison of six image models, see the October 2026 model evaluation.
gpt-image-2.5 (-model gpt-image-2.5, OpenAI id gpt-image-2.5-flare) is
fast everyday GPT Image 2.5. Sunburst
(-model gpt-image-2.5-sunburst or -model sunburst, OpenAI id
gpt-image-2.5-sunburst) is the larger 2.5 model — higher quality, longer
generation. Both take the same request surface as gpt-image-2 — the same
-size / -aspect-ratio grid, -quality (low / medium / high /
xhigh / max / auto), -background, -moderation, -output-format
and -output-compression. gpt-image-2 (-model gpt-image-2) remains
selectable for the previous generation's look.
Two Replicate-hosted models cover cases the OpenAI models handle poorly:
-model |
Use it for |
|---|---|
flux-2-pro |
Cheap, fast bulk generation with reference-image control (~$0.015/gen) |
nano-banana-2 |
Character consistency across frames, multi-image compositing, cheap 4K |
Both size their output differently from gpt-image-2, so they share a flag:
-image-resolution — 0.5mp, 1mp, 2mp, 4mp, or match_input_image
for FLUX.2; 1k, 2k, or 4k for Nano Banana 2.
# FLUX.2 [pro], 2 megapixels, 16:9
curds -model flux-2-pro -aspect-ratio 16:9 -image-resolution 2mp \
-prompt "a watercolor fox in a meadow" -output /tmp/fox.png
# Nano Banana 2, 4K composite from two references
curds -model nano-banana-2 -image-resolution 4k \
-input-image hero.png,logo.png \
-prompt "hero shot with the logo on the bottle" -output /tmp/comp.png
# FLUX.2 with an explicit pixel size (edges 256-2048)
curds -model flux-2-pro -size 1024x768 -prompt "a brass astrolabe"Both take reference images through -input-image (not -reference-image),
produce one image per request (-number-of-images must be 1), and ignore
-quality, -background, and -moderation. Neither accepts -mask. FLUX.2
supports -seed; Nano Banana 2 does not. Nano Banana 2 emits PNG or JPEG only
— curds switches a webp default to PNG for it, retargeting the -output
extension if needed.
For MP4 output with no -model, curds uses default_video_model — Seedance
2.5 (bytedance/seedance-2.5, provider replicate), which leads on
reference handling (30 images / 10 videos / 10 audios), multi-scene
continuity, native audio, and clips up to 30s. Set default_video_model = "seedance-2" or pass -model seedance-2 when you need 1080p — the one
resolution 2.5 dropped. When no replicate token is available but an xai
token is, curds falls back to xAI's native Grok Imagine Video. Kling 3.0,
MiniMax H3, and the Grok 1.5 wrapper are selectable with -model.
# Text-to-video (default; no input image required)
curds -prompt "a cinematic 20 second shot of a glass sculpture forming" \
-aspect-ratio 16:9 -video-duration 20 -output /tmp/seedance.mp4Seedance-specific support:
-input-image— one entry is the first frame; multiple become reference images (up to 30 total with-reference-image).-last-frame-image— requires exactly one-input-image.-reference-video/-reference-audio— up to 10 each.-video-duration—-1for intelligent duration, or4through30seconds. Default:5.-video-resolution—480por720p. Default:720p. 2.5 has no 1080p;-model seedance-2and-model kling-v3do.-no-audio— disables synchronized generated audio.-seed— optional deterministic seed.-aspect-ratio—16:9,4:3,1:1,3:4,9:16,21:9, oradaptive(2.5 dropped 2.0's9:21). Default:16:9.
The previous Seedance, still selectable. Same request surface, a narrower
reference set (9 images, 3 videos, 3 audios), durations of -1 or 4-15
seconds, 9:21, and 1080p via -video-resolution 1080p.
Text-to-video or image-to-video with start/end frames, plus native audio with
lip-synced dialogue. -video-resolution maps onto Kling's mode:
720p → standard, 1080p → pro, 4k → 4k.
curds -model kling-v3 -input-image start.png -last-frame-image end.png \
-prompt "the character turns and speaks to camera" \
-video-resolution 1080p -video-duration 10 -output /tmp/kling.mp4Kling 3.0 supports:
-input-image— optional start frame (0 or 1).-last-frame-image— optional end frame; requires one-input-image.-video-duration—3through15seconds. Default:5.-video-resolution—720p,1080p, or4k. Default:1080p.-aspect-ratio—16:9,9:16, or1:1. Default:16:9.-no-audio— disables Kling's generated audio (on by default in curds).
No reference images/videos/audio and no -seed — use seedance-2 for those.
Used automatically for MP4 when no replicate token is available. The native
API is roughly half the Replicate cost at 720p and supports
text-to-video (image optional), reference images, 480p/720p, and durations up to
15s. It is asynchronous: curds submits the job, polls
GET /v1/videos/{request_id}, then downloads the rendered MP4. Audio is
generated automatically.
# Text-to-video (no input image)
curds -prompt "a slow serene time-lapse of the milky way" -output /tmp/sky.mp4
# Image-to-video from a reference image, 720p, 10s
curds -provider xai -input-image still.png \
-prompt "a smooth product turn with soft studio camera motion" \
-video-resolution 720p -video-duration 10 -output /tmp/xai.mp4Native grok-imagine-video supports:
-input-image— optional image-to-video source (0 or 1).-reference-image— additional reference image(s).-video-duration—1through15seconds. Default:5.-video-resolution—480por720p. Default:720p.-aspect-ratio—auto,1:1,16:9,9:16,4:3,3:4,3:2, or2:3.autois omitted from the request so the API derives it (from the source image, for image-to-video). Default:auto.
1080p is not supported by this native model. Curds rejects it before a
request; it never silently lowers resolution or switches models. Choose
-provider replicate -model seedance-2 -video-resolution 1080p explicitly
when that model suits the job. xAI documents native 1080p for the distinct
grok-imagine-video-1.5 model, not this adapter
(provider resolution documentation).
H3 is multimodal: text-to-video, first- and/or last-frame image-to-video, and
reference-guided generation from images, videos, or audio. 4–15s at 768P
($0.08/s) or 2K ($0.13/s).
# Text-to-video
curds -model minimax-h3 -prompt "a slow serene time-lapse of the milky way" \
-output /tmp/sky.mp4
# First-frame image-to-video, 2K, 10s
curds -model minimax-h3 -input-image still.png \
-prompt "a smooth product turn with soft studio camera motion" \
-video-resolution 2k -video-duration 10 -output /tmp/h3.mp4MiniMax H3 supports:
-input-image— optional first frame (0 or 1) →first_frame_image.-last-frame-image— optional last frame; requires one-input-image.-reference-image— up to 9 reference images.-reference-video— up to 3 reference videos.-reference-audio— up to 3 reference audio clips.-video-duration—4through15seconds. Default:5.-video-resolution—768por2k(case-insensitive). Default:768p.-aspect-ratio—adaptive,21:9,16:9,4:3,1:1,3:4, or9:16. Default:16:9. There is noauto; image-to-video usesadaptive.
H3 has no -seed and no audio toggle (-no-audio is rejected); use
-strip-audio to get a silent clip.
The Replicate wrapper is image-to-video only, so pass exactly one
-input-image. Selectable via -provider replicate -model grok-imagine-video-1.5.
curds -provider replicate -model grok-imagine-video-1.5 -input-image product.png \
-prompt "a smooth product turn with soft studio camera motion" \
-output /tmp/grok.mp4Grok Imagine Video 1.5 supports:
-video-duration—1through15seconds. Default:5.-video-resolution—480por720p. Default:720p.-aspect-ratio—auto,16:9,4:3,1:1,9:16,3:4,3:2, or2:3. Default:autounless overridden with-aspect-ratio.
In prompts, refer to Seedance references as [Image1], [Video1],
[Audio1], etc.
Two Replicate models whose input is media rather than a prompt:
kling-avatar— Kling Avatar 2.0 (kwaivgi/kling-avatar-v2). One portrait (-input-image) plus one audio clip (-audio: mp3, wav, m4a, aac) become a lip-synced talking head, ~5 minutes per render.-promptis optional (actions/emotion/camera);-video-resolutionmaps onto its mode —720p→std,1080p→pro(the default). Pro renders 1080p.lipsync— Sync Labs lipsync-2-pro (sync/lipsync-2-pro). Re-animates the mouth in an existing video (-input-video, mp4) to match new audio (-audio, wav). No prompt. Optional-sync-mode(loop,bounce,cut_off,silence,remap; defaultloop),-sync-temperature(0–1, default 0.5), and-active-speaker.
# Animate a portrait with an audio clip
curds -model kling-avatar -input-image portrait.png -audio voice.mp3 \
-prompt "she smiles and explains the product" -output /tmp/avatar.mp4
# Lip-sync an existing clip
curds -model lipsync -input-video clip.mp4 -audio voice.wav \
-sync-mode loop -output /tmp/lipsync.mp4Notes:
-output-formatis forced to mp4;-aspect-ratiois ignored (the portrait or source video defines the frame).-strip-audiodefaults to false for these two models — the generated audio is the point. Pass-strip-audioexplicitly to silence a clip.-crop-captionscrops the result to the top 74% of the frame (centered horizontally), removing the caption band some avatar/lip-sync models burn into the bottom. ffmpeg re-encodes the video at crf 16 and copies the audio; without ffmpeg it logs a skip and leaves the file alone.-no-fallbackdisables Seedance's automatic face-rejection retry (see below); it is irrelevant for these models.
Both Seedance versions (bytedance/seedance-2.5 and bytedance/seedance-2.0)
reject any input image containing a realistic human face
(flagged as sensitive (E005)), where Kling 3.0 accepts the same frames.
When a Seedance prediction fails that way and the request carried an image
(-input-image, -last-frame-image, or -reference-image), curds retries
once on Kling 3.0: same prompt, first image as start_image, last frame as
end_image, duration clamped to 3–15s, -video-resolution mapped
(480p/720p → standard, 1080p → pro, 4k → 4k), and -aspect-ratio passed
through when Kling supports it (else 16:9). The retry is logged as
event=model.fallback from=… to=… reason=seedance_rejected_face.
Pass -no-fallback to disable it; curds then returns the original error and
logs hint="retry with -model kling-v3".
Three Replicate audio models are wired in, all prompt-driven, saving mp3 or
wav:
music— ElevenLabs Music (elevenlabs/music), the default music model. A score, cue, or loop from-prompt, instrumental by default (-instrumental=falsefor vocals), and it honors the requested length exactly:-duration5–300s (default 10) goes upstream asmusic_length_ms. With an ElevenLabs key it runs on the ElevenLabs API instead (music_v2_5,-duration3–600s); see ElevenLabs direct.music-vocal— MiniMax Music 2.6 (minimax/music-2.6, aliasminimax-music). A full song with vocals.-promptsets the style;-lyrics TEXTor-lyrics @song.txtsupplies the words (newlines and[Verse]/[Chorus]tags pass through), and-lyricsalso works with no-prompt. Without lyrics the model writes them from the prompt.sfx— Stable Audio 2.5 (stability-ai/stable-audio-2.5). Sound effects, ambience, and short music cues, 1–190s (-duration, default 10), with an optional-seed.
# Score: a 45-second instrumental cue with an exact length
curds -model music -duration 45 \
-prompt "tense minimal synth score, slow build, no drums" -output /tmp/score.mp3
# Song with lyrics read from a file
curds -model music-vocal -lyrics @song.txt -duration 60 \
-prompt "warm indie-folk duet, acoustic guitar and brushed drums" -output /tmp/song.mp3
# 8-second ambience sound effect
curds -model sfx -duration 8 \
-prompt "steady heavy rain on a tin roof, distant thunder" -output /tmp/rain.wavNotes:
-output-formatismp3by default for audio, orwavwhen-outputends in.wav. Anmp3/wavoutput without an audio-modelis rejected locally.- MiniMax Music 2.6 ignores the requested length upstream (it renders 2–3
minutes), so when
-durationis set curds trims the downloaded render with a 2s fade-out via ffmpeg (event=audio.trimmed). Without ffmpeg it logsaudio.trim_skippedand keeps the full render. - Stable Audio 2.5 picks its own container. When it differs from the requested
one, curds transcodes via ffmpeg if available, and otherwise saves the
container that came back under its own extension with an
audio.format_mismatchwarning. -lyricsis music-vocal only; anywhere else it is a usage error.-aspect-ratio,-size,-quality,-background, and-moderationare not sent to audio models.-seedis supported bysfxonly.
TTS models read -prompt (or stdin) aloud, all saving mp3 or wav:
tts— Gemini 3.8 Flash TTS through OpenRouter when configured, with Gemini 3.1 Flash TTS on Replicate as the fallback.-voiceselects a preset (defaultKore), and-instructionssteers delivery. Curds retains a conservative 4000-byte cap for both text and instructions. OpenRouter returns MP3; the Replicate 3.1 route returns WAV. Container conversion uses ffmpeg when the requested format differs.- Explicit Gemini versions are
tts-gemini-3.8,tts-gemini-3.8-lite(OpenRouter), andtts-gemini-3.1(Replicate/OpenRouter). A pinned version never silently falls back to another version. tts-minimax— MiniMax Speech 2.8 HD (minimax/speech-2.8-hd). Natural narration from up to 10000 characters (with<#0.5#>pause markers).-voicetakes any system voice id or a cloned id (defaultEnglish_Wiselady);-emotion(defaultauto),-speed(0.5–2), and-pitch(−12..12) tune the delivery. curds asks for 44.1 kHz output (256 kbps mp3) and turns on English normalization so numbers and dates read naturally.tts-elevenlabs— ElevenLabs v4 whenever accessible: direct ElevenLabs first, OpenRouter second, and Replicate v3 only when neither v4 route has a key (or Replicate is explicitly selected). V4 interprets audio tags such as[whispers]and supports-stability/-similarity. On OpenRouter,-voiceis a preset name (defaultgeorge); direct ElevenLabs accepts account voice IDs/names (default George). The Replicate v3 fallback uses its own voice enum (defaultRachel), supports-style/-speed, and reads bracketed markers aloud. See ElevenLabs direct.tts-openai— OpenAI gpt-4o-mini-tts, served by theopenaiprovider'sPOST /v1/audio/speech.-instructionssteers accent and tone ("crisp British RP, dry");-voicepicks from the OpenAI enum (defaultsage).tts-1-hdis the earlier model on the same endpoint and takes no-instructions. Text is capped at 4096 characters.
# Gemini: 3.8 on OpenRouter, with 3.1 on Replicate as fallback
curds -model tts -voice Kore -instructions "warm and slow, British accent" \
-prompt "The results, I'm afraid, are conclusive." -output /tmp/line.wav
# MiniMax narration line with a voice and emotion
curds -model tts-minimax -voice English_Deep-VoicedGentleman -emotion calm \
-prompt "Chapter one. The rain had not stopped for a week." -output /tmp/line.mp3
# ElevenLabs v4 automatically: direct ElevenLabs, then OpenRouter
curds -model tts-elevenlabs -stability 0.4 \
-prompt "[warmly] Welcome back." -output /tmp/welcome.mp3
# Explicit legacy v3 on Replicate (bracketed markers are spoken aloud)
curds -provider replicate -model tts-elevenlabs -voice Rachel -style 0.8 \
-prompt "Oh, brilliant. Another Monday." -output /tmp/line.mp3
# OpenAI with delivery instructions
curds -model tts-openai -voice sage -instructions "crisp British RP, dry" \
-prompt "The results, I'm afraid, are conclusive." -output /tmp/line.wav
# Read a whole script from a file
cat script.txt | curds -model tts-minimax -output /tmp/narration.wavNotes:
ttsprefers OpenRouter 3.8, then Replicate 3.1;tts-minimaxprefers Replicate, then OpenRouter.tts-elevenlabsuses direct ElevenLabs v4, then OpenRouter v4, then Replicate v3.tts-openai/tts-1-hdneed an OpenAI token. An explicit incompatible-provider/-modelpair is rejected locally.-voiceis validated against the selected route's voices. Direct ElevenLabs and MiniMax accept account/cloned IDs; OpenRouter ElevenLabs uses preset names, and Replicate ElevenLabs uses its legacy voice enum.- Flags that do not apply to the chosen model are usage errors (exit 2):
-emotion/-pitchbelong totts-minimax,-stability/-styletotts-elevenlabs, and-instructionstottsandtts-openai. -speedhas a per-model range:tts-minimax0.5–2, Replicate ElevenLabs v3 0.7–1.2,tts-openai0.25–4. ElevenLabs v4 and Gemini have no speed knob; use v4 audio tags or Gemini-instructions.0means model default.- List MiniMax voices with
curds run -schema minimax/speech-2.8-hd, and Gemini voices withcurds run -schema google/gemini-3.1-flash-tts.
Gemini voices: Achernar, Achird, Algenib, Algieba, Alnilam, Aoede, Autonoe, Callirrhoe, Charon, Despina, Enceladus, Erinome, Fenrir, Gacrux, Iapetus, Kore, Laomedeia, Leda, Orus, Pulcherrima, Puck, Rasalgethi, Sadachbia, Sadaltager, Schedar, Sulafat, Umbriel, Vindemiatrix, Zephyr, Zubenelgenubi. ElevenLabs voices: Rachel, Drew, Clyde, Paul, Aria, Domi, Dave, Roger, Fin, Sarah, James, Jane, Juniper, Arabella, Hope, Bradford, Reginald, Gaming, Austin, Kuon, Blondie, Priyanka, Alexandra, Monika, Mark, Grimblewood. OpenAI voices: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse, marin, cedar.
With an ElevenLabs key ([tokens] elevenlabs in the config, .env, or
ELEVENLABS_API_KEY), -model tts-elevenlabs and -model music call the
ElevenLabs API directly. Without that key, speech uses OpenRouter v4 if
available, then Replicate v3; music uses Replicate. -provider replicate
explicitly selects the Replicate route.
Every run logs the route it took:
event=elevenlabs.route route=direct model=eleven_v4 reason="elevenlabs token"
event=elevenlabs.route route=openrouter model=elevenlabs/eleven-v4 reason="selected OpenRouter route"
event=elevenlabs.route route=replicate model=elevenlabs/v3 reason="no elevenlabs token"
Text to speech (POST /v1/text-to-speech/{voice_id}):
-tts-modelpicks the ElevenLabsmodel_id. The default iseleven_v4, ElevenLabs' highest-quality model.eleven_v4_turbo,eleven_v3,eleven_multilingual_v2, andeleven_flash_v2_5are also available.-voiceis any voice id (your account's, a premade, or one from the shared library), or the name of a voice in your account, resolved throughGET /v2/voices. Premade names carry a tagline ("George - Warm, Captivating Storyteller"), so-voice Georgematches; an exact full name wins. The default is George (JBFqnCBsd6RMkjVDRZzb). Rachel and the rest of the Replicate enum are legacy voices that current accounts no longer list, so-voice Rachelfails here with a pointer tocurds voices. Using a library voice id makes ElevenLabs add that voice to your account.- Audio tags are interpreted on
eleven_v4andeleven_v3:[whispers],[laughs],[sighs],[strong French accent]direct the delivery instead of being read aloud. Replicate'selevenlabs/v3reads them aloud, so the same script sounds different on the two routes. - Voice settings depend on the model:
-stabilityworks everywhere;-similarityeverywhere buteleven_v3;-styleand-speaker-boostonly oneleven_multilingual_v2;-speed(0.7–1.2) oneleven_multilingual_v2andeleven_flash_v2_5. A setting the model does not take is a usage error that names-tts-model eleven_multilingual_v2.-instructions,-emotion, and-pitchdo not apply. - Text caps:
eleven_v410000 characters,eleven_v35000. - mp3 asks for
mp3_44100_192and steps down tomp3_44100_128when the plan refuses it (192 kbps needs the Creator tier). wav asks forwav_44100(Pro tier), thenwav_24000. Each step logsevent=elevenlabs.format_fallback.
Music (POST /v1/music): model_id music_v2_5, -prompt,
force_instrumental from -instrumental (default true), and
music_length_ms from -duration (3–600s; omit it and the model picks the
length). mp3 asks for mp3_48000_320, then 192 kbps, then
mp3_44100_128. The endpoint has no wav, so a .wav output is transcoded
locally via ffmpeg.
Finding a voice: curds voices lists your account's voices
(GET /v2/voices) and searches the shared library (GET /v1/shared-voices),
one logfmt line per voice with source, id, name, accent, age,
gender, and description. -search, -accent, -gender, and -age
filter both lists server-side; -source account|library picks one;
-limit sets the page size (default 30, max 100).
# Find a British female narrator, then use her
curds voices -search narrator -accent british -gender female
curds -model tts-elevenlabs -voice 3XD5qTvPWht0mpTnYXY0 -stability 0.4 \
-prompt "[whispers] It was a very long day." -output /tmp/line.mp3
# eleven_multilingual_v2 for the full set of voice settings
curds -model tts-elevenlabs -tts-model eleven_multilingual_v2 -voice George \
-style 0.3 -speed 1.1 -speaker-boost -prompt "Chapter one." -output /tmp/ch1.mp3
# Ten minutes of score on the direct route
curds -model music -duration 600 -prompt "slow ambient synth score" -output /tmp/score.mp3
# Keep the Replicate route even with a key
curds -provider replicate -model tts-elevenlabs -voice Rachel -prompt "Hi." -output /tmp/hi.mp3curds run creates one prediction on any Replicate model with exactly the
inputs you give it — no field mapping, no curds defaults:
curds run OWNER/MODEL[:VERSION] key=value ...
# What does this model accept?
curds run -schema sync/lipsync-2-pro
# Upload local media, read the output URL instead of downloading
curds run -json owner/model prompt="blend these" ref=@a.png ref=@b.png
# Name the output (default: <output.directory>/<unix_milli>.<ext>)
curds run -output /tmp/out.mp4 owner/model prompt="a slow pan"Values are sent as JSON: key=5, key=true, key=[1,2], key={"a":1}, and
key="quoted" are sent as that JSON value; anything that does not parse is a
plain string; key=@path uploads a local file as a data URL (http(s) and
data: URLs pass through). Repeat a key to send an array:
ref=@a.png ref=@b.png. One value holding a comma-separated list is not
split — use JSON or repeat the key.
Flags: -output, -schema (print name, type, default, enum, min/max, and
description for each input, then exit), -json (print the final prediction
JSON instead of downloading; also applied automatically when the output has no
URLs, e.g. a text or blob output), -token, -poll-interval, -timeout,
-verbose. Multiple outputs get -1, -2, … before the extension. Saved
paths go to stdout; tokens resolve through the same chain as the main command.
See curds run -h.
-model remove-bg runs bria/remove-background (BRIA RMBG 2.0) on
Replicate. It takes one input image and returns a transparent PNG with a
soft 256-level alpha matte — the original pixels are preserved, only the
background is removed. There's no prompt, no aspect ratio, and no
-quality knob; the output dimensions match the input image.
curds -provider replicate -model remove-bg \
-input-image photo.jpg -output cutout.pngNotes:
- Requires exactly one
-input-image(file path, http(s) URL, ordata:URL). - Output format is forced to PNG so the alpha channel survives.
bria/remove-backgroundis an official Replicate model — no version pin.- For dedicated mask outputs (rather than a finished cutout) you can also
pass any other Replicate model identifier directly via
-model owner/name, but onlybria/remove-backgroundis wired into the validation/format shortcuts.
-model upscale runs prunaai/p-image-upscale (2026-era) on Replicate. It
takes one input image and a -scale factor (1–8, default 4), multiplying each
side, and returns a single upscaled PNG. Like background removal there's no
prompt, no aspect ratio, and no -quality knob — and no face enhancement: use
-model upscale-esrgan or -model upscale-pro for that. -output-format
accepts png (the default), jpeg, or webp.
# Upscale 4x → PNG
curds -provider replicate -model upscale \
-input-image small.jpg -scale 4 -output big.png
# Same upscaler, WebP out
curds -provider replicate -model upscale -output-format webp \
-input-image small.jpg -scale 2 -output big.webpNotes:
- Requires exactly one
-input-image(file path, http(s) URL, ordata:URL). - Output format defaults to PNG;
-output-format jpegwrites.jpgbytes. -scaleaccepts 1–8; the default is 4.-face-enhanceis a usage error (exit 2) here.prunaai/p-image-upscaleis an official Replicate model — no version pin.
nightmareai/real-esrgan is the 2021-era upscaler, still selectable as
-model upscale-esrgan. It keeps the numeric -scale factor (1–10, default 4)
and is the one upscaler with GFPGAN face restoration, which helps on portraits
and low-resolution faces.
# Upscale a portrait with face enhancement
curds -provider replicate -model upscale-esrgan -face-enhance \
-input-image headshot.jpg -output headshot-4x.png- One
-input-image, no prompt, PNG out. -face-enhanceruns GFPGAN alongside the upscale.nightmareai/real-esrganis an official Replicate model — no version pin.
Real-ESRGAN dates from 2021 and goes soft on faces and text.
-model upscale-pro runs topazlabs/image-upscale instead, which is
materially better on both.
curds -model upscale-pro -input-image small.jpg -scale 4 -output big.png
# Portrait with face enhancement
curds -model upscale-pro -input-image headshot.jpg -scale 2 -face-enhance-scaleaccepts2,4, or6— omit it for enhance-only (no resize).-face-enhancerequires a-scale; Topaz applies it during the upscale pass.- Same shape as
upscaleotherwise: one-input-image, no prompt, PNG out.
-aspect-ratio accepts these named ratios (mapped to multiples-of-16
sizes for gpt-image-2 / gpt-image-2.5):
| Ratio | OpenAI size | Notes |
|---|---|---|
| 1:1 | 1024×1024 | |
| 3:2 / 2:3 | 1536×1024 / 1024×1536 | |
| 4:3 / 3:4 | 1536×1152 / 1152×1536 | |
| 16:9 | 2048×1152 | ~1080p+ landscape |
| 9:16 | 1152×2048 | ~1080p+ portrait |
| 21:9 / 9:21 | 2688×1152 / 1152×2688 | ultrawide |
| 2:1 / 1:2 | 2048×1024 / 1024×2048 | |
| 16:9-4k / 9:16-4k | 3840×2160 / 2160×3840 |
Replicate's gpt-image-2 wrapper accepts only 1:1, 3:2, 2:3.
Grok Imagine Video 1.5 accepts auto, 16:9, 4:3, 1:1, 9:16,
3:4, 3:2, and 2:3.
Native xAI grok-imagine-video accepts auto, 1:1, 16:9, 9:16,
4:3, 3:4, 3:2, and 2:3.
Seedance 2.5 accepts 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, and
adaptive; Seedance 2.0 also accepts 9:21.
Kling 3.0 accepts 16:9, 9:16, and 1:1.
FLUX.2 [pro] accepts match_input_image, 1:1, 16:9, 3:2, 2:3,
4:5, 5:4, 9:16, 3:4, 4:3 — or -size WxH with edges 256–2048.
Nano Banana 2 accepts match_input_image, 1:1, 1:4, 1:8, 2:3,
3:2, 3:4, 4:1, 4:3, 4:5, 5:4, 8:1, 9:16, 16:9, 21:9.
MiniMax H3 accepts 21:9, 16:9, 4:3, 1:1, 3:4, 9:16, and
adaptive.
Music, sound-effect, and text-to-speech models take no -aspect-ratio
(audio has no frame); -size and -quality are not sent either.
For something custom, pass -size WxH. Anything not on a 16-pixel
boundary is rounded to the nearest valid value (true 1080p 1920×1080
becomes 1920×1088, for example).
~/.config/curds/config.toml (auto-created):
provider = "" # auto; ElevenLabs speech prefers available v4
default_model = "nano-banana-2.1" # Replicate google/nano-banana-2.1; OpenRouter google/gemini-nano-banana-2.1
default_video_model = "seedance-2.5" # model used for MP4 output
[output]
directory = "~/Desktop/curds"
format = "webp"
compression = 90
[tokens]
openai = ""
replicate = ""
xai = ""
elevenlabs = "" # optional: direct ElevenLabs TTS and music
openrouter = "" # optional: mapped image/video/speech models
[defaults]
quality = "auto"
aspect_ratio = "1:1"
background = "auto"
moderation = "auto"
number_of_images = 1
[models.gpt-image-2.5]
openai_name = "gpt-image-2.5-flare"
openrouter_name = "openai/gpt-image-2.5-flare"
[models.gpt-image-2.5-sunburst]
openai_name = "gpt-image-2.5-sunburst"
[models.gpt-image-2]
openai_name = "gpt-image-2"
replicate_name = "openai/gpt-image-2"
[models.flux-2-pro]
replicate_name = "black-forest-labs/flux-2-pro"
[models.nano-banana-2]
replicate_name = "google/nano-banana-2"
[models.minimax-h3]
replicate_name = "minimax/h3"
[models.kling-v3]
replicate_name = "kwaivgi/kling-v3-video"
[models.kling-avatar]
replicate_name = "kwaivgi/kling-avatar-v2"
[models.lipsync]
replicate_name = "sync/lipsync-2-pro"
[models.upscale-pro]
replicate_name = "topazlabs/image-upscale"
[models.grok-imagine-video]
xai_name = "grok-imagine-video"
[models.grok-imagine-video-1.5]
replicate_name = "xai/grok-imagine-video-1.5"
[models.seedance-2.5]
replicate_name = "bytedance/seedance-2.5"
[models.seedance-2]
replicate_name = "bytedance/seedance-2.0"
[models.remove-bg]
replicate_name = "bria/remove-background"
[models.upscale]
replicate_name = "prunaai/p-image-upscale"
[models.upscale-esrgan]
replicate_name = "nightmareai/real-esrgan"Override the path with $CURDS_CONFIG.
Every stage emits a logfmt event to stderr. On a TTY events are colorized;
piped output stays plain logfmt for log collectors. Add -verbose for
debug-level events (request bodies, etc.).
ts=2026-04-25T17:34:00.123Z level=info event=curds.start version=0.2.0
ts=2026-04-25T17:34:00.140Z level=info event=config.loaded path=…
ts=2026-04-25T17:34:00.180Z level=info event=generation.started provider=openai size=2048x1152 prompt_chars=42
ts=2026-04-25T17:34:00.181Z level=info event=openai.request endpoint=… kind=generations
ts=2026-04-25T17:34:08.420Z level=info event=openai.response status_code=200 bytes=987432
ts=2026-04-25T17:34:08.421Z level=info event=image.received index=0 bytes=987432 format=webp
ts=2026-04-25T17:34:08.435Z level=info event=curds.completed images=1 duration_ms=8312 paths=…
Upstream errors are captured at level=error:
ts=… level=error event=openai.api_error status_code=400 body="{ \"error\": { … } }"
Run curds -h for the full list. Highlights:
-prompt— prompt text (or pipe via stdin, or use the TUI)-output— output path (default:~/Desktop/curds/<ms>.<format>)-aspect-ratio/-size— image dimensions-quality/-output-format/-output-compression-input-image— reference image(s), repeatable or comma-separated-mask— mask file for OpenAI edits-video-duration/-video-resolution— video controls-no-audio— Seedance-only audio control-audio/-input-video— talking-head and lip-sync media inputs-sync-mode/-sync-temperature/-active-speaker— lipsync-2-pro knobs-crop-captions— crop the bottom caption band off a generated video-no-fallback— disable the Seedance → Kling 3.0 face-rejection retrycurds run OWNER/MODEL key=value …— raw Replicate passthrough (-schema,-json)-reference-image/-reference-video/-reference-audio— Seedance references-duration/-instrumental/-lyrics— audio controls (-model music,music-vocal,sfx);-lyricsreads@file.txt-voice/-emotion/-speed/-pitch/-instructions/-stability/-style— text-to-speech controls (-model tts,tts-minimax,tts-elevenlabs,tts-openai)-tts-model/-similarity/-speaker-boost— direct ElevenLabs route only (-model tts-elevenlabswith an ElevenLabs key)curds voices [-search TEXT] [-accent A] [-gender G] [-age A]— list account voices and search the ElevenLabs voice library-scale/-face-enhance— upscale factor and face restoration (-model upscale-esrgan/upscale-pro;-face-enhanceis rejected by-model upscale)-provider,-token,-model-open— open generated assets in OS viewer (macOS Preview)-verbose— debug-level logs-no-tui— never enter interactive mode (fail instead)
.
├── cmd/curds/main.go package main — CLI shell
├── cmd/curds/voices.go package main — the `curds voices` subcommand
├── client.go package curds — Client, Request, Result, providers
├── openai.go ↑ OpenAI Image API impl
├── replicate.go ↑ Replicate API impl
├── elevenlabs.go ↑ ElevenLabs API impl (direct TTS, music, voices)
├── log.go ↑ shared logfmt + lipgloss formatter
├── curds_test.go ↑ unit + httptest integration tests
├── config/ package config — TOML, .env, token resolution
├── tui/ package tui — huh-driven interactive form
├── skill/ standalone agent skill (SKILL.md + reference.md)
├── install.sh builds + installs to /usr/local/bin or ~/.local/bin
└── go.mod module github.com/gersham/curds
Personal project. No license attached yet.
