Analyze: batched prompts on one audio download
Stacked on #1226 (codex/analyze-m4a). Merge that first, then retarget this PR to main.
What
POST /api/songs/analyze/batch answers many short prompts about one audio file in a single model request. Today each prompt is a separate call, so 36 prompts pay 36 base prices. A batch pays the base price once.
Modal (modal/serve_music_flamingo.py)
- New
generate_batchendpoint (POST,requires_proxy_auth, same class and container asgenerate). - Downloads and, if needed, transcodes the audio once, then runs the prompts one after another on the loaded model. Each prompt is its own conversation using the same audio file.
- The download/transcode step and the inference step are now helper methods (
download_audio,run_prompt) that bothgenerateandgenerate_batchcall.generatebehaves as before, except its temp file is now removed even when inference throws. - Caps: 40 prompts,
max_new_tokens1–2048 per prompt, unique ids. Bad input answers 422 (invalid_batch: ...) before any download.
API
FLAMINGO_GENERATE_BATCH_URLis built fromFLAMINGO_GENERATE_URL. Modal names each endpoint's host after its method, with underscores turned into hyphens, so the URL ends in-generate-batch.modal.run.- Request flow: validate (JSON → auth → body →
verifyAudioUrl) → credit gate on one base price → call Modal → charge once → 200. - The route sets
maxDuration = 800, above Modal's 600 s class timeout, because the prompts run one after another.
Request
{
"audio_url": "https://example.com/song.mp3",
"prompts": [
{ "id": "genre", "prompt": "What genre is this?", "max_new_tokens": 64 },
{ "id": "tempo", "prompt": "What is the tempo in BPM?" }
],
"temperature": 0.2,
"top_p": 1,
"do_sample": false
}
prompts: 1–40 items.idis 1–64 characters and must be unique.promptis 1–4,000 characters.max_new_tokensis 1–2048 (default 400).temperature: 0–2 (default 0.2).top_p: 0–1 (default 1).do_sample: default false.
Response (200, flat)
{
"status": "success",
"answers": [
{ "id": "genre", "response": "...", "elapsed_seconds": 1.4 },
{ "id": "tempo", "response": "...", "elapsed_seconds": 0.6 }
],
"elapsed_seconds": 3.1,
"prompts": 2
}
Errors:
- 400: invalid body, with
missing_fields. - 401: not authenticated.
- 402: insufficient credits.
- 422:
audio_urlis unreachable or not audio. - 502: Modal rejected the batch. The body carries
{ status: "error", error, upstream_status, detail }. - 500: anything else.
Pricing
One base price (FLAMINGO_BASE_CREDITS, $0.05) plus usdToCredits(total_seconds × FLAMINGO_USD_PER_SECOND), charged once after the batch succeeds (creditsForFlamingoBatch, chargeForFlamingoBatch). Example: a 90-second batch is 154,940 credits; zero seconds is the base alone. The ledger row has the same fields as a single call: source api, provider modal, the model id, and resource_url when the audio URL is recorded. The usage ledger has no description column, so the prompt count only appears in the charge-failure logs. If Modal rejects the batch, nothing is charged.
Tests
54 new vitest tests in lib/flamingo/__tests__/:
- Schema (18): limits, defaults, unique ids.
- Validation (6): 400, 401, 422, and the order of the checks.
callFlamingoGenerateBatch(10): request body, URL, proxy headers, non-OK →FlamingoServiceErrorwith the status, bad response shapes.- Credits (4): one base plus seconds; zero, negative and non-finite durations price at the base.
- Charge (4): ledger fields, failures logged instead of thrown.
- Process (4): charges once on the total seconds, omits a private audio URL, charges nothing on an upstream rejection.
- Handler (8): 200 shape, gate on one base, 402, gate throwing → 500, 400 before the gate, 502 upstream, 500 other.
Checks:
npx vitest run lib/flamingo: 214/214 pass.eslint(--max-warnings=0),prettier --checkandtsc -p tsconfig.build.jsonare clean.- The Modal file parses with
ast. Its batch validation, single download, answer order and temp-file cleanup (including when a prompt fails) were exercised locally with Modal and FastAPI stubbed out.
Not verified
modal deployhas not been run. The batch endpoint has not been called on a real GPU.- Timing: a batch at the full caps (40 prompts × 2048 tokens) could exceed Modal's 600 s class timeout. Typical short-prompt batches should fit, but this has not been measured.
🤖 Generated with Claude Code
Summary by cubic
Adds POST /api/songs/analyze/batch, which answers up to 40 short prompts about one audio file in a single model request. Today each prompt is a separate call paying the five-cent base price; a batch pays the base once, plus the batch's total seconds, and is charged after the batch succeeds.
Modal
- New
generate_batchendpoint downloads and transcodes the audio once, then runs each prompt sequentially as its own conversation. - Download, transcode, and inference became shared helpers;
generatenow removes its temp file even when inference throws. - Invalid input answers 422 before any download; caps are 40 prompts, 1–2048
max_new_tokensper prompt, and unique prompt ids. - Deploy hasn't been run; a full-size batch (40 × 2048 tokens) may exceed the 600s class timeout.
API and pricing
- The flow validates JSON → auth → body → audio URL, gates the balance on one base price (402 when short), and charges nothing when Modal rejects the batch (502 with
upstream_statusanddetail). maxDurationis 800s, above Modal's 600s timeout, because prompts run one after another.- 54 new vitest tests cover schema, validation, credits, charging, and the handler; lint, format, types, and the
lib/flamingosuite pass.