# API Keys Source: https://docs.audiopod.ai/account/api-keys Create and manage your AudioPod AI API keys ## Create Your First Key Already have an API key? Create more via cURL: ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "production-key", "scopes": ["*"]}' ``` **First time?** Follow the [Quick Start](/quickstart) to register and get your first key. 1. Go to [API Keys](https://www.audiopod.ai/dashboard/account/api-keys) 2. Click **Create New API Key** 3. Copy and save your key immediately Keys are only shown once. Save it securely! *** ## Using Your API Key ### Environment Variable (Recommended) ```bash theme={null} export AUDIOPOD_API_KEY="ap_your_key_here" ``` ### SDK Usage ```python theme={null} from audiopod import AudioPod # Auto-reads from AUDIOPOD_API_KEY env var client = AudioPod() # Or pass explicitly client = AudioPod(api_key="ap_your_key_here") ``` ```typescript theme={null} import AudioPod from 'audiopod'; // Auto-reads from AUDIOPOD_API_KEY env var const client = new AudioPod(); // Or pass explicitly const client = new AudioPod({ apiKey: 'ap_your_key_here' }); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=VIDEO_ID" \ -F "mode=six" ``` *** ## Manage Keys via API ```bash theme={null} # List all keys curl -s "https://api.audiopod.ai/api/v1/auth/api-keys" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq . # Create new key curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "staging-key", "scopes": ["*"], "expires_in_days": 90 }' # Revoke a key curl -X DELETE "https://api.audiopod.ai/api/v1/auth/api-keys/KEY_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Best Practices ```bash theme={null} # Development AUDIOPOD_API_KEY_DEV=ap_dev_... # Production AUDIOPOD_API_KEY_PROD=ap_prod_... ``` API keys should only be used server-side. Create a backend proxy if you need client-side access. Create a new key → Update your apps → Revoke the old key. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -d '{"name": "temp-key", "expires_in_days": 7}' ``` *** ## Troubleshooting | Error | Cause | Fix | | - | - | - | | `401 Unauthorized` | Invalid or missing key | Check `X-API-Key` header | | `403 Forbidden` | Key revoked or expired | Create a new key | Test your key: ```bash theme={null} curl -s "https://api.audiopod.ai/api/v1/api-wallet/balance" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq .balance_usd ``` *** ## Next Steps Top up your wallet to start making API calls Make your first API call # API Wallet & Pricing Source: https://docs.audiopod.ai/account/api-wallet Pay-as-you-go pricing. Add funds and pay only for what you use. ## Pricing **The developer API wallet is funded in USD and is separate from Studio subscription or PAYG credits.** Studio's \$1 = 7,500-credit conversion does not apply to the API wallet. See the live [API pricing page](https://audiopod.ai/developers/pricing) for current rates. Text to Speech, Voice Cloning and Audiobook Narration are priced per 1,000 characters of input text. Every other API price below is per minute of audio processed (input duration unless noted). | Service | Rate | \$10 Gets You | | - | - | - | | **Stem Separation — core outputs** | \$0.10/min | 100 min | | **Transcription** | \$0.01/min | 1000 min | | **Text to Speech** | \$0.05 per 1,000 characters | \~200,000 characters | | **Voice Cloning** | \$0.05 per 1,000 characters | \~200,000 characters | | **Voice Conversion** | \$0.13/min | 77 min | | **Speech Translation (Dubbing)** | \$0.40/min | 25 min | | **Speaker Separation** | \$0.20/min | 50 min | | **Music Generation** | \$0.04/min | 250 min | | **Audiobook Narration** | \$0.09 per 1,000 characters | \~111,000 characters | | **Media Conversion** | \$0.01/min | 1000 min | ### How characters are counted Character-priced requests are charged from your prepaid wallet based on the text as written, with a minimum charge of **\$0.01 per text-to-speech request**. * Directing/steering tags and pause tags are **not** counted. * Non-verbal tags such as `[laugh]` **are** counted. * Everything else — letters, spaces, punctuation — counts as written. For example, \$5 covers about 100,000 characters of text to speech or about 55,500 characters of audiobook narration. Minimum top-up: **$1.00**. Maximum: **$10,000**. **Premium variants and modes.** Some services charge a multiplier on the base rate: * **Music Generation — AudioMusic Premium** (`quality="premium"`): **2×** the base rate (\$0.08/min). * **Stem Separation — output groups**: lead + backing vocals cost **$0.15/min together**; male + female vocals cost **$0.15/min together**. Both pairs total **$0.30/min**. Other specialist instrument groups cost **$0.20/min each**; lead + rhythm guitar share one group. Add **\$0.10/min** if you also select core stems. Selecting more outputs within one group does not add another charge. [Group pricing details](/api-reference/stem-splitter#prices-by-output-group). Every standard mode — `single`, `two`, `four`, `six`, `producer`, `studio`, `mastering` — bills at the base \$0.10/min. **TTS billing model.** API wallet bills TTS by the **duration of the generated audio**, not by character count. The per-character rate shown elsewhere in the docs applies to account-credit billing (subscription plans), not to API-wallet billing. **Noise Reduction** API-key calls bill the API price of the chosen `quality_mode` (`balanced`, `studio` or `ultra`). Call `POST /api/v1/denoiser/quote` for the exact cost before you start a job. See [Noise Reduction](/api-reference/noise-reduction#pricing). *** ## Check Balance ```bash theme={null} curl -s "https://api.audiopod.ai/api/v1/api-wallet/balance" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq . ``` ```python theme={null} from audiopod import AudioPod client = AudioPod() balance = client.wallet.get_balance() print(f"Balance: {balance['balance_usd']}") print(f"Spent: {balance['total_spent_usd']}") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod(); const balance = await client.wallet.getBalance(); console.log(`Balance: ${balance.balance_usd}`); console.log(`Spent: ${balance.total_spent_usd}`); ``` **Response:** ```json theme={null} { "balance_cents": 2500, "balance_usd": "$25.00", "total_spent_cents": 1500, "total_spent_usd": "$15.00", "low_balance_warning": false } ``` *** ## Add Funds ```bash theme={null} # Get Stripe payment link ($25) curl -X POST "https://api.audiopod.ai/api/v1/api-wallet/topup/checkout" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"amount_cents": 2500}' ``` Open the returned `url` in your browser to complete payment. 1. Go to [API Keys](https://www.audiopod.ai/dashboard/account/api-keys) 2. Click **Add Funds** 3. Choose amount and complete Stripe checkout **Response:** ```json theme={null} { "url": "https://checkout.stripe.com/c/pay/...", "amount_usd": "$25.00" } ``` *** ## Automatic Top-Up Automatic top-up keeps long-running and unattended integrations from stalling on an empty wallet. Once it is on, we watch your balance as calls come in — the moment it would drop below your **threshold**, we charge a saved card for your chosen **amount** and credit the wallet. The wallet stays prepaid: funds are added *before* they are spent. Nothing is ever billed after the fact, and every automatic charge sends you a receipt by email. ### One-time browser step A card can only be saved from a browser. Card details are entered on a secure hosted form run by our payment provider — they never pass through the AudioPod API, and they can never be sent with an API key. This is a card-industry requirement (and the reason bank verification prompts can appear), not an AudioPod restriction. So arming automatic top-up is a two-part flow: 1. **Once, in a browser** — save a card, either from [API Keys](https://www.audiopod.ai/dashboard/account/api-keys) → **Automatic top-up** → **Add card**, or by calling `POST /auto-topup/payment-method-session` from a signed-in session and opening the returned `url`. 2. **From then on, over the API** — read the configuration, tighten the limits, or switch it off with a plain API key. **API keys have deliberately limited power here.** With an API key you can read the configuration, **lower** any limit, and **disable** automatic top-up. Turning it **on**, **raising** a limit, or saving a card requires a signed-in dashboard session and returns `403` with `detail.error = "dashboard_required"` otherwise. A leaked key can therefore never arm or widen a charge against your card — only shrink or stop one. ### Read the configuration ```bash theme={null} curl -s "https://api.audiopod.ai/api/v1/api-wallet/auto-topup" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq . ``` **Response:** ```json theme={null} { "enabled": true, "threshold_cents": 1000, "threshold_usd": "$10.00", "topup_amount_cents": 2500, "topup_amount_usd": "$25.00", "max_topups_per_day": 3, "monthly_cap_cents": 20000, "monthly_cap_usd": "$200.00", "has_payment_method": true, "card_brand": "visa", "card_last4": "4242", "day_charge_count": 1, "month_charged_cents": 2500, "last_charged_at": "2026-07-30T14:02:11Z", "next_attempt_at": null, "consecutive_failures": 0, "disabled_reason": null, "disabled_at": null, "limits": { "min_threshold_cents": 500, "max_threshold_cents": 50000, "min_amount_cents": 1000, "max_amount_cents": 50000, "max_per_day": 20, "min_monthly_cap_cents": 2500, "max_monthly_cap_cents": 500000 } } ``` Read your bounds from `limits` rather than hard-coding them — they are the authoritative ranges and can change without a breaking API version. ### Update the configuration Every field is optional; send only what you want to change. ```bash theme={null} curl -X PUT "https://api.audiopod.ai/api/v1/api-wallet/auto-topup" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"threshold_cents": 2000, "monthly_cap_cents": 10000}' ``` | Field | Type | Default | Range | | - | - | - | - | | `enabled` | boolean | `false` | — (turning on needs a dashboard session) | | `threshold_cents` | integer | `1000` (\$10) | $5 – $500 | | `topup_amount_cents` | integer | `2500` (\$25) | $10 – $500 | | `max_topups_per_day` | integer | `3` | 1 – 20 | | `monthly_cap_cents` | integer | `20000` (\$200) | $25 – $5,000 | Returns the full configuration object shown above. **The automatic minimum ($10) is higher than the manual minimum ($1)** on purpose. Card processing has a flat component, so a \$1 automatic recharge spends a third of itself on fees. Larger, less frequent top-ups keep more of your money in the wallet. ### Save or replace a card Call this from a signed-in dashboard session and send the browser to the returned `url`. The link is single-use and short-lived; `expires_at` is a Unix timestamp. ```bash theme={null} curl -X POST \ "https://api.audiopod.ai/api/v1/api-wallet/auto-topup/payment-method-session" \ -H "Content-Type: application/json" ``` **Response:** ```json theme={null} { "url": "https://", "expires_at": 1785000000 } ``` ### Turn it off `DELETE` switches automatic top-up off **and** forgets the saved card. It returns the resulting configuration, with `enabled: false` and `has_payment_method: false`. Your balance and manual top-up are untouched. ```bash theme={null} curl -X DELETE "https://api.audiopod.ai/api/v1/api-wallet/auto-topup" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` To pause automatic top-up but keep the card, send `PUT {"enabled": false}` instead. ### Guardrails Two independent ceilings bound what a runaway loop can spend. Both are enforced server-side, and a charge that would cross either is simply not placed. | Guardrail | What it caps | Resets | | - | - | - | | `max_topups_per_day` | Number of automatic charges | Daily | | `monthly_cap_cents` | Total dollars charged automatically | On the 1st of each calendar month | `day_charge_count` and `month_charged_cents` in the response tell you how much headroom is left against each. When a ceiling is reached, automatic top-up stops for the period but stays armed — nothing is disabled, and manual top-up still works. ### When a charge fails A declined charge is retried on a widening schedule, then automatic top-up switches itself off rather than hammering your card: 1. **First failure** — retried about an hour later. 2. **Second failure** — retried about six hours after that. 3. **Third failure** — automatic top-up is switched off and we email you. While a retry is pending, `next_attempt_at` holds the earliest time we will try again and `consecutive_failures` counts the run. A successful charge resets both. Some failures skip the retries and disable immediately, because a retry cannot possibly clear them — most commonly when your bank requires you to confirm the card in person, which is impossible for a charge placed while you are away. When automatic top-up disables itself, `disabled_reason` and `disabled_at` explain what happened. Save the card again (or replace it) and turn automatic top-up back on. **Cards issued in India are not supported for automatic top-up yet.** Indian regulations require a separately registered mandate with its own notification and approval steps, which we have not built. Saving an Indian card for automatic top-up is refused at the point of saving. **Manual top-up is unaffected** — Add Funds and `POST /api-wallet/topup/checkout` work normally with an Indian card. *** ## Estimate Cost Calculate cost before processing (no auth required): ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/api-wallet/estimate" \ -H "Content-Type: application/json" \ -d '{"service_type": "stem_extraction", "duration_seconds": 300}' ``` **Response:** ```json theme={null} { "service_type": "stem_extraction", "duration_minutes": 5.0, "rate_per_minute": "$0.10/min", "cost_usd": "$0.50" } ``` *** ## Usage History ```bash theme={null} curl -s "https://api.audiopod.ai/api/v1/api-wallet/usage?limit=10" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq . ``` **Response:** ```json theme={null} { "logs": [ { "service_type": "stem_extraction", "duration_minutes": 5.47, "amount_usd": "$0.55", "created_at": "2025-12-11T12:39:37Z" } ] } ``` *** ## Get Pricing (No Auth) ```bash theme={null} curl -s "https://api.audiopod.ai/api/v1/api-wallet/pricing" | jq .services ``` *** ## Error Handling | Code | Error | Meaning | | - | - | - | | `402` | `insufficient_api_balance` | Wallet is empty | | `400` | `invalid_amount` | Amount outside $1-$10,000 range | | `401` | `unauthorized` | Invalid API key | | `403` | `dashboard_required` | Automatic top-up: this change (turning it on, raising a limit, or saving a card) needs a signed-in dashboard session | **Example 402 response:** ```json theme={null} { "error": "insufficient_api_balance", "message": "Insufficient balance. Required: $0.50, Available: $0.00", "required_cents": 50, "available_cents": 0 } ``` *** ## Next Steps Make your first API call Most popular API # Webhooks Source: https://docs.audiopod.ai/account/webhooks Receive a signed HTTP callback the moment an AudioPod job finishes — no polling. Register an endpoint, verify the signature, and handle retries. ## Overview AudioPod processing is asynchronous: you create a job, it runs, and it finishes a few seconds to a few minutes later. Instead of polling the job status, register a **webhook** and AudioPod will `POST` a signed event to your server the moment the job completes or fails. Every delivery is **HMAC-signed**, retried with exponential backoff, and carries a unique event id so you can safely deduplicate. ## Events | Event | When it fires | | - | - | | `job.completed` | A job finished successfully and its output is available. | | `job.failed` | A job failed. Credits reserved for the job are refunded automatically. | | `job.cancelled` | A job was stopped on request (e.g. `POST .../narration/cancel`). Charges held for work that did not complete are released; output already produced is kept. | | `narration.review_required` | An audiobook narration job finished with passages waiting for your [review](/api-reference/audiobook#narration-review). Fires once per job. | | `webhook.test` | A synthetic event you trigger yourself to confirm your endpoint works. | ## Register an endpoint Create an endpoint with the URL to call and the events you want. The response includes a **signing secret** — it is shown **once**, so store it now. ```bash theme={null} curl -X POST https://api.audiopod.ai/api/v1/webhooks/endpoints \ -H "Authorization: Bearer $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/hooks/audiopod", "events": ["job.completed", "job.failed"], "description": "Production job notifications" }' ``` ```json theme={null} { "id": "a5500e11-2d26-4d87-a020-03b730dfa340", "url": "https://your-app.com/hooks/audiopod", "events": ["job.completed", "job.failed"], "active": true, "description": "Production job notifications", "created_at": "2026-06-30T11:19:43Z", "secret": "whsec_…" } ``` Your endpoint **must use HTTPS** and must resolve to a public address. AudioPod refuses URLs that resolve to private, loopback, or internal addresses (SSRF protection), re-checked at delivery time. ## Event payload The body is JSON. Every event carries `event_id` and `event_type`; job events add the job context. ```json theme={null} { "event_type": "job.completed", "event_id": "f0a1…", "job_id": 999001, "job_type": "text_to_speech", "status": "completed" } ``` A `job.failed` event adds `error_message` and `failure_reason`. The `job_type` field tells you which tool produced the job. Completion webhooks now fire across the audio job types — for example `transcription`, `text_to_speech`, `voice_cloning`, `voice_conversion`, `music_generation`, `stem_extraction`, `speaker_diarization`, `denoise`, `reader`, `podcast`, `media_converter`, `audiobook`, and `export_acx`. ### narration.review\_required Not subscribed by default — add it to `events` when you create or update an endpoint. It fires once when an audiobook narration job finishes with passages held for [narration review](/api-reference/audiobook#narration-review): ```json theme={null} { "event_type": "narration.review_required", "event_id": "c7d2…", "project_id": "3f9b…", "job_id": 1042, "job_type": "audiobook", "needs_review": 2, "paragraph_ids": ["7d0c…", "91aa…"] } ``` Fetch the details with `GET /api/v1/audiobook/projects/{project_id}/review`, then approve or reject each passage. Export is refused until they are resolved. ## HTTP headers | Header | Description | | - | - | | `Content-Type` | Always `application/json`. | | `X-AudioPod-Signature` | `sha256=` — the HMAC signature (see below). | | `X-AudioPod-Timestamp` | Unix seconds; part of the signed material. | | `X-AudioPod-Event` | The event type, e.g. `job.completed`. | | `X-AudioPod-Event-Id` | Unique id for this event — use it to deduplicate. | ## Verify the signature The signature is `HMAC-SHA256(secret, "{timestamp}.{raw_request_body}")`, hex-encoded. Recompute it over the **raw** body bytes you received and the `X-AudioPod-Timestamp` header, then compare in constant time. Reject anything that doesn't match. ```python Python theme={null} import hmac, hashlib def verify(secret: str, raw_body: bytes, timestamp: str, header_sig: str) -> bool: expected = hmac.new( secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256, ).hexdigest() # header_sig looks like "sha256=" received = header_sig.split("=", 1)[-1] return hmac.compare_digest(expected, received) # FastAPI / Flask: read the RAW body, not a re-serialized dict. ``` ```javascript Node.js theme={null} import crypto from "crypto"; function verify(secret, rawBody, timestamp, headerSig) { const expected = crypto .createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest("hex"); const received = headerSig.split("=")[1] || ""; return ( received.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)) ); } // Express: use express.raw({ type: "application/json" }) so rawBody is the // exact bytes AudioPod signed. ``` Sign over the **raw bytes** you received, not a parsed-then-re-serialized object. Re-serializing can reorder keys or change whitespace and break the comparison. ## Respond fast, deduplicate, and retry-proof * **Return `2xx` quickly.** Acknowledge within **10 seconds**, then do slow work asynchronously. Any non-`2xx` (or a timeout) is treated as a failed attempt. * **Deduplicate on `X-AudioPod-Event-Id`.** Retries reuse the same event id, so store processed ids and ignore repeats. * **Retries:** failed deliveries retry with exponential backoff — 1m, 2m, 4m, … capped at 1h — up to 5 attempts. After that the delivery is dead-lettered and can be replayed from the delivery log. ## Manage endpoints | Action | Request | | - | - | | List endpoints | `GET /api/v1/webhooks/endpoints` | | Get one | `GET /api/v1/webhooks/endpoints/{id}` | | Update (url / events / active / description) | `PATCH /api/v1/webhooks/endpoints/{id}` | | Delete | `DELETE /api/v1/webhooks/endpoints/{id}` | | Send a test event | `POST /api/v1/webhooks/endpoints/{id}/test` | | View delivery log | `GET /api/v1/webhooks/endpoints/{id}/deliveries` | | Replay a delivery | `POST /api/v1/webhooks/deliveries/{id}/redeliver` | ### Test your integration ```bash theme={null} curl -X POST https://api.audiopod.ai/api/v1/webhooks/endpoints/$ENDPOINT_ID/test \ -H "Authorization: Bearer $AUDIOPOD_API_KEY" ``` This delivers a `webhook.test` event so you can confirm your signature verification and `2xx` response before wiring it to real jobs. Check `GET …/deliveries` to see the recorded status and response code. # Account API Source: https://docs.audiopod.ai/api-reference/account Manage your AudioPod AI account and profile ## Get Profile ```bash theme={null} curl -X GET "https://api.audiopod.ai/api/v1/auth/me" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Response:** ```json theme={null} { "id": "uuid", "email": "dev@example.com", "full_name": "Developer Name", "is_verified": true, "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } ``` *** ## Update Profile ```bash theme={null} curl -X PATCH "https://api.audiopod.ai/api/v1/auth/me" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "full_name": "New Name" }' ``` *** ## Change Password ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/change-password" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "current_password": "OldPass123", "new_password": "NewPass456" }' ``` **Password Requirements:** * Minimum 6 characters * At least one letter * At least one number *** ## Quick Reference | Endpoint | Method | Description | | - | - | - | | `/auth/me` | GET | Get profile | | `/auth/me` | PATCH | Update profile | | `/auth/change-password` | POST | Change password | *** ## Next Steps Manage API keys Check balance & pricing # Audio to MIDI Source: https://docs.audiopod.ai/api-reference/audio-to-midi Convert audio — or stems you already separated — into MIDI using AudioPod's audio-to-MIDI engine. ## Overview AudioPod's audio-to-MIDI engine transcribes bass, vocals, and piano in a recording into note-level MIDI — bass is the strongest instrument. Guitar is supported too but is opt-in/experimental. Use it **standalone** on a full mix (we split it into stems first, then transcribe each requested stem) or as an **add-on** to a stem-separation job you already ran (transcribe-only, no re-split). **This is a starting-point transcription, not a note-perfect one.** Bass is the strongest instrument; expect to tidy timing and note lengths by ear in your DAW. Dynamics (velocity) are approximate. Drums aren't transcribed yet — a requested `drums` (or `other`) stem is accepted but comes back in `skipped_stems`. Guitar is opt-in and experimental — it over-detects on busy mixes, so it isn't requested by default. Detected tempo is surfaced as `detected_tempo_bpm`. ### Key Features * **Standalone or add-on**: convert a raw file/URL, or transcribe stems from an existing [stem-separation](/api-reference/stem-splitter) job * **Per-stem + multitrack output**: individual `.mid` per stem, plus a merged multitrack `.mid` with tempo metadata * **Tempo detection**: auto-detected via beat tracking, or pass an explicit `tempo_bpm` override * **Presigned downloads**: direct URLs for every output file ## Installation ```bash theme={null} pip install audiopod ``` ```bash theme={null} npm install audiopod ``` ## Authentication Get your API key from the [API Keys Dashboard](https://www.audiopod.ai/dashboard/account/api-keys). ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Or set AUDIOPOD_API_KEY environment variable ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Or set AUDIOPOD_API_KEY environment variable ``` ```bash theme={null} export AUDIOPOD_API_KEY="ap_your_api_key" # Use -H "X-API-Key: $AUDIOPOD_API_KEY" in requests ``` *** ## Convert From a File or URL Standalone conversion splits the mix into stems, then transcribes each requested stem. Provide either `file` or `url` — not both. `stems` defaults to `["bass", "vocals", "piano"]` — the strongest instruments — when omitted. `guitar` is supported but opt-in/experimental (it over-detects on busy mixes), so pass it explicitly if you want it. `drums` and `other` are accepted but are never transcribed — they always come back in `skipped_stems`. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/midi/convert" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "file=@/path/to/song.mp3" ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/midi/convert" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=VIDEO_ID" \ -F 'stems=["bass","vocals","piano","guitar"]' ``` ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # From a local file (or pass url="https://.../song.mp3") # Default stems: bass, vocals, piano job = client.midi.convert(file="/path/to/song.mp3") print(f"Multitrack: {job['merged_midi_url']}") for stem, url in (job["midi_urls"] or {}).items(): print(f" {stem}: {url}") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // From a local file (or pass { url: 'https://.../song.mp3' }) // Default stems: bass, vocals, piano const job = await client.midi.transcribe({ file: '/path/to/song.mp3' }); console.log(`Multitrack: ${job.merged_midi_url}`); for (const [stem, url] of Object.entries(job.midi_urls ?? {})) { console.log(` ${stem}: ${url}`); } ``` ### Options Tunables forwarded to the audio-to-MIDI engine, passed as `options` (a JSON object): | Field | Type | Default | Description | | - | - | - | - | | `onset_threshold` | float (0.05–0.95) | `0.5` | Note-onset detection sensitivity | | `frame_threshold` | float (0.05–0.95) | `0.3` | Frame-level pitch confidence threshold | | `min_note_len_ms` | float (10–2000) | `128.0` | Minimum note duration in milliseconds | | `tempo_bpm` | float (30–300) or `null` | `null` (auto-detect) | Override the detected tempo | ## Convert From an Existing Stem-Separation Job If you already ran [stem separation](/api-reference/stem-splitter), transcribe its output directly — no re-split, so it bills the add-on rate only. `stems` must be a subset of the stems the source job produced, and defaults to all of them when omitted — `drums`/`other` stems still come back in `skipped_stems` (never transcribed), and `guitar` remains opt-in/experimental. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/midi/convert/from-stems/5512" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"stems": ["vocals", "bass"]}' ``` ```python theme={null} stem_job = client.stems.separate(file="/path/to/song.mp3", mode="six") job = client.midi.convert_from_stem_job( stem_job_id=stem_job["id"], stems=["vocals", "bass"], ) print(job["merged_midi_url"]) ``` ```typescript theme={null} const stemJob = await client.stems.separate({ file: '/path/to/song.mp3', mode: 'six' }); const job = await client.midi.convertFromStemJob(stemJob.id, { stems: ['vocals', 'bass'], }); console.log(job.merged_midi_url); ``` If the source stem job's output has expired under your plan's [retention window](https://www.audiopod.ai/pricing), this returns `410 Gone` — re-run stem separation first. *** ## Job Status & Stages ```bash theme={null} curl -X GET "https://api.audiopod.ai/api/v1/midi/status/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` ```python theme={null} status = client.midi.get_job(job_id) print(f"Status: {status['status']}, stage: {status['stage']}") ``` ```typescript theme={null} const status = await client.midi.status(jobId); console.log(`Status: ${status.status}, stage: ${status.stage}`); ``` `stage` tracks pipeline progress for a `PROCESSING` job: | Stage | Description | | - | - | | `split` | Separating the mix into stems (standalone mode only) | | `transcribe` | Running the audio-to-MIDI engine on each requested stem | | `merge` | Building the merged multitrack `.mid` | ### Other Job Endpoints ```bash theme={null} # List jobs curl -X GET "https://api.audiopod.ai/api/v1/midi/jobs?skip=0&limit=50" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # Delete a job (purges its outputs) curl -X DELETE "https://api.audiopod.ai/api/v1/midi/jobs/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Output Format **Completed Job Response:** ```json theme={null} { "id": 5512, "status": "COMPLETED", "progress": 100, "stage": "merge", "mode": "standalone", "source_job_id": null, "stems_requested": ["bass", "vocals", "piano"], "midi_urls": { "bass": "https://media.audiopod.ai/...", "vocals": "https://media.audiopod.ai/...", "piano": "https://media.audiopod.ai/..." }, "merged_midi_url": "https://media.audiopod.ai/...", "stem_urls": { "bass": "https://media.audiopod.ai/...", "vocals": "https://media.audiopod.ai/..." }, "detected_tempo_bpm": 118.0, "skipped_stems": [], "failed_stems": null, "duration": 214, "original_filename": "song.mp3", "created_at": "2026-07-24T12:39:37.379848Z", "completed_at": "2026-07-24T12:41:02.104213Z" } ``` * **`midi_urls`** — presigned download URL per transcribed stem's `.mid` file * **`merged_midi_url`** — a single multitrack `.mid` with all transcribed stems as separate tracks plus tempo metadata * **`stem_urls`** — the separated stem *audio* (standalone mode only, so you can preview what was transcribed) * **`skipped_stems`** — stems requested but not transcribed (always `drums` and `other`, if requested — they're not part of the transcribable set) * **`failed_stems`** — `{stem: error_message}` for any stem the engine couldn't transcribe; the job still completes if at least one stem succeeded All download URLs are presigned and expire — call the status endpoint again to mint fresh ones. *** ## Status Codes | Status | Description | | - | - | | `PENDING` | Job queued, waiting for worker | | `PROCESSING` | Splitting and/or transcribing — see `stage` | | `COMPLETED` | At least one stem transcribed — download URLs available | | `FAILED` | No stem could be transcribed — check `error_message` | *** ## Pricing See [audiopod.ai/pricing](https://www.audiopod.ai/pricing) for current rates, or check your estimated cost before converting via the [API Wallet](/account/api-wallet). *** ## Next Steps Separate a mix into stems before (or instead of) converting to MIDI. Manage billing and view usage history. # Audiobook Source: https://docs.audiopod.ai/api-reference/audiobook Turn a manuscript into a finished, ACX-compliant audiobook through a fully automated API: create a project, upload a PDF or EPUB, narrate it, and download a retail-ready package — with completion webhooks for hands-off, backlog-scale production. ## Overview The Audiobook API exposes the **complete production pipeline** end to end, so you can drop AI audiobook generation directly into your own platform or control panel: a file goes in, a finished, ACX-compliant package comes back, with no human in the loop. It is the same API surface our own Audiobook Studio runs on — nothing here is a private endpoint. ``` create project → add manuscript → parse into chapters → narrate → export ACX package → download └──── completion webhook ────┘ ``` The API-key workflow supports asynchronous production: submit a job, get an ID back immediately, and receive a signed webhook when it finishes — so your control panel never blocks on a long render. The guided narrator-sample endpoint described below is Studio-only. ### Key features * **Inputs**: upload PDF, EPUB, DOCX, or TXT; paste raw text; or generate a book from a prompt. Chapters are detected automatically. * **Voices**: 100+ languages with standard narration voices; custom voice cloning across 20+ languages. * **Auto Direction**: a per-paragraph performance note and emotion written across the whole book, anchored to a narration brief so the tone holds from first chapter to last — dialogue directed as dialogue, instead of a flat read. Free on every plan. * **ACX-compliant export**: per-chapter MP3 at **192 kbps CBR / 44.1 kHz**, a retail sample, optional narrated opening/closing credits, a cover slot, and a `metadata.json` manifest — packaged as a single ZIP via a presigned URL. * **Automatic compliance check**: file format, audio levels, silence, and per-chapter length are validated on export; a non-compliant book is rejected with the exact reason rather than shipped silently. * **Asynchronous + webhooks**: `job.completed` / `job.failed` / `job.cancelled` callbacks for backlog-scale automation. * **White-label output**: delivered files carry the publisher's own title/author/narrator metadata — nothing points back at AudioPod. * **Prepaid usage pricing**: $0.09 per 1,000 characters of manuscript text (~$50 for a 10-hour book, at about 56,000 characters per finished hour). See [API Wallet](/account/api-wallet). ## Authentication All endpoints require authentication. Use an API key (recommended for server-to-server integrations): * **API Key (Recommended)**: `X-API-Key: your_api_key` header — [create one in the dashboard](https://www.audiopod.ai/dashboard/account/api-keys). * **JWT Token**: `Authorization: Bearer your_jwt_token` (session-based auth). ## Account audiobook settings `GET /api/v1/audiobook/settings` reads the authenticated account's preference for automatic reading direction in Audiobook Studio. `PATCH /api/v1/audiobook/settings` saves it and returns the same response shape. ```bash theme={null} # Read the account preference using session authentication curl "https://api.audiopod.ai/api/v1/audiobook/settings" \ -H "Authorization: Bearer $ACCESS_TOKEN" # Turn automatic direction off for future Studio parses curl -X PATCH "https://api.audiopod.ai/api/v1/audiobook/settings" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"auto_direction_default": false}' ``` The PATCH body requires `auto_direction_default`, a boolean. Both endpoints return: ```json theme={null} { "auto_direction_default": false, "source": "account", "active": true, "applies_to_api_key_parses": false } ``` | Field | Meaning | | - | - | | `auto_direction_default` | The author's saved preference. It is `true` if the account has never chosen. | | `source` | `account` after an explicit choice; `default` if the account has never chosen. | | `active` | Whether AudioPod currently applies account preferences to Studio parses. This is separate from the author's choice. | | `applies_to_api_key_parses` | Always `false`. API-key parses remain opt-in, regardless of this preference. | When `active` is `false`, PATCH still saves the preference and returns the saved value. Studio parses with no explicit `auto_direction` then default to `false`; the saved choice takes effect when `active` becomes `true`. Changing this preference does not direct or re-render existing books. For a single manuscript import, pass `auto_direction=true` or `auto_direction=false` explicitly to override the default without changing the saved preference. This applies to uploaded-file parsing, pasted text, and generated manuscripts. Omit the field or pass JSON `null` to use the default. The uploaded-file parse endpoint accepts this in JSON, form fields, or the query string; an explicit body value takes precedence over the query value. The paste and generate endpoints accept it in their JSON bodies. ### Guided narrator samples in Studio The Studio-only `POST /api/v1/audiobook/projects/{project_id}/production/quote` request also accepts an optional `auto_direction` boolean. Omitted or `null` follows the author's account preference; an account with no saved preference keeps the guided workflow's default of `true`. An explicit `true` or `false` overrides that choice for this production. This guided sample default applies independently of the `active` field for automatic direction during manuscript intake. A saved `false` is respected in both workflows. API-key requests to the guided quote endpoint return `400`; use the API narration workflow documented below. Preparing a guided sample takes over any outstanding automatic direction started by manuscript intake, so the sample does not wait for a whole-book direction pass. A direction run started explicitly is preserved: finish or stop it before preparing the sample, otherwise the quote request returns `409`. ## Getting the IDs Almost every call below is addressed by an ID, and how you obtain them is the first thing to get right. **Every audiobook identifier is a UUID string**, and in every response body the field is called **`id`** — not `project_id` or `chapter_id`. Those names appear only as *path parameters* (`/projects/{project_id}/chapters/{chapter_id}`) and as *back-references* on child objects (a chapter carries `project_id`; a paragraph carries both `chapter_id` and `project_id`). So you read `id` from the object you just created, and you *write* it into the next request's path. `GET /audiobook/projects/{project_id}` does **not** return chapters by default — the project body carries `total_chapters` (a count) and `chapters: null`. To get the chapter objects (and therefore their `id`s) you must either pass **`?include_chapters=true`** on the project read, or call `GET /audiobook/projects/{project_id}/chapters` separately. The full call chain, with the ID each step produces: | # | Call | Gives you | | - | - | - | | 1 | `POST /audiobook/projects` | `id` → your **project\_id** | | 2 | `POST /audiobook/projects/{project_id}/manuscript/upload` | `file_key` | | 3 | `POST /audiobook/projects/{project_id}/manuscript/parse` | `job_id` (parsing runs in the background) | | 4 | `GET /audiobook/projects/{project_id}?include_chapters=true` | poll until parsing finishes — see [Poll for parse completion](#poll-for-parse-completion) | | 5 | `GET /audiobook/projects/{project_id}/chapters` (or the `chapters` array from step 4) | each chapter's `id` → **chapter\_id** | | 6 | `GET /audiobook/projects/{project_id}/chapters/{chapter_id}/paragraphs` | each paragraph's `id` → **paragraph\_id** | | 7 | `POST /audiobook/projects/{project_id}/narration/batch` (whole book / chapters) or `POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/narrate` (one paragraph) | a narration job | Chapters are the unit you *organise* by; **paragraphs are the unit that is actually narrated, billed, and re-recorded**. If you only ever need whole-book narration you can stop at step 5 — `POST .../narration/batch` with no `chapter_ids` or `paragraph_ids` narrates the entire project. ## API Endpoints Quick Reference | Operation | Method | Endpoint | | - | - | - | | Account audiobook settings | GET·PATCH | `/audiobook/settings` | | Create project | POST | `/audiobook/projects` | | List projects (`page`, `per_page`) | GET | `/audiobook/projects` | | Get project (`?include_chapters=true`) | GET | `/audiobook/projects/{project_id}` | | Update / delete project (incl. `multi_voice`) | PUT·DELETE | `/audiobook/projects/{project_id}` | | Upload manuscript | POST | `/audiobook/projects/{project_id}/manuscript/upload` | | Parse manuscript | POST | `/audiobook/projects/{project_id}/manuscript/parse` | | Append another manuscript file | POST | `/audiobook/projects/{project_id}/manuscript/append` | | Paste text | POST | `/audiobook/projects/{project_id}/manuscript/paste` | | Generate book | POST | `/audiobook/projects/{project_id}/manuscript/generate` | | List chapters (`page`, `per_page`) | GET | `/audiobook/projects/{project_id}/chapters` | | Get / update / delete a chapter | GET·PUT·DELETE | `/audiobook/projects/{project_id}/chapters/{chapter_id}` | | List paragraphs in a chapter | GET | `/audiobook/projects/{project_id}/chapters/{chapter_id}/paragraphs` | | Get / update a paragraph | GET·PUT | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}` | | **Auto Direction — direct the book** | POST | `/audiobook/projects/{project_id}/direction/auto` | | Latest direction run | GET | `/audiobook/projects/{project_id}/direction/auto/latest` | | Direction run status | GET | `/audiobook/projects/{project_id}/direction/auto/{job_id}/status` | | **Cast — read speakers + voices** | GET | `/audiobook/projects/{project_id}/cast` — see [Characters and voices](/api-reference/audiobook-cast) | | Split lines by speaker (screenplay annotation) | POST | `/audiobook/projects/{project_id}/screenplay/annotate` | | Annotation status | GET | `/audiobook/projects/{project_id}/screenplay/annotate/{job_id}/status` | | Detect speakers (`?method=heuristic` instant, or `?method=llm` job) | POST | `/audiobook/projects/{project_id}/cast/detect` | | Speaker detection job status | GET | `/audiobook/projects/{project_id}/cast/detect/{job_id}/status` | | Merge an alternate character name | POST | `/audiobook/projects/{project_id}/cast/alias` | | Assign a voice to a character | POST | `/audiobook/projects/{project_id}/cast/assign` | | Auto-cast (designs voices — prepaid) | POST | `/audiobook/projects/{project_id}/cast/auto` | | Auto-cast status | GET | `/audiobook/projects/{project_id}/cast/auto/{job_id}/status` | | **Pronunciation dictionary — list entries** | GET | `/audiobook/projects/{project_id}/pronunciations` | | Add a pronunciation entry | POST | `/audiobook/projects/{project_id}/pronunciations` | | Update a pronunciation entry | PUT | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` | | Delete a pronunciation entry | DELETE | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` | | List voices | GET | `/audiobook/voices/available` | | List languages | GET | `/audiobook/voices/languages` | | Cost estimate | GET | `/audiobook/projects/{project_id}/cost-estimate` | | Book cost (actual spend) | GET | `/audiobook/projects/{project_id}/cost` | | **Narrate (batch — the billed path)** | POST | `/audiobook/projects/{project_id}/narration/batch` | | Narrate a single paragraph | POST | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}/narrate` | | Narration progress | GET | `/audiobook/projects/{project_id}/narration/progress` | | Cancel narration | POST | `/audiobook/projects/{project_id}/narration/cancel` | | Re-render one chapter | POST | `/audiobook/projects/{project_id}/narration/batch` with `chapter_ids` + `include_completed` | | Retry a chapter's failed paragraphs | POST | `/audiobook/projects/{project_id}/chapters/{chapter_id}/retry-failed-paragraphs` | | **Narration review — list passages** (`status`) | GET | `/audiobook/projects/{project_id}/review` | | Approve one passage | POST | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}/review/approve` | | Approve all passages (optional `chapter_id`) | POST | `/audiobook/projects/{project_id}/review/approve-all` | | Reject one passage (optional `regenerate`) | POST | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}/review/reject` | | Production settings — opening/closing announcement, music, cover | GET·PUT | `/audiobook/projects/{project_id}/production-settings` | | **Pacing** — pauses between paragraphs, speaker changes, scene breaks | GET·PATCH | `/audiobook/projects/{project_id}/pacing` — see [Pacing and scene breaks](#pacing-and-scene-breaks) | | Upload media (cover/music) | POST | `/audiobook/projects/{project_id}/media/upload` | | Start ACX export | POST | `/audiobook/projects/{project_id}/export` | | Export status | GET | `/audiobook/projects/{project_id}/export/{job_id}/status` | | Download package | GET | `/audiobook/projects/{project_id}/export/{job_id}/download` | | Dashboard stats | GET | `/audiobook/dashboard` | `POST /audiobook/projects/{project_id}/narration/start` is **deprecated and removed** — it now returns **`410 Gone`**. Use `POST /audiobook/projects/{project_id}/narration/batch`: with no `chapter_ids` and no `paragraph_ids` it narrates the whole project, which is exactly what `narration/start` used to do. `POST /audiobook/projects/{project_id}/chapters/{chapter_id}/regenerate` is **deprecated and removed** and also returns **`410 Gone`**. Use `POST /audiobook/projects/{project_id}/narration/batch` with `{"chapter_ids": [""], "include_completed": true}` — `include_completed` is required, because a chapter you are RE-rendering already has audio and would otherwise be skipped entirely. ## Quick Start (cURL) ```bash theme={null} export AUDIOPOD_API_KEY="ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" export AP="https://api.audiopod.ai/api/v1" # 1. Create a project PID=$(curl -s -X POST "$AP/audiobook/projects" -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"The Lighthouse Keeper","author":"A. P. Tester"}' | jq -r .id) # 2. Upload a manuscript (PDF / EPUB / DOCX / TXT) FILE_KEY=$(curl -s -X POST "$AP/audiobook/projects/$PID/manuscript/upload" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -F "file=@book.epub" | jq -r .file_key) # 3. Parse into chapters (async). API keys default to auto_direction=false: # pass auto_direction=true to have every paragraph directed after parsing (free) curl -s -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -F "file_key=$FILE_KEY" -F "filename=book.epub" \ -F "auto_direction=true" # 3b. Poll the PROJECT until parsing finishes — status tells you which way it went while :; do P=$(curl -s "$AP/audiobook/projects/$PID?include_chapters=true" -H "X-API-Key: $AUDIOPOD_API_KEY") S=$(echo "$P" | jq -r .status) [ "$S" = "failed" ] && { echo "parse failed: $(echo "$P" | jq -r .error_message)"; exit 1; } [ "$(echo "$P" | jq -r .parsing_completed_at)" != "null" ] && break sleep 5 done echo "$P" | jq '.chapters[] | {id, chapter_number, title}' # 4. Narrate the whole project with a voice (omit chapter_ids/paragraph_ids = everything) curl -s -X POST "$AP/audiobook/projects/$PID/narration/batch" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"voice_id":387}' # 4b. Handle passages that need review (see "Narration review" below). # When narration settles, needs_review_paragraphs > 0 means some passages # wait for a decision and export will refuse until they are resolved. curl -s "$AP/audiobook/projects/$PID/narration/progress" -H "X-API-Key: $AUDIOPOD_API_KEY" \ | jq .needs_review_paragraphs curl -s "$AP/audiobook/projects/$PID/review" -H "X-API-Key: $AUDIOPOD_API_KEY" \ | jq '.items[] | {paragraph_id, expected_text, heard_text, candidate_audio_url}' # Listen, then approve them all (free) — or approve / reject one at a time curl -s -X POST "$AP/audiobook/projects/$PID/review/approve-all" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "approved": 2, "refused": 0, "remaining": 0, "refused_reasons": {} } # Fully automated pipeline? Create the project with "narration_review_policy":"auto_accept" # instead, and nothing ever waits for review. # 5. Export an ACX package, then download it JOB=$(curl -s -X POST "$AP/audiobook/projects/$PID/export" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}' | jq -r .job_id) curl -s "$AP/audiobook/projects/$PID/export/$JOB/download" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "download_url": "https://media.audiopod.ai/...zip?...", "file_size_bytes": ..., "chapter_count": ... } ``` ## 1. Create a project A project is the container for a single audiobook. Create it first, then attach a manuscript. Audiobook title. Author name (written into the package metadata). Project description. ISBN, if available. Book genre. Default narration voice for the project (can also be set per narration request). Speech speed multiplier. Narration style. Export format. `acx` produces an Audible/ACX-ready package. Target distribution platform. What happens to a passage our narration check cannot confirm after its retry: `manual` holds it for your review (the book waits on it); `auto_accept` keeps the best render and flags it, so the book never blocks. See [Narration review](#narration-review). Also accepted on `PUT /audiobook/projects/{project_id}` (`null` leaves the stored value unchanged). ```bash theme={null} curl -X POST "$AP/audiobook/projects" -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"The Lighthouse Keeper","author":"A. P. Tester","genre":"Fiction"}' ``` ```python theme={null} import requests AP, H = "https://api.audiopod.ai/api/v1", {"X-API-Key": "ap_..."} project = requests.post(f"{AP}/audiobook/projects", headers=H, json={ "title": "The Lighthouse Keeper", "author": "A. P. Tester", "genre": "Fiction", }).json() pid = project["id"] ``` **Response** (`AudiobookProjectResponse`): Project UUID — **this is your `project_id`** for every subsequent call. Lifecycle state (see [Status values](#status-values)). Number of chapters once parsed. This is a *count* — the chapter objects are not in this response unless you ask for them. Estimated finished runtime. Uploaded filename, once attached. `pdf` · `epub` · `docx` · `txt`. Total words after parsing. ISO timestamp set when a manuscript parse finished successfully; `null` until then. ISO timestamp of the first narration job. ISO timestamp when narration finished. ISO timestamp when production/export finished. Sanitized failure reason when a stage failed; `null` otherwise. Project default narration voice. Speed multiplier (0.5–2.0). Delivery style. Export format (`acx`). Target distribution platform. `manual` or `auto_accept` — see [Narration review](#narration-review). Creation timestamp. Last-modified timestamp. ### Read one project `GET /audiobook/projects/{project_id}` returns the same body, plus a `chapters` field: When `true`, the response carries the project's full `chapters` array inline, saving a second round-trip. When omitted or `false`, `chapters` is `null` — the chapter objects (and their `id`s) are simply not there. ```bash theme={null} curl -s "$AP/audiobook/projects/$PID?include_chapters=true" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "id": "...", "status": "ready_for_narration", "total_chapters": 12, # "parsing_completed_at": "2026-07-23T09:14:02", "error_message": null, # "chapters": [ { "id": "...", "chapter_number": 1, "title": "...", ... } ] } ``` ### List projects `GET /audiobook/projects` is paginated with **`page`** and **`per_page`** (not `skip`/`limit`): 1-based page number (minimum `1`). Projects per page, `1`–`100`. Return only projects in this [status](#status-values). Case-insensitive substring match on title or author. ```bash theme={null} curl -s "$AP/audiobook/projects?page=2&per_page=50" -H "X-API-Key: $AUDIOPOD_API_KEY" ``` The page of projects (each an `AudiobookProjectResponse`). Total projects matching the filters, across all pages. Echo of the requested page. Echo of the requested page size. Whether another page follows. Whether a previous page exists. Unknown query parameters are **ignored, not rejected**. Sending `?skip=0&limit=20` returns a `200` with the *first* page every time — so a paging loop built on `skip`/`limit` never advances and silently re-reads page 1 forever. Use `page` / `per_page`, and drive your loop off `has_next`. ## 2. Add the manuscript Three ways to get text into a project — pick one. ### Upload a file `POST /audiobook/projects/{project_id}/manuscript/upload` — multipart form field `file` (PDF, EPUB, DOCX, or TXT). Returns a `file_key` to hand to the parser. Optional form field `multi_voice` (`true`/`false`) saves the [character voices](/api-reference/audiobook-cast) choice on the book, as with `PUT /audiobook/projects/{project_id}`; the parse that follows then detects the speakers and gives each character a free library voice. Storage key of the uploaded manuscript — pass this to the parser. Reference URL for the stored file. Maximum accepted size in bytes. ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/manuscript/upload" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -F "file=@book.epub" ``` ### Parse the uploaded file into chapters `POST /audiobook/projects/{project_id}/manuscript/parse` — pass `file_key` (from the upload response) and `filename`. The endpoint accepts a **JSON body** or **form fields** (`multipart/form-data` or `application/x-www-form-urlencoded`) — use whichever your HTTP client makes easiest. Parsing is asynchronous and returns a `job_id` immediately — see [Poll for parse completion](#poll-for-parse-completion) for how to wait on it. ```bash theme={null} # JSON body curl -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d "{\"file_key\": \"$FILE_KEY\", \"filename\": \"book.epub\"}" # ...or form fields (equivalent) curl -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -F "file_key=$FILE_KEY" -F "filename=book.epub" # => { "job_id": 99, "estimated_completion_time": "under 2 minutes" } ``` Storage key returned by the upload response. Original filename, including the extension. Re-parse a project that already has chapters. **Destructive** — existing chapters and paragraphs are replaced. Run [Auto Direction](#auto-direction) as soon as parsing finishes, so the book narrates expressively without a second call. **Defaults to `false` for API-key callers** — see below. Attribute dialogue to named speakers (each paragraph's `character_name`). Free. Omitted: **off for API-key requests**, on in the web studio. `multi_voice: true` turns it on. See [Characters and voices](/api-reference/audiobook-cast#finding-the-speakers). Give each character their own voice. Turns on `detect_speakers`, and every character found gets a library voice at no cost when a suitable one is available, which you can change on the cast. After parsing, our speaker detection pass reads the dialogue in context, merges alternate names of the same character, and records a gender hint used for the free voice pick; narration of a passage waits until it finishes, and Auto Direction, when requested, runs after it. See [the speaker detection pass](/api-reference/audiobook-cast#speaker-detection-pass-with-character-voices-on). Omitted: the book's setting is left as it is (new books start with it off: every line uses the narrator). See [Characters and voices](/api-reference/audiobook-cast). **API-key parses default to `auto_direction=false`.** Set `auto_direction=true` explicitly to add reading direction after parsing. The account preference does not change this API-key default. In Audiobook Studio, an omitted `auto_direction` follows the author's [account preference](#account-audiobook-settings) when that setting is active. An explicit `true` or `false` overrides the default for that parse, for either authentication method. Explicit `true` also works when the account preference is not active. You can also direct a book at any time after parsing with [`POST .../direction/auto`](#auto-direction) — nothing about it is tied to parse time. ```bash theme={null} # Parse AND direct in one call curl -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d "{\"file_key\": \"$FILE_KEY\", \"filename\": \"book.epub\", \"auto_direction\": true}" ``` ### Poll for parse completion There is **no parse-status endpoint**, and polling `GET .../chapters` "until chapters appear" is not a safe wait — a *failed* parse produces no chapters, so that loop never terminates. Poll the **project** instead: it is the object that carries the parse outcome. Poll `GET /audiobook/projects/{project_id}?include_chapters=true` — one call returns both the parse state and, on success, the chapters — and branch on these three fields: `parsing` while the job is running. On success it becomes `ready_for_narration`. On a failed **first** parse it becomes `failed`. `null` until a parse has completed successfully; an ISO timestamp afterwards. **This is the success signal.** The sanitized failure reason, set when the parse fails (e.g. an unreadable or over-length manuscript). Read it to report *why* to your user. Decision table — evaluate in this order: | Condition | Meaning | Do | | - | - | - | | `status == "failed"` | **Terminal failure.** | Stop. Surface `error_message`; fix or re-upload the manuscript. | | `parsing_completed_at != null` | **Success.** | Stop. Read `chapters` (or call the chapters endpoint). | | `status == "parsing"` | Still working. | Sleep and poll again. | | Neither, after your timeout | Treat as failed. | Give up after a bounded number of attempts. | Check `status` **before** `error_message`. `error_message` is not cleared when you retry a parse, so a leftover value from an earlier failure can persist on a project that has since parsed fine. `status` (plus `parsing_completed_at`) is the authoritative state; `error_message` is the *reason* to display once `status` says `failed`. One nuance for `manuscript/append`: if an append-parse fails on a project that **already** has chapters, the project is returned to `ready_for_narration` rather than `failed`, so the existing book is not destroyed. In that case compare `total_chapters` before and after to detect that nothing was added. ```python theme={null} import time, requests AP, H = "https://api.audiopod.ai/api/v1", {"X-API-Key": "ap_..."} def wait_for_parse(pid: str, timeout_s: int = 900, interval_s: int = 5) -> list[dict]: deadline = time.time() + timeout_s while time.time() < deadline: p = requests.get(f"{AP}/audiobook/projects/{pid}", params={"include_chapters": "true"}, headers=H).json() if p["status"] == "failed": # terminal failure raise RuntimeError(p.get("error_message") or "Manuscript parsing failed") if p.get("parsing_completed_at"): # terminal success return p["chapters"] or [] time.sleep(interval_s) # still parsing raise TimeoutError("Manuscript parsing did not finish in time") chapters = wait_for_parse(pid) chapter_ids = [c["id"] for c in chapters] ``` Parses typically finish in under two minutes for EPUB/DOCX/TXT and in 2–10 minutes for large or scanned PDFs; the `estimated_completion_time` string in the parse response reflects the format you submitted. A 15-minute poll ceiling is a safe default. ### Paste text directly `POST /audiobook/projects/{project_id}/manuscript/paste` — JSON. Best for content you already have as text. Manuscript or chapter text. Source label. Add automatic reading direction after splitting the text into chapters. Omitted or `null` follows the [account preference](#account-audiobook-settings) for Studio requests and defaults to `false` for API keys. An explicit `true` or `false` overrides that default for this import. Attribute dialogue to named speakers (each paragraph's `character_name`). Free. Omitted: **off for API-key requests**, on in the web studio. `multi_voice: true` turns it on. See [Characters and voices](/api-reference/audiobook-cast#finding-the-speakers). Give each character their own voice. Turns on `detect_speakers`, and every character found gets a library voice at no cost when a suitable one is available, which you can change on the cast. After parsing, our speaker detection pass reads the dialogue in context, merges alternate names of the same character, and records a gender hint used for the free voice pick; narration of a passage waits until it finishes, and Auto Direction, when requested, runs after it. See [the speaker detection pass](/api-reference/audiobook-cast#speaker-detection-pass-with-character-voices-on). Omitted: the book's setting is left as it is (new books start with it off: every line uses the narrator). See [Characters and voices](/api-reference/audiobook-cast). ```bash theme={null} # Paste text and direct it in one call (API keys must ask for direction explicitly) curl -X POST "$AP/audiobook/projects/$PID/manuscript/paste" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"text": "Capitolo 1\n\nC'"'"'era una volta un pezzo di legno...", "auto_direction": true}' # => { "project_id": "...", "source_type": "paste", "total_chapters": 2, "total_words": 1344, # "estimated_duration_minutes": 8.96, "requires_author_approval": true } ``` ### Generate a book from a prompt `POST /audiobook/projects/{project_id}/manuscript/generate` — JSON. Drafts an original manuscript, then chapters it. What the book should be about. Optional working title. Intended audience. Writing tone. Number of chapters. Target words per chapter. Add automatic reading direction after generating and splitting the manuscript. Omitted or `null` follows the [account preference](#account-audiobook-settings) for Studio requests and defaults to `false` for API keys. An explicit `true` or `false` overrides that default for this import. Attribute dialogue to named speakers (each paragraph's `character_name`). Free. Omitted: **off for API-key requests**, on in the web studio. `multi_voice: true` turns it on. See [Characters and voices](/api-reference/audiobook-cast#finding-the-speakers). Give each character their own voice. Turns on `detect_speakers`, and every character found gets a library voice at no cost when a suitable one is available, which you can change on the cast. After parsing, our speaker detection pass reads the dialogue in context, merges alternate names of the same character, and records a gender hint used for the free voice pick; narration of a passage waits until it finishes, and Auto Direction, when requested, runs after it. See [the speaker detection pass](/api-reference/audiobook-cast#speaker-detection-pass-with-character-voices-on). Omitted: the book's setting is left as it is (new books start with it off: every line uses the narrator). See [Characters and voices](/api-reference/audiobook-cast). The `paste` and `generate` endpoints return a `ManuscriptIntakeResponse` with `total_chapters`, `total_words`, and `estimated_duration_minutes`. When automatic direction is requested, it continues asynchronously after the manuscript is saved. Read `GET /audiobook/projects/{project_id}/direction/auto/latest` to find its job and follow [direction progress](#waiting-for-a-direction-run). Direction does not start audio narration; submit a narration request when you are ready. `manuscript/parse`, `manuscript/paste`, and `manuscript/generate` **replace the project's entire chapter/paragraph structure** — any existing narration is discarded. Parse once per manuscript; re-parse only if the manuscript file itself changed. While narration, another parse, mixing, or an export is already running for the project, these endpoints return `409 Conflict` — poll `GET .../narration/progress` and retry once it finishes. If you don't want to wait for a narration run to finish, stop it first with [`POST .../narration/cancel`](#cancel-a-narration-run); already-narrated paragraphs are kept. If the project already has completed narration, these endpoints instead return `409` with a structured body: `detail.code = "completed_narration_exists"` and `detail.completed_paragraphs = N`. To proceed anyway, pass `force=true` (accepted as a JSON field, form field, or query parameter). **Do not hard-code `force=true` in your integration** — treat this `409` as a stop-and-confirm; forcing permanently deletes the `N` narrated paragraphs. `manuscript/append` is the exception: it's additive (appends chapters after the existing ones) and never deletes narration. ## 3. Chapters List chapters with `GET /audiobook/projects/{project_id}/chapters`, or fetch one with `GET .../chapters/{chapter_id}`. 1-based page number (minimum `1`). Chapters per page, `1`–`500`. The response is the same paginated envelope as the project list — `chapters`, `total`, `page`, `per_page`, `has_next`, `has_prev` — with chapters ordered by `chapter_number`. As with projects, `skip`/`limit` are **not** recognised and are silently ignored. ```bash theme={null} curl -s "$AP/audiobook/projects/$PID/chapters?page=1&per_page=100" -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Chapter** (`AudiobookChapterResponse`): Chapter UUID — **this is your `chapter_id`**. Back-reference to the owning project. Order in the book. Chapter title. Chapter text. Words in the chapter. Characters in the chapter. Narrated length, once generated. `pending` · `queued` · `narrating` · `completed` · `failed`. Stored audio key, once narrated. Reason a chapter failed (e.g. content too short for narration). If true, the chapter is excluded from narration and export. Per-chapter voice override. Free-text narration instructions stored on the chapter. Estimated narrated length before rendering. Number of narration retries so far. You can edit a chapter (title, content, `skip_narration`, per-chapter `voice_id`, narration notes) with `PUT .../chapters/{chapter_id}` before narrating. ## 3b. Paragraphs Chapters are containers; **paragraphs are the atomic unit of narration**. Each paragraph is narrated, billed, re-recorded, and version-tracked (as "takes") independently, so paragraph-level access is what you want for fine-grained control — fixing one mispronounced sentence without re-rendering a 40-minute chapter. ### List a chapter's paragraphs `GET /audiobook/projects/{project_id}/chapters/{chapter_id}/paragraphs` — returned in `position` order. 1-based page number. **Optional.** Paragraphs per page, `1`–`200`. **Optional**; defaults to 50 when only `page` is given. Pagination here is **opt-in**: omit *both* `page` and `per_page` and you get every paragraph in the chapter in a single response. Supply either one and paging switches on. `total` is always the chapter's full paragraph count, never the length of the page you received. **Response** (`PaginatedParagraphsResponse`): The paragraphs, ordered by `position`. Total paragraphs in the chapter. Echo of the requested page; `null` in unpaginated mode. Echo of the requested page size; `null` in unpaginated mode. Whether another page follows (`false` in unpaginated mode). Whether a previous page exists (`false` in unpaginated mode). **Paragraph** (`AudiobookParagraphResponse`): Paragraph UUID — **this is your `paragraph_id`**. Back-reference to the owning chapter. Back-reference to the owning project. Order within the chapter (the sort key). The text that will be narrated. Words in the paragraph. Characters in the paragraph (narration is metered per character). Per-paragraph voice override, if set. Per-paragraph speed override. Per-paragraph delivery style. Emotional direction for this paragraph. Speaking character, when the book is cast. Screenplay-style direction applied to this line. Silence appended after this paragraph, in milliseconds (0–10000). `null` follows the book's [pacing](#pacing-and-scene-breaks); a value here always wins. Stored audio key, once narrated. Narrated length, once generated. `pending` · `queued` · `narrating` · `completed` · `failed`. A locked paragraph is protected: edits and regeneration are refused with `409` until it is unlocked. If true, the paragraph is excluded from narration and export. Sanitized reason this paragraph failed. Narration retries so far. ```bash theme={null} # every paragraph in the chapter curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq '.paragraphs[] | {id, position, status}' # or a page at a time curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs?page=1&per_page=50" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` Read one paragraph with `GET .../projects/{project_id}/paragraphs/{paragraph_id}` (handy for status polling) and edit one with `PUT .../projects/{project_id}/paragraphs/{paragraph_id}`. ### Narrate a single paragraph `POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/narrate` generates — or regenerates — audio for exactly one paragraph and records a new *take*. The body is optional; send `{}` to narrate with the paragraph's existing settings. Voice for this take; falls back to the paragraph's, then the project's, voice. Narrate this text instead of the stored paragraph text (the stored text is unchanged). Speed multiplier for this take, `0.5`–`2.0`. Delivery style for this take. Emotional direction for this take. Speaking character (max 120 chars). Performance direction for this take; takes precedence over `narration_style`. The field accepts 500 characters but only the first **110** are performed — see [Auto Direction](#auto-direction) for how to write one, and let it write them for you. **Response:** Background task identifier for this narration. The paragraph being narrated. `queued` — the paragraph's status is set to `queued` immediately. `is_free_regen`, `estimated_credits`, `estimated_chars`, `estimated_seconds`, `take_number_after`. ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID/narrate" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"voice_id":387,"emotion":"wistful"}' # => { "task_id": "...", "paragraph_id": "...", "status": "queued", # "billing": { "is_free_regen": true, "estimated_credits": 0, "take_number_after": 2 } } ``` Poll `GET .../projects/{project_id}/paragraphs/{paragraph_id}` until `status` is `completed` (or `failed`, with `error_message`). The first take of a paragraph is billed, the **first regeneration is free**, and subsequent regenerations are billed again — `billing.is_free_regen` tells you which case you are in *before* the work runs. A `409` means the paragraph is locked; unlock it with `PUT .../paragraphs/{paragraph_id}` first. ### Pacing and scene breaks How long the finished book pauses between passages is a **book setting**, not something you set line by line. Three values, each in milliseconds (`0`–`10000`): | Field | Default | The pause… | | - | - | - | | `paragraph_gap_ms` | `500` | between two passages read by the same voice. When a named character speaks two lines in a row, the gap is half this value. | | `speaker_change_gap_ms` | `500` | when the voice changes — narrator to a character, or one character to another. | | `scene_break_ms` | `1500` | at a scene break. | A paragraph's own `pause_after_ms` always wins for the gap after that paragraph. **Scene breaks are detected for you.** A paragraph made only of divider marks — `* * *`, `***`, `#`, `###`, `⁂`, `~~~`, `---`, `—`, `•••`, `o0o`, a centred `◆`, `❖` or `§` — is a scene break. It stays in the chapter's paragraph list (so you can see where it is), comes back with `skip_narration: true`, is **never narrated and never charged**, and becomes a `scene_break_ms` pause in the chapter audio. Text that merely contains those marks (`***bold***`) or a heading (`# Chapter 1`) is ordinary text. A divider at the very start or end of a chapter is dropped — there is no scene on the other side of it. Pause between passages by the same voice, `0`–`10000` ms. Pause when the voice changes, `0`–`10000` ms. Pause at a scene break, `0`–`10000` ms. Send only the fields you want to change; the response is the full, current pacing. ```bash theme={null} # read the book's pacing (a book that never set it returns the defaults) curl -s "$AP/audiobook/projects/$PID/pacing" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "paragraph_gap_ms": 500, "speaker_change_gap_ms": 500, "scene_break_ms": 1500 } # more air between voices and a longer pause at scene breaks curl -X PATCH "$AP/audiobook/projects/$PID/pacing" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"speaker_change_gap_ms": 900, "scene_break_ms": 2500}' # => { "paragraph_gap_ms": 500, "speaker_change_gap_ms": 900, "scene_break_ms": 2500 } ``` Pacing is applied when chapters are assembled, so changing it **never re-narrates anything and costs nothing**. Assembled chapter audio is refreshed the next time it is played or downloaded, and every export uses the current pacing. Saving pacing also marks divider-only paragraphs in a book imported before scene breaks were recognised, so you don't have to import it again. ## 3c. Auto Direction A narrator that reads every line the same way sounds flat, however good the voice is. **Direction** is what fixes that: a short performance note on each paragraph — how the line is delivered, and with what feeling — that the narrator acts on. You can write directions yourself, one paragraph at a time (see [Direct a single line](#direct-a-single-line)). Auto Direction writes them for the whole book in one call. `POST /audiobook/projects/{project_id}/direction/auto` It runs in two passes. First a **narration brief** for the book as a whole — its reading identity, derived from the metadata and the opening pages. Then a performance note and an emotion per paragraph, each anchored to that brief, so the tone holds across a long book instead of drifting chapter to chapter. Dialogue is directed as dialogue. Direct only these chapters. Omit to direct the whole book. ```bash theme={null} # Direct the whole book curl -X POST "$AP/audiobook/projects/$PID/direction/auto" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}' # => { "job_id": "901", "total_chapters": 23 } # ...or one chapter curl -X POST "$AP/audiobook/projects/$PID/direction/auto" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d "{\"chapter_ids\": [\"$CHAPTER_ID\"]}" ``` **Auto Direction is free on every plan**, including the free tier. It is the same class of work as chapter detection, and a book that only sounds good on a paid plan is a worse product. ### Waiting for a direction run ```bash theme={null} # Status of a specific run... curl "$AP/audiobook/projects/$PID/direction/auto/901/status" -H "X-API-Key: $AUDIOPOD_API_KEY" # ...or of the most recent one, when you did not keep the job_id curl "$AP/audiobook/projects/$PID/direction/auto/latest" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "job_id": "901", "status": "COMPLETED", "progress": 100, # "result": { "directed_count": 315, "skipped_user_edited": 4, # "chapters_processed": 23, "total_chapters": 23 } } ``` Poll until `status` is `COMPLETED` or `FAILED`. A `409` on the POST means a run is already in flight for this project. **Directing does not re-render audio.** It only writes `direction` and `emotion` onto paragraphs that already exist — it never re-splits chapters and never deletes a take. Paragraphs already narrated keep the audio they have until you narrate them again: ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d "{\"voice_id\": 389, \"chapter_ids\": [\"$CHAPTER_ID\"], \"include_completed\": true}" ``` Re-narration is billed normally, so try one chapter before committing a whole book. ### Your directions win A direction you wrote by hand is **never overwritten** by a later automatic run. Writing one via `PUT .../paragraphs/{paragraph_id}` marks that row as yours, and every subsequent Auto Direction skips it — the run reports how many it left alone in `result.skipped_user_edited`. The suggestion it replaced is kept on the row, so reverting to the automatic version costs nothing and needs no new run. ### Direct a single line `PUT /audiobook/projects/{project_id}/paragraphs/{paragraph_id}` ```bash theme={null} curl -X PUT "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"direction": "urgent, low, barely audible", "emotion": "tense"}' ``` A short stage note the narrator acts on. See the length note below. One of `neutral`, `warm`, `tense`, `sad`, `excited`, `intimate`, `dramatic`, `authoritative`. `neutral` means "no particular feeling". **Write a direction short.** The field accepts up to 500 characters, but only the first **110** shape the performance — a longer note is trimmed at a word boundary and the rest is not voiced. One instruction of roughly a dozen words works best: `"urgent, low, barely audible"`, not a paragraph of stage notes. When you send a longer one, the response carries a `warnings` entry telling you exactly how much of it will not be heard. ## Characters and voices A book with dialogue can give every character their own voice. Character voices are off by default; send `multi_voice: true` when you add the manuscript (or on `PUT /audiobook/projects/{project_id}`) and every character found gets a library voice for free (when a suitable one is available). Then review the cast, merge alternate names and change any voice before you narrate. The full workflow, with every endpoint, is on **[Characters and voices (cast)](/api-reference/audiobook-cast)**. ## 3d. Pronunciation dictionary Some words need a nudge no voice selection can give you — the wrong stress on a place name, a word from another language read as if it were English. A **pronunciation dictionary** fixes this per project: map a written word (`source`) to what the narrator should actually say (`alias`), and every future narration or regeneration in every chapter of the project picks it up automatically. The substitution happens only at synthesis time. The manuscript, the paragraph text returned by the API, and everything in the exported package all keep the original spelling — only what gets spoken changes. ### Manage entries Same `X-API-Key` auth as the rest of this API. | Operation | Method | Endpoint | | - | - | - | | List entries | GET | `/audiobook/projects/{project_id}/pronunciations` | | Add an entry | POST | `/audiobook/projects/{project_id}/pronunciations` | | Update an entry | PUT | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` | | Delete an entry | DELETE | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` | The written word or phrase to match, 1–200 characters. What the narrator says instead of `source`, minimum 1 character. See [Writing a good alias](#writing-a-good-alias) below. When `false`, `source` matches regardless of case. ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/pronunciations" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"source": "Otranto", "alias": "Òtranto"}' # => 201 { "id": "...", "source": "Otranto", "alias": "Òtranto", "case_sensitive": false } ``` `POST` returns `201` on success and `409` when `source` already has an entry for this project — update the existing one instead of retrying the create: ```bash theme={null} curl -X PUT "$AP/audiobook/projects/$PID/pronunciations/$PRONUNCIATION_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"alias": "Òtranto", "case_sensitive": true}' curl -X DELETE "$AP/audiobook/projects/$PID/pronunciations/$PRONUNCIATION_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # => 204 ``` ### Matching semantics * **Whole-word.** A match's boundaries are non-alphanumeric characters or the start/end of the text, so `source: "rise"` matches the standalone word `rise` and not the `rise` inside `surprise`. * **Multi-word sources work**, including hyphens, apostrophes, and periods — `"Mr. Smith"` is a valid `source`. * **Case-insensitive by default.** Set `case_sensitive: true` to require an exact-case match. * **Longer sources win.** If two entries' sources overlap on the same text, the longer one is applied. ### Writing a good alias The safe default, and what works on **every** voice: an accented or phonetic respelling of the same word. It doesn't need to be true IPA — anything that nudges the reader to the right sound works: | Problem | `source` | `alias` | | - | - | - | | Wrong stress on a place name | `Otranto` | `Òtranto` (or `Oh-TRAHN-toh`) | | A word read in the wrong language | `rise` | `rìse` | **IPA is advanced, and voice-dependent.** `alias` may instead be a single word's IPA transcription wrapped in forward slashes — one word per pair of slashes, no spaces inside: `/ˈɔtranto/`. This is honored only on voices that support expressive delivery (the [`delivery_mode`](#delivery-mode-how-performed-the-read-is) setting). On a voice without that support, the slash-wrapped notation is stripped before synthesis and **the word is silently dropped from the narration** — not mispronounced, missing. Unless you know the voice you're narrating with supports it, use an accented or phonetic respelling instead; it's honored on every voice. Pronunciation fixes belong here, in `alias` (or by editing the paragraph text itself) — not in a paragraph's `direction` field. `direction` shapes performance, not pronunciation, and phonetic notation placed there is stripped rather than spoken. ### Applying a fix without re-rendering the whole book New entries apply automatically to narration going forward, but a paragraph that was already narrated keeps its existing audio until it is re-rendered. `POST .../narration/batch` accepts `paragraph_ids` (in addition to `chapter_ids`), so you can re-render only the affected lines instead of a whole chapter — get the ids from [`GET .../chapters/{chapter_id}/paragraphs`](#list-a-chapters-paragraphs): ```bash theme={null} # find the paragraphs that mention the word you just fixed curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq -r '.paragraphs[] | select(.text | test("Otranto")) | .id' # re-render only those paragraphs curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d "{\"voice_id\": 387, \"paragraph_ids\": [\"$PARA_ID\"], \"include_completed\": true}" ``` Narration billing is per paragraph: the first take is billed, the first regeneration of a paragraph is free, and later regenerations bill again at the normal rate — the same rule as [manual regeneration](#narrate-a-single-paragraph). `GET /audiobook/projects/{project_id}/cost-estimate` already reflects this per paragraph, so you can check what a targeted re-render will actually cost before you run it. ## 4. Choose a voice `GET /audiobook/voices/available` returns a `VoiceSelectionResponse`: All standard voices you can narrate with (each has an integer `id`). A curated shortlist. Your own cloned voices (see [Voice Management](/api-reference/voice-management)). `GET /audiobook/voices/languages` lists every supported narration locale (100+). Before committing, `GET /audiobook/projects/{project_id}/cost-estimate` returns a per-paragraph breakdown with `word_count`, `estimated_seconds`, and `credits_to_charge`. ### Book cost `GET /audiobook/projects/{project_id}/cost` returns what narrating this book has **actually** cost so far. Unlike the cost estimate, it is read from your billing records: every charge taken for a passage of this book, including regenerations. Narration is prepaid, so a charge is taken when a passage is queued and returned if that passage fails or is cancelled. Narration is priced at **\$0.09 per 1,000 characters** (see [Pricing](#pricing)). ```bash theme={null} curl -s "https://api.audiopod.ai/api/v1/audiobook/projects/$PID/cost" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` ```json theme={null} { "project_id": "3f6c2a1e-8b4d-4c1a-9e2f-5d7b9a0c1e23", "currency": "USD", "billing_lane": "api_wallet", "totals": { "charged_cents": 4770, "refunded_cents": 90, "net_cents": 4680, "charged_credits": 0, "refunded_credits": 0, "net_credits": 0, "characters_billed": 520000 }, "breakdown": { "first_narration": { "characters": 500000, "cents": 4500, "credits": 0 }, "regenerations": { "characters": 20000, "cents": 180, "credits": 0, "count": 14 }, "free_regenerations": 37, "review_approvals": 6 }, "chapters": [ { "chapter_id": "a1b2c3d4-0000-4000-8000-000000000001", "chapter_number": 1, "title": "Chapter One", "characters": 260000, "cents": 2340, "credits": 0 }, { "chapter_id": "a1b2c3d4-0000-4000-8000-000000000002", "chapter_number": 2, "title": "Chapter Two", "characters": 260000, "cents": 2340, "credits": 0 } ], "partial": false, "notes": [], "as_of": "2026-10-09T12:00:00Z" } ``` **First narration vs regenerations.** `first_narration` is each passage's first take. When you edit text and re-render, only the edited passages are re-narrated, not the whole chapter. The first regeneration of each passage is free (counted in `free_regenerations`); every take after that is a paid regeneration (`regenerations`). Approving a flagged narration from [review](#narration-review) costs nothing (counted in `review_approvals`). The book. `"USD"`. Every `*_cents` value is in US cents. `"api_wallet"` (paid from the API wallet), `"credits"` (paid from account credits) or `"mixed"` (both). `charged_cents` / `charged_credits`: every charge taken for the book, including ones later returned. `refunded_cents` / `refunded_credits`: charges returned for passages that failed or were cancelled. `net_cents` / `net_credits`: `charged - refunded`, what the book cost; `net_cents` equals the sum of the API wallet debits for this book. `characters_billed`: billable characters of the narration kept. `first_narration`: `{characters, cents, credits}` for each passage's first narration. `regenerations`: `{characters, cents, credits, count}` for paid regenerations (every take after a passage's first). `free_regenerations`: count of regenerations that cost nothing (a passage's first regeneration is free). `review_approvals`: count of flagged narrations approved from review, which cost nothing. Per chapter, `{chapter_id, chapter_number, title, characters, cents, credits}` (net). Passages deleted since they were narrated stay counted under their chapter. `true` when older charges made while this book was being narrated can no longer be tied to one of its passages; they are left out of the figures and `notes` says how many. `false` means every charge is accounted for. Explanations, empty when there is nothing to add. When these figures were read (UTC, ISO 8601). Returns `404` if the book does not exist or is not yours. ## 5. Narrate ### Narrate the book — `narration/batch` `POST /audiobook/projects/{project_id}/narration/batch` is **the** narration endpoint. It queues one job that narrates the selected paragraphs in order. Scope is chosen by what you send: * **omit both `chapter_ids` and `paragraph_ids`** → narrate the entire project; * **`chapter_ids`** → narrate only those chapters; * **`paragraph_ids`** → narrate only those paragraphs. Voice to narrate with (from `voices/available`). Optional — a narrator for this run, in place of the book's. With character voices off it voices every selected paragraph. With [character voices](/api-reference/audiobook-cast) on (`multi_voice`) it voices only the narrator's lines: characters keep their cast voice, and a paragraph given its own voice keeps it. It is a narrator override: it is saved on the narrator lines it voices, like any narration voice, and with character voices off it replaces the voice a line had, including one set with a `PUT`. Omit it to use the [voice order](/api-reference/audiobook-cast#voice-order). Optional — turn character voices on or off, saved on the book before this run reads its voices. `true` also gives uncast characters a library voice. Optional — UUIDs of chapters to narrate. Optional — UUIDs of individual paragraphs to narrate. Omit `chapter_ids` and `paragraph_ids` to narrate the whole book. An **empty** list (`[]`) is refused with `400` and `"code": "empty_selection"`, so a filter of yours that matched nothing never narrates, or bills, the whole book. Re-narrate paragraphs that already have finished audio. Include locked paragraphs. Include paragraphs marked `skip_narration`. Per-run narration settings — see [Delivery mode](#delivery-mode-how-performed-the-read-is) below. Optional — `manual` or `auto_accept`. When sent, it is saved on the project for this and later narration. See [Narration review](#narration-review). #### Delivery mode — how performed the read is `narration_settings.delivery_mode` is the single biggest lever on how a book *sounds*: | Value | Read | | - | - | | `stable` | One steady narrator from the first paragraph to the last. Least performance range. **This is the default for book narration.** | | `balanced` | Between the two. | | `creative` | The most range and variation — usually what narrative fiction wants. | Books default to `stable` because a long narration is where drift is most audible: across several hundred paragraphs, a narrator whose timbre wanders is worse than one that is a little plain. If your title wants a performed read — fiction, dialogue-heavy work, anything theatrical — ask for it: ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d "{\"voice_id\": 389, \"chapter_ids\": [\"$CHAPTER_ID\"], \"include_completed\": true, \"narration_settings\": {\"delivery_mode\": \"creative\"}}" ``` `delivery_mode` and [Auto Direction](#auto-direction) work on different things and compose well: delivery mode sets how much range the voice has at all, direction says what to do with it line by line. A directed book on `creative` is the most expressive result available. Also accepted in `narration_settings`: `speed` (0.5–2.0) and `language` (a bare subtag such as `"it"`, overriding the project's language for this run). **Response** (`BatchNarrationResponse`): The narration job. Paragraphs queued for this run. Paragraphs excluded by the `include_*` rules above (already completed, locked, or skipped), plus passages waiting for review. Of `skipped_paragraphs`, how many are waiting for [review](#narration-review). A batch never narrates these again, whatever the `include_*` flags or `paragraph_ids` say — approve, reject or edit them. Human-readable estimate. ```bash theme={null} # whole project curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"voice_id":387}' # => {"job_id": 1042, "total_paragraphs": 318, "skipped_paragraphs": 0, # "estimated_completion_time": "Queued"} # just two chapters curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"voice_id":387,"chapter_ids":["",""]}' ``` ```python theme={null} # whole project requests.post(f"{AP}/audiobook/projects/{pid}/narration/batch", headers=H, json={"voice_id": 387}) # or a specific chapter requests.post(f"{AP}/audiobook/projects/{pid}/narration/batch", headers=H, json={"voice_id": 387, "chapter_ids": [chapter_id]}) ``` Common non-200s: **`402`** insufficient balance (nothing is queued or charged), **`409`** another narration/parse/mix/export is already live for this project, **`503`** the job could not be queued (nothing charged — retry). ### Track progress `GET /audiobook/projects/{project_id}/narration/progress` returns a `NarrationProgress`: Total chapters. Finished narrations. Failed narrations. Currently rendering. Per-chapter status detail. Each entry carries `needs_review` — the number of passages in that chapter waiting for review — and its `status` is `needs_review` when any are (and nothing is narrating or failed). Passages waiting for [review](#narration-review), across the book. Narration is complete when `completed_chapters + failed_chapters >= total_chapters`. (Or skip polling and wait for the [completion webhook](#completion-webhooks).) ### Paragraph-level & regeneration For a single line, use [`POST .../paragraphs/{paragraph_id}/narrate`](#narrate-a-single-paragraph). To re-render a whole chapter after an edit, `POST .../projects/{project_id}/narration/batch` with `{"chapter_ids": [""], "include_completed": true}` (add `voice_id` to swap the voice at the same time) — `include_completed` is what makes it a *re*-render, since the chapter's paragraphs are already complete and would otherwise all be skipped. To re-queue only the paragraphs of a chapter whose status is `failed`, `POST .../projects/{project_id}/chapters/{chapter_id}/retry-failed-paragraphs`. ### Cancel a narration run `POST /audiobook/projects/{project_id}/narration/cancel` stops a narration run that is still in progress. No request body is required. Cancelling is **resume-friendly, not destructive**: * Paragraphs that already finished keep their audio — nothing you have paid for is discarded. * Paragraphs that had not produced audio yet are returned to a re-narratable state, and the charges held for them are released. * To carry on later, call `POST .../narration/batch` again: it skips the paragraphs that are already done and picks up only what is left. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/audiobook/projects/{project_id}/narration/cancel" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` If no narration is running, the endpoint returns `404` with `detail.error_code = "no_narration_running"`. That is also what a **repeat** cancel returns — so a client that retries after a network timeout can treat this as confirmation that the earlier cancel succeeded, not as an error. If you have a webhook endpoint registered, cancelling emits a [`job.cancelled`](#completion-webhooks) event, so an integration waiting on a narration job learns it has stopped instead of polling until it times out. Existing webhook endpoints only receive the events they were registered with. If yours was created before `job.cancelled` existed, re-register it (or add the event) to receive cancellations. ### Regenerate a completed chapter By default, `POST .../narration/batch` **skips** paragraphs that already have completed narration — that's the `"No eligible paragraphs to narrate"` response. To re-narrate them, pass `"include_completed": true`: ```json theme={null} { "voice_id": 389, "chapter_ids": [""], "include_completed": true } ``` That body — one `chapter_ids` entry plus `include_completed` — *is* the "re-render one chapter" call. There is no separate endpoint for it; the old `.../chapters/{chapter_id}/regenerate` route is deprecated and returns `410 Gone`. ## Narration review For automated pipelines, set `narration_review_policy` to `auto_accept` on the project; passages are then never held and your chapters always complete. After each passage is narrated, our narration check confirms the audio matches the text. Short or highly expressive lines (laughter, interjections, a phrase in another language) occasionally can't be confirmed, so instead of failing the book, AudioPod handles them like this: 1. **Retry once.** A passage whose audio doesn't match is narrated again — at most 2 automatic renders per passage. 2. **Hold for review.** If it still doesn't match, the passage is set to `needs_review` and its best render is kept as a **candidate**. It is **never re-narrated automatically** — no batch, retry or recovery will render it again; only reject with `regenerate`, a single-paragraph narrate, or a text edit renders it again. 3. **Resolve it.** Held passages **block chapter assembly and export** until you decide: * **Approve** — the candidate becomes the passage's audio as a new take. Approving is **free** (the candidate's narration was already refunded). On a per-finished-hour book it still counts toward your narration hours. * **Approve all** — the same, for every waiting passage in the book or one chapter. * **Reject** — the candidate is discarded and the passage returns to `pending`. Pass `regenerate: true` to narrate it again right away, billed like any narration. * **Edit the text** (`PUT .../paragraphs/{paragraph_id}`) — the candidate no longer applies and is retired; the passage returns to pending for a normal narration. Restoring an earlier take also resolves it. **Replacement candidates.** When a passage that already had approved audio is narrated again and the new render can't be confirmed, the passage keeps its existing audio (`completed`) and the new render waits as a replacement candidate (`replacement: true`). It appears in the review list like any held passage, and counts toward `needs_review_paragraphs` and the per-chapter `needs_review` in progress. Approve to swap it in; reject to keep the audio you had. ### Review policy — `narration_review_policy` | Value | Behaviour | | - | - | | `manual` (default) | Unconfirmed passages are held for your review; the book waits on them. | | `auto_accept` | The best render is published and flagged (`auto_accepted: true`), so the book never blocks. You can still list flagged passages with `status=all_flagged`. | Set it on project create, on `PUT /audiobook/projects/{project_id}`, or in the `narration/batch` body (saved on the project). **For fully automated pipelines with no human in the loop, use `auto_accept`.** ```bash theme={null} curl -X PUT "$AP/audiobook/projects/$PID" -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" -d '{"narration_review_policy":"auto_accept"}' ``` ### Knowing when review is needed * `GET .../narration/progress` returns `needs_review_paragraphs` (book total) and a `needs_review` count per chapter in `chapter_status`. Both include held replacement candidates (whose passage stays `completed`). * `POST .../narration/batch` returns `skipped_needs_review` — held passages are skipped whatever the `include_*` flags or `paragraph_ids` say. When the only passages left are held for review, the batch returns `400` telling you how many wait for review (approve, reject or edit them). * `POST .../chapters/{chapter_id}/retry-failed-paragraphs` returns `needs_review` — held passages are not failures and are never retried there. * A [`narration.review_required`](/account/webhooks#narration-review-required) webhook fires once per narration job that finished with passages waiting. * `POST .../export` returns `422` with `detail.code = "narration_review_pending"` and the `count` of waiting passages. ### List passages for review `GET /audiobook/projects/{project_id}/review` `needs_review`: passages waiting for a decision. `all_flagged`: also passages accepted automatically or already approved. ```bash theme={null} curl "$AP/audiobook/projects/$PID/review?status=needs_review" -H "X-API-Key: $AUDIOPOD_API_KEY" ``` ```json theme={null} { "items": [ { "paragraph_id": "7d0c…", "chapter_id": "a41e…", "chapter_number": 3, "position": 12, "status": "needs_review", "expected_text": "Ha! Not on your life.", "heard_text": "Ha ha. Not on your life.", "coverage": 0.71, "candidate_audio_url": "https://media.audiopod.ai/…mp3?…", "duration_seconds": 2.4, "flagged_at": "2026-10-08T09:14:02Z", "attempts": 2, "auto_accepted": false, "approved_at": null, "replacement": false } ], "total": 1 } ``` The passage text. What our narration check heard in the candidate. How much of the text the check matched (0–1). Short-lived (1 hour) link to the candidate audio; re-list to refresh it. Renders tried. Published automatically under `auto_accept`. Set once approved; `null` while waiting. The candidate would replace audio the passage already has. ### Approve one passage `POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/review/approve` — no body. Returns the updated paragraph (status `completed`). The chapter is rebuilt once nothing else in it is outstanding. Approving an already-approved passage returns it unchanged. ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID/review/approve" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "id": "7d0c…", "status": "completed", ... } ``` ### Approve all `POST /audiobook/projects/{project_id}/review/approve-all` Optional — only approve passages in this chapter. Omit the body to approve the whole book. ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/review/approve-all" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d "{\"chapter_id\": \"$CHAPTER_ID\"}" # => { "approved": 3, "refused": 1, "remaining": 1, # "refused_reasons": { "9b2f…": "narration_allowance_exhausted" } } ``` A passage that can't be approved is left waiting and reported; the call still returns `200` with a partial result. Passages approved by this call. Passages that could not be approved. Passages still waiting for review in the selection. `{paragraph_id: reason_code}` for each refusal: `narration_allowance_exhausted` (a book bought per finished hour has no hours left for this audio), `review_stale` (the passage has newer narration) or `review_candidate_unavailable`. ### Reject one passage `POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/review/reject` Narrate the passage again right away (billed like any narration). ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID/review/reject" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"regenerate": true}' # => { "paragraph_id": "7d0c…", "status": "queued", # "regenerate": { "task_id": "…", "paragraph_id": "7d0c…", "status": "queued", "billing": { … } } } ``` Without `regenerate`, the response is `{"paragraph_id": "…", "status": "pending", "regenerate": null}` (or `"completed"` for a replacement candidate — the passage keeps its existing audio). ### Review errors | Status | When | | - | - | | `409` | Approve and reject return `{"detail": {"code": "…", "message": "…"}}`. `review_not_pending`: nothing waits for review on this passage. | | `409` | Approve: `review_stale` (the passage has newer narration than the candidate), `review_candidate_unavailable`, or, for a book bought per finished hour, `narration_allowance_exhausted` / `narration_allowance_changed` (approved audio counts toward the book's hours). | | `409` | Approve or approve-all while an export is running for the project. | | `404` | The paragraph is not in this project. | | `422` | `POST .../export` while passages wait: `{"detail": {"code": "narration_review_pending", "message": "…", "count": 3}}`. | ## 6. Production extras (optional) Add intro/outro music, narrated opening/closing credits, or a cover before export: * `GET·PUT /audiobook/projects/{project_id}/production-settings` — `ProductionMusicSettings` (`intro`, `outro`, `credits`, `cover`, `silence`). **This is where the spoken title/author announcement is configured.** Set `credits.narrator_name` (plus `include_opening` / `include_closing`, both default `true`) and the export produces narrated `Chapter_00_*_Credits.mp3` (title, subtitle, author, narrator) and `Chapter_99_*_Credits.mp3` as separate files in the ACX package — the opening announcement Audible/ACX expect. **No credits files are produced unless this is set.** ```json theme={null} PUT /audiobook/projects/{project_id}/production-settings {"credits": {"narrator_name": "Jane Doe", "include_opening": true, "include_closing": true}} ``` Previously named `/music-settings`. That path still works and will keep working, but it only ever described 2 of the 4 fields — use `production-settings`. * `POST /audiobook/projects/{project_id}/media/upload` — upload a cover image or custom music track to reference from the settings above. ### Chapter silence (room tone) Every exported chapter opens and closes with digital silence — this is why an exported MP3 has leading and trailing silence, and it is a retail requirement, not padding you should trim. The same values apply to the per-chapter download and to each chapter inside the ACX package, so an audition always matches what ships. Defaults are **750 ms head / 3000 ms tail** (mid-window for ACX). Override per project with `silence`: ```json theme={null} PUT /audiobook/projects/{project_id}/production-settings {"silence": {"head_ms": 2000, "tail_ms": 2000}} ``` Leading silence, in milliseconds. Accepted range **0–5000**. Trailing silence, in milliseconds. Accepted range **0–10000**. Read-only. `true` when `head_ms` is 500–1000 and `tail_ms` is 1000–5000 — the ACX opening/closing silence requirements. `silence` is always returned, populated with the defaults when you have never set it, so you can read the effective values without knowing them. Values outside the **ACX** range are **accepted, not rejected** — plenty of deliveries are not Audible, and a house standard of 2 s/2 s is a legitimate choice. Each out-of-range field adds an entry to the response's `warnings` array naming the field and the ACX range, and `acx_compliant` flips to `false`: ```json theme={null} { "silence": { "head_ms": 2000, "tail_ms": 2000, "acx_compliant": false }, "warnings": ["head_ms 2000 is outside the ACX range 500–1000 ms; files may be rejected by ACX QA."] } ``` Values outside the **accepted range** (a negative `head_ms`, a `tail_ms` above 10000\) are a `422`. Send `"silence": null` to reset both sides to the defaults; omit the field entirely to leave them unchanged. If you are delivering to ACX/Audible, keep `acx_compliant` at `true`. Out-of-range silence is one of the most common causes of a retail QA rejection, and the rejection arrives after upload — not at export time. ## 7. Export the ACX package `POST /audiobook/projects/{project_id}/export` runs an automatic ACX compliance check, then assembles the package. Optional — export only these chapters. Optional — override export settings. **Response** (`ACXExportResponse`): Export job ID. An `ACXComplianceCheck` (see below). Any additional TTS cost incurred by the export (0 when audio already exists). **`ACXComplianceCheck`:** Whether the book passes ACX requirements. Blocking problems (export is refused when non-empty). Non-blocking advisories. Container/bitrate compliant. RMS/peak within ACX bounds. Head/tail room present. File names follow the ACX convention. If a book is non-compliant, the call returns the specific issues instead of producing an invalid package: ```json theme={null} { "detail": { "message": "Project is not ACX-compliant. Fix these issues first.", "issues": ["Chapter 3: 28s < ACX minimum 30s"], "warnings": [] } } ``` Poll `GET .../export/{job_id}/status` until `COMPLETED`, then fetch the download: `GET /audiobook/projects/{project_id}/export/{job_id}/download` returns a JSON manifest with a **7-day presigned `download_url`** to the ZIP: ```json theme={null} { "download_url": "https://media.audiopod.ai/...acx_package_xxx.zip?...", "file_size_bytes": 2079760, "file_format": "zip", "chapter_count": 2 } ``` ### The ACX package | File | Contents | | - | - | | `Chapter_NN.mp3` | One per chapter — MP3, **192 kbps CBR, 44.1 kHz** | | `00_retail_sample.mp3` | Retail / audition sample | | `Chapter_00/99_*_Credits.mp3` | Optional narrated opening/closing credits (when configured) | | `metadata.json` | Title, author, narrator, per-chapter durations, format spec | | `manifest.csv` | Flat file list | | `provenance.json` | Generation provenance | ## Completion webhooks Rather than poll, register an endpoint and receive a signed event the moment a job finishes — the recommended pattern for backlog-scale automation. | Operation | Method | Endpoint | | - | - | - | | Register endpoint | POST | `/webhooks/endpoints` | | List endpoints | GET | `/webhooks/endpoints` | | Send a test event | POST | `/webhooks/endpoints/{endpoint_id}/test` | | Delivery log | GET | `/webhooks/endpoints/{endpoint_id}/deliveries` | | Redeliver | POST | `/webhooks/deliveries/{delivery_id}/redeliver` | ```bash theme={null} curl -X POST "$AP/webhooks/endpoints" -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://your-app.com/hooks/audiopod","events":["job.completed","job.failed","job.cancelled"]}' # => { "id": "...", "url": "...", "events": [...], "secret": "whsec_..." } # secret shown ONCE ``` Every delivery carries these headers: | Header | Meaning | | - | - | | `X-AudioPod-Signature` | `sha256=` HMAC of the payload (see below) | | `X-AudioPod-Timestamp` | Unix seconds; part of the signed material (replay protection) | | `X-AudioPod-Event` | `job.completed`, `job.failed`, `job.cancelled`, or `narration.review_required` | | `X-AudioPod-Event-Id` | Stable UUID per event — **dedupe on this** (delivery is at-least-once) | **Verify the signature** — HMAC-SHA256 over `"."` with your endpoint secret as the key: ```python theme={null} import hmac, hashlib def verify(secret: str, raw_body: bytes, signature_header: str, timestamp_header: str) -> bool: expected = hmac.new( secret.encode(), timestamp_header.encode() + b"." + raw_body, # exact raw bytes received hashlib.sha256, ).hexdigest() return hmac.compare_digest(f"sha256={expected}", signature_header) ``` Delivery is retried with exponential backoff and parked in a dead-letter queue after repeated failures; inspect and redeliver from the delivery log. Endpoints are SSRF-guarded (the resolved IP is re-checked at delivery time; https-only; private/internal ranges blocked). ## Status values `draft` → `parsing` → `ready_for_narration` → `narrating` → `ready_for_production` → `mixing` → `ready_for_export` → `exporting` → `completed`. `failed` if a stage cannot complete (with the reason in `error_message`). Note that the state *after a successful parse* is `ready_for_narration`, not `parsing` or `completed` — pair it with `parsing_completed_at` when waiting on a parse. `pending` → `queued` → `narrating` → `completed`, or `failed` with an `error_message`. A paragraph can also be `needs_review` (held for [narration review](#narration-review)). Jobs are prepaid: credits are reserved at job start, settled on success, and **released on failure** — a failed narration or export never leaves a stranded charge. Narration and export retry automatically; a `job.failed` webhook fires if a job ultimately can't complete, so an automated integrator can react deterministically. ## Error Handling The export found blocking issues (e.g. a chapter under the 30-second minimum). The response lists each issue under `detail.issues`. Fix the flagged chapters (lengthen, merge, or `skip_narration`) and re-export. Chapters under \~50 characters can't be narrated (often a stray title-page fragment from PDF parsing). Mark it `skip_narration: true` via `PUT .../chapters/{id}`, or merge it into an adjacent chapter, then continue. Some passages are waiting for [narration review](#narration-review). `detail.count` says how many. List them with `GET .../review`, then approve, reject or edit them and export again. Narration/export draws from your prepaid balance. Top up the [API Wallet](/account/api-wallet) and retry; reserved credits for a failed job are released automatically. Parsing, narration, mixing, and export are mutually exclusive per project — each would rewrite structure the other is reading. Poll `GET .../narration/progress` (or the project's `status`) and retry once the live job finishes. A `409` on a single paragraph means it is `locked`; unlock it via `PUT .../paragraphs/{paragraph_id}` first. `POST .../narration/start` is deprecated and removed. Send `POST .../narration/batch` instead — with no `chapter_ids` and no `paragraph_ids` it narrates the whole project. Deprecated and removed. Send `POST .../narration/batch` with `{"chapter_ids": [""], "include_completed": true}`. Without `include_completed` the request succeeds but narrates nothing — every paragraph in an already-rendered chapter is filtered out as complete, and you get `400 No eligible paragraphs to narrate`. You are probably sending `skip`/`limit`. Unknown query parameters are ignored rather than rejected, so the request still returns `200` — with the first page, every time. The project and chapter list endpoints page with `page` and `per_page`; loop on `has_next`. Send a valid `X-API-Key` header. Keys are created in the [dashboard](https://www.audiopod.ai/dashboard/account/api-keys) and can expire — check the key's status if previously working calls start returning 401. ## Pricing Usage-based on a prepaid wallet — you pay per 1,000 characters of text narrated, with no per-seat fee or monthly minimum. | Service | Rate | | - | - | | Narration | \$0.09 per 1,000 characters | | Voice cloning | \$0.05 per 1,000 characters | | Transcription (if used) | \$0.01 / min | A 10-hour audiobook (\~560,000 characters) costs roughly \*\*$50**, versus $2,000–\$4,000 for a human ACX narrator. For committed monthly volume across many titles, [contact us](mailto:sales@audiopod.ai) about partner pricing below list. See [API Wallet](/account/api-wallet) for top-ups and balance. ## Next Steps Create custom narrator voices from a short sample, then narrate with them. Top up, check your balance, and see per-minute pricing. API keys, scopes, and how to authenticate every request. The shortest path from zero to a generated audiobook. # Characters and voices (cast) Source: https://docs.audiopod.ai/api-reference/audiobook-cast Give each character in an audiobook its own voice through the API: find who speaks each line, build the book's cast, merge alternate names, choose a voice per character, then narrate. ## Overview By default every line of a book is read by one narrator: the project's `selected_voice_id`. **Character voices** give the characters their own voices. Every paragraph carries a `character_name` (empty means the narrator). The book keeps a **cast**: one voice per character. When character voices are on, each character's lines are narrated with that character's cast voice. ``` add manuscript (detect_speakers / multi_voice) → review cast → merge aliases → choose voices → narrate ``` Every endpoint on this page accepts an API key (`X-API-Key`) as well as a session token, and works only on projects you own. All paths are relative to `https://api.audiopod.ai/api/v1`. ### Turning character voices on Character voices are **off by default**. While they are off, the book narrates exactly as it always has: one narrator, plus any voice you set on a chapter or a single paragraph. Turn them on with `multi_voice: true` in any of these places: | Where | Field | | - | - | | When adding the manuscript | `multi_voice` on `POST .../manuscript/parse`, `.../manuscript/paste` or `.../manuscript/generate`, or the `multi_voice` form field on `POST .../manuscript/upload` | | On an existing book | `PUT /audiobook/projects/{project_id}` with `{"multi_voice": true}` | | When narrating | `multi_voice` on `POST .../narration/batch` (saved on the book before the run reads its voices) | | By choosing a character's voice | [`POST .../cast/assign`](#3-choose-a-voice-for-a-character) for a character, or [auto-cast](#auto-cast-design-a-voice-per-character), turns it on | Turning character voices on by any of these routes (or sending `multi_voice: true` again) does two things at no cost: * every speaker in the book gets a row in the cast; * **each character without a cast voice gets a library voice automatically, when a suitable one is available.** When `cast/assign` or auto-cast is what turns them on, the character you chose keeps its voice and every other character without a cast voice gets a library voice on the same terms. The pick prefers voices whose own language is the book's language, then voices that support it, then, when the character has a gender hint, a voice of that gender. It never takes the narrator's voice or a voice another character already has. If no suitable library voice is left for the book's language, the character stays without a cast voice and its lines read in the narrator voice; give it one with `cast/assign`. The pick is stable, so the same book is cast the same way every time. Change any pick with `cast/assign`. Send `multi_voice: false` to turn them off. Lines then follow the [voice order](#voice-order) for character voices off: a line voiced only through the cast goes back to the chapter's voice or the narrator, while a voice saved on the line stays with it (one you set, a batch `voice_id` it was narrated with, or the narrator voice narration saved on it). Locked lines keep the voice they had. The cast is kept for when you turn character voices back on. The project response carries `multi_voice` (`true` or `false`). ### Finding the speakers Character voices need to know who speaks each line. Speaker detection fills `character_name`, free. There are two passes: an instant pass over the text, described here, and [our speaker detection pass](#speaker-detection-pass-with-character-voices-on), which reads each line in context and runs by itself when character voices are on. | Field (on `parse`, `paste`, `generate`) | Default | | - | - | | `detect_speakers` | **Off for API-key requests**, on in the web studio. `multi_voice: true` turns it on. | Detection reads a chapter in order and attributes a paragraph to a named character when it is clearly that character's speech: * dialogue in `«…»`, `“…”`, `"…"`, or opened with an em dash (`— Buon giorno, — disse Geppetto.`); * a reporting verb next to a name, in English or Italian (`said Eleanor`, `Eleanor said`, `disse Geppetto`, `gridò mastr'Antonio`), titles included (`maestro Ciliegia`, `Captain Reyes`); * a narration paragraph ending in `:` that names who speaks next; * the turn-taking of a two-person exchange: once both speakers are known, untagged lines that follow alternate between them. Anything ambiguous (a third speaker, "he said", a line addressing the person it would be given to) stops the alternation. The instant pass is built for precision, so when in doubt a line stays with the narrator. A paragraph is voiced whole, so it is given to a character only when at least 60% of its words are speech; a long narration with a short quote stays with the narrator. Detection only fills paragraphs that have no `character_name` and are not locked. It never replaces a name you set, and a paragraph whose speaker you cleared stays with the narrator. You can also run either pass later with [`POST .../cast/detect`](#detect-speakers-on-an-existing-book), or use [screenplay annotation](#split-mixed-paragraphs-screenplay-annotation) to split mixed paragraphs into separate lines. ### Voice order When a paragraph is narrated, its voice is the first of these that applies. **Character voices off** (the default): 1. a voice sent for that one paragraph (`POST .../paragraphs/{id}/narrate` with `voice_id`), or a `voice_id` sent on `narration/batch`; 2. the paragraph's own `voice_id`; 3. the chapter's `voice_id`; 4. the project's `selected_voice_id` (the narrator). **Character voices on** (`multi_voice: true`): 1. a voice sent for that one paragraph (`POST .../paragraphs/{id}/narrate` with a different `voice_id`); 2. a voice **you** set on the paragraph (`PUT .../paragraphs/{id}` with `voice_id`); 3. the character's cast voice; 4. a `voice_id` sent on `narration/batch` (a narrator for that run); 5. the chapter's `voice_id`; 6. the project's `selected_voice_id` (the narrator). With character voices on, a batch `voice_id` and a chapter `voice_id` **replace the narrator only**: character lines keep their cast voice, and a line you gave its own voice keeps it. A voice you set for a single line is yours and outranks the cast: a `PUT` with `voice_id`, or narrating one paragraph with a different `voice_id`. Send `{"voice_id": null}` in a `PUT` to release it. Narration saves the voice it used on each line it narrates, except a line voiced through the cast. A batch `voice_id` is a narrator override, and it is saved this way on the narrator lines it voices. With character voices off, a batch `voice_id` voices every selected line and replaces the voice saved on it, including one you set with a `PUT`. While character voices are off, a saved voice comes before the chapter and narrator voices, so changing the narrator does not reach lines that are already narrated. With character voices on, a saved voice you did not set is ignored, so a narrator or cast change reaches every line that is narrated again; it applies again if you turn character voices off. | Step | Method | Endpoint | Cost | | - | - | - | - | | Turn character voices on or off | PUT | `/audiobook/projects/{project_id}` (`multi_voice`) | Free | | Read the cast | GET | `/audiobook/projects/{project_id}/cast` | Free | | Detect speakers | POST | `/audiobook/projects/{project_id}/cast/detect` | Free | | Split paragraphs into speaker lines | POST | `/audiobook/projects/{project_id}/screenplay/annotate` | Free | | Annotation status | GET | `/audiobook/projects/{project_id}/screenplay/annotate/{job_id}/status` | Free | | Merge an alternate name | POST | `/audiobook/projects/{project_id}/cast/alias` | Free | | Choose a voice for a character | POST | `/audiobook/projects/{project_id}/cast/assign` | Free | | Tag or voice one paragraph | PUT | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}` | Free | | Design a new voice per character | POST | `/audiobook/projects/{project_id}/cast/auto` | **Prepaid**, see [Auto-cast](#auto-cast-design-a-voice-per-character) | | Auto-cast status | GET | `/audiobook/projects/{project_id}/cast/auto/{job_id}/status` | Free | Casting itself costs nothing, except auto-cast, which designs new voices. You pay when you [narrate](/api-reference/audiobook#5-narrate), at the normal narration rate, whichever voice reads the line. **Cast before you narrate.** A new voice only takes effect on a line's next narration, so a voice change on a narrated book means narrating those lines again, at the normal rate. ## Quick start ```bash theme={null} export AUDIOPOD_API_KEY="ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" export AP="https://api.audiopod.ai/api/v1" # PID = a project you created (with "language" and "selected_voice_id", the narrator) # 1. Add the manuscript with character voices on: speakers are detected and each # character gets a free library voice while suitable ones are left (check # voice_id in step 2; null = narrator voice until you cast/assign one). Our speaker detection pass then runs in # the background, and narration started meanwhile waits for it curl -s -X POST "$AP/audiobook/projects/$PID/manuscript/paste" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"text": "Capitolo 1\n\n...", "multi_voice": true, "auto_direction": true}' # 2. Review who speaks, how often, and with which voice curl -s "$AP/audiobook/projects/$PID/cast" -H "X-API-Key: $AUDIOPOD_API_KEY" \ | jq '.members[] | {character_name, is_narrator, paragraph_count, voice_id}' # 3. Merge an alternate name onto the main one curl -s -X POST "$AP/audiobook/projects/$PID/cast/alias" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"character_name": "mastr'"'"'Antonio", "alias_of": "Maestro Ciliegia"}' # 4. Change any voice you do not like (ids from GET /audiobook/voices/available) curl -s -X POST "$AP/audiobook/projects/$PID/cast/assign" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"character_name": "Maestro Ciliegia", "voice_id": 1248}' # 5. Confirm you hold the audio rights (once per book). Without it narration returns # 400 RIGHTS_NOT_ATTESTED curl -s -X POST "$AP/audiobook/projects/$PID/rights-attestation" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "ok": true, "rights_attested_at": "2026-10-12T05:39:51.309434" } # 6. Narrate the whole book curl -s -X POST "$AP/audiobook/projects/$PID/narration/batch" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}' ``` For a file, send `"multi_voice": true` with `file_key` and `filename` on `POST .../manuscript/parse`. ## 1. Find who speaks each line Besides detection when you add the manuscript, two calls fill `character_name`. Neither touches locked paragraphs. ### Speaker detection pass with character voices on When a book has character voices on, adding a manuscript (`parse`, `paste`, `generate`, or a `parse` after an `upload` that sent `multi_voice`) runs a second, free pass after the text is parsed: our speaker detection reads each dialogue paragraph in context. It: * gives a paragraph a speaker only when it is confident, and leaves narration and unnamed speakers with the narrator; * fills only paragraphs that have no `character_name`, are not locked, and whose speaker you did not clear; * merges alternate names of the same character as [aliases](#4-merge-alternate-names) (an alias you set is never changed); * records whether each character is male or female when the text shows it. The hint is used only for a character that has no cast voice when the pass finishes: it does not change a library voice already picked (for example when the manuscript was parsed). To change such a voice, use `cast/assign`. Narration of a passage that has not started waits until this pass finishes, so lines are not narrated before their speaker is known. When you asked for `auto_direction`, direction is written after the pass. The pass covers books up to the same word limit as [annotation](#split-mixed-paragraphs-screenplay-annotation). ### Detect speakers on an existing book `POST /audiobook/projects/{project_id}/cast/detect` `heuristic`: the instant pass, answered at once. `llm`: [our speaker detection pass](#speaker-detection-pass-with-character-voices-on), run as a background job; the response carries `job_id`. Either method is free and only fills paragraphs that have no `character_name` and are not locked, so it never overwrites a name you set. With character voices on, each newly found character gets a library voice when the catalog has a suitable one left (one that suits the book's language and is not the narrator's or another character's); a character left without one reads in the narrator voice until you give it one with `cast/assign`. ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/cast/detect" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "detected_paragraphs": , "members": [ ...same shape as GET /cast... ], # "method": "heuristic", "job_id": null } curl -X POST "$AP/audiobook/projects/$PID/cast/detect?method=llm" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "detected_paragraphs": 0, "members": [ ... ], "method": "llm", "job_id": "2107" } ``` Paragraphs given a speaker by this call. Always `0` for `method=llm`; read the count from the job status. The cast after detection, same shape as [`GET /cast`](#2-review-the-cast). For `method=llm`, the cast as it is when the job starts. `heuristic` or `llm`. `method=llm` only: the detection job. Poll `GET /audiobook/projects/{project_id}/cast/detect/{job_id}/status` until `status` is `COMPLETED` or `FAILED`, or subscribe a webhook endpoint to the `audiobook.cast.detected` event: ```bash theme={null} curl "$AP/audiobook/projects/$PID/cast/detect/2107/status" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "job_id": "2107", "status": "COMPLETED", "progress": 100.0, "current_step": "completed", # "error_message": null, # "result": { "paragraphs_attributed": , "characters": ["Geppetto", ...], # "aliases_added": , "voices_picked": , "targets": } } ``` `PENDING` · `PROCESSING` · `COMPLETED` · `FAILED`. A failed job keeps the speakers it had already written. Paragraphs given a speaker. Speakers written to at least one paragraph. Names merged onto another name of the same character. Characters given a free library voice (character voices on only). Dialogue paragraphs the pass looked at. `method=llm` returns `409` while another detection job runs for the book, `400` when the book is over the annotation word limit, and `503` when speaker detection is unavailable right now. ### Split mixed paragraphs: screenplay annotation `POST /audiobook/projects/{project_id}/screenplay/annotate` Our narration engine reads each paragraph and splits it into lines: narration, and dialogue attributed to the named character who speaks it. A paragraph like *"— Ohi! tu m'hai fatto male! — gridò la vocina."* becomes a dialogue line for the speaker plus a narration line. Each line also gets a short performance `direction`. It works in any language and with any dialogue punctuation: quotation marks, guillemets, or the em dashes common in Italian, French and Spanish fiction. Annotate only these chapters (UUIDs). Omit, or send `{}`, to annotate the whole book. An empty list returns `400` `empty_selection`. ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/screenplay/annotate" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}' # => { "job_id": "1914", "total_chapters": 2 } ``` The annotation job. Poll its status. Chapters in scope. **Annotation is free**, on every plan. Poll `GET /audiobook/projects/{project_id}/screenplay/annotate/{job_id}/status` until `status` is `COMPLETED` or `FAILED`: ```bash theme={null} curl "$AP/audiobook/projects/$PID/screenplay/annotate/1914/status" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "job_id": "1914", "status": "COMPLETED", "progress": 100.0, "current_step": "completed", # "error_message": null, # "result": { "chapters_annotated": 2, "paragraphs_split": , # "characters_found": ["Geppetto", "Maestro Ciliegia", ...], # "skipped_paragraphs": } } ``` `PENDING` · `PROCESSING` · `COMPLETED` · `FAILED`. 0–100. Chapters processed. Paragraphs that were split into more than one line. Named speakers found. Paragraphs sent for annotation that were left unchanged: the engine could not annotate them, the result failed the word-count check below, or the paragraph changed (was edited, locked or queued for narration) while the job ran. Locked paragraphs, paragraphs being narrated or queued, and paragraphs marked `skip_narration` are never sent for annotation, so they are not counted here; they are simply left as they are. What annotation changes, and what it leaves alone: * **A word-count check guards the text.** A result is only applied when the lines together have within ±5% of the original paragraph's word count. Otherwise the paragraph is left alone and counted in `skipped_paragraphs`. The check counts words; it does not compare them one by one, so read the changed paragraphs before you narrate. * **Paragraph ids change where a paragraph is split.** The first line keeps the original paragraph's id and new lines are inserted after it. Re-read the chapter's paragraphs after annotation; do not reuse ids you stored before it. * **Any paragraph whose text changes loses its audio** and goes back to `pending`: every split paragraph, and any paragraph whose text is rewritten even without a split. Annotate before narrating. * **`character_name` is rewritten** on every unlocked paragraph in scope, including names you set by hand. Lock a paragraph (`PUT .../paragraphs/{id}` with `{"locked": true}`) to protect it. * **A `direction` you wrote yourself is kept**; the engine's direction fills only the others. See [Auto Direction](/api-reference/audiobook#3c-auto-direction). * **New characters get a library voice.** With character voices on, each character annotation finds that has no cast voice gets a library voice (never a designed one) when the catalog has a suitable one left (one that suits the book's language and is not the narrator's or another character's); a character left without one reads in the narrator voice until you give it one with `cast/assign`. With character voices off, no voices are picked. `409` means an annotation is already running for this project, a paragraph in scope is being narrated, or the book is exporting. `400` means no chapters matched `chapter_ids`, or the scope is too large for one pass; annotate a range of chapters instead. ## 2. Review the cast `GET /audiobook/projects/{project_id}/cast` ```bash theme={null} curl "$AP/audiobook/projects/$PID/cast" -H "X-API-Key: $AUDIOPOD_API_KEY" ``` ```json theme={null} { "members": [ { "character_name": null, "is_narrator": true, "paragraph_count": 75, "voice_id": 1186, "sample_text": "Capitolo 1.", "alias_of": null, "aliases": [] }, { "character_name": "Maestro Ciliegia", "is_narrator": false, "paragraph_count": 2, "voice_id": 1248, "sample_text": "— Insegno l'abbaco alle formicole.", "alias_of": null, "aliases": ["mastr'Antonio"] }, { "character_name": "Geppetto", "is_narrator": false, "paragraph_count": 1, "voice_id": 917, "sample_text": "— Buon pro vi faccia.", "alias_of": null, "aliases": [] }, { "character_name": "mastr'Antonio", "is_narrator": false, "paragraph_count": 1, "voice_id": 1248, "sample_text": "— Rendimi la mia parrucca! — gridò mastr'Antonio.", "alias_of": "Maestro Ciliegia", "aliases": [] } ], "total_characters": 3 } ``` One entry per speaker: the narrator bucket first, then characters by line count. Names are grouped case-insensitively. A merged alternate name keeps its own entry, with `alias_of` set, and its lines are not counted in the main speaker's `paragraph_count`. The speaker's name as tagged on the paragraphs. `null` for the narrator. `true` for the narrator bucket: every paragraph with no `character_name`. Lines this speaker has (paragraphs marked `skip_narration` are not counted). For the narrator, the project's `selected_voice_id`. For a character with character voices on, the character's cast voice (an alias shows its main speaker's) when the cast has one. Otherwise, the voice most of the character's lines carry, or `null`. One of the speaker's lines, for a quick check. Set when this name is merged onto another speaker. Names merged onto this speaker. Named speakers, not counting the narrator. ## 3. Choose a voice for a character `POST /audiobook/projects/{project_id}/cast/assign` Pick voice ids from [`GET /audiobook/voices/available`](/api-reference/audiobook#4-choose-a-voice), including your own cloned voices. For a book that is not in English, filter by the full locale (`?language=it-IT`) to see the voices suited to it. The character to voice (max 120 characters). Matching is case-insensitive and includes every alias of that name. Set to `true` (or omit `character_name`) to change the narrator instead: the project's `selected_voice_id`. The voice to use. ```bash theme={null} # A character curl -X POST "$AP/audiobook/projects/$PID/cast/assign" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"character_name": "Maestro Ciliegia", "voice_id": 1248}' # => { "updated_paragraphs": 2, "narrator_default_updated": false, "already_narrated": 0 } # The narrator curl -X POST "$AP/audiobook/projects/$PID/cast/assign" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"is_narrator": true, "voice_id": 1186}' # => { "updated_paragraphs": 0, "narrator_default_updated": true, "already_narrated": 0 } ``` The character's unlocked lines (aliases included). Each takes the cast voice, except a line you gave its own voice. `true` when the call changed the project's narrator voice. How many of those lines already have audio. They keep it until you narrate them again. How it works: * **The voice is saved on the book's cast**, not copied onto lines. Lines attributed to the character **later** (by detection, annotation or a `PUT`) take the cast voice too. Choosing a character's voice turns character voices on for the book. When this call is what turns them on, every other character without a cast voice gets a free library voice at the same time, as with `multi_voice: true`. * **Locked lines keep the voice they have.** So does a line you gave its own voice with `PUT .../paragraphs/{id}`. * **Narrated lines keep their audio.** The new voice is heard on the line's next narration; see [Re-narrate](#5-narrate-and-re-narrate). * **The narrator** is the project's `selected_voice_id`. Assigning it does not turn character voices on. ## 4. Merge alternate names Books call the same person different things: *mastr'Antonio* and *Maestro Ciliegia*, *Mr. Darcy* and *Darcy*. An alias makes them one speaker with one voice. `POST /audiobook/projects/{project_id}/cast/alias` The alternate name, as it appears in the cast (1–120 characters). The main name to merge it onto. Send `null` or omit it to remove the alias again. ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/cast/alias" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"character_name": "Darcy", "alias_of": "Mr. Darcy"}' # => { "updated_paragraphs": 1, "members": [ ...updated cast: "Darcy" now has "alias_of": "Mr. Darcy", # and "Mr. Darcy" lists it under "aliases" ... ] } ``` Lines under the alternate name whose stored voice was set to the main speaker's. The cast after the change, same shape as `GET /cast`. The paragraphs keep their original `character_name`; the merge is stored on the book. With character voices on, the alternate name's lines take the main speaker's cast voice, and locked lines keep the voice they had. `400` means the alias is not allowed, for example a name aliased onto itself. ### One paragraph at a time `PUT /audiobook/projects/{project_id}/paragraphs/{paragraph_id}` tags or voices a single line, for a correction the bulk calls do not cover: ```bash theme={null} curl -X PUT "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"character_name": "Geppetto"}' ``` Speaker for this paragraph (max 120 characters). Send `null` to give the line to the narrator. A speaker you set or clear is yours: detection never changes it. A voice for this line only. It outranks the cast and sticks to the line. Send `null` to release it, so the line follows the [voice order](#voice-order) again. With character voices on, tagging a line with a character is enough: it takes that character's cast voice. A locked paragraph refuses every change except unlocking it (`409`). Changing these fields does not delete the line's audio; the new voice is heard on its next narration. ## 5. Narrate and re-narrate Confirm rights once per book with `POST .../rights-attestation` (narration returns `400 RIGHTS_NOT_ATTESTED` until you do), then narrate with [`POST .../narration/batch`](/api-reference/audiobook#5-narrate): ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}' ``` Turn character voices on or off for this run and later ones (saved on the book first). `true` also gives uncast characters a library voice. A narrator for this run. With character voices on it voices only the narrator's lines; with them off it voices every selected line and replaces the voice saved on it. It is saved on the lines it voices, like any narration voice, but does not count as a voice you set for the line. If a selected line resolves to no voice at all (no cast voice, no chapter voice, no narrator), the batch returns `400`. To hear a new voice on lines that are already narrated, narrate just those paragraphs again with `paragraph_ids` and `include_completed`. Collect the ids of the character's lines from the chapters' paragraph lists. Paragraphs keep the name they were tagged with, so after an alias merge match the main name **and** every alias (from `GET /cast`), case-insensitively. A main name with no lines of its own has no member of its own; its aliases point to it through `alias_of`: ```bash theme={null} # the character's name plus its aliases, lower-cased NAMES=$(curl -s "$AP/audiobook/projects/$PID/cast" -H "X-API-Key: $AUDIOPOD_API_KEY" \ | jq -c --arg n "Maestro Ciliegia" '($n | ascii_downcase) as $k | [$k] + [.members[] | select(((.character_name // "") | ascii_downcase) == $k or ((.alias_of // "") | ascii_downcase) == $k) | ([.character_name] + .aliases)[] | ascii_downcase] | unique') # ids of every line Maestro Ciliegia speaks in one chapter, under any of those names IDS=$(curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs" -H "X-API-Key: $AUDIOPOD_API_KEY" \ | jq -c --argjson names "$NAMES" '[.paragraphs[] | select((.character_name // "" | ascii_downcase) as $c | $names | index($c) != null) | .id]') # nothing matched: stop (an empty list is refused anyway) [ "$IDS" = "[]" ] || [ -z "$IDS" ] && { echo "no matching paragraphs - nothing to re-narrate" >&2; exit 1; } curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d "{\"paragraph_ids\": $IDS, \"include_completed\": true}" ``` **To narrate the whole book, omit `paragraph_ids` and `chapter_ids`.** An empty list (`[]`) is refused with `400` and `"code": "empty_selection"`, so a filter that matched nothing never narrates, or bills, the whole book. The same rule applies to `chapter_ids` on annotation and Auto Direction, and to `character_names` on auto-cast. Re-narration is billed like any narration, so only the lines you send are charged. A single line can also be redone with [`POST .../paragraphs/{paragraph_id}/narrate`](/api-reference/audiobook#narrate-a-single-paragraph), where the first regeneration of a paragraph is free. ### Change voices on a narrated book * **A new narrator, characters on.** `cast/assign` with `{"is_narrator": true, "voice_id": }`, then narrate again with `include_completed: true`. Every narrator line takes the new voice, except chapters with their own `voice_id` (clear it with `PUT .../chapters/{chapter_id}` and `{"voice_id": null}`) and lines you gave their own voice. * **A new voice for one character.** `cast/assign` the character, then narrate their lines again as shown above. * **Characters off.** Narration saved the voice it used on each line. To re-voice the whole book, send the voice in the batch: `{"voice_id": , "include_completed": true}`. It voices every selected line and is saved on each, replacing the voice the line had, including one you set with a `PUT`. ## Auto-cast: design a voice per character `POST /audiobook/projects/{project_id}/cast/auto` Instead of library voices, auto-cast **designs a new custom voice for each character** from how the book portrays them, saves it to your voices and to the book's cast, and turns character voices on. It is a paid feature with a prepaid charge. Characters to cast. Omit to cast the speakers with the most lines who are not cast yet. An empty list returns `400` `empty_selection`. How many characters to cast, `1`–`12`. ```bash theme={null} curl -X POST "$AP/audiobook/projects/$PID/cast/auto" \ -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \ -d '{"character_names": ["Maestro Ciliegia", "Geppetto"]}' # => { "job_id": "2051", "characters": ["Maestro Ciliegia", "Geppetto"] } curl "$AP/audiobook/projects/$PID/cast/auto/2051/status" -H "X-API-Key: $AUDIOPOD_API_KEY" # => { "job_id": "2051", "status": "COMPLETED", "progress": 100.0, # "result": { "cast": [ { "character_name": "Geppetto", "voice_id": 4410, "display_name": "Geppetto" }, ... ], # "failed": [] } } ``` * **Prepaid.** 250 credits per character are reserved when you start the job. Each is charged when that character's voice is made, and refunded if it fails. If the balance cannot cover every character, the call returns `402` and nothing is charged. * **Plan.** Auto-cast needs an active paid subscription plan that includes monthly credits. Without one (the free plan, or a paid add-on that grants no credits) it returns `402` with `"code": "PREMIUM_TIER_REQUIRED"`. Library voices (`cast/assign` and the automatic pick) work on every plan. * **Who is skipped.** Without `character_names`, a character is already cast when its cast voice is one you chose (`cast/assign`) or one auto-cast designed, or one of its lines has a voice you set with a `PUT`. A free library voice from the automatic pick does not count, and neither does a voice narration saved on a line. So omitting `character_names` designs a voice for each character that only has a library pick, up to `max_characters`, at 250 credits per character designed. Name the characters in `character_names` to choose exactly what you pay for. * `409`: an auto-cast job is already running for this project. `400`: there are no characters to cast; detect speakers first. `503`: auto-cast is temporarily unavailable, and nothing was charged. ## Errors | Status | Meaning | | - | - | | `400` | Invalid alias, an empty selection list (`empty_selection`), no chapters to annotate, scope too large to annotate in one pass, no characters to auto-cast, or a line with no voice to narrate with. | | `402` | Auto-cast only: insufficient credits, or the plan does not include it. | | `404` | The project, or the job id, does not exist or is not yours. | | `409` | The book is exporting, a line in scope is being narrated, the same kind of job is already running, or the paragraph is locked. | | `422` | The request body failed validation (for example `character_name` over 120 characters). | ## Next steps The full pipeline: projects, manuscripts, narration, review and ACX export. Clone a voice from a short sample and cast it as a character. # Authentication API Source: https://docs.audiopod.ai/api-reference/auth Register, login, and manage API keys programmatically ## Overview AudioPod AI supports two authentication methods: * **API Key**: Use `X-API-Key` header (recommended for servers) * **JWT Token**: Use `Authorization: Bearer {token}` header (for sessions) *** ## Registration ### Step 1: Initiate Registration ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/initiate-registration" \ -H "Content-Type: application/json" \ -d '{ "email": "dev@example.com", "password": "SecurePass123", "full_name": "Developer Name" }' ``` **Response:** ```json theme={null} { "message": "Verification code sent to your email. Please check your inbox." } ``` ### Step 2: Verify Email ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/verify-registration" \ -H "Content-Type: application/json" \ -d '{ "email": "dev@example.com", "verification_code": "123456" }' ``` **Response:** ```json theme={null} { "user": { "id": "uuid", "email": "dev@example.com", "full_name": "Developer Name", "is_verified": true }, "access_token": "eyJhbGciOiJIUzI1NiIs...", "refresh_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "bearer", "expires_in": 3600 } ``` *** ## Login ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=dev@example.com&password=SecurePass123" ``` **Response:** Same as verify-registration. *** ## API Keys ### Create API Key Use JWT token from login/registration: ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "production-key", "scopes": ["*"], "expires_in_days": null }' ``` **Response:** ```json theme={null} { "id": "key-uuid", "name": "production-key", "api_key": "ap_xxxxxxxxxxxxxxxx", "scopes": ["*"], "created_at": "2025-01-01T00:00:00Z", "expires_at": null } ``` The `api_key` is only shown once. Save it immediately! ### List API Keys ```bash theme={null} curl -X GET "https://api.audiopod.ai/api/v1/auth/api-keys" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` ### Revoke API Key ```bash theme={null} curl -X DELETE "https://api.audiopod.ai/api/v1/auth/api-keys/{key_id}" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Password Reset ### Request Reset ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/forgot-password" \ -H "Content-Type: application/json" \ -d '{"email": "dev@example.com"}' ``` ### Confirm Reset ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/reset-password" \ -H "Content-Type: application/json" \ -d '{ "token": "reset_token_from_email", "new_password": "NewSecurePass123" }' ``` *** ## Token Refresh ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/refresh" \ -H "Content-Type: application/json" \ -d '{"refresh_token": "eyJhbGciOiJIUzI1NiIs..."}' ``` *** ## Get Current User ```bash theme={null} curl -X GET "https://api.audiopod.ai/api/v1/auth/me" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Response:** ```json theme={null} { "id": "uuid", "email": "dev@example.com", "full_name": "Developer Name", "is_verified": true, "created_at": "2025-01-01T00:00:00Z" } ``` *** ## Errors | Code | Error | Description | | - | - | - | | `401` | `invalid_credentials` | Wrong email or password | | `401` | `token_expired` | Access token expired | | `400` | `email_already_registered` | Email already in use | | `400` | `invalid_verification_code` | Wrong or expired code | | `429` | `rate_limit_exceeded` | Too many requests | *** ## Quick Reference | Endpoint | Method | Auth | Description | | - | - | - | - | | `/auth/initiate-registration` | POST | None | Start registration | | `/auth/verify-registration` | POST | None | Verify email | | `/auth/token` | POST | None | Login | | `/auth/refresh` | POST | None | Refresh token | | `/auth/forgot-password` | POST | None | Request reset | | `/auth/reset-password` | POST | None | Confirm reset | | `/auth/me` | GET | Required | Get user info | | `/auth/api-keys` | GET | Required | List API keys | | `/auth/api-keys` | POST | Required | Create API key | | `/auth/api-keys/{id}` | DELETE | Required | Revoke API key | # Audio Tools Source: https://docs.audiopod.ai/api-reference/music/audio-tools Cover generation, reference audio, audio analysis, stem extraction, and music editing with AudioMusic V2. ## Cover / Style Transfer ### `POST /api/v1/music/cover` Transform existing audio while maintaining structure but changing style, timbre, and genre. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Transform a pop song into jazz job = client.music.cover( src_audio_url="https://example.com/pop-song.mp3", caption="jazz piano version with upright bass and brushed drums", audio_cover_strength=0.7, wait_for_completion=True ) print(f"Cover URL: {job['output_url']}") # Lighter transformation (more of the original preserved) job = client.music.cover( src_audio_url="https://example.com/song.mp3", caption="acoustic guitar version", audio_cover_strength=0.4, lyrics="[Verse 1]\nNew lyrics for the cover version", wait_for_completion=True ) ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); const job = await client.music.cover({ srcAudioUrl: 'https://example.com/pop-song.mp3', caption: 'jazz piano version with upright bass', audioCoverStrength: 0.7, }); const completed = await client.music.waitForCompletion(job.id); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/cover" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "src_audio_url": "https://example.com/pop-song.mp3", "caption": "jazz piano version with upright bass and brushed drums", "audio_cover_strength": 0.7 }' ``` **Request Parameters:** | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `src_audio_url` | string | Yes | — | URL of the source audio to transform | | `caption` | string | Yes | — | Style description for the cover | | `lyrics` | string | No | null | New lyrics for the cover version | | `audio_cover_strength` | float | No | 0.7 | Transformation strength. 0.0 = completely new, 1.0 = closest to original | | `display_name` | string | No | null | Custom name | | + all [base parameters](/api-reference/music/overview#complete-parameters-reference) | | | | | *** ## Reference Audio ### `POST /api/v1/music/reference` Generate music guided by a reference audio's acoustic features — timbre, mixing style, spatial characteristics, and overall atmosphere. ```python theme={null} job = client.music.reference( reference_audio_url="https://example.com/reference-track.mp3", caption="upbeat pop song with similar vocal warmth and production style", lyrics="[Verse]\nHello world\n[Chorus]\nLa la la", wait_for_completion=True ) print(f"Reference-guided URL: {job['output_url']}") ``` ```typescript theme={null} const job = await client.music.reference({ referenceAudioUrl: 'https://example.com/reference-track.mp3', caption: 'upbeat pop song with similar vocal warmth', lyrics: '[Verse]\nHello world\n[Chorus]\nLa la la', }); const completed = await client.music.waitForCompletion(job.id); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/reference" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reference_audio_url": "https://example.com/reference-track.mp3", "caption": "upbeat pop song with similar vocal warmth", "lyrics": "[Verse]\nHello world\n[Chorus]\nLa la la" }' ``` **Request Parameters:** | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `reference_audio_url` | string | Yes | — | URL of reference audio for style guidance | | `caption` | string | Yes | — | Text description of desired music | | `lyrics` | string | No | null | Song lyrics | | `instrumental` | bool | No | false | Generate instrumental only | | `display_name` | string | No | null | Custom name | | + all [base parameters](/api-reference/music/overview#complete-parameters-reference) | | | | | *** ## Audio Analysis ### `POST /api/v1/music/analyze` Extract metadata from audio using AudioMusic V2. Returns caption, lyrics, BPM, key, time signature, duration, and language. ```python theme={null} analysis = client.music.analyze( audio_url="https://example.com/song.mp3" ) if analysis.get("success"): print(f"Caption: {analysis['caption']}") print(f"BPM: {analysis['bpm']}") print(f"Key: {analysis['keyscale']}") print(f"Time Sig: {analysis['timesignature']}") print(f"Duration: {analysis['duration']}s") print(f"Language: {analysis['language']}") if analysis.get('lyrics'): print(f"Lyrics:\n{analysis['lyrics']}") ``` ```typescript theme={null} const analysis = await client.music.analyze({ audioUrl: 'https://example.com/song.mp3', }); if (analysis.success) { console.log(`Caption: ${analysis.caption}`); console.log(`BPM: ${analysis.bpm}`); console.log(`Key: ${analysis.keyscale}`); } ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/analyze" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"audio_url": "https://example.com/song.mp3"}' ``` **Request Parameters:** | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `audio_url` | string | Yes | — | URL of audio to analyze | | `audio_codes` | string | No | null | Pre-extracted semantic codes | | `temperature` | float | No | 0.85 | LM temperature for analysis (0.0-2.0) | **Response:** ```json theme={null} { "success": true, "caption": "upbeat pop song with female vocals, electronic beats, 4/4 time", "lyrics": "[Verse 1]\nWalking down the street...", "bpm": 120, "keyscale": "C Major", "timesignature": "4/4", "duration": 195.5, "language": "en", "audio_codes": "..." } ``` **Rate limit:** 10 requests per minute. *** ## Stem Extraction ### `POST /api/v1/music/extract-stem` Extract a specific instrument or vocal track from audio. ```python theme={null} # Extract vocals job = client.music.extract_stem( src_audio_url="https://example.com/song.mp3", track_name="vocals", wait_for_completion=True ) print(f"Vocals URL: {job['output_url']}") # Extract drums job = client.music.extract_stem( src_audio_url="https://example.com/song.mp3", track_name="drums", wait_for_completion=True ) ``` ```typescript theme={null} const job = await client.music.extractStem({ srcAudioUrl: 'https://example.com/song.mp3', trackName: 'vocals', }); const completed = await client.music.waitForCompletion(job.id); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/extract-stem" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "src_audio_url": "https://example.com/song.mp3", "track_name": "vocals" }' ``` **Request Parameters:** | Parameter | Type | Required | Description | | - | - | - | - | | `src_audio_url` | string | Yes | URL of audio to extract stem from | | `track_name` | string | Yes | Instrument to extract (see table below) | | `display_name` | string | No | Custom name for the job | **Available Tracks:** | Track Name | Description | | - | - | | `vocals` | Lead vocals | | `drums` | Full drum kit | | `bass` | Bass guitar / bass synth | | `guitar` | Guitar (acoustic/electric) | | `keyboard` | Piano / keyboard | | `strings` | String instruments | | `synth` | Synthesizers | | `percussion` | Percussion elements | | `brass` | Brass instruments | | `woodwinds` | Woodwind instruments | | `backing_vocals` | Background vocals | | `fx` | Sound effects | *** ## Retake (Variations) ### `POST /api/v1/music/retake` Generate variations of an existing music track by re-running with different seeds. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/retake" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "original_job_id": 123, "retake_variance": 0.7, "display_name": "Jazz Variation" }' ``` **Request Parameters:** | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `original_job_id` | int | Yes | — | ID of the completed original job | | `retake_variance` | float | No | 0.7 | Variation strength (0.0-1.0) | | `retake_seeds` | int\[] | No | null | Specific seeds for reproducibility | | `display_name` | string | No | null | Custom name | **Rate limit:** 10 requests per minute. *** ## Repaint (Section Regeneration) ### `POST /api/v1/music/repaint` Regenerate a specific time segment of audio while keeping the rest intact. Useful for fixing problematic sections or changing lyrics in a segment. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/repaint" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source_job_id": 123, "repainting_start": 30.0, "repainting_end": 60.0, "caption": "energetic chorus with powerful vocals", "lyrics": "[Chorus]\nNew chorus lyrics here" }' ``` **Request Parameters:** | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `source_job_id` | int | No | null | ID of source job (alternative to `src_audio_url`) | | `src_audio_url` | string | No | null | URL of source audio (alternative to `source_job_id`) | | `repainting_start` | float | Yes | — | Start time in seconds | | `repainting_end` | float | Yes | — | End time in seconds (-1 for end of file) | | `caption` | string | Yes | — | Style description for the repainted section | | `lyrics` | string | No | null | Lyrics for the repainted section | | `display_name` | string | No | null | Custom name | **Note:** Either `source_job_id` or `src_audio_url` must be provided. **Rate limit:** 3 requests per minute. *** ## Extend ### `POST /api/v1/music/extend` Extend an existing music track by adding content at the beginning or end. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/extend" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source_job_id": 123, "right_extend_length": 30.0, "prompt": "continue with the same energy and add a dramatic outro", "lyrics": "[Outro]\nThe music fades away" }' ``` **Request Parameters:** | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `source_job_id` | int | Yes | — | ID of the completed source job | | `left_extend_length` | float | No | 0.0 | Seconds to add at the beginning (0.0-60.0) | | `right_extend_length` | float | No | 30.0 | Seconds to add at the end (0.0-60.0) | | `prompt` | string | Yes | — | Style description for the extension | | `lyrics` | string | No | null | Lyrics for the extension | | `display_name` | string | No | null | Custom name | **Rate limit:** 3 requests per minute. *** ## Edit ### `POST /api/v1/music/edit` Edit existing audio with new prompts and lyrics while preserving the overall structure. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/edit" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source_job_id": 123, "edit_target_prompt": "make the guitars more prominent and add reverb", "display_name": "Guitar Edit" }' ``` **Request Parameters:** | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `source_job_id` | int | Yes | — | ID of the completed source job | | `edit_target_prompt` | string | No | null | New style/prompt for the edit | | `edit_target_lyrics` | string | No | null | New lyrics | | `edit_type` | string | No | null | Edit type identifier | | `edit_n_min` | float | No | null | Edit range minimum | | `edit_n_max` | float | No | null | Edit range maximum | | `display_name` | string | No | null | Custom name | **Rate limit:** 3 requests per minute. *** ## LRC Timestamp Generation ### `POST /api/v1/music/{job_id}/lrc` Generate synchronized LRC (lyric) timestamps for a completed music job. LRC generation requires internal tensors from the music generation process. For best results, enable `generate_lrc: true` when creating the music generation request. This endpoint is for retroactive LRC generation which has limitations. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/123/lrc" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" ``` **Response:** ```json theme={null} { "success": true, "job_id": 123, "lrc_content": "[00:00.00] Walking down the street\n[00:05.20] On a sunny day...", "lrc_url": "https://media.audiopod.ai/lrc/123.lrc" } ``` **Rate limit:** 20 requests per minute. # Choosing how to make music Source: https://docs.audiopod.ai/api-reference/music/choosing Which AudioMusic endpoint to call: Song mode for finished tracks, Score-first for songs you want to shape before they are recorded. AudioMusic works in two modes, and almost every request belongs to the first. * **Song** — the default. Describe a song (or bring lyrics) and get a finished, mixed track. It honors a requested length, can write the lyrics for you, and returns synced lyrics. It is also the mode for everything that starts from audio you already have. * **Score-first** — for when you want to see and shape the composition itself (melody, chords, sections) before it is recorded. You get an editable score first, change it in plain words as often as you like, and record it when it is right. There is no single "make music" endpoint: pick the route from the table below. ## Routing table | You want | Mode | Endpoint | | - | - | - | | A song from a description ("a sad indie song about rain") — AudioPod writes the lyrics | Song | [`POST /api/v1/music/simple`](/api-reference/music/overview) | | A song with your own lyrics | Song | [`POST /api/v1/music/text2music`](/api-reference/music/generation), `lyric2vocals` or `text2rap` | | An instrumental or beat | Song | [`POST /api/v1/music/prompt2instrumental`](/api-reference/music/generation) | | A sound effect or sample | Song | [`POST /api/v1/music/text2samples`](/api-reference/music/generation) | | An exact length, or synced lyrics (LRC) | Song | any Song generation route | | A new version of existing audio, or "like this reference" | Song | [`/cover`, `/reference`](/api-reference/music/audio-tools) | | Extend, retake or repaint a track | Song | [extend / retake / repaint](/api-reference/music/audio-tools) | | "Write it so I can change it" | Score-first | [`POST /api/v1/music/v3/plan`](/api-reference/music/score-first#write-a-score) | | Changes to a score, in plain words | Score-first | [`POST /api/v1/music/v3/revise`](/api-reference/music/score-first#revise-a-score-in-plain-words) | | Record a score | Score-first | [`POST /api/v1/music/v3/edit`](/api-reference/music/score-first#record-a-score) | | The same tune with a new style or new words | Score-first | [`POST /api/v1/music/v3/edit`](/api-reference/music/score-first#record-a-score) with `style` / `lyrics` | | You already have the notation | Score-first | `POST /api/v1/music/v3/generate` with `abc`, or `/music/v3/edit` | When in doubt, use Song mode. ## The Score-first loop ```text theme={null} plan ──► revise (plain words, repeat) ──► edit (record) 165 credits 165 credits each ~16.5 credits per second of score ``` 1. **Write** a score with [`/music/v3/plan`](/api-reference/music/score-first#write-a-score): either a `description` (AudioPod writes the lyrics, sized to an optional `target_seconds`) or your own `lyrics` with a `style`. It returns in about ten seconds with a score and no audio. 2. **Revise** it with [`/music/v3/revise`](/api-reference/music/score-first#revise-a-score-in-plain-words): "make the chorus brighter", "take it down a key", "simpler melody in bars 5–8". Each revision is checked so that everything you did not ask to change stays the same, and returns a new score job you can revise again. 3. **Record** it with [`/music/v3/edit`](/api-reference/music/score-first#record-a-score) when you are happy with it, sending the latest score from `GET /music/v3/jobs/{job_id}/score`. Every step is prepaid and refunded if it fails. Song length in Score-first mode follows the amount of lyrics, so `target_seconds` is a target rather than a promise — use Song mode when you need an exact length. Plain-English revision (`/music/v3/revise`) is available when it is enabled for your account — `GET /api/v1/music/v3/availability` returns `"revise": true`. Otherwise the route answers `404`; you can still write a score and record it. ## For agents The AudioPod MCP server exposes the same choice as tools: `generate_music` (Song, the default), and `compose_music` → `revise_music_score` → `record_music_score` (Score-first), with `get_music_score` to read a score for free. See [MCP server](/sdks/mcp). # Music Generation Source: https://docs.audiopod.ai/api-reference/music/generation Generate songs, instrumentals, rap, vocals, samples, and more using AudioMusic V2 endpoints. ## Text-to-Music ### `POST /api/v1/music/text2music` Generate complete musical compositions with vocals from text prompts and lyrics. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Generate a pop song with lyrics job = client.music.song( prompt="upbeat pop, female vocals, catchy melody, electronic beats", lyrics="""[Verse 1] Walking down the street on a sunny day Everything feels right in every way [Chorus] This is our time to shine Leave the worries behind Dancing through the night Everything's gonna be alright""", duration=120, wait_for_completion=True ) print(f"Music URL: {job['output_url']}") # Advanced generation with AudioMusic V2 parameters job = client.music.generate( prompt="epic orchestral soundtrack with dramatic crescendos", task="text2music", duration=180, inference_steps=64, guidance_scale=10.0, thinking=True, generate_lrc=True, format="flac", wait_for_completion=True ) ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Generate a pop song with lyrics const job = await client.music.song( 'upbeat pop, female vocals, catchy melody', `[Verse 1] Walking down the street on a sunny day Everything feels right in every way [Chorus] This is our time to shine Leave the worries behind`, 120 ); const completed = await client.music.waitForCompletion(job.id); console.log(`Music URL: ${completed.output_url}`); // Advanced generation with AudioMusic V2 parameters const advancedJob = await client.music.create({ prompt: 'epic orchestral soundtrack with dramatic crescendos', task: 'text2music', duration: 180, inferenceSteps: 64, guidanceScale: 10.0, thinking: true, generateLrc: true, format: 'flac', }); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/text2music" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "caption": "upbeat pop, female vocals, catchy melody, electronic beats", "lyrics": "[Verse 1]\nWalking down the street on a sunny day\nEverything feels right\n[Chorus]\nThis is our time to shine", "audio_duration": 120, "inference_steps": 64, "guidance_scale": 7.0, "format": "flac", "generate_lrc": true }' ``` **Request Parameters:** | Parameter | Type | Required | Description | | - | - | - | - | | `caption` | string | Yes | Text description of desired music style (0-2048 chars). Also accepts `prompt` | | `lyrics` | string | No | Song lyrics with structure tags: `[verse]`, `[chorus]`, `[bridge]`, `[intro]`, `[outro]`, `[Instrumental]` | | `instrumental` | bool | No | If true, ignores lyrics and generates instrumental. Default: false | | `genre_preset` | string | No | Genre preset name (see [Overview](/api-reference/music/overview#genre-presets)) | | `display_name` | string | No | Custom name for the track | | + all [base parameters](/api-reference/music/overview#complete-parameters-reference) | | | | **Response:** `MusicGenerationResponse` with `job` object and `message`. *** ## Instrumental Generation ### `POST /api/v1/music/prompt2instrumental` Create instrumental tracks without vocals from text prompts. ```python theme={null} client = AudioPod(api_key="ap_your_api_key") job = client.music.instrumental( prompt="chill jazz instrumental with piano and saxophone", duration=90, wait_for_completion=True ) print(f"Instrumental URL: {job['output_url']}") ``` ```typescript theme={null} const job = await client.music.instrumental( 'chill jazz instrumental with piano and saxophone', 90 ); const completed = await client.music.waitForCompletion(job.id); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/prompt2instrumental" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "chill jazz instrumental with piano and saxophone", "audio_duration": 90, "format": "flac" }' ``` **Request Parameters:** | Parameter | Type | Required | Description | | - | - | - | - | | `prompt` | string | Yes | Text description of desired instrumental | | `genre_preset` | string | No | Genre preset name | | `display_name` | string | No | Custom name for the track | | + all [base parameters](/api-reference/music/overview#complete-parameters-reference) | | | | *** ## Rap Generation ### `POST /api/v1/music/text2rap` Generate rap music with custom lyrics and beats. ```python theme={null} client = AudioPod(api_key="ap_your_api_key") job = client.music.rap( prompt="hard-hitting trap beat with heavy bass and 808s", lyrics="""[Verse 1] Started from the bottom now we here Grinding every day throughout the year Making moves, breaking through the ceiling Got that feeling, no time for healing [Chorus] This is our time to shine Leaving all the doubt behind Every step we take is gold Watch the story unfold""", duration=120, wait_for_completion=True ) print(f"Rap URL: {job['output_url']}") ``` ```typescript theme={null} const job = await client.music.rap( 'hard-hitting trap beat with heavy bass and 808s', `[Verse 1] Started from the bottom now we here Grinding every day throughout the year [Chorus] This is our time to shine Leaving all the doubt behind`, 120 ); const completed = await client.music.waitForCompletion(job.id); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/text2rap" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "hard-hitting trap beat with heavy bass and 808s", "lyrics": "[Verse 1]\nStarted from the bottom now we here\nGrinding every day throughout the year\n[Chorus]\nThis is our time to shine", "audio_duration": 120 }' ``` **Request Parameters:** Same as Text-to-Music. Lyrics are recommended for best results. *** ## Vocals Generation ### `POST /api/v1/music/lyric2vocals` Generate vocal tracks from lyrics without full instrumental backing. ```python theme={null} job = client.music.generate( prompt="female pop vocals with emotional delivery", task="lyric2vocals", lyrics="[Verse 1]\nWalking through the city lights\nEverything feels so bright", duration=90, wait_for_completion=True ) ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/lyric2vocals" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "female pop vocals with emotional delivery", "lyrics": "[Verse 1]\nWalking through the city lights\nEverything feels so bright", "audio_duration": 90 }' ``` *** ## Sample Generation ### `POST /api/v1/music/text2samples` Create audio loops, one-shots, and samples for music production. ```python theme={null} job = client.music.generate( prompt="analog drum loop with vintage character, 120 BPM", task="text2samples", duration=8, wait_for_completion=True ) ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/text2samples" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "analog drum loop with vintage character, 120 BPM", "audio_duration": 8, "format": "wav" }' ``` **Additional Parameters:** | Parameter | Type | Default | Description | | - | - | - | - | | `sample_type` | string | null | Type of sample: `loop`, `one-shot` | | `tempo` | int | null | Target tempo in BPM (60-200) | **Note:** Maximum duration for samples is 120 seconds. *** ## Simple Mode ### `POST /api/v1/music/simple` The easiest way to generate music. Describe what you want in natural language and AudioMusic V2 automatically generates the caption, lyrics, BPM, key, time signature, and duration. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Just describe what you want job = client.music.simple( query="energetic K-pop dance track with catchy hooks and a powerful chorus", wait_for_completion=True ) print(f"Music URL: {job['output_url']}") # Instrumental with language preference job = client.music.simple( query="epic cinematic trailer music with orchestral elements", instrumental=True, wait_for_completion=True ) # Specify language for vocals job = client.music.simple( query="a romantic ballad about lost love", vocal_language="ja", # Japanese wait_for_completion=True ) ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); const job = await client.music.simple({ query: 'energetic K-pop dance track with catchy hooks', }); const completed = await client.music.waitForCompletion(job.id); // Instrumental const instrumental = await client.music.simple({ query: 'epic cinematic trailer music', instrumental: true, }); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/simple" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "energetic K-pop dance track with catchy hooks and a powerful chorus" }' ``` **Request Parameters:** | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `query` | string | Yes | — | Natural language description of desired music (1-1000 chars) | | `instrumental` | bool | No | false | Generate instrumental only | | `vocal_language` | string | No | `"unknown"` | Vocal language code (auto-detected if `"unknown"`) | | `display_name` | string | No | null | Custom name for the track | | `duration` | float | No | -1 | Duration in seconds (-1 for auto) | | `format` | string | No | `"flac"` | Output format | **How Simple Mode Works:** AudioMusic V2 uses Chain-of-Thought reasoning to automatically: 1. Generate a detailed caption/tags from your natural language query 2. Write lyrics (unless instrumental) 3. Determine optimal BPM, musical key, and time signature 4. Estimate appropriate duration This makes Simple Mode ideal for non-musicians who want to quickly generate high-quality music. *** ## Response Format All generation endpoints return the same response structure: ```json theme={null} { "job": { "id": 123, "task": "text2music", "status": "PENDING", "progress": null, "output_url": null, "output_urls": null, "display_name": "My Pop Song", "audio_duration": 120.0, "created_at": "2026-01-15T10:30:00Z", "user_id": "user_123" }, "message": "Text-to-music generation started" } ``` **Completed Job** (poll `GET /api/v1/music/jobs/{id}/status`): ```json theme={null} { "id": 123, "task": "text2music", "status": "COMPLETED", "progress": 100, "output_url": "https://media.audiopod.ai/music/123.flac", "output_urls": { "flac": "https://media.audiopod.ai/music/123.flac", "mp3": "https://media.audiopod.ai/music/123.mp3", "wav": "https://media.audiopod.ai/music/123.wav" }, "audio_duration": 120.5, "actual_seeds": [12345], "lrc_content": "[00:00.00] Walking down the street...", "quality_score": 0.87, "display_name": "My Pop Song", "created_at": "2026-01-15T10:30:00Z", "completed_at": "2026-01-15T10:32:15Z" } ``` # Job Management Source: https://docs.audiopod.ai/api-reference/music/jobs Query, filter, manage, and retry music generation jobs. ## Get Job Status ### `GET /api/v1/music/jobs/{job_id}/status` Get the current status of a music generation job. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") job = client.music.get_job(123) print(f"Status: {job['status']}") if job['status'] == 'COMPLETED': print(f"Audio URL: {job['output_url']}") print(f"Duration: {job['audio_duration']}s") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); const job = await client.music.get(123); console.log(`Status: ${job.status}`); if (job.status === 'COMPLETED') { console.log(`Audio URL: ${job.output_url}`); } ``` ```bash theme={null} curl "https://api.audiopod.ai/api/v1/music/jobs/123/status" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Response:** Full `MusicGenerationJob` object (see [response format](/api-reference/music/generation#response-format)). *** ## List Jobs ### `GET /api/v1/music/jobs` List music generation jobs with filtering, search, and sorting. ```python theme={null} # List recent jobs jobs = client.music.list_jobs(limit=20) # Filter by task type songs = client.music.list_jobs(task="text2music", limit=50) ``` ```typescript theme={null} // List recent jobs const jobs = await client.music.list({ limit: 20 }); // Filter by status const completed = await client.music.list({ status: 'COMPLETED', limit: 50, }); ``` ```bash theme={null} # List with filters curl "https://api.audiopod.ai/api/v1/music/jobs?status=COMPLETED&sort_by=created_at&sort_order=desc&limit=20" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Query Parameters:** | Parameter | Type | Default | Description | | - | - | - | - | | `skip` | int | 0 | Number of records to skip | | `limit` | int | 50 | Number of records to return (max 100) | | `task` | string | null | Filter by task type (`text2music`, `prompt2instrumental`, etc.) | | `status` | string | null | Filter by status (`PENDING`, `PROCESSING`, `COMPLETED`, `FAILED`) | | `song_type` | string | null | Filter by library category: `song`, `instrumental`, `edits`, `scores` (or the narrower `rap`, `vocals`, `samples`). See [Library categories](#library-categories) | | `liked` | bool | null | Filter to liked (true) or not-liked (false) tracks | | `search` | string | null | Search across display name, prompt, and lyrics | | `language` | string | null | Filter by vocal language | | `genre` | string | null | Filter by genre | | `emotion` | string | null | Filter by emotion tag | | `occasion` | string | null | Filter by occasion tag | | `sort_by` | string | `"created_at"` | Sort field: `created_at`, `display_name`, `audio_duration`, `status` | | `sort_order` | string | `"desc"` | Sort direction: `asc` or `desc` | | `include_interactions` | bool | false | Include like/dislike status for each job | **Response Headers:** * `X-Total-Count`: Total number of matching records (for pagination) ### Library categories Every job belongs to exactly one of four categories, so `song` + `instrumental` + `edits` + `scores` always adds up to your whole library. The same rule drives `song_type` here, on `/liked` and `/jobs/counts`, and the counts in `/filters/stats`. | `song_type` | What it returns | | - | - | | `song` | Tracks with vocals: `text2music` and `text2rap` with lyrics to sing, and every `lyric2vocals` job | | `instrumental` | `prompt2instrumental` and `text2samples` jobs, plus `text2music` / `text2rap` jobs generated as instrumentals: `instrumental: true`, no lyrics, lyrics of just `[Instrumental]`, or lyrics made only of section tags such as `[Intro]` | | `edits` | Jobs made from an existing track: `retake`, `repaint`, `extend`, `edit`, `cover`, `lego`, `extract`, `complete` | | `scores` | Written scores without a recording: score-only (plan) jobs, and generations that kept their score but finished without audio | | `rap` | The `text2rap` jobs within `song` | | `vocals` | The `lyric2vocals` jobs within `song` | | `samples` | The `text2samples` jobs within `instrumental` | `song` used to mean "every `text2music` job", and `instrumental` meant only `prompt2instrumental`. A `text2music` track generated as an instrumental is now in `instrumental`, rap tracks with vocals are in `song`, and edits and scores have their own categories. Filter by `task` to get the old task-only behaviour. *** ## Delete Job ### `DELETE /api/v1/music/jobs/{job_id}` Delete a music generation job and its associated audio files. ```python theme={null} result = client.music.delete_job(123) print(result['message']) # "Job deleted successfully" ``` ```typescript theme={null} await client.music.delete(123); ``` ```bash theme={null} curl -X DELETE "https://api.audiopod.ai/api/v1/music/jobs/123" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Update Job Metadata ### `PATCH /api/v1/music/jobs/{job_id}` Update a job's display name or thumbnail URL. ```bash theme={null} curl -X PATCH "https://api.audiopod.ai/api/v1/music/jobs/123" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "display_name": "My Awesome Track", "thumbnail_url": "https://example.com/thumbnail.jpg" }' ``` **Request Body:** | Parameter | Type | Description | | - | - | - | | `display_name` | string | New display name | | `thumbnail_url` | string | New thumbnail image URL | *** ## Retry Failed Job ### `POST /api/v1/music/jobs/{job_id}/retry` Retry a failed music generation job without consuming additional credits. The original credit reservation is reused. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/jobs/123/retry" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Constraints:** * Only works on jobs with `status: "FAILED"` * Original credit reservation is reused (no additional charge) * Returns a `MusicGenerationResponse` with the re-queued job **Rate limit:** 10 requests per minute. *** ## Job Counts ### `GET /api/v1/music/jobs/counts` Get aggregated counts of your music generation jobs, broken down by task type and song category. ```bash theme={null} # All counts curl "https://api.audiopod.ai/api/v1/music/jobs/counts" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # Filter by status curl "https://api.audiopod.ai/api/v1/music/jobs/counts?status=COMPLETED" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Query Parameters:** | Parameter | Type | Description | | - | - | - | | `status` | string | Filter by status | | `song_type` | string | Filter by library category: `song`, `instrumental`, `edits`, `scores` (or `rap`, `vocals`, `samples`). See [Library categories](#library-categories) | **Response:** `by_song_type` has one count per [library category](#library-categories). `songs` + `instrumental` + `edits` + `scores` equals `total`; `rap` is the part of `songs` made with `text2rap`. ```json theme={null} { "total": 150, "by_task": { "text2music": 76, "prompt2instrumental": 40, "text2rap": 20, "retake": 10, "lyric2vocals": 4 }, "by_song_type": { "songs": 85, "instrumental": 50, "edits": 10, "scores": 5, "rap": 18 }, "filters": { "status": null, "song_type": null } } ``` *** ## Liked Tracks ### `GET /api/v1/music/liked` Get all tracks you've liked. ```bash theme={null} curl "https://api.audiopod.ai/api/v1/music/liked?limit=20" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Query Parameters:** | Parameter | Type | Default | Description | | - | - | - | - | | `skip` | int | 0 | Pagination offset | | `limit` | int | 50 | Number of records | | `song_type` | string | null | Filter by [library category](#library-categories) | | `status` | string | null | Filter by status | *** ## Filter Statistics ### `GET /api/v1/music/filters/stats` Get statistics for building filter UI dropdowns — total tracks, liked count, task distribution, and available filter values. ```bash theme={null} curl "https://api.audiopod.ai/api/v1/music/filters/stats" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Response:** `song_types` counts each [library category](#library-categories): `songs` + `instrumental` + `edits` + `scores` equals `total_tracks`, and each count matches what `GET /api/v1/music/jobs?song_type=...` returns (`songs` is the `song` filter). `rap` is the part of `songs` made with `text2rap`. ```json theme={null} { "total_tracks": 150, "liked_count": 25, "song_types": { "songs": 85, "instrumental": 50, "edits": 10, "scores": 5, "rap": 18 }, "available_filters": { "song_types": ["songs", "rap", "instrumental", "edits", "scores"], "interactions": ["liked"], "statuses": ["pending", "processing", "completed", "failed"], "languages": ["en", "zh", "ja", "ko"], "genres": ["pop", "rock", "jazz"], "emotions": ["happy", "sad", "energetic"], "occasions": ["party", "relaxation", "workout"] } } ``` *** ## Genre Presets ### `GET /api/v1/music/presets` Get available genre presets with their pre-configured parameters. ```python theme={null} presets = client.music.get_presets() for name, config in presets['genre_presets'].items(): print(f"{name}: {config}") ``` ```typescript theme={null} const presets = await client.music.getPresets(); console.log(presets); ``` ```bash theme={null} curl "https://api.audiopod.ai/api/v1/music/presets" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Model Status ### `GET /api/v1/music/model/status` Get AudioMusic V2 model health and status information. ```bash theme={null} curl "https://api.audiopod.ai/api/v1/music/model/status" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Sample Data ### `GET /api/v1/music/sample-data` Get sample/test data for development and testing purposes. ```bash theme={null} curl "https://api.audiopod.ai/api/v1/music/sample-data" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` # AudioMusic V2 Source: https://docs.audiopod.ai/api-reference/music/overview Generate original music, instrumentals, covers, and audio transformations using AudioPod AI's AudioMusic V2 engine. ## Overview AudioPod AI's **AudioMusic V2** is a state-of-the-art music generation engine that transforms text descriptions into original musical compositions. Powered by a Transformer Model with Chain-of-Thought reasoning, AudioMusic V2 delivers studio-quality results. ### Key Capabilities * **Text-to-Music**: Generate complete songs with vocals from prompts and lyrics * **Simple Mode**: Describe what you want in natural language — AudioMusic V2 handles everything * **Cover / Style Transfer**: Transform existing audio into different styles and genres * **Reference Audio**: Guide generation using a reference track's timbre and mixing style * **Audio Analysis**: Extract metadata (BPM, key, lyrics, caption) from any audio * **Stem Extraction**: Isolate specific instruments (vocals, drums, bass, guitar, and more) * **Repaint / Edit / Extend**: Modify specific sections, edit with new prompts, or extend tracks * **Batch Generation**: Generate 1-8 variations in a single request * **LRC Timestamps**: Synchronized lyric timestamps for karaoke-style playback * **Quality Scoring**: Automatic quality assessment for every generation * **50+ Languages**: Generate music with vocals in over 50 languages * **Multiple Formats**: Output in WAV, MP3, FLAC, or OGG * **Royalty-Free**: All generated music is 100% royalty-free for commercial use ## Authentication All music endpoints require authentication. Use one of these methods: * **API Key (Recommended)**: `X-API-Key: your_api_key` header * **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth) ```bash theme={null} # Example with API Key curl -X POST "https://api.audiopod.ai/api/v1/music/simple" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "upbeat pop song with catchy melody"}' ``` ## Quick Start — Simple Mode The fastest way to generate music. Just describe what you want: ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Simple mode — AudioMusic V2 auto-generates everything job = client.music.simple( query="a soft Bengali love song for a quiet evening", wait_for_completion=True ) print(f"Music URL: {job['output_url']}") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Simple mode — AudioMusic V2 auto-generates everything const job = await client.music.simple({ query: 'a soft Bengali love song for a quiet evening', }); const completed = await client.music.waitForCompletion(job.id); console.log(`Music URL: ${completed.output_url}`); ``` ```bash theme={null} # Create job JOB=$(curl -s -X POST "https://api.audiopod.ai/api/v1/music/simple" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "a soft Bengali love song for a quiet evening"}') JOB_ID=$(echo $JOB | jq -r '.job.id') echo "Job created: $JOB_ID" # Poll for completion while true; do STATUS=$(curl -s "https://api.audiopod.ai/api/v1/music/jobs/$JOB_ID/status" \ -H "X-API-Key: $AUDIOPOD_API_KEY") STATE=$(echo $STATUS | jq -r '.status') echo "Status: $STATE" [ "$STATE" = "COMPLETED" ] && echo "URL: $(echo $STATUS | jq -r '.output_url')" && break [ "$STATE" = "FAILED" ] && echo "Failed" && break sleep 5 done ``` ## Complete Parameters Reference All music generation endpoints inherit from a common base. Here are all available parameters: ### Core Parameters | Parameter | Type | Default | Description | | - | - | - | - | | `duration` | float | -1.0 | Audio duration in seconds. Use -1 for automatic duration based on lyrics length. Max: 600 | | `format` | string | `"flac"` | Output audio format: `wav`, `mp3`, `flac`, `ogg` | | `model_version` | string | `"audiomusic-v2"` | Model version: `audiomusic-v2` (recommended) or `audiomusic-v1.5` | | `batch_size` | int | 1 | Number of variations to generate (1-8) | | `seed` | int | -1 | Random seed for reproducibility. Use -1 for random | ### Diffusion Parameters | Parameter | Type | Default | Description | | - | - | - | - | | `inference_steps` | int | 64 | Number of diffusion steps. Higher = better quality, slower. Range: 32-100 | | `guidance_scale` | float | 7.0 | Classifier-free guidance scale. Higher = more prompt adherence. Range: 1.0-15.0 | | `shift` | float | 3.0 | Timestep shift factor. Range: 1.0-5.0 | | `infer_method` | string | `"ode"` | Inference method: `ode` (Euler, faster) or `sde` (stochastic, more diverse) | | `use_adg` | bool | false | Enable Adaptive Dual Guidance for enhanced control | | `cfg_interval_start` | float | 0.0 | CFG interval start ratio (0.0-1.0) | | `cfg_interval_end` | float | 1.0 | CFG interval end ratio (0.0-1.0) | ### Language Model Parameters AudioMusic V2 includes a language model that can auto-generate and refine metadata, captions, lyrics, and more. | Parameter | Type | Default | Description | | - | - | - | - | | `thinking` | bool | true | Enable Chain-of-Thought reasoning for higher quality | | `lm_temperature` | float | 0.85 | Sampling temperature (0.0-2.0). Lower = more deterministic | | `lm_cfg_scale` | float | 2.0 | LM classifier-free guidance (0.0-10.0) | | `lm_top_k` | int | 0 | Top-k sampling (0-100, 0 disables) | | `lm_top_p` | float | 0.9 | Nucleus sampling threshold (0.0-1.0) | | `lm_negative_prompt` | string | `"NO USER INPUT"` | Negative prompt for LM guidance | | `use_cot_metas` | bool | true | Auto-generate BPM, key, time signature via Chain-of-Thought | | `use_cot_caption` | bool | true | Refine caption/tags via Chain-of-Thought | | `use_cot_lyrics` | bool | false | Generate or refine lyrics via Chain-of-Thought | | `use_cot_language` | bool | true | Auto-detect vocal language via Chain-of-Thought | | `use_constrained_decoding` | bool | false | Enable structured LM output | ### Music Metadata Parameters These are auto-detected when `thinking: true`, but can be specified manually. | Parameter | Type | Default | Description | | - | - | - | - | | `bpm` | int | null | Beats per minute (30-300). Auto-detected if null | | `keyscale` | string | null | Musical key, e.g. `"C Major"`, `"Am"`. Auto-detected if null | | `timesignature` | string | null | Time signature: `"2/4"`, `"3/4"`, `"4/4"`, `"6/8"`. Auto-detected if null | | `vocal_language` | string | `"unknown"` | ISO 639-1 language code (e.g. `"en"`, `"zh"`, `"ja"`). Use `"unknown"` for auto-detection | ### Output & Quality Parameters | Parameter | Type | Default | Description | | - | - | - | - | | `generate_lrc` | bool | false | Generate synchronized LRC lyric timestamps | | `calculate_quality` | bool | true | Calculate quality score (0.0-1.0) for the output | | `thumbnail_url` | string | null | URL for custom track thumbnail image | ### LoRA Parameters | Parameter | Type | Default | Description | | - | - | - | - | | `lora_name_or_path` | string | null | Path to LoRA adapter for fine-tuned styles | | `lora_weight` | float | 1.0 | LoRA adapter weight (-3.0 to 3.0) | ## Genre Presets Pre-configured genre settings for quick generation: | Preset | Description | | - | - | | `Modern Pop` | Contemporary pop with electronic elements | | `Rock` | Guitar-driven rock music | | `Hip Hop` | Modern hip hop beats and production | | `Jazz` | Jazz compositions with improvisation | | `Classical` | Orchestral and classical arrangements | | `Electronic` | EDM, synth-driven electronic music | | `Country` | Country music with acoustic instruments | | `Folk` | Folk music with traditional elements | | `Blues` | Blues with soulful expression | | `Reggae` | Reggae with Caribbean rhythms | | `Latin` | Latin music styles | | `R&B` | Rhythm and blues | | `Metal` | Heavy metal and rock subgenres | | `Custom` | Custom genre (specify in caption/prompt) | ## Output Formats | Format | Extension | Quality | Use Case | | - | - | - | - | | FLAC | `.flac` | Lossless | Default. Best quality, larger files | | WAV | `.wav` | Lossless | Uncompressed, maximum compatibility | | MP3 | `.mp3` | Lossy | Smaller files, streaming | | OGG | `.ogg` | Lossy | Open format, good compression | ## Job Lifecycle All music generation is asynchronous: 1. **Submit** a generation request → receive a job with `status: "PENDING"` 2. **Poll** the job status endpoint → `"PROCESSING"` while generating 3. **Complete** → `status: "COMPLETED"` with `output_url` and `output_urls` (multiple formats) 4. **Failed** → `status: "FAILED"` with `error_message` (use retry endpoint to re-queue) ## Pricing Music generation is billed per minute of audio generated. Two billing paths apply depending on how you authenticate: | Path | Rate | | - | - | | Account credits (subscription / JWT) | **990 credits/minute** | | API wallet (API key, USD-denominated) | **\$0.04/minute** | | Duration | Credits | API wallet (USD) | | - | - | - | | 30 seconds | 495 | \$0.02 | | 1 minute | 990 | \$0.04 | | 2 minutes | 1,980 | \$0.08 | | 5 minutes | 4,950 | \$0.20 | | 10 minutes | 9,900 | \$0.40 | **AudioMusic Premium** (`quality: "premium"`) is a higher-fidelity variant that bills at **2× the base rate** — 1,980 credits/minute or \$0.08/minute. Requires a Pro-tier or higher subscription; calls from Free/Basic tiers return `402 PREMIUM_TIER_REQUIRED`. Credits are reserved when the job is created and refunded if generation fails. ## Best Practices 1. **Use Simple Mode for quick results** — describe what you want in natural language and let AudioMusic V2 handle the details 2. **Enable `thinking: true`** (default) for best quality — Chain-of-Thought reasoning significantly improves output 3. **Use `caption` over `prompt`** — AudioMusic V2 uses `caption` internally; `prompt` is a legacy alias 4. **Set `inference_steps: 64`** for production quality. Use 32 for fast drafts, 8 for turbo previews 5. **Default `guidance_scale: 7.0`** works well for most cases. Increase to 10-15 for stronger prompt adherence 6. **Use structure tags in lyrics** — `[verse]`, `[chorus]`, `[bridge]`, `[intro]`, `[outro]`, `[Instrumental]` 7. **Use `batch_size > 1`** to generate multiple variations and pick the best one 8. **Enable `generate_lrc: true`** when you need synchronized lyric timestamps ## Error Handling | Status Code | Description | | - | - | | 200 | Success | | 400 | Invalid parameters (check error message for details) | | 401 | Authentication required | | 402 | Insufficient credits | | 404 | Job or resource not found | | 429 | Rate limit exceeded (wait and retry) | | 500 | Internal server error | **"Insufficient credits"** — Your wallet balance is too low. Top up at [Dashboard](https://www.audiopod.ai/dashboard/account). **"Job must be completed"** — You tried to retake/extend/edit a job that hasn't finished yet. Wait for completion first. **"Original music job not found"** — The source job ID doesn't exist or doesn't belong to your account. **"Rate limit exceeded"** — You've exceeded the rate limit. Wait a moment and try again. ## Next Steps Text-to-Music, Instrumental, Rap, Vocals, Samples, Simple Mode Cover, Reference Audio, Analysis, Stem Extraction, Edit, Extend Job status, listing, filtering, retry, presets # Score-first music Source: https://docs.audiopod.ai/api-reference/music/score-first Write an editable score, revise it in plain words, and record it — the AudioMusic Score-first endpoints. Score-first is the AudioMusic mode for shaping a song before it is recorded. Every job here returns a score (melody, chords and sections, as ABC notation plus MIDI) alongside — or, for plan and revise jobs, instead of — the audio. Not sure which mode you need? See [Choosing how to make music](/api-reference/music/choosing). All routes live under `/api/v1/music/v3` and take the usual `X-API-Key` header. Check `GET /api/v1/music/v3/availability` before showing a Score-first option in your own product: ```json theme={null} { "available": true, "revise": true } ``` `available` says whether Score-first is offered; `revise` whether plain-English revision is enabled for your account. ## Pricing | Step | Endpoint | Credits | | - | - | - | | Write a score (lyrics included when you send a description) | `POST /music/v3/plan` | **165** flat | | Revise a score | `POST /music/v3/revise` | **165** flat per revision | | Record a score | `POST /music/v3/edit` | \~**16.5 per second** of the score's length (15 s minimum, 600 s maximum) | | Read a job or score | `GET /music/v3/jobs/{job_id}`, `/score` | free | Credits are reserved when you call and refunded if the job fails or the request is refused. ## Write a score ### `POST /api/v1/music/v3/plan` Writes a score only — no audio. Send a `description` and AudioPod writes the lyrics first, within the same charge; or send your own `lyrics`. | Field | Type | Required | Description | | - | - | - | - | | `description` | string | one of | What the song is about — topic, mood, genre (1–1000 characters). AudioPod writes the lyrics. Cannot be combined with `lyrics` | | `lyrics` | string | one of | Your own lyrics; section tags such as `[verse]` and `[chorus]` help | | `style` | string | with `lyrics` | Genre, instrumentation, mood, tempo feel, vocal character (up to 2000 characters). Required with `lyrics`; with a `description` it defaults to the description | | `target_seconds` | integer | no | 30–300. Roughly how long the song should be; used to size the lyrics AudioPod writes. A guide, not a guarantee | | `language` | string | no | Language to write the lyrics in (ISO 639-1, e.g. `en`, `hi`). Defaults to the language of the description, else English | | `cot` | string | no | `full` (default) writes melody and harmony; `melody` the tune alone | | `seed` | integer | no | Reuse to get closer to an earlier result | | `display_name` | string | no | Your name for the track | ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/v3/plan" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "style": "warm acoustic folk, fingerpicked guitar, gentle male vocal", "description": "a lighthouse keeper waiting for the ships to come home", "target_seconds": 120 }' ``` The response is a [job](#job-response) with `workflow: "plan"`, plus: * `lyrics_source` — `audiopod` (written from your description) or `user` (you supplied them); absent for instrumentals. * `target_seconds` — the length you asked for, if any. * `score.duration_seconds` — the score's own length once it is written. Song length follows the amount of lyrics, so this can differ from `target_seconds`; use Song mode when you need an exact length. | Status | `detail.code` | Meaning | | - | - | - | | `422` | `DESCRIPTION_NOT_ALLOWED` | The description asks for the words of an existing song. AudioPod writes original songs; describe the mood, genre and topic instead. Nothing is charged | | `422` | `LYRICS_FAILED` | Lyrics could not be written for that description. Nothing is charged; rephrase it or send your own lyrics | | `503` | `LYRICS_BUSY` | Lyric writing is busy; retry after `Retry-After` seconds | ## Revise a score in plain words ### `POST /api/v1/music/v3/revise` Available when plain-English revision is enabled for your account (`revise: true` from `/availability`); otherwise this route answers `404`. Applies a change described in words and returns a **new** score-only job linked to the one you revised; the original is kept. Parts of the score you did not ask about are kept exactly as they were, and the tempo only changes when `allow_tempo_change` is true. Revise as often as you like, then record the version you want. | Field | Type | Required | Description | | - | - | - | - | | `source_job_id` | integer | yes | The score to change: a plan, revise or recorded job you own that has a score | | `instruction` | string | yes | The change, in plain words (1–500 characters) — "make the chorus higher", "add a bridge after the second chorus" | | `section` | string | no | Limit the change to one section by its label (up to 60 characters): `chorus` for every chorus, `chorus 2` for the second | | `bars` | object | no | Limit the change to a bar range, numbered from 1, both ends included: `{"start": 5, "end": 8}` (1–10000) | | `allow_tempo_change` | boolean | no | Permit the revision to change the tempo. Off by default | | `display_name` | string | no | Your name for the revised score | Send an **`Idempotency-Key`** header (up to 200 characters) to make a retry safe: the same key from you within 10 minutes returns the first result and is not charged again. ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/v3/revise" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Idempotency-Key: 2f6c1d7e-revise-chorus" \ -H "Content-Type: application/json" \ -d '{"source_job_id": 4301, "instruction": "make the chorus higher", "section": "chorus"}' ``` The job has `workflow: "revise"` and its `source_job_id`. `note` says in one sentence what changed, and `score_comparison.changed_bars` says where — the changed bar ranges per voice. Errors — nothing is charged for any of these: | Status | `detail.code` | Meaning | | - | - | - | | `404` | — | The source is not yours or has no score, or revision is not enabled for your account | | `409` | `REVISE_IN_FLIGHT` | The same change to the same score is already being made; it will appear in your library shortly | | `402` | — | Not enough credits | | `422` | `REVISE_TARGET_NOT_FOUND` | The score has no such section or bars; the message lists what it has | | `422` | `REVISE_FAILED` | The change could not be made as a valid score. Describe it differently, or narrow it with `section` / `bars` | | `422` | `INVALID_SCORE` | The source score itself cannot be read | | `422` | `IDEMPOTENCY_KEY_REUSED` | The `Idempotency-Key` was already used with a different request body | | `429` | `REVISE_RATE_LIMITED` | Too many revisions (20 a minute, 300 a day) or several failed in a row; wait `Retry-After` seconds | | `503` | `REVISE_UNAVAILABLE` | Briefly unavailable; retry shortly | ## Record a score ### `POST /api/v1/music/v3/edit` Records a score as a finished track. Send the score to perform — usually the latest revision's score from `GET /music/v3/jobs/{job_id}/score` — and optionally a new style or new words to perform the same tune differently. | Field | Type | Required | Description | | - | - | - | - | | `source_job_id` | integer | yes | The job the score came from | | `abc` | string | yes | The score to record, in ABC notation | | `style` | string | no | A new style for the same tune; defaults to the source's | | `lyrics` | string | no | New words for the same tune; defaults to the source's | | `cot` | string | no | `melody` (default) or `full` | | `allow_tempo_change` | boolean | no | Permit the score to change the source's tempo | ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/music/v3/edit" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"source_job_id": 4302, "abc": "X:1\n..."}' ``` A recording is a new performance of the whole piece: sections you did not change will still sound different from an earlier take. An identical request for the same score while one is still recording is refused with `409` (`SCORE_ALREADY_RECORDING`, with the `job_id` already in progress) and nothing is charged. ## Jobs | Endpoint | Returns | | - | - | | `GET /api/v1/music/v3/jobs/{job_id}` | One job: status, audio, score, comparison and billing | | `GET /api/v1/music/v3/jobs/{job_id}/score` | Just the score, ready to revise or record | | `GET /api/v1/music/v3/jobs` | Your Score-first jobs, newest first | | `GET /api/v1/music/v3/availability` | `{"available": true, "revise": true}` | ### Job response ```json theme={null} { "job_id": 4302, "workflow": "revise", "status": "COMPLETED", "style": "warm acoustic folk, fingerpicked guitar, gentle male vocal", "lyrics": "[verse]\n...", "lyrics_source": "audiopod", "audio_url": null, "score": { "abc": "X:1\n...", "midi_url": "https://...", "editable": true, "duration_seconds": 118.0 }, "score_comparison": { "changed_bars": { "Vocal": [[10, 10]], "Ins": [] } }, "source_job_id": 4301, "note": "Changed the melody in chorus 1 (bar 10); everything else is unchanged." } ``` `audio_url` is `null` for plan and revise jobs — they are scores. Poll until `status` is `COMPLETED` or `FAILED`. # Noise Reduction Source: https://docs.audiopod.ai/api-reference/noise-reduction Remove background noise, enhance audio quality, and clean up recordings using AudioPod AI's advanced noise reduction technology. ## Overview AudioPod AI's Noise Reduction API uses advanced AI models to remove background noise, enhance speech clarity, and improve overall audio quality. Process recordings to eliminate unwanted sounds while preserving the natural characteristics of speech and music. ### Key Features * **AI-Powered Denoising**: Advanced neural networks for intelligent noise removal * **Three Quality Modes**: `balanced` (faster, lighter), `studio` (the default) and `ultra` (highest fidelity) * **Spectral Processing**: Traditional spectral subtraction for specific noise types * **Voice Preservation**: Maintain natural speech characteristics during cleanup * **Video Support**: Process audio from video files while preserving video streams * **Batch Processing**: Handle multiple files efficiently ## Authentication All endpoints require authentication. Use one of these methods: * **API Key (Recommended)**: `X-API-Key: your_api_key` header * **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth) ## AI Noise Reduction ### Standard Denoising Remove background noise using AI-powered denoising models. ```python theme={null} from audiopod import Client client = Client() # Simple noise reduction denoised = client.denoiser.denoise_audio( audio_file="noisy_recording.wav", quality_mode="balanced", # "balanced", "studio" (default) or "ultra" wait_for_completion=True ) print(f"Denoising completed!") print(f"Input file: {denoised.input_file}") print(f"Denoised audio URL: {denoised.denoised_audio_url}") print(f"Noise reduction level: {denoised.noise_reduction_db}dB") # Download cleaned audio clean_audio = client.download_file(denoised.denoised_audio_url) with open("clean_recording.wav", "wb") as f: f.write(clean_audio) # Studio quality denoising studio_denoised = client.denoiser.denoise_audio( audio_file="interview_with_background_noise.wav", quality_mode="studio", # Higher quality processing wait_for_completion=True ) print(f"Studio denoising completed!") print(f"Noise reduction level: {studio_denoised.noise_reduction_db}dB") # Batch processing multiple files noisy_files = [ "meeting_room_recording.wav", "interview_outdoors.mp3", "podcast_home_setup.wav", "lecture_hall_audio.m4a" ] batch_results = client.denoiser.denoise_batch( audio_files=noisy_files, quality_mode="balanced", wait_for_completion=True ) for i, result in enumerate(batch_results): print(f"\nFile {i+1}: {noisy_files[i]}") print(f"Status: {result.status}") if result.status == "completed": print(f"Noise reduced: {result.noise_reduction_db}dB") print(f"Clean audio: {result.denoised_audio_url}") # Download and save clean_audio = client.download_file(result.denoised_audio_url) output_name = f"clean_{i+1}_{noisy_files[i].split('.')[0]}.wav" with open(output_name, "wb") as f: f.write(clean_audio) print(f"Saved: {output_name}") # Process video files (audio cleaning) video_denoised = client.denoiser.denoise_video( video_file="presentation_with_noise.mp4", quality_mode="studio", # Higher quality processing preserve_video=True, # Keep original video stream wait_for_completion=True ) print(f"Video denoising completed!") print(f"Clean video URL: {video_denoised.video_output_url}") print(f"Audio-only URL: {video_denoised.denoised_audio_url}") ``` ```javascript theme={null} // Note: SDK examples shown are conceptual and may not reflect the current SDK implementation // For production use, refer to the Raw HTTP examples or check the latest SDK documentation const { AudioPodClient } = require('audiopod-js'); const fs = require('fs'); const client = new AudioPodClient(); async function denoiseAudio() { try { // Simple noise reduction const denoised = await client.denoiser.denoiseAudio({ audioFile: fs.createReadStream('noisy_recording.wav'), qualityMode: 'balanced', // "balanced", "studio" (default) or "ultra" waitForCompletion: true }); console.log('Denoising completed!'); console.log(`Input file: ${denoised.inputFile}`); console.log(`Denoised audio URL: ${denoised.denoisedAudioUrl}`); console.log(`Noise reduction level: ${denoised.noiseReductionDb}dB`); // Download cleaned audio const cleanAudio = await client.downloadFile(denoised.denoisedAudioUrl); fs.writeFileSync('clean_recording.wav', cleanAudio); // Studio quality denoising const studioDenoised = await client.denoiser.denoiseAudio({ audioFile: fs.createReadStream('interview_with_background_noise.wav'), qualityMode: 'studio', // Higher quality processing waitForCompletion: true }); console.log('Studio denoising completed!'); console.log(`Noise reduction level: ${studioDenoised.noiseReductionDb}dB`); // Batch processing multiple files const noisyFiles = [ 'meeting_room_recording.wav', 'interview_outdoors.mp3', 'podcast_home_setup.wav', 'lecture_hall_audio.m4a' ]; const fileStreams = noisyFiles.map(file => ({ file: fs.createReadStream(file), name: file })); const batchResults = await client.denoiser.denoiseBatch({ audioFiles: fileStreams, qualityMode: 'balanced', waitForCompletion: true }); batchResults.forEach((result, index) => { console.log(`\nFile ${index + 1}: ${noisyFiles[index]}`); console.log(`Status: ${result.status}`); if (result.status === 'completed') { console.log(`Noise reduced: ${result.noiseReductionDb}dB`); console.log(`Clean audio: ${result.denoisedAudioUrl}`); // Download and save client.downloadFile(result.denoisedAudioUrl).then(cleanAudio => { const outputName = `clean_${index + 1}_${noisyFiles[index].split('.')[0]}.wav`; fs.writeFileSync(outputName, cleanAudio); console.log(`Saved: ${outputName}`); }); } }); // Process video files (audio cleaning) const videoDenoised = await client.denoiser.denoiseVideo({ videoFile: fs.createReadStream('presentation_with_noise.mp4'), qualityMode: 'studio', // Higher quality processing preserveVideo: true, // Keep original video stream waitForCompletion: true }); console.log('Video denoising completed!'); console.log(`Clean video URL: ${videoDenoised.videoOutputUrl}`); console.log(`Audio-only URL: ${videoDenoised.denoisedAudioUrl}`); } catch (error) { console.error('Denoising error:', error.message); } } denoiseAudio(); ``` ```python theme={null} import requests import time # Standard denoising with open("noisy_recording.wav", "rb") as audio_file: response = requests.post( "https://api.audiopod.ai/api/v1/denoiser/denoise", headers={"X-API-Key": api_key}, data={ "quality_mode": "balanced" # "balanced", "studio" (default) or "ultra" }, files={"file": audio_file} ) if response.status_code == 200: job_data = response.json() job_id = job_data["id"] print(f"Denoising job created: {job_id}") # Poll for completion while True: status_response = requests.get( f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}", headers={"X-API-Key": api_key} ) if status_response.status_code == 200: job_status = status_response.json() print(f"Status: {job_status['status']}") if job_status["status"] == "COMPLETED": print(f"Denoising completed!") print(f"Clean audio URL: {job_status.get('denoised_audio_url')}") print(f"Noise reduction: {job_status.get('noise_reduction_db', 0)}dB") # Download the cleaned audio if job_status.get('denoised_audio_url'): audio_response = requests.get(job_status['denoised_audio_url']) with open("clean_recording.wav", "wb") as f: f.write(audio_response.content) print("Downloaded clean audio: clean_recording.wav") break elif job_status["status"] == "FAILED": print(f"Denoising failed: {job_status.get('error', 'Unknown error')}") break time.sleep(5) # Check every 5 seconds else: print(f"Error: {response.status_code} - {response.text}") ``` ```bash theme={null} # Upload file for denoising curl -X POST "https://api.audiopod.ai/api/v1/denoiser/denoise" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "file=@noisy_recording.wav" \ -F "quality_mode=balanced" # Check job status (replace JOB_ID with actual job ID) curl -X GET "https://api.audiopod.ai/api/v1/denoiser/jobs/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # Studio quality denoising curl -X POST "https://api.audiopod.ai/api/v1/denoiser/denoise" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "file=@very_noisy_audio.wav" \ -F "quality_mode=studio" ``` ### Denoise from URL Process audio/video from URLs (YouTube, etc.) to remove noise. ```python theme={null} # Note: SDK examples shown are conceptual and may not reflect the current SDK implementation # For production use, refer to the Raw HTTP examples or check the latest SDK documentation from audiopod import Client client = Client() # Denoise from YouTube URL url_denoised = client.denoiser.denoise_from_url( url="https://youtube.com/watch?v=example123", quality_mode="studio", # Higher quality processing for online content wait_for_completion=True ) print(f"URL denoising completed!") print(f"Source: {url_denoised.source_url}") print(f"Clean audio URL: {url_denoised.denoised_audio_url}") print(f"Noise reduction: {url_denoised.noise_reduction_db}dB") # Batch URL processing urls = [ "https://youtube.com/watch?v=lecture1", "https://vimeo.com/meeting_recording", "https://example.com/noisy_podcast.mp3" ] batch_url_results = client.denoiser.denoise_batch_from_urls( urls=urls, quality_mode="balanced", wait_for_completion=True ) for i, result in enumerate(batch_url_results): print(f"\nURL {i+1}: {urls[i]}") print(f"Status: {result.status}") if result.status == "completed": print(f"Clean audio: {result.denoised_audio_url}") ``` ```javascript theme={null} // Note: SDK examples shown are conceptual and may not reflect the current SDK implementation // For production use, refer to the Raw HTTP examples or check the latest SDK documentation const { AudioPodClient } = require('audiopod-js'); const client = new AudioPodClient(); async function denoiseFromUrl() { try { // Denoise from YouTube URL const urlDenoised = await client.denoiser.denoiseFromUrl({ url: "https://youtube.com/watch?v=example123", qualityMode: "studio", // Higher quality processing for online content waitForCompletion: true }); console.log('URL denoising completed!'); console.log(`Source: ${urlDenoised.sourceUrl}`); console.log(`Clean audio URL: ${urlDenoised.denoisedAudioUrl}`); console.log(`Noise reduction: ${urlDenoised.noiseReductionDb}dB`); // Batch URL processing const urls = [ "https://youtube.com/watch?v=lecture1", "https://vimeo.com/meeting_recording", "https://example.com/noisy_podcast.mp3" ]; const batchUrlResults = await client.denoiser.denoiseBatchFromUrls({ urls: urls, qualityMode: "balanced", waitForCompletion: true }); batchUrlResults.forEach((result, index) => { console.log(`\nURL ${index + 1}: ${urls[index]}`); console.log(`Status: ${result.status}`); if (result.status === "completed") { console.log(`Clean audio: ${result.denoisedAudioUrl}`); } }); } catch (error) { console.error('URL denoising error:', error.message); } } denoiseFromUrl(); ``` ```python theme={null} import requests import time response = requests.post( "https://api.audiopod.ai/api/v1/denoiser/denoise", headers={"X-API-Key": api_key}, data={ "url": "https://youtube.com/watch?v=example123", "quality_mode": "studio" # Higher quality processing } ) if response.status_code == 200: job_data = response.json() job_id = job_data["id"] print(f"URL denoising started: {job_id}") # Poll for completion while True: status_response = requests.get( f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}", headers={"X-API-Key": api_key} ) if status_response.status_code == 200: job_status = status_response.json() print(f"Status: {job_status['status']}") if job_status["status"] == "COMPLETED": print(f"URL denoising completed!") print(f"Source: {job_status.get('source_url')}") print(f"Clean audio: {job_status.get('denoised_audio_url')}") break elif job_status["status"] == "FAILED": print(f"Denoising failed: {job_status.get('error')}") break time.sleep(10) # URL processing may take longer ``` ```bash theme={null} # Denoise from URL (multipart form fields, not a JSON body) curl -X POST "https://api.audiopod.ai/api/v1/denoiser/denoise" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=example123" \ -F "quality_mode=ultra" # Check job status curl -X GET "https://api.audiopod.ai/api/v1/denoiser/jobs/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Quality Modes:** Every mode removes background noise and improves speech clarity. Send the mode as the `quality_mode` form field. * **`balanced`**: The fastest, lightest mode, with clean results. * **`studio`** (default): Broadcast and podcast quality. Used when `quality_mode` is omitted or blank. * **`ultra`**: The highest fidelity, restoring lost high frequencies. The value is case-insensitive. Any other value is rejected with a `422` before a job is created or any credits are reserved. **Response:** ```json theme={null} { "id": 123, "status": "PROCESSING", "progress": 0, "input_path": "/uploads/noisy_recording.wav", "is_video": false, "task_id": "celery_task_uuid_here", "created_at": "2024-01-15T10:30:00Z", "original_filename": "noisy_recording.wav", "display_name": "noisy_recording.wav" } ``` ## Spectral Noise Reduction ### Advanced Spectral Processing Use traditional spectral subtraction for specific noise types and fine control. ```http theme={null} POST /api/v1/denoiser/spectral/denoise X-API-Key: {api_key} Content-Type: multipart/form-data file: (audio file) noise_type: general prop_decrease: 0.8 stationary: true ``` ```python theme={null} with open("recording_with_hum.wav", "rb") as audio_file: response = requests.post( "https://api.audiopod.ai/api/v1/denoiser/spectral/denoise", headers={"X-API-Key": api_key}, data={ "noise_type": "electrical_hum", # Specific noise type "prop_decrease": 0.8, # 80% noise reduction "stationary": True, # Constant noise "time_constant_s": 2.0, # Adaptation time "freq_mask_smooth_hz": 500, # Frequency smoothing "time_mask_smooth_ms": 50 # Time smoothing }, files={"file": audio_file} ) if response.status_code == 200: job_data = response.json() print(f"Spectral denoising job: {job_data['id']}") ``` **Noise Types:** * **`general`**: General background noise * **`electrical_hum`**: 50/60Hz electrical interference * **`air_conditioning`**: HVAC and fan noise * **`traffic`**: Vehicle and road noise * **`crowd`**: Background conversations and crowd noise ## Job Management ### Get Job Status Monitor the progress of noise reduction jobs. ```http theme={null} GET /api/v1/denoiser/jobs/{job_id} X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}", headers={"X-API-Key": api_key} ) if response.status_code == 200: job_status = response.json() print(f"Status: {job_status['status']}") print(f"Progress: {job_status['progress']}%") if job_status["status"] == "COMPLETED": print("Denoising complete!") print(f"Output URL: {job_status['output_url']}") ``` **Response (Completed):** ```json theme={null} { "id": 123, "status": "COMPLETED", "progress": 100, "input_path": "/uploads/noisy_recording.wav", "output_path": "/processed/clean_recording_123.wav", "video_output_path": null, "is_video": false, "task_id": "celery_task_uuid_here", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:32:15Z", "completed_at": "2024-01-15T10:32:15Z", "output_url": "https://api.audiopod.ai/download/clean_recording_123.wav", "video_output_url": null, "original_filename": "noisy_recording.wav", "display_name": "Clean Recording", "stats": { "noise_reduction_db": 15.2, "processing_time": 135.4, "audio_duration": 180.0 } } ``` ### List Denoising Jobs Get all noise reduction jobs for the authenticated user. ```http theme={null} GET /api/v1/denoiser/jobs?limit=50&skip=0 X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( "https://api.audiopod.ai/api/v1/denoiser/jobs", headers={"X-API-Key": api_key}, params={ "limit": 50, "skip": 0 } ) if response.status_code == 200: jobs = response.json() for job in jobs: print(f"Job {job['id']}: {job['status']} - {job['original_filename']}") ``` ### Retry Failed Job Retry a failed noise reduction job. ```http theme={null} POST /api/v1/denoiser/jobs/{job_id}/retry X-API-Key: {api_key} ``` ### Cancel Job Cancel a pending denoising job. ```http theme={null} POST /api/v1/denoiser/jobs/{job_id}/cancel X-API-Key: {api_key} ``` ```python theme={null} response = requests.post( f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}/cancel", headers={"X-API-Key": api_key} ) if response.status_code == 200: job_status = response.json() print(f"Job {job_id} cancelled: {job_status['status']}") ``` ### Delete Job Remove a completed job and its results. ```http theme={null} DELETE /api/v1/denoiser/jobs/{job_id} X-API-Key: {api_key} ``` ```python theme={null} response = requests.delete( f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}", headers={"X-API-Key": api_key} ) if response.status_code == 204: print(f"Job {job_id} and associated files deleted successfully") ``` ### Health Check Check the denoising service availability and status. ```http theme={null} GET /api/v1/denoiser/health X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( "https://api.audiopod.ai/api/v1/denoiser/health", headers={"X-API-Key": api_key} ) if response.status_code == 200: health_status = response.json() print(f"Service status: {health_status['status']}") ``` **Health Response:** ```json theme={null} { "status": "available" } ``` ## Error Handling **Causes:** - Audio file corrupted or invalid format - File contains no audio content - Unsupported codec **Solutions:** - Use supported formats (WAV, MP3, M4A, MP4) - Verify file integrity - Check audio content exists **Causes:** - The uploaded file is larger than your plan allows per upload **Solutions:** - Upgrade your plan for larger uploads - Trim or compress the file before uploading **Causes:** - `quality_mode` is not one of `balanced`, `studio` or `ultra` - Audio quality too poor for processing - Extreme noise levels - Very short audio duration **Solutions:** - Try different quality mode - Use spectral processing for specific noise types - Ensure minimum 5 seconds duration **Causes:** - Not enough credits for processing duration **Solutions:** - Purchase additional credits - Check credit requirements before processing ## Pricing Signed-in accounts are billed from the **account credit balance** at the rate of the chosen quality mode, with a 10-credit minimum per job. API-key calls pay the API price of the chosen mode. Call `POST /api/v1/denoiser/quote` to see the exact cost before you start a job. | Service | Quality mode | Cost | | - | - | - | | AI Denoising | `balanced` | 330 credits/minute | | AI Denoising | `studio` (default) | 660 credits/minute | | AI Denoising | `ultra` | 1,320 credits/minute | | Spectral Processing | — | 660 credits/minute | ### Cost Examples | Duration | Method | Credits | | - | - | - | | 10 minutes | AI Denoising (balanced) | 3,300 | | 10 minutes | AI Denoising (studio) | 6,600 | | 10 minutes | AI Denoising (ultra) | 13,200 | | 30 minutes | Spectral | 19,800 | | 1 hour | AI Denoising (studio) | 39,600 | ## Next Steps Use cleaned audio for better voice model training. Improve transcription accuracy with clean audio. # Podcast Source: https://docs.audiopod.ai/api-reference/podcast Generate multi-speaker podcast episodes from your source material — outline, transcript, and narrated audio. ## Overview The Podcast API turns your topic and source material into a finished, multi-speaker episode. A project moves through a simple lifecycle: create the project and its speakers, add sources, generate an outline, generate the transcript, then render the narrated audio. The RSS feed is limited while it is rebuilt; see [RSS feed](#rss-feed-limited) below. ## Authentication * **API Key (Recommended)**: `X-API-Key: your_api_key` header * **JWT Token**: `Authorization: Bearer your_jwt_token` ## Lifecycle at a glance | Step | Endpoint | | - | - | | 1. Create a project (+ speakers) | `POST /api/v1/podcast/projects` | | 2. Add source material | `POST /api/v1/podcast/projects/{id}/sources` | | 3. Generate the outline | `POST /api/v1/podcast/projects/{id}/outline/generate` | | 4. Generate the transcript | `POST /api/v1/podcast/projects/{id}/transcript/generate` | | 5. Estimate + render audio | `GET /api/v1/podcast/projects/{id}/audio/estimate` · `POST /api/v1/podcast/projects/{id}/audio/generate` | | 6. Track progress | `GET /api/v1/podcast/projects/{id}/progress` | | 7. RSS feed (limited) | `POST /api/v1/podcast/feed` | ## Create a Project ```http theme={null} POST /api/v1/podcast/projects X-API-Key: {api_key} Content-Type: application/json { "title": "The Future of Audio AI", "topic": "How on-device audio models change creator workflows", "briefing": "Keep it upbeat and accessible for a general audience.", "language": "en", "target_duration_minutes": 15, "speakers": [ { "name": "Host", "voice": "aura" }, { "name": "Guest", "voice": "sage" } ] } ``` ```python theme={null} import requests project = requests.post( "https://api.audiopod.ai/api/v1/podcast/projects", headers={"X-API-Key": api_key, "Content-Type": "application/json"}, json={ "title": "The Future of Audio AI", "topic": "How on-device audio models change creator workflows", "language": "en", "target_duration_minutes": 15, "speakers": [ {"name": "Host", "voice": "aura"}, {"name": "Guest", "voice": "sage"}, ], }, ).json() print(f"Project {project['id']} created") ``` **Key fields:** * `title` (required): Episode title (1–200 characters) * `topic` (required): Main topic (1–500 characters) * `briefing` (optional): Extra context / tone guidance * `key_points` (optional): List of points to cover * `language` (optional): Primary language code (default `en`) * `target_duration_minutes` (optional): Target length, 5–60 * `speakers` (required): 1–4 initial speakers ## Manage Projects | Action | Request | | - | - | | List projects | `GET /api/v1/podcast/projects` | | Get one | `GET /api/v1/podcast/projects/{id}` | | Update | `PUT /api/v1/podcast/projects/{id}` | | Delete | `DELETE /api/v1/podcast/projects/{id}` | ## Update a Speaker Change a speaker's name or assigned voice on a project. ```http theme={null} PUT /api/v1/podcast/projects/{project_id}/speakers/{speaker_id} X-API-Key: {api_key} Content-Type: application/json { "name": "Co-Host", "voice": "willow" } ``` ## Generate Audio Preview the cost, then render. Audio generation follows the prepaid model — credits are reserved up front and refunded automatically on failure. ```bash theme={null} # Estimate curl -s "https://api.audiopod.ai/api/v1/podcast/projects/$PROJECT_ID/audio/estimate" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # Render curl -X POST "https://api.audiopod.ai/api/v1/podcast/projects/$PROJECT_ID/audio/generate" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` Poll `GET /api/v1/podcast/projects/{id}/progress` until generation completes. ## RSS Feed (limited) The RSS feed is being rebuilt and is limited until then. Today it lists every completed episode on your account, and anyone with the feed URL can read it. Listing is moving to per-episode opt-in. The feed has no show artwork yet, so podcast directories such as Apple Podcasts and Spotify will not accept it. To publish an episode today, download its audio and upload it to your podcast host. The feed URL carries a signed, feed-scoped token. ```http theme={null} POST /api/v1/podcast/feed X-API-Key: {api_key} ``` ```json theme={null} { "feed_url": "https://api.audiopod.ai/api/v1/podcast/feed/.xml" } ``` The feed itself is fetched anonymously by podcast players: ```http theme={null} GET /api/v1/podcast/feed/{token}.xml ``` ## Next Steps Narrate a single article or block of text. Browse voices or design a custom one for your hosts. # Audio Reader Source: https://docs.audiopod.ai/api-reference/reader Turn any article, blog post, or pasted text into natural-sounding narrated audio — from a URL or raw content — and share it with a public link. ## Overview The Audio Reader converts a web article or pasted text into a narrated audio track. Give it a URL (AudioPod extracts the readable article) or raw `content`, pick a voice, and get back a hosted audio file plus an optional public share/embed page. ### Key features * **URL or pasted text**: Point at an article URL or send `content` directly * **Any voice**: Free standard narration, or a premium/custom voice from your catalog * **Cost preview**: Estimate credits before you commit * **Public share + embed**: Each job can expose a minimal public page and embed HTML ## Authentication * **API Key (Recommended)**: `X-API-Key: your_api_key` header * **JWT Token**: `Authorization: Bearer your_jwt_token` ## Estimate Cost Preview the cost before generating. No charge is incurred for an estimate. ```http theme={null} POST /api/v1/reader/estimate X-API-Key: {api_key} Content-Type: application/json { "url": "https://example.com/blog/my-article", "voice": "aura" } ``` **Body:** * `url` (optional): Article URL to read (provide `url` **or** `content`) * `content` (optional): Raw text to narrate * `voice` (optional): Voice name or ID from `GET /api/v1/voice/voice-profiles` **Response:** ```json theme={null} { "is_premium": true, "estimated_credits": 1840, "word_count": 920, "char_count": 5230, "measured": true, "title": "My Article", "note": null } ``` When `is_premium` is `false`, standard narration is used at no credit cost. When `measured` is `false`, the article couldn't be scraped ahead of time, so `estimated_credits` is a baseline and you're billed for the actual length. ## Generate Create a reader job. Billing follows the standard prepaid model — credits are reserved on creation and refunded automatically if processing fails. ```http theme={null} POST /api/v1/reader/generate X-API-Key: {api_key} Content-Type: application/json { "url": "https://example.com/blog/my-article", "voice": "aura", "show_branding": true } ``` ```python theme={null} import requests job = requests.post( "https://api.audiopod.ai/api/v1/reader/generate", headers={"X-API-Key": api_key, "Content-Type": "application/json"}, json={"url": "https://example.com/blog/my-article", "voice": "aura"}, ).json() print(f"Reader job {job['id']} — {job['status']}") ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/reader/generate" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/blog/my-article", "voice": "aura"}' ``` **Body:** * `url` (optional): Article URL to read (provide `url` **or** `content`) * `content` (optional): Raw text to narrate * `title` (optional): Override the detected title * `voice` (optional): Voice name or ID; defaults to a standard narration voice * `webhook_url` (optional): Receive a callback when the job finishes * `show_branding` (optional): Show AudioPod branding on the public page (default `true`) **Response:** ```json theme={null} { "id": 4210, "status": "processing", "progress": 0, "title": "My Article", "word_count": 920, "audio_url": null, "embed_url": null, "embed_html": null, "audio_duration_seconds": null, "created_at": "2026-07-09T10:30:00Z", "completed_at": null } ``` ## Get a Job ```http theme={null} GET /api/v1/reader/{job_id} X-API-Key: {api_key} ``` Poll until `status` is `completed`; the response then carries `audio_url`, `audio_duration_seconds`, and `embed_url` / `embed_html` for embedding. ## List Jobs ```http theme={null} GET /api/v1/reader/ X-API-Key: {api_key} ``` Returns a paginated list of your reader jobs. ## Public Share Page Fetch the minimal, unauthenticated view for a shared job — safe to call without a key. It exposes only what the share link already implies (title, audio URL, duration), never the source URL, content, or any internals. ```http theme={null} GET /api/v1/reader/public/{job_id} ``` **Response:** ```json theme={null} { "id": 4210, "status": "completed", "title": "My Article", "audio_url": "https://media.audiopod.ai/...", "audio_duration_seconds": 372, "show_branding": true } ``` ## Delete a Job ```http theme={null} DELETE /api/v1/reader/{job_id} X-API-Key: {api_key} ``` ## Next Steps Fine-grained control over voice, directing, and timestamps. Turn source material into a multi-speaker episode. # Speaker Extraction Source: https://docs.audiopod.ai/api-reference/speaker-separation Extract individual speakers from multi-speaker audio recordings into separate, clean audio files using advanced AI-powered speaker separation technology. ## Overview AudioPod AI's Speaker Extraction API automatically separates multiple speakers in audio recordings into individual speaker-specific audio files. The service identifies who speaks when and creates clean, separate audio tracks for each speaker while preserving original audio quality. ### Key Features * **Speaker Separation**: Generate separate audio files for each detected speaker * **Timeline Generation**: Get detailed RTTM files with speaker timestamps * **Speaker Analytics**: Duration and quality statistics for each speaker * **Multi-Format Support**: Process audio and video files (WAV, MP3, M4A, MP4, etc.) * **URL Processing**: Extract speakers from YouTube and other video platforms * **Smart Detection**: Automatic speaker detection or specify expected number * **Quality Preservation**: Maintains original audio quality in extracted files ## Authentication All endpoints require authentication. Use one of these methods: * **API Key (Recommended)**: `X-API-Key: your_api_key` header * **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth) ## Speaker Extraction ### Extract from File Upload Upload an audio or video file to extract individual speaker tracks. ```http theme={null} POST /api/v1/speaker/extract X-API-Key: {api_key} Content-Type: multipart/form-data file: (audio/video file) num_speakers: 4 ``` ```python theme={null} import requests with open("podcast_episode.mp3", "rb") as audio_file: response = requests.post( "https://api.audiopod.ai/api/v1/speaker/extract", headers={"X-API-Key": api_key}, data={"num_speakers": 4}, # Optional: specify expected speakers files={"file": audio_file} ) if response.status_code == 200: extraction_job = response.json() job_id = extraction_job["id"] print(f"Speaker extraction job created: {job_id}") ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/speaker/extract" \ -H "X-API-Key: your_api_key" \ -F "file=@podcast_episode.mp3" \ -F "num_speakers=4" ``` ### Extract from URL Extract speakers from audio/video URLs (YouTube, Vimeo, etc.). ```http theme={null} POST /api/v1/speaker/extract X-API-Key: {api_key} Content-Type: application/x-www-form-urlencoded url=https://youtube.com/watch?v=example123&num_speakers=3 ``` ```python theme={null} response = requests.post( "https://api.audiopod.ai/api/v1/speaker/extract", headers={"X-API-Key": api_key}, data={ "url": "https://youtube.com/watch?v=example123", "num_speakers": 3 # Optional: specify expected speakers } ) if response.status_code == 200: job_data = response.json() print(f"URL extraction started: {job_data['id']}") ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/speaker/extract" \ -H "X-API-Key: your_api_key" \ -d "url=https://youtube.com/watch?v=example123" \ -d "num_speakers=3" ``` **Response:** ```json theme={null} { "id": 123, "job_type": "extraction", "status": "PENDING", "created_at": "2024-01-15T10:30:00Z", "user_id": "550e8400-e29b-41d4-a716-446655440000", "task_id": "celery_task_uuid_here" } ``` ## Job Management ### Get Job Status Monitor the progress of speaker extraction jobs. ```http theme={null} GET /api/v1/speaker/jobs/{job_id} X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( f"https://api.audiopod.ai/api/v1/speaker/jobs/{job_id}", headers={"X-API-Key": api_key} ) if response.status_code == 200: job_status = response.json() print(f"Status: {job_status['status']}") if job_status["status"] == "COMPLETED": print("Extraction complete!") if job_status["result"]: result = job_status["result"] print(f"Extracted {len(result['speakers'])} speakers") for speaker in result['speakers']: print(f"- {speaker['label']}: {speaker.get('download_url', 'Processing...')}") ``` **Response (Completed Extraction):** ```json theme={null} { "id": 123, "job_type": "extraction", "status": "COMPLETED", "created_at": "2024-01-15T10:30:00Z", "completed_at": "2024-01-15T10:35:30Z", "user_id": "550e8400-e29b-41d4-a716-446655440000", "task_id": "celery_task_uuid_here", "result": { "speakers": [ { "id": 0, "label": "SPEAKER_0", "audio_path": "processed/123/speaker_0.wav", "download_url": "https://s3.amazonaws.com/...", "audio_stats": { "rms_db": -12.3, "peak": 0.85 } }, { "id": 1, "label": "SPEAKER_1", "audio_path": "processed/123/speaker_1.wav", "download_url": "https://s3.amazonaws.com/...", "audio_stats": { "rms_db": -15.7, "peak": 0.72 } } ], "files": [ { "type": "audio", "speaker": "SPEAKER_0", "path": "processed/123/speaker_0.wav", "download_url": "https://s3.amazonaws.com/..." }, { "type": "audio", "speaker": "SPEAKER_1", "path": "processed/123/speaker_1.wav", "download_url": "https://s3.amazonaws.com/..." }, { "type": "rttm", "path": "processed/123/extraction.rttm", "download_url": "https://s3.amazonaws.com/..." } ], "rttm_path": "processed/123/extraction.rttm" } } ``` ### List Extraction Jobs Get all speaker extraction jobs for the authenticated user. ```http theme={null} GET /api/v1/speaker/jobs?job_type=extraction&status=COMPLETED&limit=50 X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( "https://api.audiopod.ai/api/v1/speaker/jobs", headers={"X-API-Key": api_key}, params={ "job_type": "extraction", "status": "COMPLETED", # Optional filter "skip": 0, "limit": 50 } ) if response.status_code == 200: jobs_data = response.json() print(f"Total jobs: {jobs_data['total']}") print(f"Has more: {jobs_data['hasMore']}") for job in jobs_data["items"]: print(f"Job {job['id']}: {job['status']} - {job.get('filename', 'N/A')}") if job['status'] == 'COMPLETED' and job.get('outputFiles'): print(f" Output files: {len(job['outputFiles'])}") ``` **Response:** ```json theme={null} { "items": [ { "id": 123, "job_type": "extraction", "status": "COMPLETED", "created_at": "2024-01-15T10:30:00Z", "completed_at": "2024-01-15T10:35:30Z", "user_id": "550e8400-e29b-41d4-a716-446655440000", "task_id": "celery_task_uuid_here", "filename": "podcast_episode.mp3", "display_name": "podcast_episode.mp3", "outputFiles": [ { "type": "audio", "speaker": "SPEAKER_0", "path": "processed/123/speaker_0.wav" }, { "type": "audio", "speaker": "SPEAKER_1", "path": "processed/123/speaker_1.wav" } ] } ], "hasMore": false, "total": 1 } ``` ### Retry Failed Job Retry a failed speaker extraction job. ```http theme={null} POST /api/v1/speaker/jobs/{job_id}/retry X-API-Key: {api_key} ``` ```python theme={null} response = requests.post( f"https://api.audiopod.ai/api/v1/speaker/jobs/{job_id}/retry", headers={"X-API-Key": api_key} ) if response.status_code == 200: retried_job = response.json() print(f"Extraction job {retried_job['id']} retried successfully") print(f"New task ID: {retried_job['task_id']}") ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/speaker/jobs/123/retry" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Response:** ```json theme={null} { "id": 123, "job_type": "extraction", "status": "PROCESSING", "created_at": "2024-01-15T10:30:00Z", "task_id": "new_celery_task_uuid_here", "user_id": "550e8400-e29b-41d4-a716-446655440000" } ``` ### Delete Job Remove a speaker extraction job and its associated files. ```http theme={null} DELETE /api/v1/speaker/jobs/{job_id} X-API-Key: {api_key} ``` ```python theme={null} response = requests.delete( f"https://api.audiopod.ai/api/v1/speaker/jobs/{job_id}", headers={"X-API-Key": api_key} ) if response.status_code == 204: print("Extraction job and files deleted successfully") elif response.status_code == 404: print("Job not found or access denied") ``` ```bash theme={null} curl -X DELETE "https://api.audiopod.ai/api/v1/speaker/jobs/123" \ -H "X-API-Key: your_api_key" ``` **Response:** `204 No Content` on successful deletion ## Supported Formats **Audio Formats:** * WAV, MP3, M4A, AAC, FLAC, OGG, OPUS, WebM * WMA, Speex, and other common formats **Video Formats:** * MP4, AVI, MOV, MKV, WebM * Audio will be extracted automatically from video files **URL Sources:** * YouTube, Vimeo, and other video platforms * Direct audio/video file URLs ## Error Handling ```json theme={null} { "error_code": "INVALID_AUDIO_FORMAT", "message": "Invalid file type. Must be audio or video file.", "details": { "content_type": "text/plain", "extension": ".txt", "supported_formats": ["audio/wav", "audio/mp3", "audio/m4a", "video/mp4"], "supported_extensions": [".wav", ".mp3", ".m4a", ".mp4", ".avi", ".mov"] } } ``` **Causes:** Invalid file format, missing file/URL, or both file and URL provided **Solutions:** Use supported audio/video formats, provide either file OR URL (not both) ```json theme={null} { "detail": "Insufficient credits for processing. Required: 8250, Available: 1000" } ``` **Causes:** Not enough credits for the audio duration **Solutions:** Purchase additional credits or process shorter audio files ```json theme={null} { "error_code": "PROCESSING_FAILED", "message": "Failed to extract speakers from audio", "details": { "reason": "Audio quality too poor or no distinguishable speakers found" } } ``` **Causes:** Poor audio quality, no speech content, or indistinguishable speakers **Solutions:** Ensure clear speech content, try noise reduction first, or verify multiple speakers exist ```json theme={null} { "detail": "Job not found or access denied" } ``` **Causes:** Invalid job ID or trying to access another user's job **Solutions:** Verify job ID and ensure you own the job ```json theme={null} { "detail": "Rate limit exceeded. Try again later." } ``` **Causes:** Exceeded 100 requests per minute limit **Solutions:** Wait before making additional requests or implement request throttling ## Pricing Speaker extraction costs are based on audio duration: | Service | Cost | Description | | - | - | - | | Speaker Extraction | 330 credits/minute | Generate separate audio files for each speaker | *Note: Credits are charged per second of audio (5.5 credits/second)* ### Cost Examples | Duration | Service | Credits | USD Cost\* | | - | - | - | - | | 5 minutes | Extraction | 1,650 | \~\$0.22 | | 15 minutes | Extraction | 4,950 | \~\$0.66 | | 30 minutes | Extraction | 9,900 | \~\$1.32 | | 1 hour | Extraction | 19,800 | \~\$2.64 | \*USD cost estimates based on standard credit pricing. Actual costs may vary based on subscription plan. ## Rate Limits * **100 requests per minute** per API key * Rate limits apply per endpoint * Exceeding limits returns `429 Too Many Requests` ## Next Steps Transcribe individual speaker tracks with improved accuracy. Clean up audio before speaker extraction for better results. # Speech-to-Text Source: https://docs.audiopod.ai/api-reference/speech-to-text Convert audio and video to accurate text transcriptions with speaker identification, timestamps, and multi-language support. ## Overview AudioPod AI's Speech-to-Text API converts audio and video content into accurate text transcriptions using AudioPod's transcription engine. Get detailed transcriptions with speaker diarization, word-level timestamps, and confidence scores. ### Key Features * **Multiple accuracy / speed tiers**: choose the engine variant that matches your latency and quality needs * **Speaker Diarization**: Automatic speaker identification and separation * **Word-Level Timestamps**: Precise timing for each word * **Confidence Scores**: Quality metrics for transcription accuracy * **50+ Languages**: Automatic language detection or manual specification * **Large File Support**: Handle videos up to 15 hours with chunking * **Multiple Sources**: Upload files or provide YouTube/video URLs * **Editable Transcripts**: Edit and refine transcription results ## Authentication All endpoints require authentication. Use one of these methods: * **API Key (Recommended)**: `X-API-Key: your_api_key` header * **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth) ## Transcribe from URLs ### Transcribe YouTube Videos Transcribe audio from YouTube or other video platforms. ```python theme={null} from audiopod import Client client = Client() # Simple YouTube transcription transcription = client.transcription.transcribe_from_url( url="https://youtube.com/watch?v=example123", wait_for_completion=True # Wait for result ) print(f"Transcription completed!") print(f"Duration: {transcription.duration}s") print(f"Language: {transcription.language}") print(f"Full text: {transcription.text}") # Advanced transcription with speaker diarization advanced_transcription = client.transcription.transcribe_from_url( url="https://youtube.com/watch?v=example123", language="en", # Optional: auto-detect if not specified model_type="premium", # "premium" / "premium-diarized"; omit for Standard accuracy enable_speaker_diarization=True, min_speakers=2, max_speakers=5, enable_word_timestamps=True, enable_confidence_scores=True, chunk_duration=1800, # 30 minutes per chunk wait_for_completion=True ) # Access speaker-separated text for segment in advanced_transcription.segments: speaker = segment.get('speaker', 'Unknown') text = segment['text'] start_time = segment['start'] end_time = segment['end'] confidence = segment.get('confidence', 0.0) print(f"[{start_time:.2f}s - {end_time:.2f}s] {speaker}: {text} (confidence: {confidence:.2f})") # Batch processing multiple URLs urls = [ "https://youtube.com/watch?v=video1", "https://youtube.com/watch?v=video2", "https://vimeo.com/123456789" ] batch_results = client.transcription.transcribe_batch_from_urls( urls=urls, enable_speaker_diarization=True, model_type="premium", wait_for_completion=True ) for i, result in enumerate(batch_results): print(f"\nVideo {i+1}: {urls[i]}") print(f"Status: {result.status}") if result.status == "completed": print(f"Text preview: {result.text[:100]}...") ``` ```javascript theme={null} const { AudioPodClient } = require('audiopod-js'); const client = new AudioPodClient(); async function transcribeYouTubeVideo() { try { // Simple transcription const transcription = await client.transcription.transcribeFromUrl({ url: "https://youtube.com/watch?v=example123", waitForCompletion: true }); console.log(`Transcription completed!`); console.log(`Duration: ${transcription.duration}s`); console.log(`Language: ${transcription.language}`); console.log(`Full text: ${transcription.text}`); // Advanced transcription with speaker diarization const advancedTranscription = await client.transcription.transcribeFromUrl({ url: "https://youtube.com/watch?v=example123", language: "en", modelType: "premium", enableSpeakerDiarization: true, minSpeakers: 2, maxSpeakers: 5, enableWordTimestamps: true, enableConfidenceScores: true, chunkDuration: 1800, waitForCompletion: true }); // Process speaker-separated segments advancedTranscription.segments.forEach(segment => { const speaker = segment.speaker || 'Unknown'; const text = segment.text; const startTime = segment.start; const endTime = segment.end; const confidence = segment.confidence || 0.0; console.log(`[${startTime.toFixed(2)}s - ${endTime.toFixed(2)}s] ${speaker}: ${text} (confidence: ${confidence.toFixed(2)})`); }); // Batch processing const urls = [ "https://youtube.com/watch?v=video1", "https://youtube.com/watch?v=video2", "https://vimeo.com/123456789" ]; const batchResults = await client.transcription.transcribeBatchFromUrls({ urls: urls, enableSpeakerDiarization: true, modelType: "premium", waitForCompletion: true }); batchResults.forEach((result, index) => { console.log(`\nVideo ${index + 1}: ${urls[index]}`); console.log(`Status: ${result.status}`); if (result.status === "completed") { console.log(`Text preview: ${result.text.substring(0, 100)}...`); } }); } catch (error) { console.error('Transcription error:', error.message); } } transcribeYouTubeVideo(); ``` ```python theme={null} import requests import time # Start transcription job response = requests.post( "https://api.audiopod.ai/api/v1/transcription/transcribe", headers={"X-API-Key": api_key}, json={ "source_urls": [ "https://youtube.com/watch?v=example123", "https://vimeo.com/123456789" ], "language": "en", # Optional: auto-detect if not specified "model_type": "premium", "enable_speaker_diarization": True, "min_speakers": 2, "max_speakers": 5, "enable_word_timestamps": True, "enable_confidence_scores": True, "chunk_duration": 1800 # 30 minutes } ) if response.status_code == 200: job_data = response.json() job_id = job_data["job_id"] print(f"Transcription job created: {job_id}") # Poll for completion while True: status_response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/status/{job_id}", headers={"X-API-Key": api_key} ) if status_response.status_code == 200: status_data = status_response.json() print(f"Status: {status_data['status']}") if status_data['status'] == 'completed': # Get final result result_response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/result/{job_id}", headers={"X-API-Key": api_key} ) if result_response.status_code == 200: result = result_response.json() print(f"Transcription text: {result['transcription']}") # Print speaker segments if diarization was enabled if 'segments' in result: for segment in result['segments']: speaker = segment.get('speaker', 'Unknown') text = segment['text'] start = segment['start'] end = segment['end'] print(f"[{start:.2f}s - {end:.2f}s] {speaker}: {text}") break elif status_data['status'] == 'failed': print(f"Transcription failed: {status_data.get('error', 'Unknown error')}") break time.sleep(10) # Wait 10 seconds before checking again ``` ```bash theme={null} # Start transcription job curl -X POST "https://api.audiopod.ai/api/v1/transcription/transcribe" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source_urls": ["https://youtube.com/watch?v=example123"], "language": "en", "model_type": "premium", "enable_speaker_diarization": true, "enable_word_timestamps": true, "enable_confidence_scores": true }' # Check job status (replace JOB_ID with actual job ID) curl -X GET "https://api.audiopod.ai/api/v1/transcription/status/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # Get transcription result when completed curl -X GET "https://api.audiopod.ai/api/v1/transcription/result/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` **Response:** ```json theme={null} { "job_id": 123, "task_id": "celery_task_uuid_here", "status": "PENDING", "message": "Transcription job created successfully", "estimated_credits": 150, "estimated_duration": 1800.0, "source_urls": [ "https://youtube.com/watch?v=example123" ] } ``` ## Transcribe from Files ### Upload Audio/Video Files Transcribe from uploaded audio or video files. ```python theme={null} from audiopod import Client import os client = Client() # Single file transcription transcription = client.transcription.transcribe_from_file( audio_file="meeting_recording.mp3", language="en", model_type="premium", enable_speaker_diarization=True, min_speakers=2, max_speakers=8, enable_word_timestamps=True, enable_confidence_scores=True, wait_for_completion=True ) print(f"Transcription completed!") print(f"File: {transcription.source_file}") print(f"Duration: {transcription.duration}s") print(f"Language: {transcription.language}") print(f"Full text: {transcription.text}") # Access detailed segments with speakers for segment in transcription.segments: speaker = segment.get('speaker', 'Unknown') text = segment['text'] start_time = segment['start'] end_time = segment['end'] confidence = segment.get('confidence', 0.0) print(f"[{start_time:.2f}s - {end_time:.2f}s] {speaker}: {text}") # Batch file transcription audio_files = [ "meeting_recording.mp3", "interview.wav", "presentation.mp4", "podcast_episode.m4a" ] batch_results = client.transcription.transcribe_batch_from_files( audio_files=audio_files, enable_speaker_diarization=True, model_type="premium", enable_word_timestamps=True, chunk_duration=1800, # 30 minutes per chunk wait_for_completion=True ) # Process results for i, result in enumerate(batch_results): print(f"\nFile {i+1}: {audio_files[i]}") print(f"Status: {result.status}") if result.status == "completed": print(f"Duration: {result.duration}s") print(f"Language: {result.language}") print(f"Text preview: {result.text[:100]}...") # Save transcription to file output_file = f"transcript_{i+1}.txt" with open(output_file, 'w', encoding='utf-8') as f: f.write(result.text) print(f"Saved to: {output_file}") # Advanced file processing with custom settings def process_interview_file(file_path): """Process interview with optimized settings""" transcription = client.transcription.transcribe_from_file( audio_file=file_path, model_type="premium", # Premium accuracy enable_speaker_diarization=True, min_speakers=2, # Interviewer + interviewee max_speakers=4, # Allow for additional participants enable_word_timestamps=True, enable_confidence_scores=True, language="auto", # Auto-detect language wait_for_completion=True ) # Generate formatted transcript formatted_output = [] current_speaker = None for segment in transcription.segments: speaker = segment.get('speaker', 'Unknown') text = segment['text'].strip() if speaker != current_speaker: formatted_output.append(f"\n{speaker}:") current_speaker = speaker formatted_output.append(f" {text}") return ''.join(formatted_output) # Process interview interview_transcript = process_interview_file("important_interview.wav") print("\nFormatted Interview Transcript:") print(interview_transcript) ``` ```javascript theme={null} const { AudioPodClient } = require('audiopod-js'); const fs = require('fs'); const path = require('path'); const client = new AudioPodClient(); async function transcribeAudioFiles() { try { // Single file transcription const transcription = await client.transcription.transcribeFromFile({ audioFile: fs.createReadStream('meeting_recording.mp3'), language: "en", modelType: "premium", enableSpeakerDiarization: true, minSpeakers: 2, maxSpeakers: 8, enableWordTimestamps: true, enableConfidenceScores: true, waitForCompletion: true }); console.log(`Transcription completed!`); console.log(`File: ${transcription.sourceFile}`); console.log(`Duration: ${transcription.duration}s`); console.log(`Language: ${transcription.language}`); console.log(`Full text: ${transcription.text}`); // Process segments with speakers transcription.segments.forEach(segment => { const speaker = segment.speaker || 'Unknown'; const text = segment.text; const startTime = segment.start; const endTime = segment.end; const confidence = segment.confidence || 0.0; console.log(`[${startTime.toFixed(2)}s - ${endTime.toFixed(2)}s] ${speaker}: ${text}`); }); // Batch processing const audioFiles = [ 'meeting_recording.mp3', 'interview.wav', 'presentation.mp4', 'podcast_episode.m4a' ]; const fileStreams = audioFiles.map(file => ({ file: fs.createReadStream(file), name: path.basename(file) })); const batchResults = await client.transcription.transcribeBatchFromFiles({ audioFiles: fileStreams, enableSpeakerDiarization: true, modelType: "premium", enableWordTimestamps: true, chunkDuration: 1800, waitForCompletion: true }); // Process batch results batchResults.forEach((result, index) => { console.log(`\nFile ${index + 1}: ${audioFiles[index]}`); console.log(`Status: ${result.status}`); if (result.status === "completed") { console.log(`Duration: ${result.duration}s`); console.log(`Language: ${result.language}`); console.log(`Text preview: ${result.text.substring(0, 100)}...`); // Save transcription to file const outputFile = `transcript_${index + 1}.txt`; fs.writeFileSync(outputFile, result.text, 'utf8'); console.log(`Saved to: ${outputFile}`); } }); // Advanced processing function for interviews async function processInterviewFile(filePath) { const transcription = await client.transcription.transcribeFromFile({ audioFile: fs.createReadStream(filePath), modelType: "premium", enableSpeakerDiarization: true, minSpeakers: 2, maxSpeakers: 4, enableWordTimestamps: true, enableConfidenceScores: true, language: "auto", waitForCompletion: true }); // Format transcript with speaker labels const formattedOutput = []; let currentSpeaker = null; transcription.segments.forEach(segment => { const speaker = segment.speaker || 'Unknown'; const text = segment.text.trim(); if (speaker !== currentSpeaker) { formattedOutput.push(`\n${speaker}:`); currentSpeaker = speaker; } formattedOutput.push(` ${text}`); }); return formattedOutput.join(''); } // Process an interview const interviewTranscript = await processInterviewFile('important_interview.wav'); console.log('\nFormatted Interview Transcript:'); console.log(interviewTranscript); } catch (error) { console.error('Transcription error:', error.message); } } transcribeAudioFiles(); ``` ```python theme={null} import requests import os # Prepare files for upload files_to_transcribe = [ "meeting_recording.mp3", "interview.wav", "presentation.mp4" ] files = [] for file_path in files_to_transcribe: if os.path.exists(file_path): files.append(('files', open(file_path, 'rb'))) # Upload and transcribe response = requests.post( "https://api.audiopod.ai/api/v1/transcription/transcribe-upload", headers={"X-API-Key": api_key}, data={ "language": "en", "model_type": "premium", "enable_speaker_diarization": True, "min_speakers": 1, "max_speakers": 10, "enable_word_timestamps": True, "enable_confidence_scores": True, "chunk_duration": 1800 }, files=files ) # Close file handles for _, file_obj in files: file_obj.close() if response.status_code == 200: job_data = response.json() job_id = job_data['job_id'] print(f"Upload transcription job: {job_id}") # Poll for completion import time while True: status_response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}", headers={"X-API-Key": api_key} ) if status_response.status_code == 200: status_data = status_response.json() print(f"Status: {status_data['status']}") if status_data['status'] == 'COMPLETED': # Get transcription result result_response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/transcript/{job_id}", headers={"X-API-Key": api_key} ) if result_response.status_code == 200: transcript_data = result_response.json() print(f"Transcription completed!") print(f"Full text: {transcript_data['transcript']}") # Save to file with open(f"transcript_{job_id}.txt", 'w', encoding='utf-8') as f: f.write(transcript_data['transcript']) print(f"Saved to transcript_{job_id}.txt") break elif status_data['status'] == 'FAILED': print(f"Transcription failed: {status_data.get('error', 'Unknown error')}") break time.sleep(15) # Check every 15 seconds else: print(f"Upload failed: {response.status_code}") print(response.text) ``` ```bash theme={null} # Upload files for transcription curl -X POST "https://api.audiopod.ai/api/v1/transcription/transcribe-upload" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "files=@meeting_recording.mp3" \ -F "files=@interview.wav" \ -F "language=en" \ -F "model_type=premium" \ -F "enable_speaker_diarization=true" \ -F "enable_word_timestamps=true" # Check job status (replace JOB_ID with actual job ID) curl -X GET "https://api.audiopod.ai/api/v1/transcription/jobs/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # Get completed transcription curl -X GET "https://api.audiopod.ai/api/v1/transcription/transcript/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` ## Job Management ### Get Transcription Status Check the progress and status of transcription jobs. ```http theme={null} GET /api/v1/transcription/jobs/{job_id} X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}", headers={"X-API-Key": api_key} ) job_status = response.json() print(f"Status: {job_status['status']}") print(f"Progress: {job_status['progress']}%") if job_status["status"] == "COMPLETED": print(f"Transcript ready! Duration: {job_status['total_duration']} seconds") print(f"Detected language: {job_status['detected_language']}") print(f"Confidence: {job_status['confidence_score']}") ``` **Response (Completed):** ```json theme={null} { "id": 123, "user_id": "550e8400-e29b-41d4-a716-446655440000", "source_urls": ["https://youtube.com/watch?v=example123"], "language": "en", "model_type": "premium", "enable_speaker_diarization": true, "min_speakers": 2, "max_speakers": 5, "status": "COMPLETED", "progress": 100, "transcript_path": "/transcripts/job_123.json", "total_duration": 1847.5, "detected_language": "en", "confidence_score": 0.92, "created_at": "2024-01-15T10:30:00Z", "completed_at": "2024-01-15T10:45:30Z", "estimated_credits": 150, "display_name": "YouTube Video Transcription" } ``` ### List Transcription Jobs Get all transcription jobs for the authenticated user. ```http theme={null} GET /api/v1/transcription/jobs?status=COMPLETED&limit=50&offset=0 X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( "https://api.audiopod.ai/api/v1/transcription/jobs", headers={"X-API-Key": api_key}, params={ "status": "COMPLETED", # Optional filter "limit": 50, "offset": 0 } ) jobs = response.json() for job in jobs: print(f"Job {job['id']}: {job['status']} - {job['total_duration']}s") ``` ## Download Transcripts ### Get Transcript in Multiple Formats Download transcripts in various formats including JSON, TXT, PDF, SRT, VTT, DOCX, and HTML. ```http theme={null} GET /api/v1/transcription/jobs/{job_id}/transcript?format=json X-API-Key: {api_key} ``` ```python theme={null} # Download as JSON with full details response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/transcript", headers={"X-API-Key": api_key}, params={"format": "json"} ) transcript_data = response.json() # Download as SRT subtitle file response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/transcript", headers={"X-API-Key": api_key}, params={"format": "srt"} ) with open("transcript.srt", "w") as f: f.write(response.text) # Download as PDF document response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/transcript", headers={"X-API-Key": api_key}, params={"format": "pdf"} ) with open("transcript.pdf", "wb") as f: f.write(response.content) ``` **JSON Response Format:** ```json theme={null} { "job_id": 123, "detected_language": "en", "confidence_score": 0.92, "total_duration": 1847.5, "segments": [ { "id": 1, "start": 0.0, "end": 4.5, "text": "Welcome to our podcast about artificial intelligence.", "confidence": 0.95, "speaker_id": 0, "speaker_label": "SPEAKER_00", "words": [ { "word": "Welcome", "start": 0.0, "end": 0.8, "probability": 0.98 }, { "word": "to", "start": 0.8, "end": 1.0, "probability": 0.99 } ] }, { "id": 2, "start": 5.0, "end": 8.2, "text": "Thank you for having me on the show.", "confidence": 0.89, "speaker_id": 1, "speaker_label": "SPEAKER_01", "words": [...] } ], "speakers": [ { "id": 0, "label": "SPEAKER_00", "total_speaking_time": 920.3 }, { "id": 1, "label": "SPEAKER_01", "total_speaking_time": 827.2 } ], "video_metadata": [ { "video_id": "example123", "title": "AI Technology Discussion", "description": "A deep dive into AI technology trends", "duration": 1847.5, "uploader": "Tech Channel", "upload_date": "20240115" } ] } ``` ## Edit Transcripts ### Get Editable Transcript Retrieve transcript in editable format for corrections. ```http theme={null} GET /api/v1/transcription/jobs/{job_id}/edit X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/edit", headers={"X-API-Key": api_key} ) editable_transcript = response.json() print(f"Found {len(editable_transcript['segments'])} segments to edit") ``` ### Update Transcript Submit edited transcript with corrections. ```http theme={null} PUT /api/v1/transcription/jobs/{job_id}/edit X-API-Key: {api_key} Content-Type: application/json { "segments": [ { "id": 1, "start": 0.0, "end": 4.5, "text": "Welcome to our podcast about artificial intelligence.", "speaker_label": "SPEAKER_00", "confidence": 0.95 } ], "edit_notes": "Corrected technical terms and speaker labels" } ``` ```python theme={null} # Get current transcript response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/edit", headers={"X-API-Key": api_key} ) transcript = response.json() segments = transcript["segments"] # Make edits segments[0]["text"] = "Welcome to our podcast about artificial intelligence." segments[0]["speaker_label"] = "Host" # Submit updates response = requests.put( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/edit", headers={"X-API-Key": api_key}, json={ "segments": segments, "edit_notes": "Corrected speaker labels and technical terms" } ) if response.status_code == 200: update_info = response.json() print(f"Updated {update_info['changes_count']} segments") ``` ### Get Transcript Versions View edit history and versions of transcripts. ```http theme={null} GET /api/v1/transcription/jobs/{job_id}/versions X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/versions", headers={"X-API-Key": api_key} ) versions = response.json() for version in versions["versions"]: print(f"Version {version['version']}: {version['edit_notes']}") print(f" Updated: {version['updated_at']}") print(f" Changes: {version['changes_count']}") ``` ## Extract Audio ### Download Extracted Audio Get clean audio files extracted from videos during transcription. ```http theme={null} GET /api/v1/transcription/jobs/{job_id}/audio/{audio_index} X-API-Key: {api_key} ``` ```python theme={null} # Download first audio file (index 0) response = requests.get( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/audio/0", headers={"X-API-Key": api_key} ) if response.status_code == 200: with open("extracted_audio.wav", "wb") as f: f.write(response.content) print("Audio file downloaded") ``` ## Delete Jobs ### Delete Transcription Job Remove transcription jobs and associated data. ```http theme={null} DELETE /api/v1/transcription/jobs/{job_id} X-API-Key: {api_key} ``` ```python theme={null} response = requests.delete( f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}", headers={"X-API-Key": api_key} ) if response.status_code == 204: print("Transcription job deleted successfully") ``` ## Supported Languages AudioPod AI supports automatic language detection or manual specification for 50+ languages: | Language | Code | Quality | Notes | | - | - | - | - | | English | `en` | Excellent | Best supported language | | Spanish | `es` | Excellent | High accuracy | | French | `fr` | Excellent | Good speaker diarization | | German | `de` | Excellent | Technical content support | | Portuguese | `pt` | Very Good | Brazilian and European | | Italian | `it` | Very Good | Good word timestamps | | Russian | `ru` | Very Good | Cyrillic text support | | Japanese | `ja` | Good | Hiragana/Katakana/Kanji | | Chinese | `zh` | Good | Simplified and Traditional | | Arabic | `ar` | Good | RTL text support | | Hindi | `hi` | Good | Devanagari script | | Korean | `ko` | Good | Hangul script | ## Model Comparison Choose an accuracy tier for your use case. **Standard** is the default — just omit `model_type`. **Premium** unlocks our highest-accuracy engine (punctuation, per-word confidence, language detection, and native speaker labels). | Accuracy tier | `model_type` value | Speaker labels | Best for | | - | - | - | - | | **Standard** (default) | *omit `model_type`* | set `enable_speaker_diarization: true` | Fast, low-cost transcription at scale | | **Premium** | `"premium"` | set `enable_speaker_diarization: true` | Highest accuracy, punctuation, confidence | | **Premium + speaker labels** | `"premium-diarized"` | built in | Interviews, meetings, podcasts | Premium accuracy is a paid feature (see [Pricing](#pricing)). Standard accuracy is available on every plan, including the free tier. ## Real-Time Streaming Need captions as the speaker talks? Stream audio over a WebSocket and receive **interim** (partial) results within a fraction of a second, followed by **final**, stabilized segments with word timestamps and optional speaker labels. ``` wss://api.audiopod.ai/api/v1/transcription/stream?api_key=ap_your_key ``` Live sessions run on **Premium accuracy** and, when finished, are saved as a normal transcription job you can fetch, edit, and export like any batch transcript. Talk to us at [support@audiopod.ai](mailto:support@audiopod.ai) if you need the streaming protocol for your integration. ## Best Practices ### Audio Quality Guidelines For best transcription results: ```python theme={null} # Recommended audio specifications audio_requirements = { "sample_rate": "16kHz or higher", "format": "WAV, MP3, M4A, or video formats", "duration": "Up to 15 hours supported", "background_noise": "Minimize for better accuracy", "speech_clarity": "Clear articulation preferred", "multiple_speakers": "Distinct voices work best" } # Chunking for long content chunking_strategy = { "chunk_duration": 1800, # 30 minutes per chunk "overlap": 30, # 30 seconds overlap "boundary_detection": "sentence_level" # Smart chunk boundaries } ``` ### Cost Optimization ```python theme={null} # Efficient transcription workflow def transcribe_efficiently(audio_files, language="auto"): # Use appropriate model based on needs model_choice = { "cost_priority": None, # omit model_type -> Standard (lowest cost) "accuracy_priority": "premium", "speaker_priority": "premium-diarized", } # Batch similar files together batch_files = group_by_language_and_type(audio_files) for batch in batch_files: job = create_transcription_job( files=batch, language=language, model_type=model_choice["accuracy_priority"], enable_speaker_diarization=True, chunk_duration=1800 # Optimal chunk size ) monitor_job_progress(job["job_id"]) ``` ## Error Handling **Causes:** - Unsupported audio format - Corrupted audio file - Audio too short **Solutions:** - Use supported formats (WAV, MP3, M4A, MP4) - Verify file integrity - Ensure minimum 10 seconds audio **Causes:** - File size exceeds limits - Too many files in single request **Solutions:** - Split large files into smaller chunks - Reduce number of files per request - Use URL transcription for large videos **Causes:** - Audio has no speech content - Extremely poor audio quality **Solutions:** - Verify audio contains speech - Improve audio quality - Try different transcription model ## Pricing Transcription pricing is based on audio duration: | Service | Cost | Description | | - | - | - | | Basic Transcription | 220 credits/minute | Text-only transcription | | With Speaker Diarization | 220 credits/minute | Speaker identification included | | With Word Timestamps | 220 credits/minute | Word-level timing data | | Transcript Editing | Free | No additional cost for edits | ### Cost Examples | Duration | Features | Credits | USD Cost | | - | - | - | - | | 10 minutes | Basic transcription | 2,200 | \$0.29 | | 30 minutes | With speakers + timestamps | 6,600 | \$0.88 | | 1 hour | Full features | 13,200 | \$1.76 | | 2 hours | Full features | 26,400 | \$3.52 | ## Next Steps Turn text into speech with a catalog or cloned voice. Identify and separate individual speakers from audio. Clean up audio for better transcription accuracy. # Stem Separation Source: https://docs.audiopod.ai/api-reference/stem-splitter Extract individual audio components from mixed recordings using AI-powered audio source separation. ## Overview AudioPod AI's Stem Separation API uses state-of-the-art AI models to separate mixed audio recordings into individual components (stems). Extract vocals, drums, bass, guitar, piano, and more from any song. ### Key Features * **Granular Separation**: `two`, `four`, `six`, plus the detailed `producer`, `studio`, and `mastering` decompositions * **Single-Instrument Isolation**: Target one instrument from a 45-instrument catalog and get it back alongside an everything-else track * **File Upload & URL Support**: Process local files or YouTube/SoundCloud URLs * **Quality Scores**: Get AI-assessed quality metrics for each stem * **Presigned Downloads**: Direct S3 URLs for fast stem downloads ## Installation ```bash theme={null} pip install audiopod ``` ```bash theme={null} npm install audiopod ``` ## Authentication Get your API key from the [API Keys Dashboard](https://www.audiopod.ai/dashboard/account/api-keys). ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Or set AUDIOPOD_API_KEY environment variable ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Or set AUDIOPOD_API_KEY environment variable ``` ```bash theme={null} export AUDIOPOD_API_KEY="ap_your_api_key" # Use -H "X-API-Key: $AUDIOPOD_API_KEY" in requests ``` ## Separation Modes Pass one of these as `mode` to `POST /api/v1/stem-extraction/api/extract`. | Mode | Output | Plan | Use Case | | - | - | - | - | | `single` | One named stem + everything-else | Basic | Vocal isolation, drum extraction | | `two` | vocals + instrumental | Basic | Karaoke tracks | | `four` | vocals, drums, bass, other | Basic | Standard remixing | | `six` | + guitar, piano | Basic | Full instrument separation | | `producer` | Drums split into kick, snare, hi-hat — up to 8 | Creator | Beat production | | `studio` | Full drum kit plus a bass frequency split — up to 12 | Pro | Professional mixing | | `mastering` | Full kit, bass split, and vocal stereo separation — up to 16 | Pro | Maximum detail | **Counts are a ceiling, not a promise.** `producer`, `studio`, and `mastering` decompose whatever is actually in the recording, so the stem count depends on the track — a song with no cymbals yields no cymbal stem. Plan for "up to N". **Plan gating.** `producer` requires **Creator** or above; `studio` and `mastering` require **Pro** or above. A request from a lower plan is rejected — call `GET /api/v1/stem-extraction/packages/available` to see what the authenticated key can use before you dispatch a job. For `mode="single"` this endpoint accepts the six core stems — `vocals`, `drums`, `bass`, `guitar`, `piano`, `other`. To reach the full instrument catalog, use the advanced endpoint below. *** ## The 45-Instrument Catalog Beyond the fixed modes, you can name **one** instrument out of a 45-entry catalog and get that instrument back plus an everything-else track. The 45 is the size of the menu you can choose from — not the number of files a job returns. Use `POST /api/v1/stem-extraction/extract/advanced` with `single_stem` set to a catalog id. The service resolves the right processing path and plan check for you, so you never pick an engine. ```bash theme={null} # Isolate the saxophone from a track curl -X POST https://api.audiopod.ai/api/v1/stem-extraction/extract/advanced \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=VIDEO_ID" \ -F "single_stem=saxophone" ``` ```bash theme={null} # Everything EXCEPT the saxophone (JSON array, one id) curl -X POST https://api.audiopod.ai/api/v1/stem-extraction/extract/advanced \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=VIDEO_ID" \ -F 'exclude_stems=["saxophone"]' ``` ### Discover the catalog at runtime Don't hard-code the list — plan gating and catalog contents change. `GET /api/v1/stem-extraction/catalog` returns every stem with the plan it needs and whether the authenticated key can use it right now: ```bash theme={null} curl -s https://api.audiopod.ai/api/v1/stem-extraction/catalog \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` Each entry carries an `id` (what you send as `single_stem`), a `label` and `description` for your UI, a `group`, the `public_tier` it requires, an `allowed` boolean for the current key, and `excludable` (whether it can be used with `exclude_stems`). ### Catalog contents **Core — available on every plan including Basic (7)** `vocals` · `backing_track` · `drums` · `bass` · `guitar` · `piano` · `other` **Specialty vocals — Creator and above (4)** `lead_vocal` · `backing_vocal` · `male_vocal` · `female_vocal` **Instruments — Creator and above (34)** `acoustic_guitar` · `electric_guitar` · `lead_guitar` · `rhythm_guitar` · `bagpipes` · `banjo` · `bassoon` · `brass` · `braam` · `celesta` · `cello` · `choir` · `clarinet` · `double_bass` · `flute` · `french_horn` · `fx` · `harp` · `harmonica` · `keys` · `mandolin` · `oboe` · `organ` · `percussion` · `plucked_strings` · `strings` · `saxophone` · `trombone` · `trumpet` · `tuba` · `viola` · `violin` · `woodwind` · `xylophone` Requesting a Creator-tier stem with a Basic key is rejected. Check `allowed` on the catalog response first, or handle the error and prompt for an upgrade. **Pricing.** Catalog isolation is metered per minute of audio like every other stem job. A few premium selections carry a multiplier — see [API Wallet & Pricing](/account/api-wallet). *** ## Quick Start ### Six-Stem Separation ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Extract and wait for completion result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="six" ) # Download stems print("Stems ready:") for stem, url in result["download_urls"].items(): print(f" {stem}: {url[:60]}...") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Extract and wait for completion const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'six' }); // Download stems console.log('Stems ready:'); for (const [stem, url] of Object.entries(result.download_urls)) { console.log(` ${stem}: ${url.substring(0, 60)}...`); } ``` ```bash theme={null} # Submit job curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=VIDEO_ID" \ -F "mode=six" # Check status (replace JOB_ID) curl -X GET "https://api.audiopod.ai/api/v1/stem-extraction/status/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Examples by Mode ### Four-Stem Separation (Standard) Extract vocals, drums, bass, and other. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="four" ) print(f"Vocals: {result['download_urls']['vocals']}") print(f"Drums: {result['download_urls']['drums']}") print(f"Bass: {result['download_urls']['bass']}") print(f"Other: {result['download_urls']['other']}") ``` ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") result = client.stems.separate( file="/path/to/song.mp3", mode="four" ) for stem, url in result["download_urls"].items(): print(f"{stem}: {url}") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'four' }); console.log(`Vocals: ${result.download_urls.vocals}`); console.log(`Drums: ${result.download_urls.drums}`); console.log(`Bass: ${result.download_urls.bass}`); console.log(`Other: ${result.download_urls.other}`); ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); const result = await client.stems.separate({ file: '/path/to/song.mp3', mode: 'four' }); for (const [stem, url] of Object.entries(result.download_urls)) { console.log(`${stem}: ${url}`); } ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=VIDEO_ID" \ -F "mode=four" ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "file=@/path/to/song.mp3" \ -F "mode=four" ``` *** ### Single Stem Extraction (Vocals Only) ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Extract only vocals result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="single", stem="vocals" # Options: vocals, drums, bass, guitar, piano, other ) print(f"Vocals: {result['download_urls']['vocals']}") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Extract only vocals const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'single', stem: 'vocals' // Options: vocals, drums, bass, guitar, piano, other }); console.log(`Vocals: ${result.download_urls.vocals}`); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=VIDEO_ID" \ -F "mode=single" \ -F "stem=vocals" ``` *** ### Two-Stem Separation (Karaoke) Separate vocals from instrumental. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="two" ) # Returns: vocals + no_vocals (instrumental) print(f"Vocals: {result['download_urls']['vocals']}") print(f"Instrumental: {result['download_urls']['no_vocals']}") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'two' }); // Returns: vocals + no_vocals (instrumental) console.log(`Vocals: ${result.download_urls.vocals}`); console.log(`Instrumental: ${result.download_urls.no_vocals}`); ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=VIDEO_ID" \ -F "mode=two" ``` *** ### Producer Mode (8 Stems with Drum Kit) ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="producer" ) # Returns: vocals, kick, snare, hihat, bass, guitar, piano, other for stem, url in result["download_urls"].items(): print(f"{stem}: {url[:60]}...") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'producer' }); // Returns: vocals, kick, snare, hihat, bass, guitar, piano, other for (const [stem, url] of Object.entries(result.download_urls)) { console.log(`${stem}: ${url.substring(0, 60)}...`); } ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=VIDEO_ID" \ -F "mode=producer" ``` *** ## Advanced Usage ### Async Job Handling For more control, use `extract()` and `wait_for_completion()` separately: ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Submit job (returns immediately) job = client.stems.extract( url="https://youtube.com/watch?v=VIDEO_ID", mode="six" ) print(f"Job submitted: {job['id']}") # Do other work... # Wait for completion when ready result = client.stems.wait_for_completion(job["id"], timeout=600) if result["status"] == "COMPLETED": print("Download URLs:") for stem, url in result["download_urls"].items(): print(f" {stem}: {url}") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Submit job (returns immediately) const job = await client.stems.extract({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'six' }); console.log(`Job submitted: ${job.id}`); // Do other work... // Wait for completion when ready const result = await client.stems.waitForCompletion(job.id, 600000); if (result.status === 'COMPLETED') { console.log('Download URLs:'); for (const [stem, url] of Object.entries(result.download_urls)) { console.log(` ${stem}: ${url}`); } } ``` ### Get Available Modes ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") modes = client.stems.modes() print("Available modes:") for m in modes["modes"]: print(f" {m['mode']}: {m['description']} ({m['stem_count']} stems)") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); const modes = await client.stems.modes(); console.log('Available modes:'); modes.modes.forEach(m => { console.log(` ${m.mode}: ${m.description} (${m.stem_count} stems)`); }); ``` ```bash theme={null} curl -X GET "https://api.audiopod.ai/api/v1/stem-extraction/modes" ``` ### Check Job Status ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") status = client.stems.status(5512) print(f"Status: {status['status']}") if status["status"] == "COMPLETED": print("Quality scores:") for stem, score in status["quality_scores"].items(): print(f" {stem}: {score:.2f}") ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); const status = await client.stems.status(5512); console.log(`Status: ${status.status}`); if (status.status === 'COMPLETED') { console.log('Quality scores:'); for (const [stem, score] of Object.entries(status.quality_scores)) { console.log(` ${stem}: ${score.toFixed(2)}`); } } ``` ```bash theme={null} curl -X GET "https://api.audiopod.ai/api/v1/stem-extraction/status/5512" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Response Format **Completed Job Response:** ```json theme={null} { "id": 5512, "status": "COMPLETED", "source_type": "URL", "stem_paths": { "vocals": "stems/5512/vocals.wav", "drums": "stems/5512/drums.wav", "bass": "stems/5512/bass.wav", "guitar": "stems/5512/guitar.wav", "piano": "stems/5512/piano.wav", "other": "stems/5512/other.wav" }, "quality_scores": { "vocals": 0.64, "drums": 0.35, "bass": 0.87, "guitar": 0.38, "piano": 0.0, "other": 0.36 }, "download_urls": { "vocals": "https://media.audiopod.ai/...", "drums": "https://media.audiopod.ai/...", "bass": "https://media.audiopod.ai/...", "guitar": "https://media.audiopod.ai/...", "piano": "https://media.audiopod.ai/...", "other": "https://media.audiopod.ai/..." }, "completed_at": "2025-12-11T12:39:37.379848" } ``` *** ## Pricing **Base rate:** \$0.10 per minute of audio processed for every standard mode — `single`, `two`, `four`, `six`, `producer`, `studio`, `mastering`. | Duration | Cost (standard modes) | | - | - | | 3 minutes | \$0.30 | | 5 minutes | \$0.50 | | 10 minutes | \$1.00 | | 1 hour | \$6.00 | ### Prices by output group Different output groups add together. Selecting one or both outputs within a vocal pair costs the same. Lead and rhythm guitar also share one specialist group. | Selected output group | Developer API wallet | Studio credits | | - | - | - | | Core vocals and instruments | \$0.10/min | 660/min | | Lead + backing vocals | \$0.15/min together | 990/min together | | Male + female vocals | \$0.15/min together | 990/min together | | Other specialist instrument groups | \$0.20/min each | 1,320/min each | All four vocal specialties total **$0.30/min on the API** or **1,980 Studio credits/min**. Adding core instruments totals **$0.40/min** or **2,640 Studio credits/min**. These are separate payment systems; API dollars are not a conversion of subscription credits. Creator unlocks specialty vocals and the instrument catalog. Pro and Studio provide larger credit allowances; detailed decomposition presets require Pro. All public plans meter usage. Read `GET /api/v1/stem-extraction/pricing` for the current group rate card. For credit-funded Studio jobs, `POST /api/v1/stem-extraction/quote` returns the measured duration, each group's price and the complete total. It does not quote API-wallet jobs. Studio quotes round duration up to whole seconds, then round the total down to whole credits; minimum charges apply. Group line items sum to that total and can differ by one credit due to allocation rounding. Review the full quote before starting. Credits are reserved at creation. Groups that fail to deliver every requested output are refunded. A missing-output retry has its own quote; it is not permission to charge again for outputs already delivered. Check your balance and usage at [API Wallet](/account/api-wallet). *** ## Error Handling ```python theme={null} from audiopod import AudioPod, InsufficientBalanceError, AuthenticationError try: client = AudioPod(api_key="ap_...") result = client.stems.separate(url="...", mode="six") except AuthenticationError: print("Invalid API key") except InsufficientBalanceError as e: print(f"Need ${e.required_cents / 100:.2f}, have ${e.available_cents / 100:.2f}") except ValueError as e: print(f"Invalid input: {e}") ``` ```typescript theme={null} import AudioPod, { InsufficientBalanceError, AuthenticationError } from 'audiopod'; try { const client = new AudioPod({ apiKey: 'ap_...' }); const result = await client.stems.separate({ url: '...', mode: 'six' }); } catch (error) { if (error instanceof AuthenticationError) { console.log('Invalid API key'); } else if (error instanceof InsufficientBalanceError) { console.log(`Need $${error.requiredCents / 100}, have $${error.availableCents / 100}`); } else { console.log(`Error: ${error.message}`); } } ``` *** ## Status Codes | Status | Description | | - | - | | `PENDING` | Job queued, waiting for worker | | `PROCESSING` | Stem separation in progress | | `COMPLETED` | Success - download URLs available | | `FAILED` | Error occurred - check error\_message | *** ## Next Steps Manage billing and view usage history. Clean up stems for better quality. # Text to Speech Source: https://docs.audiopod.ai/api-reference/text-to-speech Generate natural-sounding speech from text using AudioPod AI's unified voice generation technology with support for custom and standard voices. ## Overview AudioPod AI's Text to Speech service provides unified text-to-speech capabilities for both **standard pre-built voices** and **custom voice clones**. Generate speech with any of **200+ voices** across **200+ languages** using AudioSonic, AudioPod's text-to-speech engine. ### Supported Voice Types * **Standard Voices**: Pre-built professional voices with various characteristics * **Custom Voices**: Your own voice clones created via [Voice Management](/api-reference/voice-management) * **Unified API**: Same endpoint works for both voice types seamlessly with automatic routing ### Key Features * **200+ Voices**: Pre-built voices with unique characteristics, plus your own custom clones * **Inline Directing**: Steer emotion, delivery, non-verbal sounds, pauses, and pronunciation right inside the text — on every plan * **Word / Character Timestamps**: Follow-along timing sidecar for captions and highlighting * **Custom Voice Support**: Use your own voice clones and collections * **Variable Speed Control**: Adjust speech speed from 0.25x to 4.0x * **Multiple Formats**: MP3, WAV, OGG audio formats * **Async Processing**: Background job processing with real-time status tracking * **Credit Management**: Automatic credit reservation and billing ## Authentication All endpoints require authentication. Use one of these methods: * **API Key (Recommended)**: `X-API-Key: your_api_key` header * **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth) ## API Endpoints Quick Reference | Operation | Method | Endpoint | | - | - | - | | List voices | GET | `/api/v1/voice/voice-profiles` | | Generate TTS | POST | `/api/v1/voice/voices/{voice_id}/generate` | | Get job status | GET | `/api/v1/voice/tts-jobs/{job_id}/status` | ## Quick Start (cURL) ```bash theme={null} # 1. List available voices to get a voice UUID curl -X GET "https://api.audiopod.ai/api/v1/voice/voice-profiles?include_public=true&limit=10" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # 2. Generate speech (replace VOICE_UUID with actual UUID from step 1) curl -X POST "https://api.audiopod.ai/api/v1/voice/voices/VOICE_UUID/generate" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "input_text=Hello, this is a test." \ -d "audio_format=mp3" \ -d "speed=1.0" # Returns: {"job_id": 1234, "status": "pending", ...} # 3. Poll job status until completed curl -X GET "https://api.audiopod.ai/api/v1/voice/tts-jobs/1234/status" \ -H "X-API-Key: $AUDIOPOD_API_KEY" # Returns: {"status": "completed", "output_url": "https://...", ...} # 4. Download the audio from output_url curl -o output.mp3 "OUTPUT_URL_FROM_STEP_3" ``` ## Generate Speech ### Basic Text to Speech Generate speech from text using any voice (standard or custom) by voice UUID, ID, or name. All generation is processed asynchronously with job tracking. ```python theme={null} from audiopod import Client import requests import time # Initialize client client = Client() # Generate speech using voice UUID job = client.voice.generate_speech( voice_id="550e8400-e29b-41d4-a716-446655440000", # Voice UUID text="Hello! This is AudioPod AI generating natural speech.", audio_format="mp3", language="en", speed=1.0 ) print(f"Generation job created: {job.id}") print(f"Status: {job.status}") # Check job status until completion while True: status = client.voice.get_job_status(job.id) print(f"Job status: {status.status}") if status.status == 'completed': # Get output URL from job result if status.result and 'output_url' in status.result: audio_url = status.result['output_url'] print(f"Audio ready: {audio_url}") # Download the audio file audio_response = requests.get(audio_url) with open("generated_speech.mp3", "wb") as f: f.write(audio_response.content) print("Audio saved as generated_speech.mp3") break elif status.status == 'failed': print(f"Job failed: {status.error_message}") break time.sleep(2) # Wait 2 seconds before checking again # Alternative: Generate using voice name (for standard voices) job = client.voice.generate_speech( voice_id="aura", # Voice name text="Hello! This uses a standard voice by name.", audio_format="mp3", speed=1.0 ) ``` ```javascript theme={null} const { AudioPodClient } = require('audiopod-js'); const fs = require('fs'); // Initialize client const client = new AudioPodClient(); async function generateSpeech() { try { // Generate speech using voice UUID const job = await client.voice.generateSpeech( '550e8400-e29b-41d4-a716-446655440000', // Voice ID/UUID 'Hello! This is AudioPod AI generating natural speech.', // Text { language: 'en', audioFormat: 'mp3', generationParams: { speed: 1.0 } } ); console.log(`Generation job created: ${job.id}`); console.log(`Status: ${job.status}`); // Check job status until completion while (true) { const status = await client.voice.getJobStatus(job.id); console.log(`Job status: ${status.status}`); if (status.status === 'completed') { const audioUrl = status.output_url; console.log(`Audio ready: ${audioUrl}`); // Download the audio file const fetch = require('node-fetch'); const audioResponse = await fetch(audioUrl); const buffer = await audioResponse.buffer(); fs.writeFileSync('generated_speech.mp3', buffer); console.log('Audio saved as generated_speech.mp3'); break; } else if (status.status === 'failed') { console.log(`Job failed: ${status.error_message}`); break; } await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds } } catch (error) { console.error('Error:', error.message); } } generateSpeech(); ``` ```python theme={null} import requests import time # Create TTS job using voice UUID response = requests.post( "https://api.audiopod.ai/api/v1/voice/voices/550e8400-e29b-41d4-a716-446655440000/generate", headers={"X-API-Key": api_key}, data={ "input_text": "Hello! This is AudioPod AI generating natural speech.", "audio_format": "mp3", "speed": 1.0, "language": "en" } ) if response.status_code == 200: job_data = response.json() job_id = job_data["job_id"] print(f"Voice generation job created: {job_id}") print(f"Credits reserved: {job_data.get('credits_reserved')}") # Poll job status until completion while True: status_response = requests.get( f"https://api.audiopod.ai/api/v1/voice/tts-jobs/{job_id}/status", headers={"X-API-Key": api_key} ) if status_response.status_code == 200: status_data = status_response.json() print(f"Job status: {status_data['status']}") if status_data['status'] == 'completed': audio_url = status_data['output_url'] print(f"Audio ready: {audio_url}") # Download the audio file audio_response = requests.get(audio_url) with open("generated_speech.mp3", "wb") as f: f.write(audio_response.content) print("Audio saved as generated_speech.mp3") break elif status_data['status'] == 'failed': print(f"Job failed: {status_data.get('error_message')}") break time.sleep(2) # Wait 2 seconds before checking again ``` ```bash theme={null} # Create TTS job using voice UUID JOB_RESPONSE=$(curl -s -X POST "https://api.audiopod.ai/api/v1/voice/voices/550e8400-e29b-41d4-a716-446655440000/generate" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d 'input_text=Hello! This is AudioPod AI generating natural speech.' \ -d 'audio_format=mp3' \ -d 'speed=1.0' \ -d 'language=en') # Extract job ID JOB_ID=$(echo $JOB_RESPONSE | jq -r '.job_id') echo "Job created: $JOB_ID" # Poll job status while true; do STATUS_RESPONSE=$(curl -s -X GET "https://api.audiopod.ai/api/v1/voice/tts-jobs/$JOB_ID/status" \ -H "X-API-Key: $AUDIOPOD_API_KEY") STATUS=$(echo $STATUS_RESPONSE | jq -r '.status') echo "Job status: $STATUS" if [ "$STATUS" = "completed" ]; then AUDIO_URL=$(echo $STATUS_RESPONSE | jq -r '.output_url') echo "Audio ready: $AUDIO_URL" # Download the audio curl -o generated_speech.mp3 "$AUDIO_URL" echo "Audio saved as generated_speech.mp3" break elif [ "$STATUS" = "failed" ]; then echo "Job failed" break fi sleep 2 done ``` **Parameters** (submit as `application/x-www-form-urlencoded` form fields): * `voice_id` (required): Voice UUID, ID, or name from your voice collection (path parameter) * `input_text` (required): Text to convert to speech (max **20,000 characters** per request; may include inline directing — see below) * `audio_format` (optional): Output format - mp3, wav, ogg (default: mp3) * `speed` (optional): Speech speed 0.25-4.0 (default: 1.0) * `language` (optional): Language code - auto-detected if not provided * `generation_params` (optional): JSON string of premium-voice controls — `delivery_mode`, `timestamp_type`, `text_normalization` (see [Delivery & timestamp controls](#delivery-timestamp-controls)) **Response:** ```json theme={null} { "job_id": 12345, "status": "pending", "message": "Voice generation job created successfully", "voice_id": 123, "estimated_duration": 30, "credits_reserved": 25 } ``` ## Inline Directing Steer the performance directly inside `input_text` — no separate parameters needed. Inline directing is available on **every plan**, including the free tier. ### Emotion & delivery cues Lead a segment with a bracketed direction of one or two words. It sets the mood for the text that follows, until the next cue. ```text theme={null} [whispering, tense] Did you hear that? [excited] It's finally here! ``` ### Non-verbal sounds Drop a bracketed tag where you want a natural, non-verbal sound: ```text theme={null} That's hilarious [laugh]. Okay [sigh], let's get back to it. ``` Supported tags: `[laugh]` `[sigh]` `[clear throat]` `[breathe]` `[cough]` `[yawn]` `[chuckle]` `[gasp]` `[groan]`. ### Pauses Insert a timed silence with a self-closing break tag. Each pause is capped at **10 seconds**, and you can use up to **20 breaks per request**. ```text theme={null} Let me think about that yes, that works. ``` ### Pronunciation (inline IPA) Wrap a word's [IPA](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet) pronunciation in forward slashes to override how it's spoken: ```text theme={null} Welcome to /ˈwʊstər/ — you'd never guess it from the spelling. ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/voice/voices/VOICE_UUID/generate" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode 'input_text=[warm] Hello there [laugh]. Welcome to /ˈwʊstər/ let'\''s begin.' \ -d 'audio_format=mp3' ``` ```python theme={null} import requests directed_text = ( "[warm] Hello there [laugh]. " 'Welcome to /ˈwʊstər/ ' "let's begin." ) response = requests.post( "https://api.audiopod.ai/api/v1/voice/voices/VOICE_UUID/generate", headers={"X-API-Key": api_key}, data={"input_text": directed_text, "audio_format": "mp3"}, ) print(response.json()["job_id"]) ``` ## Delivery & timestamp controls Pass a JSON string in the `generation_params` form field to tune delivery and request a timestamp sidecar. | Key | Values | Description | | - | - | - | | `delivery_mode` | `stable`, `balanced`, `creative` | How expressive vs. consistent the read is. `stable` hugs the text closely; `creative` gives the voice more range. | | `timestamp_type` | `word`, `character` | Emit follow-along timestamps at word or character granularity. | | `text_normalization` | `true`, `false` | Expand numbers, dates, and abbreviations into spoken form. | ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/voice/voices/VOICE_UUID/generate" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode 'input_text=The meeting is at 3:30 on 04/09.' \ --data-urlencode 'generation_params={"delivery_mode":"balanced","timestamp_type":"word","text_normalization":true}' ``` ### Timestamps When you request `timestamp_type`, the completed job response includes a `timestamps_url` — a short-lived presigned URL to a sidecar file with per-word (or per-character) timing. Use it to drive live captions, karaoke-style highlighting, or word-accurate video alignment. ```json theme={null} { "id": 12345, "status": "completed", "output_url": "https://presigned-url-to-audio-file", "timestamps_url": "https://presigned-url-to-timestamps-sidecar.json", "total_duration": 4.2 } ``` Each entry pairs a token with its `start`/`end` time in seconds: ```json theme={null} { "type": "word", "tokens": [ { "text": "The", "start": 0.00, "end": 0.18 }, { "text": "meeting", "start": 0.18, "end": 0.61 } ] } ``` ### Voice Identification Examples ```python theme={null} from audiopod import Client client = Client() # Using standard pre-built voice by name job1 = client.voice.generate_speech( voice_identifier="aura", # Standard voice name input_text="Hello! This uses a standard pre-built voice.", audio_format="mp3", speed=1.0 ) print(f"Standard voice job: {job1.job_id}") # Using custom voice clone (by integer ID) job2 = client.voice.generate_speech( voice_identifier=123, # Your custom voice ID input_text="Hello! This uses my custom cloned voice.", audio_format="mp3", speed=1.0 ) print(f"Custom voice job: {job2.job_id}") # Using voice via UUID (works for both standard and custom voices) job3 = client.voice.generate_speech( voice_identifier="550e8400-e29b-41d4-a716-446655440000", # Voice UUID input_text="Hello! This uses voice identification via UUID.", audio_format="mp3", speed=1.0 ) print(f"UUID voice job: {job3.job_id}") # List available voices voices = client.voice.list_voices(include_public=True) print("Available voices:") for voice in voices: print(f"- {voice.name} (UUID: {voice.uuid}): {voice.description}") ``` ```javascript theme={null} const { AudioPodClient } = require('audiopod-js'); const client = new AudioPodClient(); async function demonstrateVoiceIdentification() { try { // Using standard pre-built voice by name const job1 = await client.voice.generateSpeech({ voiceIdentifier: 'aura', // Standard voice name inputText: 'Hello! This uses a standard pre-built voice.', audioFormat: 'mp3', speed: 1.0 }); console.log(`Standard voice job: ${job1.jobId}`); // Using custom voice clone (by integer ID) const job2 = await client.voice.generateSpeech({ voiceIdentifier: 123, // Your custom voice ID inputText: 'Hello! This uses my custom cloned voice.', audioFormat: 'mp3', speed: 1.0 }); console.log(`Custom voice job: ${job2.jobId}`); // Using voice via UUID (works for both standard and custom voices) const job3 = await client.voice.generateSpeech({ voiceIdentifier: '550e8400-e29b-41d4-a716-446655440000', // Voice UUID inputText: 'Hello! This uses voice identification via UUID.', audioFormat: 'mp3', speed: 1.0 }); console.log(`UUID voice job: ${job3.jobId}`); // List available voices const voices = await client.voice.listVoices({ includePublic: true }); console.log('Available voices:'); voices.forEach(voice => { console.log(`- ${voice.name} (UUID: ${voice.uuid}): ${voice.description}`); }); } catch (error) { console.error('Error:', error.message); } } demonstrateVoiceIdentification(); ``` ```python theme={null} import requests headers = {"X-API-Key": api_key} # Generate with a standard voice by name response1 = requests.post( "https://api.audiopod.ai/api/v1/voice/voices/aura/generate", headers=headers, data={ "input_text": "Hello! This uses a standard pre-built voice.", "audio_format": "mp3", "speed": 1.0 } ) print(f"Standard voice job: {response1.json()['job_id']}") # Generate with your custom voice by ID response2 = requests.post( "https://api.audiopod.ai/api/v1/voice/voices/123/generate", # Your custom voice ID headers=headers, data={ "input_text": "Hello! This uses my custom cloned voice.", "audio_format": "mp3", "speed": 1.0 } ) print(f"Custom voice job: {response2.json()['job_id']}") # Generate with voice using UUID response3 = requests.post( "https://api.audiopod.ai/api/v1/voice/voices/550e8400-e29b-41d4-a716-446655440000/generate", headers=headers, data={ "input_text": "Hello! This uses voice identification via UUID.", "audio_format": "mp3", "speed": 1.0 } ) print(f"UUID voice job: {response3.json()['job_id']}") # List available voices voices_response = requests.get( "https://api.audiopod.ai/api/v1/voice/voice-profiles?include_public=true", headers=headers ) voices = voices_response.json() print("Available voices:") for voice in voices: print(f"- {voice['name']} (UUID: {voice['uuid']}): {voice['description']}") ``` ## Available Voices AudioPod AI offers a diverse collection of voices including both standard pre-built voices and custom voice clones: ### Voice Types * **Standard Voices**: Professional pre-built voices with unique characteristics * **Custom Voices**: Your own voice clones created from audio samples * **Public Voices**: Community-shared voices available to all users ### Popular Standard Voices | Voice Name | Gender | Languages | Style | Description | | - | - | - | - | - | | `aura` | Female | 60+ | Bright | Luminous voice with crystal-clear delivery | | `jester` | Male | 60+ | Playful | Upbeat voice with theatrical flair | | `sage` | Male | 60+ | Wise | Authoritative voice perfect for narration | | `ava` | Female | 60+ | Professional | Commanding voice for business content | | `surge` | Male | 60+ | Energetic | High-energy voice for exciting content | | `willow` | Female | 60+ | Gentle | Delicate, youthful voice with elegance | ### Listing Available Voices Use the voice profiles endpoint to discover all available voices: ```python theme={null} # List all voices (including your custom voices) voices = client.voice.list_voices(include_public=True) # Filter by voice type standard_voices = [v for v in voices if v.voice_type == "standard"] custom_voices = [v for v in voices if v.voice_type == "custom"] print(f"Found {len(standard_voices)} standard voices") print(f"Found {len(custom_voices)} custom voices") ``` ## Job Status Tracking ### Check Job Status Monitor your text-to-speech generation jobs with real-time status updates: ```python theme={null} # Check job status status = client.voice.get_job_status(job_id) print(f"Job {job_id} status: {status['status']}") print(f"Progress: {status.get('progress', 'N/A')}%") if status['status'] == 'completed': print(f"Audio ready: {status['output_url']}") print(f"Duration: {status.get('total_duration')} seconds") elif status['status'] == 'failed': print(f"Error: {status.get('error_message')}") ``` ### Job Status Response ```json theme={null} { "id": 12345, "job_type": "single", "user_id": "user-uuid", "status": "completed", "progress": 100, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:45Z", "completed_at": "2024-01-15T10:30:45Z", "output_path": "generated/12345.mp3", "output_url": "https://presigned-url-to-audio-file", "input_text": "Hello! This is AudioPod AI generating natural speech.", "target_language": "en", "voice_info": { "id": 123, "name": "aura", "display_name": "Aura" }, "estimated_duration": 30, "credits_reserved": 25 } ``` ### Status Values * `pending`: Job created and waiting for processing * `processing`: Currently generating audio * `completed`: Audio generation finished successfully * `failed`: Generation failed with error ## Configuration Options ### Audio Format Options | Parameter | Options | Description | Notes | | - | - | - | - | | `audio_format` | `mp3`, `wav`, `ogg` | Audio file format | MP3 recommended for most uses | | `speed` | `0.25` to `4.0` | Speech speed | 1.0 = normal speed | | `language` | Language codes | Target language | Auto-detected if not specified | ### Basic Voice Customization ```python theme={null} # Generate with custom settings job = client.voice.generate_speech( voice_identifier="aura", input_text="Customized voice output with speed control", audio_format="wav", # High quality WAV format speed=1.1, # 10% faster than normal language="en" # English language ) ``` ### Language Detection AudioPod AI automatically detects the language of your input text, but you can specify it explicitly for better results: ```python theme={null} # Automatic language detection job1 = client.voice.generate_speech( voice_identifier="aura", input_text="Hello, how are you today?" # Language will be auto-detected as English ) # Explicit language specification job2 = client.voice.generate_speech( voice_identifier="aura", input_text="Bonjour, comment allez-vous?", language="fr" # Specify French ) ``` ## Multi-Language Support ### Language Codes AudioPod AI supports 100+ languages with automatic detection and consistent quality across all supported languages: | Language | Code | Description | | - | - | - | | English (US) | `en-US` | American English | | English (UK) | `en-GB` | British English | | Spanish | `es` | Spanish (Spain) | | Spanish (Mexico) | `es-MX` | Mexican Spanish | | French | `fr` | French (France) | | German | `de` | German | | Chinese (Simplified) | `zh-CN` | Simplified Chinese (Mandarin) | | Chinese (Traditional) | `zh-TW` | Traditional Chinese | | Japanese | `ja` | Japanese | | Korean | `ko` | Korean | | Portuguese | `pt` | Portuguese (Portugal) | | Portuguese (Brazil) | `pt-BR` | Brazilian Portuguese | | Russian | `ru` | Russian | | Arabic | `ar` | Arabic | | Hindi | `hi` | Hindi | | Italian | `it` | Italian | | Dutch | `nl` | Dutch | | Polish | `pl` | Polish | | Turkish | `tr` | Turkish | | Swedish | `sv` | Swedish | ### Multi-Language Example ```python theme={null} # Generate speech in multiple languages using the same voice languages = [ {"text": "Hello, welcome to AudioPod AI", "lang": "en-US"}, {"text": "Hola, bienvenido a AudioPod AI", "lang": "es"}, {"text": "Bonjour, bienvenue chez AudioPod AI", "lang": "fr"}, {"text": "Hallo, willkommen bei AudioPod AI", "lang": "de"} ] voice_uuid = "550e8400-e29b-41d4-a716-446655440000" # Your multi-language voice for item in languages: job = client.voice.generate_speech( voice_identifier=voice_uuid, input_text=item["text"], language=item["lang"], audio_format="mp3" ) # Wait for completion and download while True: status = client.voice.get_job_status(job.job_id) if status['status'] == 'completed': # Download audio audio_response = requests.get(status['output_url']) filename = f"welcome_{item['lang']}.mp3" with open(filename, "wb") as f: f.write(audio_response.content) print(f"Generated {filename}") break elif status['status'] == 'failed': print(f"Failed to generate {item['lang']}: {status.get('error_message')}") break time.sleep(2) ``` ### Get Supported Languages for a Voice ```python theme={null} # Get supported languages for a specific voice supported_languages = client.voice.get_supported_languages(voice_identifier="aura") print(f"Voice supports {len(supported_languages)} languages:") for code, name in supported_languages.items(): print(f"- {code}: {name}") ``` ## Use Cases & Examples ### Audiobook Narration ```python theme={null} from audiopod import Client import re client = Client() def split_text_by_sentences(text, max_length=2000): """Split text into chunks by sentences, respecting max length""" sentences = re.split(r'[.!?]+', text) chunks = [] current_chunk = "" for sentence in sentences: if len(current_chunk + sentence) > max_length: if current_chunk: chunks.append(current_chunk.strip()) current_chunk = sentence else: current_chunk += sentence + ". " if current_chunk: chunks.append(current_chunk.strip()) return chunks def create_audiobook_chapter(chapter_text, narrator_voice="sage"): """Create audiobook chapter with professional narration""" # Split long text into manageable chunks chunks = split_text_by_sentences(chapter_text, max_length=2000) audio_urls = [] for i, chunk in enumerate(chunks): print(f"Processing chunk {i+1}/{len(chunks)}: {chunk[:50]}...") result = client.voice.generate_speech( voice_id=narrator_voice, text=chunk, audio_format="wav", # High quality for audiobooks speed=0.95, # Slightly slower for audiobooks wait_for_completion=True ) audio_urls.append(result.output_url) print(f"Generated audio for chunk {i+1}") return audio_urls # Example usage chapter_text = """ Chapter 1: The Beginning It was the best of times, it was the worst of times. The era was filled with contradictions that would shape the destiny of nations. In this tumultuous period, heroes would rise and fall, love would conquer fear, and the very fabric of society would be tested by forces beyond imagination. As dawn broke over the ancient city, our protagonist began a journey that would change everything they thought they knew about the world. """ audio_urls = create_audiobook_chapter(chapter_text, "sage") print(f"Audiobook chapter complete! Generated {len(audio_urls)} audio segments.") ``` ```javascript theme={null} const { AudioPodClient } = require('audiopod-js'); const client = new AudioPodClient(); function splitTextBySentences(text, maxLength = 2000) { const sentences = text.split(/[.!?]+/); const chunks = []; let currentChunk = ''; for (const sentence of sentences) { if ((currentChunk + sentence).length > maxLength) { if (currentChunk) { chunks.push(currentChunk.trim()); } currentChunk = sentence; } else { currentChunk += sentence + '. '; } } if (currentChunk) { chunks.push(currentChunk.trim()); } return chunks; } async function createAudiobookChapter(chapterText, narratorVoice = 'sage') { try { // Split long text into manageable chunks const chunks = splitTextBySentences(chapterText, 2000); const audioUrls = []; for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; console.log(`Processing chunk ${i+1}/${chunks.length}: ${chunk.substring(0, 50)}...`); const result = await client.voice.generateSpeech({ voiceId: narratorVoice, text: chunk, audioFormat: 'wav', // High quality for audiobooks speed: 0.95, // Slightly slower for audiobooks waitForCompletion: true }); audioUrls.push(result.outputUrl); console.log(`Generated audio for chunk ${i+1}`); } return audioUrls; } catch (error) { console.error('Error creating audiobook:', error.message); throw error; } } // Example usage const chapterText = ` Chapter 1: The Beginning It was the best of times, it was the worst of times. The era was filled with contradictions that would shape the destiny of nations. In this tumultuous period, heroes would rise and fall, love would conquer fear, and the very fabric of society would be tested by forces beyond imagination. As dawn broke over the ancient city, our protagonist began a journey that would change everything they thought they knew about the world. `; createAudiobookChapter(chapterText, 'sage') .then(audioUrls => { console.log(`Audiobook chapter complete! Generated ${audioUrls.length} audio segments.`); audioUrls.forEach((url, index) => { console.log(`Segment ${index + 1}: ${url}`); }); }) .catch(error => { console.error('Failed to create audiobook:', error); }); ``` ```python theme={null} from audiopod import Client import asyncio import aiohttp import json client = Client() async def create_audiobook_chapter_parallel(chapter_text, narrator_voice="sage"): """Create audiobook with parallel processing for faster generation""" chunks = split_text_by_sentences(chapter_text, max_length=2000) # Create all jobs first (parallel creation) jobs = [] for chunk in chunks: job = client.voice.generate_speech( voice_id=narrator_voice, text=chunk, audio_format="wav", speed=0.95, wait_for_completion=False # Don't wait, create jobs in parallel ) jobs.append(job) print(f"Created {len(jobs)} generation jobs") # Wait for all jobs to complete audio_urls = [] for i, job in enumerate(jobs): print(f"Waiting for job {i+1}/{len(jobs)}") # Poll job status while True: status = client.voice.get_job_status(job.id) if status.status == 'completed': audio_urls.append(status.output_url) break elif status.status == 'failed': print(f"Job {job.id} failed: {status.error_message}") break await asyncio.sleep(2) return audio_urls # Example with error handling and progress tracking def create_audiobook_with_progress(chapter_text, narrator_voice="sage"): """Create audiobook with detailed progress tracking""" chunks = split_text_by_sentences(chapter_text, max_length=2000) total_chunks = len(chunks) completed_chunks = 0 audio_urls = [] print(f"Starting audiobook generation: {total_chunks} chunks") for i, chunk in enumerate(chunks): try: print(f"[{i+1}/{total_chunks}] Processing: {chunk[:50]}...") result = client.voice.generate_speech( voice_id=narrator_voice, text=chunk, audio_format="wav", speed=0.95, wait_for_completion=True ) audio_urls.append(result.output_url) completed_chunks += 1 progress = (completed_chunks / total_chunks) * 100 print(f"[{i+1}/{total_chunks}] ✅ Complete ({progress:.1f}%)") except Exception as e: print(f"[{i+1}/{total_chunks}] ❌ Failed: {e}") # You could implement retry logic here print(f"Audiobook generation complete! {completed_chunks}/{total_chunks} chunks successful") return audio_urls ``` ### Podcast Introduction Direct the delivery with **inline markup** — a leading `[direction]`, timed `` pauses, and (optionally) `generation_params`. No SSML. ```python theme={null} intro_script = ( "[speak with warm upbeat energy] Welcome back to the Tech Talk Podcast! " ' ' "Today we're diving deep into artificial intelligence and its impact on " 'content creation. Let\'s get started!' ) response = requests.post( "https://api.audiopod.ai/api/v1/voice/voices/ava/generate", headers={"Authorization": f"Bearer {API_KEY}"}, data={ "input_text": intro_script, "audio_format": "mp3", # Expressive read; see "Delivery & timestamp controls" above. "generation_params": '{"delivery_mode":"creative"}', }, ) job_id = response.json()["job_id"] # poll to COMPLETED, then download audio_url ``` ### E-Learning Content ```python theme={null} def create_lesson_audio(lesson_content): # Direct the pacing with a leading note + timed pauses (not SSML). directed = ( "[speak clearly and calmly at a measured pace] Lesson begins now. " ' ' f"{lesson_content} " ' ' "That concludes this lesson. Take a moment to review what you've learned." ) return requests.post( "https://api.audiopod.ai/api/v1/voice/voices/sage/generate", headers={"Authorization": f"Bearer {API_KEY}"}, data={ "input_text": directed, "audio_format": "mp3", "generation_params": '{"delivery_mode":"stable"}', }, ) ``` ### Interactive Voice Response (IVR) ```python theme={null} ivr_prompts = { "welcome": "Thank you for calling AudioPod AI. Your call is important to us.", "menu": "Press 1 for sales, 2 for support, or 3 for billing.", "hold": "Please hold while we connect you to the next available agent.", } for prompt_name, text in ivr_prompts.items(): response = requests.post( "https://api.audiopod.ai/api/v1/voice/voices/ava/generate", headers={"Authorization": f"Bearer {API_KEY}"}, data={"input_text": text, "audio_format": "wav"}, ) # Poll response.json()["job_id"] to COMPLETED, then download its audio_url. ``` ## Best Practices ### Text Optimization **✅ Good Practices:** * Use proper punctuation for natural pauses * Write numbers in word form for better pronunciation * Include context for abbreviations * Break long sentences into shorter ones **❌ Common Issues:** * ALL CAPS TEXT (sounds like shouting) * Missing punctuation (unnatural flow) * Technical jargon without context * Extremely long paragraphs ### Cost Optimization ```python theme={null} # Efficient: Single request for multiple sentences long_text = "First sentence. Second sentence. Third sentence." job = client.voice.generate_speech( voice_identifier="aura", input_text=long_text, audio_format="mp3" ) # Inefficient: Multiple requests for short texts # This creates multiple jobs and uses more credits due to per-job overhead sentences = ["First sentence.", "Second sentence.", "Third sentence."] for sentence in sentences: job = client.voice.generate_speech( voice_identifier="aura", input_text=sentence, audio_format="mp3" ) ``` ### Caching Strategy ```python theme={null} import hashlib import os import requests import time def get_cached_audio(text, voice_identifier, cache_dir="audio_cache"): # Create cache key from text and voice identifier cache_key = hashlib.md5(f"{text}_{voice_identifier}".encode()).hexdigest() cache_file = os.path.join(cache_dir, f"{cache_key}.mp3") if os.path.exists(cache_file): with open(cache_file, "rb") as f: return f.read() # Generate new audio using job-based API job = client.voice.generate_speech( voice_identifier=voice_identifier, input_text=text, audio_format="mp3" ) # Wait for completion while True: status = client.voice.get_job_status(job.job_id) if status['status'] == 'completed': # Download audio audio_response = requests.get(status['output_url']) audio_data = audio_response.content # Cache the result os.makedirs(cache_dir, exist_ok=True) with open(cache_file, "wb") as f: f.write(audio_data) return audio_data elif status['status'] == 'failed': raise Exception(f"Audio generation failed: {status.get('error_message')}") time.sleep(2) ``` ## Error Handling ### Common Errors and Solutions **Causes:** - Text too long (>20,000 characters) - Invalid characters or encoding - Empty text field - Malformed `generation_params` JSON **Solutions:** - Split long text into chunks - Check text encoding (UTF-8) - Validate text is not empty - Verify `generation_params` is a valid JSON object **Causes:** - Voice identifier doesn't exist - Voice not accessible by user * Voice UUID format invalid **Solutions:** - Check available voices with voice profiles endpoint - Verify voice UUID format - Ensure voice is public or owned by user **Causes:** - Not enough credits for audio generation - Credit limit exceeded **Solutions:** - Check credit balance - Purchase more credits - Wait for credit reset **Causes:** - Rate limit exceeded - Too many concurrent requests **Solutions:** - Implement exponential backoff - Use request queuing - Upgrade to higher rate limits ### Robust Error Handling ```python theme={null} import time import random import requests from requests.exceptions import HTTPError def generate_speech_with_retry(text, voice_identifier, max_retries=3): for attempt in range(max_retries): try: # Create generation job job = client.voice.generate_speech( voice_identifier=voice_identifier, input_text=text, audio_format="mp3" ) # Monitor job with retry logic for status checks while True: try: status = client.voice.get_job_status(job.job_id) if status['status'] == 'completed': # Download and return audio audio_response = requests.get(status['output_url']) audio_response.raise_for_status() return audio_response.content elif status['status'] == 'failed': raise Exception(f"Job failed: {status.get('error_message')}") time.sleep(2) # Wait before checking again except HTTPError as e: if e.response.status_code == 429: # Rate limit on status check - wait longer time.sleep(5) continue else: raise except HTTPError as e: if e.response.status_code == 429: # Rate limit # Exponential backoff with jitter delay = (2 ** attempt) + random.uniform(0, 1) print(f"Rate limited, retrying in {delay:.1f}s (attempt {attempt + 1})") time.sleep(delay) continue elif e.response.status_code == 400: # Bad request - don't retry print(f"Bad request: {e.response.text}") raise e elif e.response.status_code == 402: # Insufficient credits - don't retry print("Insufficient credits") raise e elif e.response.status_code == 404: # Voice not found - don't retry print(f"Voice not found: {voice_identifier}") raise e else: # Other errors - retry with delay print(f"Error {e.response.status_code}, retrying...") time.sleep(1) continue except Exception as e: print(f"Unexpected error: {e}") time.sleep(1) continue raise Exception("Max retries exceeded") # Usage example try: audio_data = generate_speech_with_retry( text="Hello, this is a test with retry logic.", voice_identifier="aura" ) with open("output.mp3", "wb") as f: f.write(audio_data) print("Audio generated successfully with retry logic") except Exception as e: print(f"Failed to generate audio: {e}") ``` ## Pricing Text to Speech is billed differently depending on how you authenticate: | Path | Rate | Basis | | - | - | - | | **Account credits** (subscription / JWT) | **0.5 credits per character** | Input character count, charged before synthesis | | **API wallet** (API key, USD) | **\$0.05 per 1,000 characters** | Input character count, prepaid; minimum \$0.01 per request | All voice types (standard and custom) use the same rate. Quality settings do not affect pricing. ### Cost Examples — account credits (per character) | Text Length | Credits Used | | - | - | | 100 characters | 50 credits | | 500 characters | 250 credits | | 1,000 characters | 500 credits | | 5,000 characters | 2,500 credits | ### Cost Examples — API wallet (per character) | Text Length | USD Cost | | - | - | | 100 characters | \$0.01 (minimum) | | 1,000 characters | \$0.05 | | 10,000 characters | \$0.50 | | 100,000 characters | \$5.00 | Directing/steering tags and pause tags are not counted; non-verbal tags such as `[laugh]` are. See [How characters are counted](/account/api-wallet#how-characters-are-counted). ## Next Steps Create custom voices from audio samples. Listen to voice samples and find your perfect voice. # Voice Changer Source: https://docs.audiopod.ai/api-reference/voice-changer Transform voice characteristics in audio recordings using AudioPod AI's advanced voice-to-voice conversion technology. ## Overview AudioPod AI's Voice Changer API converts source audio to match target voice characteristics. Transform any speech recording to sound like a different voice while preserving the original speech content, timing, and emotional expression. ### Key Features * **Voice Conversion**: Transform source audio to match target voice characteristics * **Content Preservation**: Maintains original speech content, timing, and emotional expression * **Multiple Voice Sources**: Use any completed voice profile as a target voice * **High Quality Processing**: Advanced voice-to-voice conversion for natural-sounding results * **Public Voice Support**: Access to both user-owned and public voice profiles * **Flexible Input**: Support for various audio formats (WAV, MP3, M4A, etc.) * **Real-time Processing**: Fast conversion for production workflows ## Authentication All endpoints require authentication: * **API Key (Recommended)**: `X-API-Key: your_api_key` header * **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth) ## Voice Conversion ### Convert Audio to Target Voice Transform the voice characteristics in a source audio file to match a target voice profile. ```http theme={null} POST /api/v1/voice/voice-convert X-API-Key: {api_key} Content-Type: multipart/form-data file: (source audio file) voice_uuid: "550e8400-e29b-41d4-a716-446655440000" ``` ```python theme={null} import requests # Convert audio to match target voice with open("source_speech.wav", "rb") as audio_file: response = requests.post( "https://api.audiopod.ai/api/v1/voice/voice-convert", headers={"X-API-Key": api_key}, data={ "voice_uuid": "550e8400-e29b-41d4-a716-446655440000" # Target voice UUID }, files={"file": audio_file} ) if response.status_code == 200: conversion_job = response.json() job_id = conversion_job["id"] print(f"Voice conversion job created: {job_id}") print(f"Status: {conversion_job['status']}") ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/voice/voice-convert" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "file=@source_speech.wav" \ -F "voice_uuid=550e8400-e29b-41d4-a716-446655440000" ``` **Parameters:** * **`file`** (required): Source audio file containing speech to convert * **`voice_uuid`** (required): UUID of the target voice profile to match **Voice UUID Sources:** * User's own custom voice profiles (from voice cloning) * Public voice profiles available in the voice library * Voice must have status "COMPLETED" and available audio file **Response:** ```json theme={null} { "id": 123, "status": "PROCESSING", "progress": 0, "source_type": "FILE", "original_filename": "source_speech.wav", "target_voice_uuid": "550e8400-e29b-41d4-a716-446655440000", "target_voice_name": "Professional Speaker Voice", "created_at": "2024-01-15T10:30:00Z", "task_id": "celery_task_uuid_here", "user_id": "user_abc123def456" } ``` ## Job Management ### Get Conversion Status Monitor the progress of voice conversion jobs. ```http theme={null} GET /api/v1/voice/convert/{conversion_id}/status X-API-Key: {api_key} ``` ```python theme={null} conversion_id = 123 response = requests.get( f"https://api.audiopod.ai/api/v1/voice/convert/{conversion_id}/status", headers={"X-API-Key": api_key} ) if response.status_code == 200: conversion_status = response.json() print(f"Status: {conversion_status['status']}") if conversion_status["status"] == "COMPLETED": print("Voice conversion complete!") print(f"Output path: {conversion_status.get('output_path')}") print(f"Original filename: {conversion_status.get('original_filename')}") ``` **Response (Completed Job):** ```json theme={null} { "id": 123, "status": "COMPLETED", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:33:45Z", "completed_at": "2024-01-15T10:33:45Z", "output_path": "outputs/converted/uuid/voice_converted.wav", "error_message": null, "task_id": "celery_task_uuid_here", "source_path": "temp/uploads/uuid/source_speech.wav", "original_filename": "source_speech.wav", "conversion_metadata": { "voice_uuid": "550e8400-e29b-41d4-a716-446655440000", "voice_id": 456, "voice_file_path": "voices/456/uuid/voice.wav", "duration": 45.2 } } ``` ### List Conversion Jobs Get all voice conversion jobs for the authenticated user. ```http theme={null} GET /api/v1/voice/convert/jobs?status=COMPLETED&limit=10 X-API-Key: ``` ```python theme={null} response = requests.get( "https://api.audiopod.ai/api/v1/voice/convert/jobs", headers={"X-API-Key": api_key}, params={ "status": "COMPLETED", # Filter by status "limit": 10, "skip": 0 } ) if response.status_code == 200: jobs = response.json() for job in jobs: print(f"Job {job['id']}: {job['status']} - {job.get('original_filename', 'N/A')}") voice_uuid = job.get('conversion_metadata', {}).get('voice_uuid', 'N/A') print(f" Target voice UUID: {voice_uuid}") ``` ### Download Converted Audio ```python theme={null} def download_converted_audio(conversion_id, api_key): """Download converted audio from completed job""" # Get conversion details response = requests.get( f"https://api.audiopod.ai/api/v1/voice/convert/{conversion_id}/status", headers={"X-API-Key": api_key} ) if response.status_code != 200: return {"error": "Conversion not found"} conversion = response.json() if conversion["status"] != "COMPLETED": return {"error": f"Conversion not completed. Status: {conversion['status']}"} if not conversion.get("output_path"): return {"error": "No output file available"} # Note: The output_path is the S3 key. You would need to use your S3 service # or a separate download endpoint to get the actual file. return { "success": True, "output_path": conversion["output_path"], "original_filename": conversion.get("original_filename"), "target_voice_uuid": conversion.get("conversion_metadata", {}).get("voice_uuid"), "status": "File ready for download" } # Usage result = download_converted_audio(123, "your_api_key") if result.get("success"): print(f"Conversion completed: {result['output_path']}") print(f"Original file: {result['original_filename']}") print(f"Target voice UUID: {result['target_voice_uuid']}") ``` ## Voice Profile Management ### Find Available Voices Before converting, you can browse available voice profiles to use as targets. ```python theme={null} # Get available voices for conversion response = requests.get( "https://api.audiopod.ai/api/v1/voice/voice-profiles", headers={"X-API-Key": api_key}, params={ "voice_type": "CUSTOM", # Filter by voice type "include_public": "true", # Include public voices "limit": 20 } ) if response.status_code == 200: voices_data = response.json() print("Available voices for conversion:") for voice in voices_data["voices"]: if voice["is_custom"] or voice.get("is_public"): print(f"- {voice['name']} (UUID: {voice['voice_id']})") print(f" Category: {voice['category']}") print(f" Language: {voice['language']}") print(f" Type: {'Custom' if voice['is_custom'] else 'Public'}") print() ``` ## Use Cases & Examples ### Podcast Voice Consistency ```python theme={null} def convert_podcast_guest_to_host_voice(guest_audio, host_voice_uuid, api_key): """Convert guest audio to match host voice for consistency""" print("Converting guest audio to match host voice...") with open(guest_audio, "rb") as audio_file: response = requests.post( "https://api.audiopod.ai/api/v1/voice/voice-convert", headers={"X-API-Key": api_key}, data={"voice_uuid": host_voice_uuid}, files={"file": audio_file} ) if response.status_code != 200: return {"error": "Failed to start conversion"} job_data = response.json() job_id = job_data["id"] # Wait for completion import time while True: status_response = requests.get( f"https://api.audiopod.ai/api/v1/voice/convert/{job_id}/status", headers={"X-API-Key": api_key} ) job_status = status_response.json() print(f"Progress: {job_status['progress']}%") if job_status["status"] == "COMPLETED": break elif job_status["status"] == "FAILED": return {"error": "Conversion failed"} time.sleep(5) # Download result output_url = job_status["output_url"] converted_filename = f"guest_as_host_{job_id}.wav" audio_response = requests.get(output_url) with open(converted_filename, "wb") as f: f.write(audio_response.content) return { "success": True, "job_id": job_id, "output_file": converted_filename, "original_duration": job_status["original_duration"], "processing_time": job_status["processing_stats"]["total_time"] } # Usage result = convert_podcast_guest_to_host_voice( "guest_interview.wav", "host_voice_uuid_here", "your_api_key" ) if result.get("success"): print(f"Guest voice converted successfully!") print(f"Output: {result['output_file']}") print(f"Processing time: {result['processing_time']:.1f}s") ``` ### Content Localization ```python theme={null} def localize_content_voice(source_audio, target_locale_voices, api_key): """Convert content to different regional voice variations""" localized_versions = {} for locale, voice_uuid in target_locale_voices.items(): print(f"Creating {locale} version...") with open(source_audio, "rb") as audio_file: response = requests.post( "https://api.audiopod.ai/api/v1/voice/voice-convert", headers={"X-API-Key": api_key}, data={"voice_uuid": voice_uuid}, files={"file": audio_file} ) if response.status_code == 200: job_data = response.json() localized_versions[locale] = { "job_id": job_data["id"], "status": "processing", "target_voice": job_data["target_voice_name"] } # Monitor all jobs completed_versions = {} while len(completed_versions) < len(localized_versions): for locale, job_info in localized_versions.items(): if locale in completed_versions: continue status_response = requests.get( f"https://api.audiopod.ai/api/v1/voice/convert/{job_info['job_id']}/status", headers={"X-API-Key": api_key} ) job_status = status_response.json() if job_status["status"] == "COMPLETED": # Download localized version audio_response = requests.get(job_status["output_url"]) filename = f"content_{locale}_{job_info['job_id']}.wav" with open(filename, "wb") as f: f.write(audio_response.content) completed_versions[locale] = { "filename": filename, "target_voice": job_status["target_voice_name"], "duration": job_status["converted_duration"] } print(f"Completed {locale} version: {filename}") time.sleep(5) return completed_versions # Usage - create multiple regional versions locale_voices = { "US": "us_professional_voice_uuid", "UK": "uk_professional_voice_uuid", "AU": "au_professional_voice_uuid" } versions = localize_content_voice( "original_content.wav", locale_voices, "your_api_key" ) print("Localized versions created:") for locale, info in versions.items(): print(f" {locale}: {info['filename']} ({info['duration']:.1f}s)") ``` ### Voice Anonymization ```python theme={null} def anonymize_voice_recording(source_audio, api_key): """Convert sensitive audio to an anonymous voice for privacy""" # Use a generic public voice for anonymization anonymous_voice_uuid = "public_generic_voice_uuid" with open(source_audio, "rb") as audio_file: response = requests.post( "https://api.audiopod.ai/api/v1/voice/voice-convert", headers={"X-API-Key": api_key}, data={"voice_uuid": anonymous_voice_uuid}, files={"file": audio_file} ) job_id = response.json()["id"] # Wait for completion while True: status_response = requests.get( f"https://api.audiopod.ai/api/v1/voice/convert/{job_id}/status", headers={"X-API-Key": api_key} ) job_status = status_response.json() if job_status["status"] == "COMPLETED": break time.sleep(3) # Download anonymized audio audio_response = requests.get(job_status["output_url"]) anonymized_filename = f"anonymized_{job_id}.wav" with open(anonymized_filename, "wb") as f: f.write(audio_response.content) return { "anonymized_file": anonymized_filename, "original_content_preserved": True, "voice_anonymized": True, "duration": job_status["converted_duration"] } ``` ## Error Handling **Causes:** - Target voice UUID doesn't exist - Voice is not accessible to user - Voice is not completed or ready **Solutions:** - Verify voice UUID is correct - Ensure voice is completed and has audio file available - Use public voices or your own custom voices **Causes:** - Source audio file has no speech content - Unsupported audio format - Audio file corrupted **Solutions:** - Ensure audio contains clear speech - Use supported formats (WAV, MP3, M4A) - Verify file integrity **Causes:** - Source audio quality too poor - Incompatible voice characteristics - Processing timeout **Solutions:** - Improve source audio quality - Try different target voice - Use shorter audio segments **Causes:** - Not enough credits for conversion duration **Solutions:** - Purchase additional credits - Check credit requirements for audio duration ## Best Practices ### Audio Quality Guidelines For optimal voice conversion results: ```python theme={null} # Audio requirements for best results audio_guidelines = { "sample_rate": "16kHz or higher recommended", "format": "WAV preferred, MP3/M4A acceptable", "duration": "10 seconds to 10 minutes optimal", "content": "Clear speech without background music", "speaker": "Single speaker recommended", "noise": "Minimal background noise" } # Pre-processing recommendations preprocessing_tips = [ "Use noise reduction for noisy recordings", "Normalize audio levels for consistent volume", "Remove long silent periods to improve processing speed", "Separate speakers if multiple voices present" ] ``` ### Target Voice Selection ```python theme={null} def choose_optimal_target_voice(source_characteristics, available_voices): """Choose the best target voice for conversion""" recommendations = [] for voice in available_voices: score = 0 # Language match if voice["language"] == source_characteristics["language"]: score += 30 # Gender match (if applicable) if voice["gender"] == source_characteristics.get("gender"): score += 20 # Age range compatibility if voice["age_range"] == source_characteristics.get("age_range"): score += 15 # Style appropriateness if voice["style"] in source_characteristics.get("preferred_styles", []): score += 25 # Quality indicators if voice.get("usage_stats", {}).get("avg_rating", 0) >= 4.5: score += 10 recommendations.append({ "voice": voice, "compatibility_score": score, "reasons": [] }) # Sort by compatibility score recommendations.sort(key=lambda x: x["compatibility_score"], reverse=True) return recommendations[:5] # Top 5 recommendations ``` ## Pricing Voice conversion is billed by audio duration: | Path | Rate | Basis | | - | - | - | | **API wallet** (API key, USD) | **\$0.13 per minute** | Duration of the source audio | | **Account credits** (subscription / JWT) | **990 credits per minute** | Duration of the source audio | ### Cost Examples — API wallet (per minute) | Duration | USD Cost | | - | - | | 30 seconds | \$0.07 | | 2 minutes | \$0.26 | | 5 minutes | \$0.65 | | 10 minutes | \$1.30 | ### Cost Optimization Tips 1. **Pre-process audio** to remove silence and optimize duration 2. **Batch similar conversions** using the same target voice 3. **Test with shorter clips** before converting long content 4. **Use high-quality source audio** to avoid re-processing ## Next Steps Browse and manage available voice profiles for conversion. Extract text from converted audio for verification. # Voice Management Source: https://docs.audiopod.ai/api-reference/voice-management Manage custom voices, browse available voices, and control voice collections using AudioPod AI's Voice Management API. ## Overview AudioPod AI's Voice Management API provides comprehensive tools for managing custom and pre-built voices. Browse available voices, organize custom voice collections, preview voices, and manage voice metadata for text-to-speech and voice cloning workflows. ### Key Features * **Voice Browsing**: Explore available pre-built voices by category * **Custom Voice Creation**: Create new voice models from audio samples * **Custom Voice Management**: Organize and manage cloned voices * **Voice Collections**: Group voices into organized collections * **Voice Preview**: Generate preview samples to test voices * **Metadata Management**: Update voice names, descriptions, and tags * **Batch Operations**: Manage multiple voices efficiently ## Authentication All endpoints require authentication. Use one of these methods: * **API Key (Recommended)**: `X-API-Key: your_api_key` header * **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth) ## Voice Discovery ### List Available Voices Browse all available voices including pre-built and custom voices. ```http theme={null} GET /api/v1/voice/voice-profiles?limit=50 X-API-Key: {api_key} ``` ```python theme={null} import requests response = requests.get( "https://api.audiopod.ai/api/v1/voice/voice-profiles", headers={"X-API-Key": api_key}, params={ "limit": 50, "skip": 0 } ) if response.status_code == 200: voices_data = response.json() for voice in voices_data["voices"]: print(f"Voice: {voice['name']} ({voice['voice_id']})") print(f" Category: {voice['category']}") print(f" Language: {voice['language']}") print(f" Gender: {voice['gender']}") print(f" Preview: {voice['preview_url']}") print() ``` ```bash theme={null} curl -X GET "https://api.audiopod.ai/api/v1/voice/voice-profiles" \ -H "X-API-Key: your_api_key" ``` **Filter Parameters:** * **`voice_type`**: `CUSTOM`, `STANDARD` * **`is_public`**: `true`, `false` * **`include_public`**: `true`, `false` * **`skip`**: Number to skip for pagination * **`limit`**: Number of results to return (max 50) **Response:** ```json theme={null} { "voices": [ { "voice_id": "voice_abc123def456", "name": "Sarah Professional", "category": "professional", "language": "en", "gender": "female", "age_range": "adult", "style": "conversational", "description": "Clear, professional female voice perfect for business presentations", "preview_url": "https://api.audiopod.ai/voice-previews/sarah_professional.mp3", "is_custom": false, "created_by": null, "tags": ["business", "clear", "professional"], "usage_stats": { "total_generations": 15420, "avg_rating": 4.7 }, "supported_features": { "speed_control": true, "pitch_control": true, } }, { "voice_id": "voice_xyz789ghi012", "name": "My Custom Voice", "category": "custom", "language": "en", "gender": "male", "age_range": "adult", "style": "conversational", "description": "Custom voice cloned from my recordings", "preview_url": "https://api.audiopod.ai/voice-previews/custom_xyz789.mp3", "is_custom": true, "created_by": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-10T15:30:00Z", "tags": ["personal", "english"], "clone_job_id": 456, "training_status": "completed", "supported_features": { "speed_control": true, "pitch_control": true, } } ], "total": 127, "hasMore": true, "filters_applied": { "category": "professional", "language": "en" } } ``` ### Get Voice Details Retrieve detailed information about a specific voice. ```http theme={null} GET /api/v1/voice/voices/{voice_id}/status X-API-Key: {api_key} ``` ```python theme={null} voice_id = 123 # Voice ID is actually an integer response = requests.get( f"https://api.audiopod.ai/api/v1/voice/voices/{voice_id}/status", headers={"X-API-Key": api_key} ) if response.status_code == 200: voice = response.json() print(f"Voice Name: {voice['name']}") print(f"Description: {voice['description']}") print(f"Language: {voice['language']}") print(f"Category: {voice['category']}") if voice['is_custom']: print(f"Custom voice created: {voice['created_at']}") print(f"Training status: {voice['training_status']}") # Check supported features features = voice['supported_features'] print(f"Speed control: {features['speed_control']}") print(f"Pitch control: {features['pitch_control']}") ``` ## Create Custom Voices ### Upload Single Audio Sample Create a custom voice model from a single audio file. ```http theme={null} POST /api/v1/voice/voice-profiles X-API-Key: {api_key} Content-Type: multipart/form-data name: My Custom Voice description: Professional voice for presentations file: (audio file) denoise: true ``` ```python theme={null} import requests with open("voice_sample.wav", "rb") as audio_file: response = requests.post( "https://api.audiopod.ai/api/v1/voice/voice-profiles", headers={"X-API-Key": api_key}, data={ "name": "My Custom Voice", "description": "Professional voice for presentations", "denoise": True }, files={"file": audio_file} ) if response.status_code == 200: voice_data = response.json() voice_id = voice_data["id"] print(f"Voice created with ID: {voice_id}") ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/voice/voice-profiles" \ -H "X-API-Key: your_api_key" \ -F "name=My Custom Voice" \ -F "description=Professional voice for presentations" \ -F "denoise=true" \ -F "file=@voice_sample.wav" ``` **Response:** ```json theme={null} { "id": 123, "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "My Custom Voice", "description": "Professional voice for presentations", "voice_type": "CUSTOM", "status": "PROCESSING", "created_at": "2024-01-15T10:30:00Z", "processed_at": null, "is_public": false, "file_path": "/voices/custom/voice_123.wav" } ``` ### Upload Multiple Audio Samples Create a higher-quality voice model using multiple audio samples. ```http theme={null} POST /api/v1/voice/voice-profiles/multi-sample X-API-Key: {api_key} Content-Type: multipart/form-data name: High Quality Voice description: Multi-sample voice for better quality is_public: false voice_files: (multiple audio files) ``` ```python theme={null} voice_files = [ "sample1.wav", "sample2.wav", "sample3.wav" ] files = [] for i, file_path in enumerate(voice_files): files.append(('voice_files', open(file_path, 'rb'))) response = requests.post( "https://api.audiopod.ai/api/v1/voice/voice-profiles/multi-sample", headers={"X-API-Key": api_key}, data={ "name": "High Quality Voice", "description": "Multi-sample voice for better quality", "is_public": False }, files=files ) # Close files for _, file_obj in files: file_obj.close() ``` ### Audio Quality Requirements For best voice creation results: * **Duration**: 10-60 seconds of clean audio per sample * **Format**: WAV, MP3, or M4A * **Sample Rate**: 22kHz or higher * **Quality**: Clear speech without background noise * **Content**: Natural speech with varied intonation * **Multiple Samples**: 2-5 samples recommended for higher quality ## Instant Voice Cloning (IVC) Cloning is **instant** — a custom voice is ready to use in **seconds** from a single short reference clip. Provide **5–30 seconds** of clean speech and the returned voice can be used immediately for text-to-speech and voice conversion. ```http theme={null} POST /api/v1/voice/voice-clone X-API-Key: {api_key} Content-Type: multipart/form-data name: My Instant Clone file: (5-30s reference audio) ``` ```python theme={null} import requests with open("reference_5s.wav", "rb") as audio_file: response = requests.post( "https://api.audiopod.ai/api/v1/voice/voice-clone", headers={"X-API-Key": api_key}, data={"name": "My Instant Clone"}, files={"file": audio_file}, ) voice = response.json() print(f"Voice ready: {voice['id']} — status {voice['status']}") ``` ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/voice/voice-clone" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "name=My Instant Clone" \ -F "file=@reference_5s.wav" ``` For the highest fidelity, provide a clean 5–30s clip of a single speaker with minimal background noise. Once created, use the voice with [Text to Speech](/api-reference/text-to-speech) or [Voice Changer](/api-reference/voice-changer). ## Design a Voice Don't have a reference recording? **Design a voice from a text description.** Describe the voice you want and get back audition-ready candidate previews, then publish the one you like into a durable custom voice. Voice Design is a **paid-tier feature** (Creator and above). Each generated preview costs **250 credits**; publishing a chosen preview is free. ### Generate previews `POST /api/v1/voice/design` describes a voice and returns short presigned previews to audition. ```http theme={null} POST /api/v1/voice/design X-API-Key: {api_key} Content-Type: application/json { "design_prompt": "A warm, middle-aged British narrator with a calm, unhurried delivery", "preview_text": "This is a preview of the voice you just designed.", "num_samples": 3 } ``` ```python theme={null} import requests response = requests.post( "https://api.audiopod.ai/api/v1/voice/design", headers={"X-API-Key": api_key, "Content-Type": "application/json"}, json={ "design_prompt": "A warm, middle-aged British narrator with a calm, unhurried delivery", "preview_text": "This is a preview of the voice you just designed.", "num_samples": 3, }, ) data = response.json() print(f"Charged {data['credits_charged']} credits") for preview in data["previews"]: print(f"{preview['preview_id']}: {preview['audio_url']}") ``` **Body:** * `design_prompt` (required): Natural-language description of the voice (30–250 characters) * `preview_text` (optional): The line the previews speak (20–300 characters) * `num_samples` (optional): Number of candidate previews to generate, 1–3 (default 3). Each preview costs 250 credits * `language` (optional): BCP-47 language code; auto-detected if omitted **Response:** ```json theme={null} { "previews": [ { "preview_id": "b1f2c3d4-....", "audio_url": "https://presigned-url-to-preview.mp3", "expires_at": "2026-07-09T12:00:00Z", "preview_text": "This is a preview of the voice you just designed." } ], "credits_charged": 750 } ``` ### Publish a preview Turn a chosen preview into a permanent custom voice with `POST /api/v1/voice/design/publish`. Publishing is free and the resulting voice works everywhere a cloned voice does. ```http theme={null} POST /api/v1/voice/design/publish X-API-Key: {api_key} Content-Type: application/json { "preview_id": "b1f2c3d4-....", "name": "Calm British Narrator", "description": "Designed voice for long-form narration" } ``` ```python theme={null} response = requests.post( "https://api.audiopod.ai/api/v1/voice/design/publish", headers={"X-API-Key": api_key, "Content-Type": "application/json"}, json={ "preview_id": "b1f2c3d4-....", "name": "Calm British Narrator", "description": "Designed voice for long-form narration", }, ) voice = response.json() print(f"Published voice: {voice['id']} ({voice['name']})") ``` **Body:** * `preview_id` (required): The `preview_id` of the preview you want to keep * `name` (required): Display name for the new voice (1–100 characters) * `description` (optional): Notes about the voice ## Voice Preview ### Generate Voice Preview Create a preview sample to test how a voice sounds. ```http theme={null} GET /api/v1/voice/voice-profiles/{voice_identifier}/preview?audio_format=mp3 X-API-Key: {api_key} ``` ```python theme={null} voice_identifier = "123" # Voice ID or UUID response = requests.get( f"https://api.audiopod.ai/api/v1/voice/voice-profiles/{voice_identifier}/preview", headers={"X-API-Key": api_key}, params={ "audio_format": "mp3", "generate_if_missing": True } ) if response.status_code == 200: preview_result = response.json() print(f"Preview URL: {preview_result['audio_url']}") print(f"Duration: {preview_result['duration']} seconds") # Download preview audio audio_response = requests.get(preview_result['audio_url']) with open("voice_preview.mp3", "wb") as f: f.write(audio_response.content) print("Preview saved as voice_preview.mp3") ``` ## Custom Voice Management ### List Custom Voices Get all custom voices created by the authenticated user. ```http theme={null} GET /api/v1/voice/voice-profiles?voice_type=CUSTOM&limit=25 X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( "https://api.audiopod.ai/api/v1/voice/voice-profiles", headers={"X-API-Key": api_key}, params={ "voice_type": "CUSTOM", # Filter for custom voices only "limit": 25, "skip": 0 } ) if response.status_code == 200: custom_voices = response.json() for voice in custom_voices["voices"]: print(f"Custom Voice: {voice['name']}") print(f" Voice ID: {voice['voice_id']}") print(f" Status: {voice['training_status']}") print(f" Created: {voice['created_at']}") print(f" Clone Job: {voice['clone_job_id']}") print() ``` ### Update Custom Voice Update metadata and settings for a custom voice. ```http theme={null} PATCH /api/v1/voice/voices/{voice_id} X-API-Key: {api_key} Content-Type: application/json { "name": "Updated Voice Name", "description": "My updated custom voice description", "is_public": false } ``` ```python theme={null} voice_id = 123 # Voice ID is an integer update_data = { "name": "Updated Voice Name", "description": "My updated custom voice description", "is_public": False # Keep voice private } response = requests.patch( f"https://api.audiopod.ai/api/v1/voice/voices/{voice_id}", headers={ "X-API-Key": api_key, "Content-Type": "application/json" }, json=update_data ) if response.status_code == 200: updated_voice = response.json() print(f"Voice updated successfully:") print(f" Name: {updated_voice['name']}") print(f" Description: {updated_voice['description']}") print(f" Tags: {', '.join(updated_voice['tags'])}") ``` ### Delete Custom Voice Remove a custom voice and all associated data. ```http theme={null} DELETE /api/v1/voice/voices/{voice_id} X-API-Key: {api_key} ``` ```python theme={null} voice_id = 123 # Voice ID is an integer or an UUID response = requests.delete( f"https://api.audiopod.ai/api/v1/voice/voices/{voice_id}", headers={"X-API-Key": api_key} ) if response.status_code == 204: print("Custom voice deleted successfully") elif response.status_code == 404: print("Voice not found or already deleted") else: print(f"Error deleting voice: {response.status_code}") ``` ## Voice Collections ### Create Voice Collection Organize voices into collections for better management. ```http theme={null} POST /api/v1/voice/collections X-API-Key: {api_key} Content-Type: application/json { "name": "My Podcast Voices", "description": "Voices for podcast production", "voice_ids": ["voice_abc123", "voice_def456"], "tags": ["podcast", "production"] } ``` ```python theme={null} collection_data = { "name": "My Podcast Voices", "description": "Voices for podcast production", "voice_ids": ["voice_abc123", "voice_def456"], "tags": ["podcast", "production"], "is_public": False } response = requests.post( "https://api.audiopod.ai/api/v1/voice/collections", headers={ "X-API-Key": api_key, "Content-Type": "application/json" }, json=collection_data ) if response.status_code == 201: collection = response.json() print(f"Collection created: {collection['name']}") print(f"Collection ID: {collection['collection_id']}") print(f"Contains {len(collection['voice_ids'])} voices") ``` ### List Voice Collections Get all voice collections for the authenticated user. ```http theme={null} GET /api/v1/voice/collections?limit=25 X-API-Key: {api_key} ``` ```python theme={null} response = requests.get( "https://api.audiopod.ai/api/v1/voice/collections", headers={"X-API-Key": api_key}, params={"limit": 25, "skip": 0} ) if response.status_code == 200: collections = response.json() for collection in collections["collections"]: print(f"Collection: {collection['name']}") print(f" Description: {collection['description']}") print(f" Voices: {len(collection['voice_ids'])}") print(f" Created: {collection['created_at']}") print() ``` ### Add Voices to Collection Add voices to an existing collection. ```http theme={null} POST /api/v1/voice/collections/{collection_id}/voices X-API-Key: {api_key} Content-Type: application/json { "voice_ids": ["voice_ghi789", "voice_jkl012"] } ``` ```python theme={null} collection_id = "collection_abc123" new_voices = { "voice_ids": ["voice_ghi789", "voice_jkl012"] } response = requests.post( f"https://api.audiopod.ai/api/v1/voice/collections/{collection_id}/voices", headers={ "X-API-Key": api_key, "Content-Type": "application/json" }, json=new_voices ) if response.status_code == 200: updated_collection = response.json() print(f"Added voices to collection") print(f"Collection now has {len(updated_collection['voice_ids'])} voices") ``` ## Error Handling **Causes:** - Voice ID doesn't exist - Voice was deleted - Access denied to private voice **Solutions:** - Verify voice ID is correct - Check if voice still exists - Ensure you have access permissions **Causes:** - Invalid filter parameters - Malformed voice collection data - Invalid preview text **Solutions:** - Check parameter values - Validate collection structure - Ensure preview text is not empty **Causes:** - Trying to modify another user's custom voice - Accessing private voice without permission **Solutions:** - Only modify your own custom voices - Use public voices or get proper access **Causes:** - Not enough credits for voice preview generation **Solutions:** - Purchase additional credits - Check credit requirements Returned when creating a custom voice would exceed your plan's voice slots. Free/Basic plans include **3 custom voice slots**; Creator, Pro, and Studio plans have **unlimited** custom voices. Existing voices are never removed — deleting a voice frees a slot. ```json theme={null} { "detail": { "code": "CUSTOM_VOICE_LIMIT_REACHED", "message": "You've used 3 of 3 custom voices on the Basic plan. Delete a voice or upgrade for more.", "feature": "custom_voices", "current_plan": "basic", "limit": 3, "used": 3 } } ``` **Solutions:** - Delete an unused custom voice to free a slot - Upgrade to a paid plan for unlimited custom voices Check your current usage with `GET /api/v1/voice/voice-profiles/limits`: ```json theme={null} { "used": 3, "limit": 3, "unlimited": false, "plan": "basic" } ``` `limit` is `-1` and `unlimited` is `true` on plans without a cap. ## Use Cases & Examples ### Voice Selection Assistant ```python theme={null} def find_best_voice_for_content(content_type, language, gender_preference, api_key): """Help users find the best voice for their content""" # Define voice selection criteria based on content type criteria_map = { "podcast": {"category": "professional", "style": "conversational"}, "audiobook": {"category": "professional", "style": "narrative"}, "assistant": {"category": "professional", "style": "assistant"}, "character": {"category": "character", "style": "character"}, "casual": {"category": "casual", "style": "conversational"} } criteria = criteria_map.get(content_type, {"category": "professional"}) # Search for matching voices params = { "language": language, "limit": 10 } if gender_preference: params["gender"] = gender_preference if "category" in criteria: params["category"] = criteria["category"] if "style" in criteria: params["style"] = criteria["style"] response = requests.get( "https://api.audiopod.ai/api/v1/voice/voice-profiles", headers={"X-API-Key": api_key}, params=params ) if response.status_code != 200: return {"error": "Failed to fetch voices"} voices_data = response.json() voices = voices_data["voices"] # Rank voices by suitability ranked_voices = [] for voice in voices: score = 0 # Prefer voices with higher ratings if voice.get("usage_stats", {}).get("avg_rating"): score += voice["usage_stats"]["avg_rating"] * 10 # Prefer voices with proven usage if voice.get("usage_stats", {}).get("total_generations", 0) > 1000: score += 20 # Prefer exact style matches if voice.get("style") == criteria.get("style"): score += 30 ranked_voices.append({ "voice": voice, "suitability_score": score }) # Sort by score ranked_voices.sort(key=lambda x: x["suitability_score"], reverse=True) return { "content_type": content_type, "language": language, "criteria": criteria, "recommended_voices": ranked_voices[:5], # Top 5 recommendations "total_matches": len(voices) } # Usage recommendations = find_best_voice_for_content( content_type="podcast", language="en", gender_preference="female", api_key="your_api_key" ) print(f"Top recommendations for {recommendations['content_type']} in {recommendations['language']}:") for i, rec in enumerate(recommendations["recommended_voices"], 1): voice = rec["voice"] print(f"{i}. {voice['name']} (Score: {rec['suitability_score']:.1f})") print(f" Category: {voice['category']}, Style: {voice.get('style', 'N/A')}") print(f" Rating: {voice.get('usage_stats', {}).get('avg_rating', 'N/A')}") print() ``` ### Voice Collection Manager ```python theme={null} def organize_voices_by_project(voices_to_organize, api_key): """Organize voices into project-based collections""" # Group voices by characteristics for project organization voice_groups = { "narrative_voices": [], "character_voices": [], "professional_voices": [], "multilingual_voices": [] } for voice_id in voices_to_organize: # Get voice details response = requests.get( f"https://api.audiopod.ai/api/v1/voice/voices/{voice_id}", headers={"X-API-Key": api_key} ) if response.status_code == 200: voice = response.json() # Categorize voice style = voice.get("style", "") category = voice.get("category", "") if style == "narrative" or "audiobook" in voice.get("tags", []): voice_groups["narrative_voices"].append(voice_id) elif category == "character" or style == "character": voice_groups["character_voices"].append(voice_id) elif category == "professional": voice_groups["professional_voices"].append(voice_id) # Check if multilingual if voice.get("language") != "en" or "multilingual" in voice.get("tags", []): voice_groups["multilingual_voices"].append(voice_id) # Create collections for non-empty groups created_collections = [] collection_configs = { "narrative_voices": { "name": "Narrative & Audiobook Voices", "description": "Voices optimized for storytelling and long-form content" }, "character_voices": { "name": "Character & Creative Voices", "description": "Unique voices for character work and creative projects" }, "professional_voices": { "name": "Professional & Business Voices", "description": "Clear, professional voices for business content" }, "multilingual_voices": { "name": "International & Multilingual Voices", "description": "Voices for international content and multiple languages" } } for group_name, voice_ids in voice_groups.items(): if voice_ids: # Only create collection if there are voices config = collection_configs[group_name] collection_data = { "name": config["name"], "description": config["description"], "voice_ids": voice_ids, "tags": [group_name.replace("_", "-"), "auto-organized"] } response = requests.post( "https://api.audiopod.ai/api/v1/voice/collections", headers={ "X-API-Key": api_key, "Content-Type": "application/json" }, json=collection_data ) if response.status_code == 201: collection = response.json() created_collections.append({ "collection_id": collection["collection_id"], "name": collection["name"], "voice_count": len(voice_ids), "voices": voice_ids }) return { "organized": True, "collections_created": len(created_collections), "collections": created_collections, "total_voices_organized": sum(len(group) for group in voice_groups.values()) } # Usage voice_list = ["voice_abc123", "voice_def456", "voice_ghi789", "voice_jkl012"] organization_result = organize_voices_by_project(voice_list, "your_api_key") print(f"Organized {organization_result['total_voices_organized']} voices into {organization_result['collections_created']} collections:") for collection in organization_result["collections"]: print(f" {collection['name']}: {collection['voice_count']} voices") ``` ## Best Practices ### Voice Selection Guidelines ```python theme={null} # Guidelines for choosing the right voice for different use cases voice_selection_guide = { "content_types": { "podcast": { "recommended_categories": ["professional", "casual"], "preferred_styles": ["conversational"], "features_needed": ["speed_control"], "avoid": ["character", "synthetic"] }, "audiobook": { "recommended_categories": ["professional"], "preferred_styles": ["narrative"], "features_needed": ["pitch_control", "speed_control"], "duration_considerations": "Choose voices with consistent quality for long content" }, "e_learning": { "recommended_categories": ["professional"], "preferred_styles": ["assistant", "educational"], "features_needed": ["speed_control"], "clarity_priority": "High clarity and pronunciation accuracy required" }, "marketing": { "recommended_categories": ["professional", "character"], "preferred_styles": ["promotional", "conversational"], "brand_alignment": "Choose voice that matches brand personality" } }, "technical_considerations": { "processing_speed": "Pre-built voices are faster than custom voices", "customization": "Custom voices offer more personalization but require training time", "quality": "Professional category voices generally have higher quality", "cost": "Custom voices have training costs, pre-built voices are pay-per-use" } } ``` ## Pricing Voice management operations have different pricing structures: | Operation | Cost | Description | | - | - | - | | Custom Voice Creation | Free | Create new voice model from audio samples | | Voice Preview | Free | Generate preview samples (up to 50 words) | | Voice Listing | Free | Browse available voices | | Collection Management | Free | Create and manage voice collections | | Custom Voice Updates | Free | Update custom voice metadata | ## Next Steps Generate speech using both standard and custom voices. Transform existing audio to use different voice characteristics. # Audio AI Platform for Developers Source: https://docs.audiopod.ai/index AI-powered audio processing APIs. Audiobook production, stem separation, transcription, voice cloning, music generation, and more. ## Get Started in 5 Minutes Create account, get API key, make your first call. Prefer to click rather than type? Fork the **Getting Started** collection and hit a real `200` in two requests — no account, no API key ([what's in it](/sdks/postman)). [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b%26entityType%3Dcollection%26workspaceId%3D8013472d-5897-47d1-bf5c-92683a027b35\&utm_source=audiopod-docs-index) *** ## Install SDK ```bash theme={null} pip install audiopod ``` ```bash theme={null} npm install audiopod ``` No installation needed — use cURL directly with `X-API-Key` header. *** ## Hello World ```python theme={null} from audiopod import AudioPod client = AudioPod() # Uses AUDIOPOD_API_KEY env var # Separate a song into vocals, drums, bass, guitar, piano, other result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="six" ) print(result["download_urls"]) ``` *** ## What Can You Build? Manuscript to ACX-ready audiobook via API. Upload PDF/EPUB, narrate, export — with completion webhooks. Isolate vocals, drums, bass, guitar from any song — or target one instrument from a 45-instrument catalog. Clone any voice from 10 seconds of audio. Generate speech in that voice. Convert audio/video to text. Speaker diarization, 50+ languages. Generate music from text prompts. Any genre, any duration. Remove background noise from audio. Multiple quality modes. Identify and separate speakers from recordings. 150+ voices, 64+ languages. SSML support. Transform voices with AI-powered style transfer. *** ## Pricing Pay-as-you-go. No subscriptions required. Add funds to your wallet and use. | Service | Rate | | - | - | | Stem Separation | \$0.10/min | | Transcription | \$0.01/min | | Voice Cloning/TTS | \$0.05 per 1,000 characters | | Voice Conversion | \$0.13/min | | Speech Translation | \$0.40/min | | Music Generation | \$0.04/min | | Speaker Separation | \$0.20/min | | Karaoke Generation | \$0.25/min | Complete pricing for all 14+ services *** ## Resources Zero to API call in 5 minutes Full Python SDK reference Full Node.js SDK reference Complete endpoint documentation Check service health Get help from our team # Quick Start Source: https://docs.audiopod.ai/quickstart Go from zero to your first API call in under 5 minutes **TL;DR:** Register → Verify email → Create API key → Add funds → Make API calls. ## Try it first — no signup Want to hear a result before creating an account? The **sandbox** issues a short-lived key (valid \~1 hour, small built-in credit balance) so you can make a real text-to-speech call in two requests. ```bash theme={null} # 1. Mint a sandbox key (no auth required) SANDBOX_KEY=$(curl -s -X POST "https://api.audiopod.ai/api/v1/sandbox/key" | jq -r '.key') # 2. Synthesize a short snippet (max 200 characters) — returns audio bytes curl -s -X POST "https://api.audiopod.ai/api/v1/sandbox/tts" \ -H "Content-Type: application/json" \ -d "{\"key\": \"$SANDBOX_KEY\", \"text\": \"Hello from the AudioPod sandbox!\"}" \ --output sandbox.mp3 ``` ```python theme={null} import requests # 1. Mint a sandbox key (no auth required) key = requests.post("https://api.audiopod.ai/api/v1/sandbox/key").json()["key"] # 2. Synthesize a short snippet (max 200 characters) audio = requests.post( "https://api.audiopod.ai/api/v1/sandbox/tts", json={"key": key, "text": "Hello from the AudioPod sandbox!"}, ) with open("sandbox.mp3", "wb") as f: f.write(audio.content) ``` Rather not copy-paste? The same two requests are the first folder of our **Getting Started** Postman collection. Fork it and run them — it stores the sandbox key for you between the two calls. [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b%26entityType%3Dcollection%26workspaceId%3D8013472d-5897-47d1-bf5c-92683a027b35\&utm_source=audiopod-docs-quickstart) The sandbox is rate-limited and capped at 200 characters per call — it's for a first taste, not production. Ready for the full API? Continue below. ## Choose Your Path Complete setup directly with cURL commands. Login to AudioPod AI app to get your API key. *** ## Step 1: Create Your Account ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/initiate-registration" \ -H "Content-Type: application/json" \ -d '{ "email": "you@example.com", "password": "YourSecurePassword123", "full_name": "Your Name" }' ``` ✅ Check your email for a 6-digit verification code. Create your free account *** ## Step 2: Verify Your Email ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/auth/verify-registration" \ -H "Content-Type: application/json" \ -d '{ "email": "you@example.com", "verification_code": "123456" }' ``` **Response includes your JWT token:** ```json theme={null} { "access_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "bearer" } ``` Save the `access_token` — you need it for Step 3! Enter the 6-digit code from your email on the verification page. *** ## Step 3: Create Your API Key ```bash theme={null} # Use the access_token from Step 2 curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "my-first-key", "scopes": ["*"]}' ``` **Response:** ```json theme={null} { "api_key": "ap_xxxxxxxxxxxxxxxxxxxxxxxx" } ``` **Save this key immediately!** It's only shown once. 1. Go to [API Keys](https://www.audiopod.ai/dashboard/account/api-keys) 2. Click **Create New API Key** 3. Copy and save your key *** ## Step 4: Set Up Your Environment ```bash theme={null} # Add to your shell profile (~/.bashrc, ~/.zshrc) export AUDIOPOD_API_KEY="ap_your_api_key_here" ``` Or create a `.env` file: ```bash theme={null} AUDIOPOD_API_KEY=ap_your_api_key_here ``` *** ## Step 5: Add Funds to Your Wallet ```bash theme={null} # Get a Stripe payment link curl -X POST "https://api.audiopod.ai/api/v1/api-wallet/topup/checkout" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"amount_cents": 500}' ``` Open the returned `url` in your browser to complete payment. 1. Go to [API Keys](https://www.audiopod.ai/dashboard/account/api-keys) 2. Click **Add Funds** in the Wallet Balance section 3. Choose amount (min \$1) and complete Stripe checkout *** ## Step 6: Make Your First API Call! 🎉 ```bash theme={null} pip install audiopod ``` ```python theme={null} from audiopod import AudioPod client = AudioPod() # Uses AUDIOPOD_API_KEY env var # Separate a song into stems result = client.stems.separate( url="https://youtube.com/watch?v=dQw4w9WgXcQ", mode="six" ) print("🎵 Stems ready!") for stem, url in result["download_urls"].items(): print(f" {stem}: {url[:50]}...") ``` ```bash theme={null} npm install audiopod ``` ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod(); const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=dQw4w9WgXcQ', mode: 'six' }); console.log('🎵 Stems ready!'); Object.entries(result.download_urls).forEach(([stem, url]) => { console.log(` ${stem}: ${url.substring(0, 50)}...`); }); ``` ```bash theme={null} # Separate audio into 6 stems curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -F "url=https://youtube.com/watch?v=dQw4w9WgXcQ" \ -F "mode=six" # Check job status (replace JOB_ID) curl -X GET "https://api.audiopod.ai/api/v1/stem-extraction/status/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Pricing Pay only for what you use. No subscriptions required for API access. | Service | Rate | \$10 Gets You | | - | - | - | | Stem Separation | \$0.10/min | 100 min | | Transcription | \$0.01/min | 1,000 min | | Voice Cloning/TTS | \$0.05 per 1,000 characters | \~200,000 characters | | Voice Conversion | \$0.13/min | 77 min | | Speech Translation | \$0.40/min | 25 min | | Music Generation | \$0.04/min | 250 min | | Speaker Separation | \$0.20/min | 50 min | | Karaoke Generation | \$0.25/min | 40 min | View complete pricing for all services *** ## Try More Features ```python theme={null} # Convert speech to text result = client.transcription.create( url="https://youtube.com/watch?v=VIDEO_ID" ) print(result["transcript"]) ``` ```python theme={null} # Generate music from text song = client.music.generate( prompt="upbeat electronic dance music" ) print(song["output_url"]) ``` ```python theme={null} # Clone a voice from audio voice = client.voice.clone( file="./sample.wav", name="My Voice" ) # Generate speech with cloned voice audio = client.tts.generate( text="Hello world!", voice_id=voice["id"] ) ``` ```python theme={null} # Remove background noise clean = client.denoiser.denoise( file="./noisy_audio.wav" ) print(clean["output_url"]) ``` *** ## Manage Your Account (via CLI) Once you have an API key, manage everything via cURL: ```bash theme={null} # Check wallet balance curl -s "https://api.audiopod.ai/api/v1/api-wallet/balance" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq . # List your API keys curl -s "https://api.audiopod.ai/api/v1/auth/api-keys" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq . # Create another API key curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "production-key", "scopes": ["*"]}' # View usage history curl -s "https://api.audiopod.ai/api/v1/api-wallet/usage" \ -H "X-API-Key: $AUDIOPOD_API_KEY" | jq . ``` *** ## Common Issues Your API key is invalid or missing. **Fix:** Check the `X-API-Key` header is set correctly: ```bash theme={null} echo $AUDIOPOD_API_KEY # Should print your key ``` Your wallet is empty. **Fix:** Add funds via CLI or dashboard: ```bash theme={null} curl -X POST "https://api.audiopod.ai/api/v1/api-wallet/topup/checkout" \ -H "X-API-Key: $AUDIOPOD_API_KEY" \ -d '{"amount_cents": 500}' ``` Audio processing jobs can take time depending on file length. **Fix:** Poll the status endpoint: ```bash theme={null} curl "https://api.audiopod.ai/api/v1/stem-extraction/status/JOB_ID" \ -H "X-API-Key: $AUDIOPOD_API_KEY" ``` *** ## Building a startup? **AudioPod for Startups** gives eligible early-stage teams free Pro access plus API credits to build on. Apply at [audiopod.ai/startups](https://www.audiopod.ai/startups). ## Next Steps Full SDK reference for Python & Node.js Detailed endpoint documentation Complete pricing & wallet management Fork the collections instead of copy-pasting # Claude Plugin Source: https://docs.audiopod.ai/sdks/claude-plugin Use AudioPod tools inside Claude Code — transcription, stems, noise removal, audio-to-MIDI and more through skills and slash commands. **What is the AudioPod plugin?** It brings AudioPod tools into Claude Code. The plugin adds skills and slash commands that call the AudioPod [MCP server](/sdks/mcp), so you can transcribe, split stems, or turn a recording into MIDI without leaving your session. There are two plugins: `audiopod` (the main plugin) and `audiopod-studio` (the full toolkit). ## Install In Claude Code, run: ``` /plugin marketplace add AudiopodAI/claude-plugin /plugin install audiopod@audiopod ``` For the full toolkit, install `audiopod-studio` instead: ``` /plugin install audiopod-studio@audiopod ``` Install one of them, not both. Both register an MCP server named `audiopod`, so installing both would duplicate the tools. The first time Claude uses an AudioPod tool it asks you to connect. Choose Connect, or run `/mcp`, select AudioPod and choose Authenticate. A browser window opens, you sign in to your AudioPod account, review what the connection can do, and choose Allow access. There's no API key to copy. You can disconnect at any time from your AudioPod account page. ### Using an API key instead (CI or headless) [Create a key in your dashboard](https://audiopod.ai/dashboard/account/api-keys), then run: ``` claude mcp add --transport http audiopod https://mcp.audiopod.ai/core --header "X-API-Key: " ``` That is the endpoint for the main `audiopod` plugin. For `audiopod-studio`, use `https://mcp.audiopod.ai` instead. The key is added separately from the plugin, and the plugin's skills still work with it. *** ## The two plugins | Plugin | What it covers | Endpoint | | - | - | - | | `audiopod` (AudioPod) | The main plugin: transcription (speaker labels, SRT/VTT/TXT), stem separation, noise removal, speaker separation, format conversion and audio-to-MIDI. | `https://mcp.audiopod.ai/core` | | `audiopod-studio` (AudioPod Studio) | The full toolkit: everything in `audiopod` plus music, text to speech and voice tools. | `https://mcp.audiopod.ai` | *** ## Commands | `audiopod` command | `audiopod-studio` command | Example | | - | - | - | | `/audiopod:transcribe` | `/audiopod-studio:transcribe` | `/audiopod:transcribe https://example.com/interview.mp3 srt` | | `/audiopod:stems` | `/audiopod-studio:stems` | `/audiopod:stems https://example.com/song.mp3 4stem` | | `/audiopod:midi` | `/audiopod-studio:midi` | `/audiopod:midi https://example.com/melody.mp3` | | `/audiopod:status` | `/audiopod-studio:status` | `/audiopod:status transcribe` | | not available | `/audiopod-studio:tts` | `/audiopod-studio:tts "Welcome to the show" British English` | `/audiopod-studio:tts` exists only in the `audiopod-studio` plugin. You can also ask in plain language, like "remove the background noise from this clip", and the matching skill picks it up. *** ## How jobs work AudioPod jobs run asynchronously. Claude starts the job, polls its status until it finishes, and hands you the output links. Output links expire after 1 hour. If one has expired, ask again (or run `/audiopod:status ` or `/audiopod-studio:status `, since job ids are unique per tool) for fresh links. ## Credits Jobs use your AudioPod account's credits. See [pricing](https://audiopod.ai/pricing) for plans and credit details. ## Privacy Audio you send is processed on AudioPod's servers under the [AudioPod privacy policy](https://audiopod.ai/privacy). *** ## Troubleshooting | Symptom | Fix | | - | - | | Claude says AudioPod isn't connected or the connection expired | Run `/mcp`, select AudioPod and choose Authenticate. | | `402` | Your account doesn't have enough credits for the job. Top up or upgrade on the [pricing page](https://audiopod.ai/pricing). | | Tools not showing up | Run `/mcp` to check the connection status of the AudioPod server. | | The browser sign-in page doesn't open (remote or headless machine) | Copy the URL Claude shows into a browser on any machine, or use the API-key option above. | ## Self-hosted MCP alternative Prefer to wire up MCP yourself, or use a different client? See the [MCP Server](/sdks/mcp) page for the endpoint, auth, and client configs. # CLI Source: https://docs.audiopod.ai/sdks/cli Generate music, voiceover, transcribe, separate stems, and clone voices from your terminal. The `audiopod` CLI ships bundled with both SDKs. Same commands, same flags whether you `pip install audiopod` or `npm install -g audiopod`. ## Install ```bash theme={null} pip install audiopod ``` Requires Python 3.8+. ```bash theme={null} npm install -g audiopod ``` Requires Node 16+. ```bash theme={null} pnpm add -g audiopod ``` *** ## Authenticate Get an API key from [your dashboard](https://www.audiopod.ai/dashboard/account/api-keys), then either: ```bash theme={null} audiopod login # Paste ap_* → stored at ~/.audiopod/config.json (mode 600) ``` …or set the environment variable: ```bash theme={null} export AUDIOPOD_API_KEY=ap_YOUR_KEY ``` Per-call override: ```bash theme={null} audiopod --api-key ap_OTHER_KEY music "test" ``` *** ## Commands ### `audiopod music` Generate a song, instrumental, rap, or vocal stem. ```bash theme={null} audiopod music "lo-fi rainy 90 BPM" --duration 60 --out song.wav audiopod music "intro for podcast about climate" --kind instrumental --out intro.wav audiopod music "uplifting wedding ballad" --lyrics "We met..." --kind song ``` **Flags** | Flag | Default | Description | | - | - | - | | `--kind` | `song` | One of `song`, `instrumental`, `rap`, `vocals`. | | `--duration` | `60` | Seconds (10–600). | | `--lyrics` | — | Optional lyric text for vocal modes. | | `--out` | — | If set, the CLI waits for completion and downloads to this path. | | `--timeout` | `600` | Max seconds to wait when `--out` is given. | ### `audiopod tts` Synthesize speech. ```bash theme={null} audiopod tts "Welcome to AudioPod." --voice 368 --out hello.mp3 audiopod tts "Bienvenue à AudioPod." --voice 368 --lang fr --out hello-fr.mp3 ``` **Flags** | Flag | Default | Description | | - | - | - | | `--voice` | `368` | AudioPod voice ID (integer) or public catalog name. | | `--lang` | `en` | ISO language code. | | `--speed` | `1.0` | Multiplier (0.25–4.0). | | `--format` | `mp3` | `mp3`, `wav`, or `ogg`. | | `--out` | — | If set, downloads on completion. | ### `audiopod transcribe` Transcribe a local file or URL. ```bash theme={null} audiopod transcribe meeting.mp3 --diarize --format srt > meeting.srt audiopod transcribe https://example.com/podcast.mp3 --lang en --format json ``` | Flag | Description | | - | - | | `--lang` | Force a language (auto-detected if omitted). | | `--diarize` | Emit speaker labels. | | `--format` | `text`, `srt`, `vtt`, `json`. | | `--out` | Write the transcript to a file. | ### `audiopod stems` Separate audio into stems. ```bash theme={null} audiopod stems track.wav --mode six audiopod stems https://example.com/song.mp3 --mode two ``` | Flag | Description | | - | - | | `--mode` | `two` (vocal+instr), `four` (vocals/drums/bass/other), `six` (+guitar+piano), `sixteen`. | ### `audiopod clone` Train a voice from a 5–30s reference clip. ```bash theme={null} audiopod clone reference.wav --name "Narrator" --description "Calm narrator voice" ``` | Flag | Description | | - | - | | `--name` (required) | Display name for the cloned voice. | | `--description` | Optional voice characteristics description. | ### `audiopod audiobook` Produce a finished, ACX-compliant audiobook from a manuscript — upload, narrate, export, and (with `--out`) download the package in one command. ```bash theme={null} audiopod audiobook book.epub --title "The Lighthouse Keeper" --author "A. P. Tester" --voice 387 --out lighthouse-acx.zip audiopod audiobook --text "Once upon a time..." --title "A Short Tale" --voice 387 ``` | Flag | Description | | - | - | | `` | Manuscript file (PDF/EPUB/DOCX/TXT). Omit and use `--text` to narrate raw text. | | `--title` (required) | Audiobook title. | | `--author` | Author name (written into the package metadata). | | `--text` | Narrate this text instead of a file. | | `--voice` | Narration voice ID (default 387). | | `--out` | Download the ACX package ZIP to this path. | | `--timeout` | Max seconds to wait for the full pipeline (default 1800). | ### `audiopod jobs` Look up any async job by id. ```bash theme={null} audiopod jobs job_abc123 audiopod jobs job_abc123 --json | jq .output_url ``` *** ## Output Human-readable by default; pass `--json` for scripting: ```bash theme={null} audiopod tts "hi" --voice 368 --json | jq -r '.id' ``` *** ## Exit codes | Code | Meaning | | - | - | | 0 | Success | | 1 | API or runtime error | | 2 | Bad arguments / missing API key | | 130 | Interrupted (Ctrl-C) | *** ## See also Wire AudioPod into any agent. Programmatic access from Python. Programmatic access from Node.js / TypeScript. Every developer surface in one page. # MCP Server Source: https://docs.audiopod.ai/sdks/mcp Connect any agent to AudioPod via Model Context Protocol — music generation, TTS, voice cloning, transcription, and more from one endpoint. **What is MCP?** [Model Context Protocol](https://modelcontextprotocol.io) is an open standard that lets AI assistants (Claude Desktop, Cursor, Continue, Cline, Goose, Codex…) call external tools through a single transport. AudioPod publishes a Streamable-HTTP MCP server that exposes ten audio AI tools. ## Endpoint | Field | Value | | - | - | | URL | `https://mcp.audiopod.ai` | | Transport | `streamable-http` (MCP 2025-06-18) | | Auth | `X-API-Key: ap_*` (from your dashboard) **or** `Authorization: Bearer ` | | Discovery | [`/.well-known/mcp/server-card.json`](https://audiopod.ai/.well-known/mcp/server-card.json) and [`/.well-known/oauth-protected-resource`](https://audiopod.ai/.well-known/oauth-protected-resource) | Need a key? [Create one in your dashboard](https://www.audiopod.ai/dashboard/account/api-keys). Free tier credits unlock the full tool surface — no card required. *** ## Tools exposed Schemas are returned by `tools/list` — these are the names and one-liners: | Tool | Description | Required scope | | - | - | - | | `generate_music` | Songs, instrumentals, rap, vocal stems from a text prompt. The default music tool. | `music:generate` | | `compose_music` | Score-first: write an editable score (no audio yet) for a song you want to shape before recording. 165 credits. | `music:generate` | | `revise_music_score` | Change a score in plain words; the rest of the score is kept. 165 credits per revision. Available when enabled for the account. | `music:generate` | | `record_music_score` | Record a score, optionally with a new style or new words for the same tune. \~16.5 credits per second of score. | `music:generate` | | `get_music_score` | Read a score you own (key, tempo, sections, length). Free. | `audio:read` | | `edit_music_score` | Deprecated alias of `record_music_score`. | `music:generate` | | `text_to_speech` | TTS in 200+ languages with 200+ voices and custom clones — powered by AudioSonic. | `voice:synthesize` | | `clone_voice` | Clone a voice from a 5–30s reference clip. | `voice:clone` | | `separate_stems` | Up to 16 stems (vocals, drums, bass, guitar, piano, other…). Two-stem mode returns a vocals/instrumental (karaoke) split. | `stems:separate` | | `transcribe_audio` | Word-level timestamps, speaker diarization, SRT/VTT/JSON. | `transcribe` | | `denoise_audio` | Remove background noise while preserving voice character. | `audio:write` | | `convert_media` | MP3 / WAV / FLAC / OGG / M4A / MP4 / MOV conversion. | `audio:write` | | `separate_speakers` | Isolate each speaker into a separate track from multi-speaker audio. | `audio:write` | | `change_voice` | Convert a recording to a different target voice (voice-to-voice). | `voice:synthesize` | | `audio_to_midi` | Convert a recording of a melody, instrument or song into MIDI files (per-instrument plus one multitrack). Specialist piano and vocal engines need a Creator plan or above; see [pricing](https://audiopod.ai/pricing). | `audio:write` | | `check_job_status` | Poll the status and result of a long-running job. | *none (free)* | Scopes are enforced when authenticating with an API key. JWT-based auth implies full user privileges. Not sure which music tool to use? See [Choosing how to make music](/api-reference/music/choosing). *** ## Setup ### One-line install (recommended) Most modern MCP hosts now register an HTTP MCP server with a single command — no manual JSON edit, no restart needed beyond the host's normal config reload. Pick the one that matches your tool: ```bash theme={null} claude mcp add --transport http audiopod https://mcp.audiopod.ai \ --header "X-API-Key: ap_YOUR_KEY" --scope user ``` The `--scope user` flag installs at the user level so every Claude Code project picks up the server. Run `claude mcp list` to verify, then `/mcp` inside the TUI to see the eleven tools. ```bash theme={null} codex mcp add audiopod --url https://mcp.audiopod.ai \ --header "X-API-Key: ap_YOUR_KEY" ``` Verify with `codex mcp list`. Inside the Codex TUI, `/mcp` shows the connected servers and their tools. For any MCP client that only speaks stdio (older Claude Desktop builds, Continue, Cline before they ship native HTTP support, etc.), the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) bridge spawns a stdio process that proxies to our HTTP endpoint: ```bash theme={null} npx -y mcp-remote https://mcp.audiopod.ai \ --header "X-API-Key: ap_YOUR_KEY" ``` Wire it in as a stdio server in your client's config. Useful when manual JSON is too fiddly. ### Manual config (for desktop apps without a CLI) Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): ```json theme={null} { "mcpServers": { "audiopod": { "url": "https://mcp.audiopod.ai", "headers": { "X-API-Key": "ap_YOUR_KEY" } } } } ``` Restart Claude Desktop. Try: *"Generate a 30-second lo-fi instrumental for studying."* Edit `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "audiopod": { "url": "https://mcp.audiopod.ai", "headers": { "X-API-Key": "ap_YOUR_KEY" } } } } ``` Reload Cursor. Try: *"Use audiopod to generate a 60s upbeat intro track and download it."* Edit `~/.continue/config.json`: ```json theme={null} { "experimental": { "modelContextProtocolServers": [ { "transport": { "type": "streamable-http", "url": "https://mcp.audiopod.ai", "headers": { "X-API-Key": "ap_YOUR_KEY" } } } ] } } ``` Edit `~/.config/cline/cline_mcp_settings.json`: ```json theme={null} { "mcpServers": { "audiopod": { "url": "https://mcp.audiopod.ai", "transport": "streamable-http", "headers": { "X-API-Key": "ap_YOUR_KEY" } } } } ``` Run `goose configure` and add a Streamable-HTTP extension: ``` Type: streamable-http URL: https://mcp.audiopod.ai Header: X-API-Key=ap_YOUR_KEY ``` Probe the server directly to confirm your key works: ```bash theme={null} curl https://mcp.audiopod.ai \ -H "X-API-Key: ap_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' ``` *** ## JSON-RPC examples ### Initialize ```json theme={null} POST https://mcp.audiopod.ai { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "clientInfo": { "name": "my-agent", "version": "0.1.0" } } } ``` ### List tools ```json theme={null} { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } ``` ### Call a tool ```json theme={null} { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "generate_music", "arguments": { "prompt": "lo-fi rainy 90 BPM, soft pads", "duration": 60, "task_type": "text2music" } } } ``` The response includes a `structuredContent.job_id` — long-running tools return a job id immediately. Poll the matching `/api/v1//jobs/` endpoint, or call `tools/call` again with the `jobs` lookup tool wired into your CLI. *** ## Auth flow AudioPod publishes RFC 9728 OAuth Protected Resource Metadata so clients can discover how to authenticate: ```bash theme={null} curl https://audiopod.ai/.well-known/oauth-protected-resource ``` Returns the issuer, supported scopes, and the bearer-token pattern. Most agent clients use a simple `X-API-Key: ap_*` header — get your key in the [dashboard](https://www.audiopod.ai/dashboard/account/api-keys). *** ## Troubleshooting 1. Re-check your config file path — most agents won't reload after edits without a restart. 2. Confirm the URL is exactly `https://mcp.audiopod.ai` (no trailing slash, no `/sse`). 3. Hit the discovery URL: `curl https://mcp.audiopod.ai` returns a small JSON descriptor when the proxy is healthy. * Verify your key starts with `ap_`. * Check the key's scopes — the [dashboard](https://www.audiopod.ai/dashboard/account/api-keys) shows the per-tool scopes granted. * For wildcard access use scope `*`. Music and stem extraction return job ids immediately and resolve over the next 30–180 seconds. Most clients display the partial result; if yours doesn't, fall back to the [REST API](/api-reference/music/jobs) to poll for completion. *** ## Next steps Same eleven tools, terminal-first. Single-pane overview of every developer surface. Free credits, no card required. Model Context Protocol reference. # Node.js SDK Source: https://docs.audiopod.ai/sdks/nodejs Official Node.js SDK for AudioPod AI - Professional Audio Processing powered by AI [![npm version](https://badge.fury.io/js/audiopod.svg)](https://www.npmjs.com/package/audiopod) [![Node 16+](https://img.shields.io/badge/node-16+-green.svg)](https://nodejs.org/) ## Installation ```bash theme={null} npm install audiopod # or yarn add audiopod ``` ## Quick Start ```typescript theme={null} import AudioPod from 'audiopod'; // Initialize client const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Separate audio into 6 stems const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'six' }); // Download stems for (const [stem, url] of Object.entries(result.download_urls)) { console.log(`${stem}: ${url}`); } ``` ## Authentication ```typescript theme={null} import AudioPod from 'audiopod'; // Method 1: Pass API key directly const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Method 2: Use environment variable (recommended) // Set AUDIOPOD_API_KEY environment variable, then: const client = new AudioPod(); ``` *** ## Stem Separation Extract individual audio components from mixed recordings. ### Available Modes | Mode | Stems | Output | | - | - | - | | `single` | 1 | Specified stem only (vocals, drums, bass, guitar, piano, other) | | `two` | 2 | Vocals + Instrumental | | `four` | 4 | Vocals, Drums, Bass, Other | | `six` | 6 | Vocals, Drums, Bass, Guitar, Piano, Other | | `producer` | 8 | + Kick, Snare, Hihat | | `studio` | 12 | Full production toolkit | | `mastering` | 16 | Maximum detail | ### Examples ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Six-stem separation from YouTube const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'six' }); console.log(result.download_urls); // From local file const result = await client.stems.separate({ file: './song.mp3', mode: 'four' }); // Extract only vocals const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'single', stem: 'vocals' }); // Async job handling (for more control) const job = await client.stems.extract({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'six' }); console.log(`Job ID: ${job.id}`); // Wait for completion const result = await client.stems.waitForCompletion(job.id); // Get available modes const modes = await client.stems.modes(); modes.modes.forEach(m => { console.log(`${m.mode}: ${m.description}`); }); ``` *** ## Transcription Convert audio to text with speaker diarization. ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // From URL const job = await client.transcription.create({ url: 'https://...' }); const result = await client.transcription.waitForCompletion(job.id); console.log(result.transcript); // From file const job = await client.transcription.create({ file: './audio.mp3' }); const result = await client.transcription.waitForCompletion(job.id); ``` *** ## Voice Cloning & TTS Create custom voices and generate speech. ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Clone a voice from audio sample const voice = await client.voice.clone({ file: './sample.wav', name: 'My Voice' }); console.log(`Voice ID: ${voice.id}`); // Generate speech with cloned voice const audio = await client.voice.generate({ voiceId: voice.id, text: 'Hello, this is my cloned voice speaking!' }); ``` *** ## Music Generation (AudioMusic V2) Generate AI music with AudioMusic V2 — text-to-music, covers, style transfer, audio analysis, and more. ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Simple mode — just describe what you want const job = await client.music.simple({ query: 'a soft Bengali love song for a quiet evening', }); const song = await client.music.waitForCompletion(job.id); console.log(`Music URL: ${song.output_url}`); // Full control with text2music const advanced = await client.music.create({ prompt: 'upbeat pop, female vocals, catchy melody', task: 'text2music', lyrics: '[verse]\nSun is shining bright\n[chorus]\nDance the night away', duration: 120, inferenceSteps: 64, guidanceScale: 7.0, format: 'flac', }); // Instrumental const instrumental = await client.music.instrumental( 'chill jazz piano with saxophone', 90 ); // Cover / style transfer const cover = await client.music.cover({ srcAudioUrl: 'https://example.com/song.mp3', caption: 'jazz piano version with upright bass', audioCoverStrength: 0.7, }); // Analyze existing audio const analysis = await client.music.analyze({ audioUrl: 'https://example.com/song.mp3', }); console.log(`BPM: ${analysis.bpm}, Key: ${analysis.keyscale}`); // Extract stems const vocals = await client.music.extractStem({ srcAudioUrl: 'https://example.com/song.mp3', trackName: 'vocals', }); const completed = await client.music.waitForCompletion(vocals.id); console.log(`Vocals URL: ${completed.output_url}`); // Reference audio for style guidance const ref = await client.music.reference({ referenceAudioUrl: 'https://example.com/reference.mp3', caption: 'upbeat pop with similar warmth', lyrics: '[verse]\nHello world', }); ``` *** ## Audiobook Turn a manuscript into a finished, ACX-compliant audiobook. Use `produce()` for the whole pipeline in one call, or drive each step. See the [Audiobook API reference](/api-reference/audiobook) for the full surface. ```typescript theme={null} // One call: manuscript in, ACX package out const result = await client.audiobook.produce({ title: 'The Lighthouse Keeper', author: 'A. P. Tester', file: 'book.epub', // PDF / EPUB / DOCX / TXT (or text: '...') voiceId: 387, }); console.log(result.download_url); // presigned ACX package ZIP console.log(result.compliance_check.is_compliant); // Or drive each step const project = await client.audiobook.createProject({ title: 'My Book', author: 'Me' }); const up = await client.audiobook.uploadManuscript(project.id, 'book.epub'); await client.audiobook.parseManuscript(project.id, up.file_key, 'book.epub'); const voices = await client.audiobook.listVoices(); // available / recommended / custom const estimate = await client.audiobook.costEstimate(project.id); await client.audiobook.narrate(project.id, { voiceId: 387 }); // waits for all chapters const exp = await client.audiobook.exportAndWait(project.id); // waits; runs ACX compliance check const manifest = await client.audiobook.download(project.id, exp.job_id); console.log(manifest.download_url); ``` Generate an original book from a prompt instead of uploading: ```typescript theme={null} const project = await client.audiobook.createProject({ title: 'Bedtime Tales' }); await client.audiobook.generateBook(project.id, { prompt: 'Three calm bedtime stories about the sea', chapterCount: 3, wordsPerChapter: 500, }); await client.audiobook.narrate(project.id, { voiceId: 387 }); ``` *** ## Noise Reduction Clean up audio by removing background noise. ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Denoise audio file const clean = await client.denoiser.denoise({ file: './noisy.wav' }); console.log(`Clean audio: ${clean.outputUrl}`); ``` *** ## Speaker Separation Identify and separate multiple speakers. ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Diarize speakers const speakers = await client.speaker.diarize({ url: 'https://...' }); console.log(`Found ${speakers.segments.length} speaker segments`); ``` *** ## API Wallet Check balance and manage billing. ```typescript theme={null} import AudioPod from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Check balance const balance = await client.wallet.balance(); console.log(`Balance: ${balance.balance_usd}`); // Estimate cost before processing const estimate = await client.wallet.estimate('stem_extraction', 180); console.log(`Estimated cost: ${estimate.cost_usd}`); // Get usage history const usage = await client.wallet.usage(); usage.logs.forEach(log => { console.log(`${log.service_type}: ${log.amount_usd}`); }); ``` *** ## Error Handling ```typescript theme={null} import AudioPod, { AuthenticationError, InsufficientBalanceError } from 'audiopod'; try { const client = new AudioPod({ apiKey: 'ap_...' }); const result = await client.stems.separate({ url: '...', mode: 'six' }); } catch (error) { if (error instanceof AuthenticationError) { console.log('Invalid API key'); } else if (error instanceof InsufficientBalanceError) { console.log(`Need more credits. Required: ${error.requiredCents} cents`); } else { console.log(`Error: ${error.message}`); } } ``` *** ## TypeScript Support Full TypeScript support with exported types: ```typescript theme={null} import AudioPod, { StemExtractionJob, StemMode, WalletBalance } from 'audiopod'; const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Fully typed responses const job: StemExtractionJob = await client.stems.extract({ url: 'https://...', mode: 'six' as StemMode }); const balance: WalletBalance = await client.wallet.balance(); ``` *** ## Environment Variables ```bash theme={null} export AUDIOPOD_API_KEY="ap_your_api_key" ``` ```typescript theme={null} // Client reads from env automatically import AudioPod from 'audiopod'; const client = new AudioPod(); ``` *** ## Resources View on npm Source code Generate your API key Raw API documentation # Official SDKs Source: https://docs.audiopod.ai/sdks/overview Get started quickly with AudioPod AI's official SDKs for Python and Node.js ## Ready-to-Use SDKs AudioPod AI provides official SDKs that make it easy to integrate audio processing into your applications. Our SDKs handle authentication, request formatting, polling for job completion, and error handling automatically. **pip install audiopod** Full-featured Python SDK with type hints and async support. **npm install audiopod** TypeScript-ready SDK with promise-based API. ## Quick Comparison | Feature | Python | Node.js | | - | - | - | | Package | `audiopod` | `audiopod` | | Min Version | Python 3.8+ | Node 16+ | | Type Support | Type hints | Full TypeScript | | Async Support | Yes | Yes | | File Upload | Yes | Yes | | URL Processing | Yes | Yes | | Auto-polling | Yes | Yes | ## Installation ```bash theme={null} pip install audiopod ``` ```bash theme={null} npm install audiopod # or yarn add audiopod ``` ## Quick Start ```python theme={null} from audiopod import AudioPod # Initialize client (uses AUDIOPOD_API_KEY env var) client = AudioPod() # Or pass API key directly client = AudioPod(api_key="ap_your_api_key") # Separate audio into stems result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="six" ) # Download stems for stem, url in result["download_urls"].items(): print(f"{stem}: {url}") ``` ```typescript theme={null} import AudioPod from 'audiopod'; // Initialize client (uses AUDIOPOD_API_KEY env var) const client = new AudioPod(); // Or pass API key directly const client = new AudioPod({ apiKey: 'ap_your_api_key' }); // Separate audio into stems const result = await client.stems.separate({ url: 'https://youtube.com/watch?v=VIDEO_ID', mode: 'six' }); // Download stems for (const [stem, url] of Object.entries(result.download_urls)) { console.log(`${stem}: ${url}`); } ``` ## Available Services Both SDKs provide access to the same set of services: | Service | Python | Node.js | Description | | - | - | - | - | | Audiobook | `client.audiobook` | `client.audiobook` | Manuscript → ACX-ready audiobook | | Stems | `client.stems` | `client.stems` | Audio stem separation | | Transcription | `client.transcription` | `client.transcription` | Speech-to-text | | Voice | `client.voice` | `client.voice` | Voice cloning and TTS | | Music | `client.music` | `client.music` | AI music generation | | Denoiser | `client.denoiser` | `client.denoiser` | Noise reduction | | Speaker | `client.speaker` | `client.speaker` | Speaker diarization | | Wallet | `client.wallet` | `client.wallet` | Balance and billing | ## Error Handling Both SDKs provide typed exceptions for common error scenarios: ```python theme={null} from audiopod import AudioPod, AuthenticationError, InsufficientBalanceError try: client = AudioPod(api_key="ap_...") result = client.stems.separate(url="...", mode="six") except AuthenticationError: print("Invalid API key") except InsufficientBalanceError as e: print(f"Need ${e.required_cents / 100:.2f}") ``` ```typescript theme={null} import AudioPod, { AuthenticationError, InsufficientBalanceError } from 'audiopod'; try { const client = new AudioPod({ apiKey: 'ap_...' }); const result = await client.stems.separate({ url: '...', mode: 'six' }); } catch (error) { if (error instanceof AuthenticationError) { console.log('Invalid API key'); } else if (error instanceof InsufficientBalanceError) { console.log(`Need $${error.requiredCents / 100}`); } } ``` ## Environment Variables Both SDKs automatically read from the `AUDIOPOD_API_KEY` environment variable: ```bash theme={null} export AUDIOPOD_API_KEY="ap_your_api_key" ``` ## Next Steps Complete Python SDK documentation Complete Node.js SDK documentation Raw API endpoint documentation Generate your API key # Postman Collections Source: https://docs.audiopod.ai/sdks/postman Fork the AudioPod AI API into Postman and make a real call without writing any code. Two published collections live in the public **AudioPod** workspace on Postman. Fork either one into your own workspace and it stays linked to ours, so you can pull updates later. ## Start here — Getting Started Eight requests in three folders. The first folder reaches a real `200` in two requests **with no account and no API key**: it mints a short-lived sandbox key, stores it in a collection variable, and synthesizes a snippet of speech. Folder 2 walks the submit-a-job / poll-the-status pattern the rest of the API uses, and folder 3 covers your wallet balance and registering a webhook so you can stop polling altogether. [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b%26entityType%3Dcollection%26workspaceId%3D8013472d-5897-47d1-bf5c-92683a027b35\&utm_source=audiopod-docs-postman) ## The full reference Every endpoint we support — 299 requests across 13 folders: text to speech and voice design, transcription, music, audiobook, podcast, reader, stem separation, speaker separation, noise removal, audio to MIDI, webhooks, your wallet and your API keys. Use it as a browsable reference once you know the shape of a call. [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/58401244-584f22a4-0bef-4783-92fc-4f62bc6df4c3?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D58401244-584f22a4-0bef-4783-92fc-4f62bc6df4c3%26entityType%3Dcollection%26workspaceId%3D8013472d-5897-47d1-bf5c-92683a027b35\&utm_source=audiopod-docs-postman) The reference collection is **regenerated from the live OpenAPI document** on a schedule and pushed to the same collection id, so a fork tracks the real API instead of drifting away from it. The Getting Started collection is hand-maintained and deliberately small. ## Authenticating your fork Every authenticated request sends `X-API-Key: {{apiKey}}`. You have two ways to fill that in. 1. [Create an API key](https://www.audiopod.ai/dashboard/account/api-keys) in your dashboard. 2. Open the forked collection → **Variables** tab. 3. Paste the key into the **current value** of `apiKey`, and leave `baseUrl` at `https://api.audiopod.ai`. Keep the key in *current value*, not *initial value* — initial values travel with the collection when you share it. The workspace also publishes an **AudioPod Production** environment holding `baseUrl` and an empty `apiKey`. Fork it from the workspace, select it in the environment picker, and set `apiKey` there instead. This is the better option if you switch between several keys. Folder 1 of Getting Started needs neither — the sandbox is keyless. If you run an authenticated request with `apiKey` still blank, the collection's pre-request script stops it with an explanatory message rather than sending an unauthorized call. ## Browse without forking Eight requests, keyless first call The full 299-request reference ## Next steps The same flow in cURL, Python and Node.js Create, scope and rotate keys One subscription, every job type — stop polling # Python SDK Source: https://docs.audiopod.ai/sdks/python Official Python SDK for AudioPod AI - Professional Audio Processing powered by AI [![PyPI version](https://badge.fury.io/py/audiopod.svg)](https://pypi.org/project/audiopod/) [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/) ## Installation ```bash theme={null} pip install audiopod ``` ## Quick Start ```python theme={null} from audiopod import AudioPod # Initialize client client = AudioPod(api_key="ap_your_api_key") # Separate audio into 6 stems result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="six" ) # Download stems for stem, url in result["download_urls"].items(): print(f"{stem}: {url}") ``` ## Authentication ```python theme={null} from audiopod import AudioPod # or: from audiopod import Client # Method 1: Pass API key directly client = AudioPod(api_key="ap_your_api_key") # Method 2: Use environment variable (recommended) # Set AUDIOPOD_API_KEY environment variable, then: client = AudioPod() ``` *** ## Stem Separation Extract individual audio components from mixed recordings. ### Available Modes | Mode | Stems | Output | | - | - | - | | `single` | 1 | Specified stem only (vocals, drums, bass, guitar, piano, other) | | `two` | 2 | Vocals + Instrumental | | `four` | 4 | Vocals, Drums, Bass, Other | | `six` | 6 | Vocals, Drums, Bass, Guitar, Piano, Other | | `producer` | 8 | + Kick, Snare, Hihat | | `studio` | 12 | Full production toolkit | | `mastering` | 16 | Maximum detail | ### Examples ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Six-stem separation from YouTube (waits for completion by default) result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="six" ) print(result["download_urls"]) # From local file result = client.stems.separate( file="./song.mp3", mode="four" ) # Extract only vocals result = client.stems.separate( url="https://youtube.com/watch?v=VIDEO_ID", mode="single", stem="vocals" ) # Async job handling (for more control) job = client.stems.extract( url="https://youtube.com/watch?v=VIDEO_ID", mode="six" ) print(f"Job ID: {job['id']}") # Check status status = client.stems.status(job["id"]) print(f"Status: {status['status']}") # Wait for completion result = client.stems.wait_for_completion(job["id"]) # Get available modes modes = client.stems.modes() for m in modes["modes"]: print(f"{m['mode']}: {m['description']}") ``` *** ## Transcription Convert audio to text with speaker diarization. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # From URL job = client.transcription.create(url="https://...") result = client.transcription.wait_for_completion(job["id"]) print(result["transcript"]) # From file job = client.transcription.create(file="./audio.mp3") result = client.transcription.wait_for_completion(job["id"]) ``` *** ## Voice Cloning and TTS Create custom voices and generate speech. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Clone a voice from audio sample voice = client.voice.clone( file="./sample.wav", name="My Voice" ) print(f"Voice ID: {voice['id']}") ``` *** ## Music Generation (AudioMusic V2) Generate AI music with AudioMusic V2 — text-to-music, covers, style transfer, audio analysis, and more. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Simple mode — just describe what you want song = client.music.simple( query="a soft Bengali love song for a quiet evening", wait_for_completion=True ) print(f"Music URL: {song['output_url']}") # Full control with text2music song = client.music.generate( prompt="upbeat pop, female vocals, catchy melody", task="text2music", lyrics="[verse]\nSun is shining bright today\n[chorus]\nLet's dance the night away", duration=120, inference_steps=64, guidance_scale=7.0, format="flac", wait_for_completion=True ) # Instrumental instrumental = client.music.instrumental( prompt="chill jazz piano with saxophone", duration=90, wait_for_completion=True ) # Cover / style transfer cover = client.music.cover( src_audio_url="https://example.com/song.mp3", caption="jazz piano version with upright bass", audio_cover_strength=0.7, wait_for_completion=True ) # Analyze existing audio analysis = client.music.analyze( audio_url="https://example.com/song.mp3" ) print(f"BPM: {analysis['bpm']}, Key: {analysis['keyscale']}") # Extract stems vocals = client.music.extract_stem( src_audio_url="https://example.com/song.mp3", track_name="vocals", wait_for_completion=True ) print(f"Vocals URL: {vocals['output_url']}") # Reference audio for style guidance ref_song = client.music.reference( reference_audio_url="https://example.com/reference.mp3", caption="upbeat pop with similar warmth", lyrics="[verse]\nHello world", wait_for_completion=True ) ``` *** ## Audiobook Turn a manuscript into a finished, ACX-compliant audiobook. Use `produce()` for the whole pipeline in one call, or drive each step. See the [Audiobook API reference](/api-reference/audiobook) for the full surface. ```python theme={null} # One call: manuscript in, ACX package out result = client.audiobook.produce( title="The Lighthouse Keeper", author="A. P. Tester", file="book.epub", # PDF / EPUB / DOCX / TXT (or text="...") voice_id=387, ) print(result["download_url"]) # presigned ACX package ZIP print(result["compliance_check"]["is_compliant"]) # Or drive each step project = client.audiobook.create_project(title="My Book", author="Me") up = client.audiobook.upload_manuscript(project["id"], file="book.epub") client.audiobook.parse_manuscript(project["id"], up["file_key"], "book.epub") voices = client.audiobook.list_voices() # available / recommended / custom estimate = client.audiobook.cost_estimate(project["id"]) client.audiobook.narrate(project["id"], voice_id=387) # waits for all chapters export = client.audiobook.export(project["id"]) # waits; runs ACX compliance check manifest = client.audiobook.download(project["id"], export["job_id"]) print(manifest["download_url"]) ``` Generate an original book from a prompt instead of uploading: ```python theme={null} project = client.audiobook.create_project(title="Bedtime Tales") client.audiobook.generate_book(project["id"], prompt="Three calm bedtime stories about the sea", chapter_count=3, words_per_chapter=500) client.audiobook.narrate(project["id"], voice_id=387) ``` *** ## Noise Reduction Clean up audio by removing background noise. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Denoise audio file clean = client.denoiser.denoise(file="./noisy.wav") print(f"Clean audio: {clean['output_url']}") ``` *** ## Speaker Separation Identify and separate multiple speakers. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Diarize speakers speakers = client.speaker.diarize(url="https://...") print(f"Found {len(speakers['segments'])} speaker segments") ``` *** ## API Wallet Check balance and manage billing. ```python theme={null} from audiopod import AudioPod client = AudioPod(api_key="ap_your_api_key") # Check balance balance = client.wallet.balance() print(f"Balance: {balance['balance_usd']}") # Estimate cost before processing estimate = client.wallet.estimate("stem_extraction", duration_seconds=180) print(f"Estimated cost: {estimate['cost_usd']}") # Get usage history usage = client.wallet.usage() for log in usage["logs"]: print(f"{log['service_type']}: {log['amount_usd']}") ``` *** ## Error Handling ```python theme={null} from audiopod import AudioPod, AuthenticationError, InsufficientBalanceError try: client = AudioPod(api_key="ap_...") result = client.stems.separate(url="...", mode="six") except AuthenticationError: print("Invalid API key") except InsufficientBalanceError as e: print(f"Need more credits. Required: {e.required_cents} cents") except ValueError as e: print(f"Invalid input: {e}") ``` *** ## Environment Variables ```bash theme={null} export AUDIOPOD_API_KEY="ap_your_api_key" ``` ```python theme={null} # Client reads from env automatically from audiopod import AudioPod client = AudioPod() ``` *** ## Resources View on PyPI Source code Generate your API key Raw API documentation