{"id":"f88148c5-a115-47d1-b17d-0847d72ffdab","entityType":"agent","slug":"clawhub-spotify-save-to-spotify","name":"Save To Spotify","canonicalUrl":"https://www.xpersona.co/agent/clawhub-spotify-save-to-spotify","canonicalPath":"/agent/clawhub-spotify-save-to-spotify","generatedAt":"2026-10-10T14:44:23.785Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-10T11:58:02.991Z","emptyReason":null},"description":"Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and... Skill: Save To Spotify Owner: spotify Summary: Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and... Tags: latest:0.2.0 Version history: v0.2.0 | 2026-07-20T09:12:21.820Z | auto save-to-spotify v0.2.0 - Added comprehensive onboarding documentation and flow for first-time users ($1), enabling guided setup if the","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.4K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s178q1njn7prgvajksxt46zez58649bp:save-to-spotify","sourceUrl":"https://clawhub.ai/spotify/save-to-spotify","homepage":"https://clawhub.ai/spotify/skills/save-to-spotify","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/spotify/save-to-spotify","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/spotify/skills/save-to-spotify","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":63,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and..."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T11:58:02.991Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T11:58:02.991Z","emptyReason":null},"stars":null,"forks":null,"downloads":1448,"packageName":null,"latestVersion":"0.2.0","tractionLabel":"1.4K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T11:58:02.991Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T11:58:02.991Z","lastCrawledAt":"2026-10-10T11:58:02.991Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T11:58:02.991Z","lastVerifiedAt":null,"highlights":[{"version":"0.2.0","createdAt":"2026-07-20T09:12:21.820Z","changelog":"## save-to-spotify v0.2.0 - Added comprehensive onboarding documentation and flow for first-time users ([references/onboarding.md](references/onboarding.md)), enabling guided setup if the user is new or requests onboarding, regardless of existing shows. - Introduced [references/local-preview.md](references/local-preview.md), covering previewing and validating episodes before upload. - Included [references/recipes.md](references/recipes.md) detailing voice and pipeline defaults by content type. - Updated the user interview: streamlined to only ask what is missing, with sensible defaults for language, length, voice, cover image, and companion images. - Expanded installation instructions with clearer Windows support and SmartScreen details. - Removed outdated skill-card documentation; reorganized and updated references for clarity and maintainability.","fileCount":13,"zipByteSize":49978},{"version":"0.1.5","createdAt":"2026-07-17T11:54:34.939Z","changelog":"save-to-spotify 0.1.5 - Removed the sample file skill-card.md to streamline documentation. - Updated references/cli-usage.md with revised or additional CLI instructions and workflows.","fileCount":10,"zipByteSize":35729},{"version":"0.1.4","createdAt":"2026-07-13T15:11:34.768Z","changelog":"save-to-spotify 0.1.4 - References updated for clarity and completeness; SKILL.md was revised. - Timeline reference and CLI usage documentation improved. - Obsolete skill-card.md file removed.","fileCount":10,"zipByteSize":35711},{"version":"0.1.3","createdAt":"2026-06-12T17:40:44.887Z","changelog":"save-to-spotify 0.1.3 - Updated cover image and artwork handling: options now include user-provided, AI-generated, and CDN fallback images with improved typography and RTL font support. - Simplified and clarified cover image selection in the user interview, with explicit fallback order and Pillow compositing guidance. - Expanded [references/cover-image.md] to detail cover path types, typography, and image design rules. - Removed skill-card.md and streamlined documentation references—no impact on production pipeline or CLI usage.","fileCount":10,"zipByteSize":35579},{"version":"0.1.1","createdAt":"2026-05-07T14:44:21.457Z","changelog":"save-to-spotify 0.1.1 - Updated reference documentation in cli-usage.md and timeline.md. - No changes to logic or functionality; this is a documentation update only.","fileCount":10,"zipByteSize":34378},{"version":"0.1.0","createdAt":"2026-05-07T12:39:45.118Z","changelog":"Initial release — introduces audio content production and Spotify save functionality. - Create polished audio episodes with TTS narration and rich timeline, including chapters, images, external links, and Spotify entity companions. - Support for cover image generation: AI-generated (DALL-E, Stable Diffusion), user-provided, or mixed. - Enforce a mandatory user interview to confirm key production preferences before any content is generated. - Show management features: add episodes to new or existing shows, organize saves. - Reference directory included for production rules, CLI usage, Spotify API integration, and content quality guidelines. - Ensures incremental saving, source-to-segment mapping, and respect for third-party rights throughout the pipeline.","fileCount":9,"zipByteSize":32759}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s178q1njn7prgvajksxt46zez58649bp:save-to-spotify","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T14:44:23.781Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-spotify-save-to-spotify/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-10T11:58:02.991Z","emptyReason":null},"readme":"Skill: Save To Spotify\n\nOwner: spotify\n\nSummary: Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and...\n\nTags: latest:0.2.0\n\nVersion history:\n\nv0.2.0 | 2026-07-20T09:12:21.820Z | auto\n\n## save-to-spotify v0.2.0\n\n- Added comprehensive onboarding documentation and flow for first-time users ([references/onboarding.md](references/onboarding.md)), enabling guided setup if the user is new or requests onboarding, regardless of existing shows.\n- Introduced [references/local-preview.md](references/local-preview.md), covering previewing and validating episodes before upload.\n- Included [references/recipes.md](references/recipes.md) detailing voice and pipeline defaults by content type.\n- Updated the user interview: streamlined to only ask what is missing, with sensible defaults for language, length, voice, cover image, and companion images.\n- Expanded installation instructions with clearer Windows support and SmartScreen details.\n- Removed outdated skill-card documentation; reorganized and updated references for clarity and maintainability.\n\nv0.1.5 | 2026-07-17T11:54:34.939Z | auto\n\nsave-to-spotify 0.1.5\n\n- Removed the sample file skill-card.md to streamline documentation.\n- Updated references/cli-usage.md with revised or additional CLI instructions and workflows.\n\nv0.1.4 | 2026-07-13T15:11:34.768Z | auto\n\nsave-to-spotify 0.1.4\n\n- References updated for clarity and completeness; SKILL.md was revised.\n- Timeline reference and CLI usage documentation improved.\n- Obsolete skill-card.md file removed.\n\nv0.1.3 | 2026-06-12T17:40:44.887Z | auto\n\nsave-to-spotify 0.1.3\n\n- Updated cover image and artwork handling: options now include user-provided, AI-generated, and CDN fallback images with improved typography and RTL font support.\n- Simplified and clarified cover image selection in the user interview, with explicit fallback order and Pillow compositing guidance.\n- Expanded [references/cover-image.md] to detail cover path types, typography, and image design rules.\n- Removed skill-card.md and streamlined documentation references—no impact on production pipeline or CLI usage.\n\nv0.1.1 | 2026-05-07T14:44:21.457Z | auto\n\nsave-to-spotify 0.1.1\n\n- Updated reference documentation in cli-usage.md and timeline.md.\n- No changes to logic or functionality; this is a documentation update only.\n\nv0.1.0 | 2026-05-07T12:39:45.118Z | user\n\nInitial release — introduces audio content production and Spotify save functionality.\n\n- Create polished audio episodes with TTS narration and rich timeline, including chapters, images, external links, and Spotify entity companions.\n- Support for cover image generation: AI-generated (DALL-E, Stable Diffusion), user-provided, or mixed.\n- Enforce a mandatory user interview to confirm key production preferences before any content is generated.\n- Show management features: add episodes to new or existing shows, organize saves.\n- Reference directory included for production rules, CLI usage, Spotify API integration, and content quality guidelines.\n- Ensures incremental saving, source-to-segment mapping, and respect for third-party rights throughout the pipeline.\n\nArchive index:\n\nArchive v0.2.0: 13 files, 49978 bytes\n\nFiles: references/audio-providers.md (18389b), references/cli-usage.md (15496b), references/content-quality.md (9644b), references/cover-image.md (8239b), references/episode-description.md (2802b), references/local-preview.md (5817b), references/onboarding.md (12585b), references/recipes.md (6087b), references/spotify-api.md (7045b), references/timeline.md (10562b), skill-card.md (3133b), SKILL.md (12546b), _meta.json (134b)\n\nFile v0.2.0:SKILL.md\n\n---\nid: save-to-spotify\nname: save-to-spotify\ndescription: Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and Spotify entity cards), and a cover image. Also use for raw media saves, show/episode management, and timeline navigation.\nenabled: true\n---\n\n# Audio Content Production Skill\n\n`save-to-spotify` saves audio files to the user's Spotify library. Anything they can play locally — lecture recordings, voice memos, conference talks, language lessons — they can save to Spotify and listen from any device.\n\nShows are folders for organizing saves.\n\nYou are a podcast and audio content production agent. You create polished audio episodes from a variety of sources and formats, produce them with a rich in-player timeline (chapters plus image, link, and Spotify entity companions that appear during playback in the Now Playing View), and save to Spotify.\n\nThis skill defines the **shared production pipeline** — core principles, the user interview checkpoint, and the execution checklist.\n\n## Reference Directory\n\nThese files cover the detailed rules. Load the one you need — don't inline them.\n\n- [references/cli-usage.md](references/cli-usage.md) — Binary install, auth, `upload`/`shows`/`episodes`/`timeline` commands, JSON mode, error handling, troubleshooting, and common end-to-end workflows\n- [references/spotify-api.md](references/spotify-api.md) — Using `developer.spotify.com/llms.txt`, the Spotify Web API OpenAPI spec, and the CLI's token to resolve album / track / artist / playlist / show / episode names to `spotify:...` URIs for `spotify_entity` timeline companions\n- [references/audio-providers.md](references/audio-providers.md) — TTS engine selection, voice config, ffmpeg assembly, silence generation, timeline timestamp calculation\n- [references/cover-image.md](references/cover-image.md) — Cover image paths (user-provided, AI-generated, CDN artwork), typography rules, font & RTL, Pillow compositing recipe\n- [references/timeline.md](references/timeline.md) — Timeline data model, validation rules, companion images (sourced / AI-generated / mixed / skip), including DALL-E / Stable Diffusion code and batch generation\n- [references/episode-description.md](references/episode-description.md) — HTML description format, Python builder from `timeline.json`, formatting rules\n- [references/content-quality.md](references/content-quality.md) — Editorial guidelines: voice, transitions, person context, depth control, visual description, pacing, self-critique\n\n---\n\n## Install\n\nIf `save-to-spotify` is not available on `PATH`, ask the user to confirm CLI installation first, then install it:\n\n```shell\ncurl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\nOn Windows, run this in **Git Bash** (ships with Git for Windows) — it installs `save-to-spotify.exe` to `~/.local/bin`. The unsigned .exe may trigger a SmartScreen prompt on first run; unblock with `Unblock-File` or right-click → Properties → Unblock.\n\nSee [references/cli-usage.md](references/cli-usage.md) for manual binary downloads, source builds, authentication, command usage, and troubleshooting.\n\n---\n\n## Core Principles\n\n### Read-only. Always.\n\nWhen sourcing content, always respect platform terms of service and robots.txt and third-party IP rights. Use only authorized APIs and user-provided content. Never interact with source platforms beyond reading — do not post, like, follow, or modify content.\n\n### Be the listener's eyes\n\nPodcast listeners can't see anything. You are their eyes. Every piece of visual content — screenshots, images, charts — must be described in the script. If it matters to the segment, say what's in it.\n\n### Deep-link everything\n\nEvery segment in the show notes must link to the original source when possible. A link to a specific moment or post is 10x more valuable than a link to a homepage.\n\n### Respect Third-Party Rights\n\nThe final product must be a noninfringing synthesis of source materials, and must not infringe copyright or other third-party IP rights. It must not mislead as to the source or sponsorship of any material or information.\n\n### Prefer Spotify-native references\n\nWhen a segment points to something that already exists on Spotify — music, podcasts, audiobook titles, artists, albums, playlists, episodes, creators — capture the Spotify URI and use a `spotify_entity` timeline item whenever possible. Prefer the full `spotify:...` URI form, not a bare ID or `open.spotify.com` URL. Use external `link` companions for off-Spotify destinations such as articles, stores, docs, newsletters, and event pages. A `spotify_entity` and a `link` can both appear for the same segment/chapter when both the Spotify destination and the original source are valuable; just place them at non-overlapping times.\n\n### Segment-to-source integrity\n\nThe script has a strict 1:1 mapping: segment [N] corresponds to source item N. This mapping drives chapters, timeline companions, and show notes alignment. Never reorder, merge, or skip segments after assignment.\n\n### Save incrementally\n\nWrite collected data to disk after each sourcing step. If a later step fails, previous work is preserved.\n\n### Pacing and silence\n\nDon't fear strategic silence. Pauses between segments give the listener time to absorb. The 300ms gaps between segments are a minimum — use longer pauses (500ms+) between major topic shifts. Vary the pacing: slow down for important analysis or emotional moments, keep it brisk for roundups and quick hits.\n\n### The user made this\n\nIn every user-facing string, emphasise what the user has created rather than you (the agent) taking credit. Strings should centre the user. For example, instead of \"we created your episode,\" or \"your podcast is ready\", use strings like \"your episode is ready\". Reinforce that this is something the user made.\n\n---\n\n## First-Time Onboarding\n\nUse the streamlined onboarding flow from [references/onboarding.md](references/onboarding.md) instead of the full interview below when **any** of these are true:\n\n1. The user has no shows (`save-to-spotify --json shows` returns an empty `shows` array)\n2. The user says things like \"get started\", \"help me start\", \"first episode\", \"set up\", \"onboard me\", or any phrasing that signals they want the guided experience — **even if they already have shows**\n\nDo NOT check shows first and skip onboarding when the user explicitly asked for the guided flow. The explicit ask always wins.\n\n---\n\n## User Interview\n\n**Chapter-skip playback is NOT an interview question** — never ask about or enable it unprompted; the `configure-chapter-skip` skill owns the trigger rules and workflow.\n\n**Default everything. Only ask what the user's prompt didn't cover.**\n\nMost preferences have sensible defaults — apply them silently. The user's prompt usually provides the content scope; everything else can be defaulted. Do NOT present a numbered list of questions. Do NOT dump all options at once.\n\n### What to default (never ask unless the user brings it up)\n\n- **Language** — user's system locale\n- **Length** — pick from content (briefings ~8min, deep dives ~8min, recaps ~3min)\n- **Voice** — use the configured default from `save-to-spotify tts status --json`. If none is configured, follow the provider selection in [references/audio-providers.md](references/audio-providers.md). On Kokoro, prefer the content type's default voice (the `Kokoro voice` row in [references/recipes.md](references/recipes.md) — e.g. the softer sleep voice for sleep content) over a generic default\n- **Cover image** — AI-generated (see [references/cover-image.md](references/cover-image.md))\n- **Timeline companion images** — mixed (sourced where available, AI-generated fill)\n\n### What to ask (only if not obvious from the prompt)\n\n1. **Content scope** — if the user didn't specify what the episode is about, ask. Otherwise proceed.\n2. **Show** — pick the most relevant existing show by name. If none fits, create one. Only ask if ambiguous.\n\n### Plan confirmation\n\nPresent a one-line plan with choices:\n\n> \"Making a ~8 min deep dive on [topic], adding to [Show Name] with [voice].\"\n\nThen present options:\n- **Go** — start production\n- **Change voice** — pick a different TTS voice\n- **Change length** — shorter or longer\n- **Change topic** — adjust the content scope\n\nAlways guide with choices, never wait for free-text input. Embed whatever the user is judging (plan, chapter list, preview URL) inside the choice prompt itself — text printed before a choice popup can be hidden by it.\n\nIf the user asks to change the skip-forward action (`15 seconds` default vs `Next chapter`), treat it as an explicit request — see the `configure-chapter-skip` skill. Do not start production until the user confirms the plan.\n\n---\n\n## Execution Checklist\n\nEvery episode — regardless of content type — must complete these steps.\n\n0. **Preflight: install, auth, and voice engine** — Run `save-to-spotify --json doctor` before any sourcing. This checks the binary, auth, TTS engines, and ffmpeg in one call. If the binary is missing, ask the user to confirm installation, install it with the command in the Install section after they approve, then run doctor again. If unauthenticated, run `save-to-spotify setup` directly (do not ask the user to run it — just run it). The setup command handles auth + TTS detection in one pass and auto-detects headless environments. **Then confirm a TTS engine is available** via the `tts_engines` field in the doctor output: if one is already set up or the user has a preference, use it; otherwise check for an existing API key (`OPENAI_API_KEY`, `ELEVENLABS_API_KEY`) and suggest that engine first — no install, higher quality. If no key is present, **ask the user** whether to install **Kokoro** (free, local, ~340 MB, [Apache-2.0 licensed](https://raw.githubusercontent.com/hexgrad/kokoro/refs/heads/main/LICENSE)) — put the license link in the question text above the choices, where markdown renders it clickable; never inside option labels, which are plain text. Do not install silently. If they accept, run `save-to-spotify tts setup`. Confirm an engine is *available* before scripting; the interactive voice pick and preview can be deferred until the content is approved — content before audio. Voice previews use the player page in [references/local-preview.md](references/local-preview.md) (\"Voice preview page\"), never auto-play.\n1. **Interview** — Ask the user about preferences, including companion-image source. Present a plan and **wait for confirmation**\n2. **Script** — Present a short chapter overview (compact numbered list, bold chapter names, one line each, no blank lines between items) and get it approved first (revising an outline is free; never dump a full transcript on the user), then write the script following this skill's universal rules (see [references/content-quality.md](references/content-quality.md))\n3. **Critique** — Self-review the script, revise without reordering or removing segments\n4. **Produce** — Generate audio per-segment, concatenate, convert to MP3 (see [references/audio-providers.md](references/audio-providers.md)). Build `timeline.json` with chapters, Spotify entity companions where applicable, image companions with `url` set when image + source belong together, standalone links only for imageless or extra destinations, and additional images as needed (sourced and/or AI-generated per the interview answer) — see [references/timeline.md](references/timeline.md)\n5. **Describe** — Build the timestamped HTML description from the chapter entries in `timeline.json` and source URLs (see [references/episode-description.md](references/episode-description.md))\n6. **Cover image** — Generate or select cover image (square, max 1 MB). **MANDATORY — never skip this step** (see [references/cover-image.md](references/cover-image.md))\n7. **Save** — Start the local browser preview and offer it before saving (serve first, open on request — see [references/local-preview.md](references/local-preview.md)), then save MP3 with title, description, and cover image via `save-to-spotify --json upload` (see [references/cli-usage.md](references/cli-usage.md)). State proactively that the episode is saved private, visible only to the user\n8. **Timeline** — Push `timeline.json` with `timeline set` (uploads image files automatically)\n9. **Verify** — Poll `episodes status` until `READY`\n\nFile v0.2.0:_meta.json\n\n{\n  \"ownerId\": \"kn73w4eqhcyrcqet64x9rz87a184g2fx\",\n  \"slug\": \"save-to-spotify\",\n  \"version\": \"0.2.0\",\n  \"publishedAt\": 1784538741820\n}\n\nFile v0.2.0:references/audio-providers.md\n\n# Audio Providers & Assembly\n\nReference for generating speech and assembling audio files for saving via `save-to-spotify`. The user picks their own TTS engine and voice — this documents how to use each one.\n\nTell the user:\n\n> The skill produces audio content that may be distributed via a streaming platform. \n> Every episode must be grounded in content you have the right to reproduce in this form.\n\n## Production pipeline\n\nEvery episode walks the same steps. Recipes define what to write (sourcing, scripting, segment map). This reference covers generation and assembly.\n\n1. Generate TTS audio per segment (one file each for exact chapter timing)\n2. Generate silence files for transitions (300ms minimum between segments, 500ms+ between major shifts)\n3. Concatenate all segments into a single MP3\n4. Normalize volume levels\n5. Calculate chapter timestamps from cumulative segment durations\n6. Build `timeline.json` with chapters, Spotify entity companions, external link companions, and image companions (see [timeline.md](timeline.md))\n\n**Accepted formats:** `.mp3`, `.m4a`, `.wav`, `.ogg` (max 1 GB). Default to `.mp3`. Convert anything else with ffmpeg before upload — see \"Convert formats\" below.\n\n### Voice selection guide\n\n- **Kokoro** (local, free): suggest the recipe's default voice first (the `Kokoro voice` row in [recipes.md](recipes.md)); full voice list in the Kokoro section below\n- **ElevenLabs** (high quality, paid): Amelia, George, Bella\n- **Edge TTS** (free, 300+ voices): `en-US-AriaNeural` (F), `en-US-GuyNeural` (M)\n- **OpenAI TTS** (high quality, paid): `nova`, `alloy`, `echo`, `onyx`\n\n### Multi-voice / bilingual segments\n\nDefault to one voice per episode, but when the content is bilingual, role-played, or otherwise needs multiple voices, use a part-based segment schema and a flattened render manifest.\n\nExample source schema:\n\n```json\n{\n  \"segments\": [\n    {\n      \"title\": \"Swedish intro\",\n      \"parts\": [\n        {\"lang\": \"sv\", \"voice\": \"sv-SE-SofieNeural\", \"text\": \"Hej och valkommen.\"},\n        {\"lang\": \"en\", \"voice\": \"en-GB-LibbyNeural\", \"text\": \"Welcome back to the show.\"}\n      ]\n    }\n  ]\n}\n```\n\nFlatten that into a render manifest before synthesis:\n\n```json\n[\n  {\"segment_index\": 0, \"part_index\": 0, \"lang\": \"sv\", \"voice\": \"sv-SE-SofieNeural\", \"text\": \"Hej och valkommen.\", \"out\": \"seg_00_00.mp3\"},\n  {\"segment_index\": 0, \"part_index\": 1, \"lang\": \"en\", \"voice\": \"en-GB-LibbyNeural\", \"text\": \"Welcome back to the show.\", \"out\": \"seg_00_01.mp3\"}\n]\n```\n\nConcatenate manifest outputs in order, then compute chapter timestamps from the flattened audio segments.\n\n## Provider selection\n\nWhenever offering Kokoro to the user, include its license — [Apache-2.0](https://raw.githubusercontent.com/hexgrad/kokoro/refs/heads/main/LICENSE) — as a clickable link in the **question text**, where markdown renders. Never put the link inside choice option labels: those render as plain text and the raw URL is unreadable.\n\nWhen a content creation recipe is triggered and no TTS provider has been established, resolve one in this order:\n\n**1. Check for a configured default.** Run `save-to-spotify tts status --json`. If a default engine is set and ready, use it — no questions needed.\n\n**2. Reuse a key the user already has.** If no default is set, `tts status` reports which cloud engines have API keys present. Suggest those first — they're higher quality and need no install:\n\n> You already have an **OpenAI** key set — I can use that with no setup. Prefer that, or a free local voice?\n\n**3. No key? Fall back to Kokoro (the default).**\n\n> No TTS key found, so I'll default to **Kokoro** — a free, local voice engine (no key, no limits, ~340 MB, [Apache-2.0 licensed](https://raw.githubusercontent.com/hexgrad/kokoro/refs/heads/main/LICENSE)). Want that, or pick your own?\n\nIf they accept and Kokoro isn't installed, run `save-to-spotify tts setup` to install it.\n\n**4. Or pick explicitly.** If the user would rather choose:\n\n> Which TTS engine would you like to use? Kokoro is [Apache-2.0 licensed](https://raw.githubusercontent.com/hexgrad/kokoro/refs/heads/main/LICENSE).\n>\n> - **Kokoro** (recommended) -- free, local, no API key, no limits, Apache-2.0\n> - **OpenAI TTS** -- high quality, needs OPENAI_API_KEY\n> - **ElevenLabs** -- most natural, needs ELEVENLABS_API_KEY\n> - Something else? (register any engine with `save-to-spotify tts add`)\n\n### Other providers\n\nThese are fully supported but not front-and-center for new users. See the provider reference below for usage:\n\n- **Edge TTS** (`edge-tts`) -- free, 300+ voices, 70+ languages\n- **Piper** -- offline, fast, open-source\n- **Google Cloud TTS** -- high quality, paid (service account)\n- **gTTS** -- free, Google Translate quality\n- **Amazon Polly** -- cloud, paid\n- **macOS `say`** -- built-in, zero setup, low quality; use only for a quick test\n\nRegister any engine with `save-to-spotify tts add --name <name> --check-cmd <cmd>`.\n\n### Verification\n\nBefore generating, verify the chosen provider is available:\n\n```shell\nsave-to-spotify tts status --json\n```\n\nThis checks all built-in and custom engines, API keys, and ffmpeg in one call. If a specific engine needs manual verification:\n\n`save-to-spotify --json tts status` is the canonical readiness check. For cloud engines, `ready` means the API key is present — that is all the CLI needs; `tts test` synthesizes previews natively with no Python involved.\n\n**Production synthesis is different**: the full-episode snippets below run through Python, so before generating episode audio (not previews), also verify the packages the chosen snippet imports:\n\n```shell\npython3 -c \"import openai\"      # OpenAI production snippet\npython3 -c \"import elevenlabs\"  # ElevenLabs production snippet\npython3 -c \"import kokoro_onnx\" # Kokoro (use the venv python — see the Kokoro section)\nffmpeg -version && ffprobe -version   # Required for assembly (portable check)\n```\n\nInstall what's missing with pip before synthesis. On Windows the interpreter is `python` (or the `py` launcher), not `python3`. If ffmpeg is missing: `brew install ffmpeg` (macOS), `apt install ffmpeg` (Linux), `winget install ffmpeg` (Windows).\n\nIf nothing is ready, run `save-to-spotify tts setup` to install the free default (Kokoro), or set a cloud TTS API key and run `save-to-spotify tts status` again.\n\n## TTS provider reference\n\n### macOS `say`\n\n```shell\nsay --voice '?'                                          # List voices\nsay -v <Voice> -o output.m4a --data-format=aac \"Text\"    # Generate m4a\nsay -v <Voice> -o output.m4a --data-format=aac -f in.txt # From file\n```\n\nVoices: `Samantha` (en-US), `Daniel` (en-GB), `Alex` (en-US high quality). Limited languages.\n\n### Edge TTS (recommended free option)\n\n```shell\nedge-tts --list-voices                                                  # List all\nedge-tts --voice \"en-US-AriaNeural\" --text \"Hello\" --write-media o.mp3  # Generate\nedge-tts --voice \"en-US-GuyNeural\" -f input.txt --write-media o.mp3     # From file\nedge-tts --voice \"en-US-AriaNeural\" --rate=\"+10%\" --text \"Fast\" --write-media o.mp3  # Faster\nedge-tts --voice \"en-US-AriaNeural\" --rate=\"-30%\" --text \"Slow\" --write-media o.mp3  # Slower\n```\n\nKey voices: `en-US-AriaNeural` (F), `en-US-GuyNeural` (M), `en-GB-SoniaNeural` (F), `en-GB-RyanNeural` (M), `es-ES-ElviraNeural`, `fr-FR-DeniseNeural`, `de-DE-KatjaNeural`, `ja-JP-NanamiNeural`, `zh-CN-XiaoxiaoNeural`.\n\n### OpenAI TTS\n\n```python\nfrom openai import OpenAI\nclient = OpenAI()\nresp = client.audio.speech.create(model=\"tts-1\", voice=\"nova\", input=\"Text\")\nresp.write_to_file(\"output.mp3\")\n```\n\nVoices: `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`. Use `tts-1-hd` for higher quality. (Older `stream_to_file` is deprecated in current SDK versions.)\n\n### ElevenLabs\n\n```python\nfrom elevenlabs.client import ElevenLabs\n\nclient = ElevenLabs()  # reads ELEVENLABS_API_KEY\n\n# Resolve the voice name to a voice_id first — convert() takes ids, not names.\nvoice_id = next(v.voice_id for v in client.voices.get_all().voices if v.name == \"Rachel\")\n\naudio = client.text_to_speech.convert(\n    voice_id=voice_id,\n    model_id=\"eleven_multilingual_v2\",\n    text=\"Text\",\n)\nwith open(\"output.mp3\", \"wb\") as f:\n    for chunk in audio:\n        f.write(chunk)\n```\n\nMost natural. Supports multiple languages. (Older `from elevenlabs import generate, save` no longer exists in current SDK versions.)\n\n### Piper (offline)\n\n```shell\necho \"Text\" | piper --model en_US-lessac-medium.onnx --output_file output.wav\nffmpeg -i output.wav -codec:a libmp3lame -qscale:a 2 output.mp3  # convert\n```\n\n### Kokoro (local, free, no API limits)\n\n**Run synthesis with the Kokoro venv's Python, never the system one** — the packages live in the venv. Don't construct the path yourself: read the `kokoro_python` field from `save-to-spotify --json tts status`, which resolves the platform layout (`kokoro-env/bin/python3` on macOS/Linux, `kokoro-env\\Scripts\\python.exe` on Windows).\n\n```python\nfrom kokoro_onnx import Kokoro\nimport soundfile as sf\nimport numpy as np\nimport json\nimport os, glob\n\n# Find kokoro model files — search the save-to-spotify config dir, never hardcode versions\nconfig_dir = os.path.expanduser(\"~/.config/save-to-spotify\")\nsearch_dirs = [os.path.join(config_dir, \"kokoro-env\"), config_dir]\n\ndef find_model(pattern):\n    for d in search_dirs:\n        hits = glob.glob(os.path.join(d, pattern))\n        if hits:\n            return sorted(hits)[-1]  # latest version\n    raise FileNotFoundError(f\"No {pattern} found — run: save-to-spotify tts setup\")\n\nkokoro = Kokoro(find_model('kokoro-v*.onnx'), find_model('voices-v*.bin'))\n\nsegments = [\n    (\"Introduction\", intro_text),\n    (\"Main Topic\", body_text),\n    (\"Sign-off\", outro_text),\n]\n\ntimeline_items = []\nall_samples = []\ncursor_ms = 0\n\nfor title, text in segments:\n    samples, sr = kokoro.create(text, voice='af_heart', speed=1.0)\n    timeline_items.append({\"chapter\": {\"title\": title, \"start_time_ms\": cursor_ms}})\n    all_samples.append(samples)\n    # 300ms silence between segments\n    silence = np.zeros(int(sr * 0.3))\n    all_samples.append(silence)\n    cursor_ms += int((len(samples) + len(silence)) / sr * 1000)\n\ncombined = np.concatenate(all_samples)\nsf.write('episode.wav', combined, sr)\n\nwith open('timeline.json', 'w') as f:\n    json.dump({\"items\": timeline_items}, f, indent=2)\n```\n\nConvert to MP3: `ffmpeg -i episode.wav -codec:a libmp3lame -b:a 192k episode.mp3`\n\nVoices: `af_heart` (American female, recommended default), `af_nicole` (American female, soft — sleep content), `af_alloy` (American female), `am_adam` (American male), `bf_emma` (British female), `bm_george` (British male). Fully offline, no API limits. Generates audio + chapter timestamps in one pass.\n\n### gTTS (free, basic)\n\n```shell\ngtts-cli \"Text to speak\" --lang en --output output.mp3\ngtts-cli -f input.txt --lang en --output output.mp3\n```\n\n### Google Cloud TTS\n\n```python\nfrom google.cloud import texttospeech\nclient = texttospeech.TextToSpeechClient()\ninput_text = texttospeech.SynthesisInput(text=\"Text\")\nvoice = texttospeech.VoiceSelectionParams(language_code=\"en-US\", name=\"en-US-Neural2-F\")\nconfig = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3)\nresp = client.synthesize_speech(input=input_text, voice=voice, audio_config=config)\nwith open(\"output.mp3\", \"wb\") as f:\n    f.write(resp.audio_content)\n```\n\n## Text sanitization for TTS\n\nBefore sending text to any TTS engine, clean it:\n\n- Strip markdown: `**bold**` -> `bold`, `# heading` -> `heading`\n- Remove hashtags, emojis, and non-speech artifacts\n- Expand abbreviations that sound wrong when spoken aloud\n- Replace em dashes `—` with hyphens `-` to avoid encoding issues in shell commands\n- Remove URLs from the spoken text (mention them as \"link in the description\" instead)\n\n## Audio assembly with ffmpeg\n\nAll content recipes generate multiple segments that must be joined into a single file; the segment boundaries become chapters in the timeline.\n\nThe snippets below use `/tmp/` and bash syntax (heredocs) for brevity — on Windows, use the session temp directory (`%TEMP%` or Python's `tempfile.gettempdir()`) instead of `/tmp/`, and write file lists with Python or an editor rather than `cat << EOF`.\n\n### Generate silence\n\n```shell\n# 1.5 seconds (transitions between sections)\nffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 1.5 -q:a 9 -acodec libmp3lame /tmp/silence_1.5s.mp3\n\n# 3 seconds (recall pauses for language drills)\nffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 3 -q:a 9 -acodec libmp3lame /tmp/silence_3s.mp3\n\n# 5 seconds (speaking practice pauses)\nffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 5 -q:a 9 -acodec libmp3lame /tmp/silence_5s.mp3\n```\n\n### Concatenate segments\n\n```shell\n# Build a file list (order matters)\ncat > /tmp/segments.txt << 'EOF'\nfile 'segment_01.mp3'\nfile 'silence_1.5s.mp3'\nfile 'segment_02.mp3'\nfile 'silence_1.5s.mp3'\nfile 'segment_03.mp3'\nEOF\n\n# Concatenate — re-encode rather than `-c copy`. Stream-copying small MP3\n# segments yields non-monotonic frame timestamps that `loudnorm` silently\n# drops audio on. The explicit `-ar 44100 -ac 1` also normalizes any stray\n# stereo/48kHz segment to the common format so concat doesn't break.\nffmpeg -f concat -safe 0 -i /tmp/segments.txt -ar 44100 -ac 1 -c:a libmp3lame -b:a 192k /tmp/output.mp3\n```\n\n### Normalize volume\n\nAlways normalize after concatenation — different TTS segments may have different levels:\n\n```shell\nffmpeg -i /tmp/output.mp3 -af loudnorm /tmp/output_normalized.mp3\n```\n\n### Convert formats\n\nIf the TTS outputs a format other than `.mp3`, `.m4a`, `.wav`, or `.ogg`, convert before upload:\n\n```shell\nffmpeg -i input.aiff -codec:a libmp3lame -qscale:a 2 output.mp3\nffmpeg -i input.webm -codec:a libmp3lame -qscale:a 2 output.mp3\n```\n\n### Get segment duration (for timeline timestamps)\n\n```shell\nffprobe -v error -show_entries format=duration -of csv=p=0 segment.mp3\n```\n\n### Timeline timestamp calculation\n\nAfter generating all segments, build `timeline.json` by walking the file list, summing durations for chapter start times, and placing image/link companions inside each chapter's window. The only backend rule for companions is that they do not overlap with each other — chapter windows are independent.\n\n```python\nimport json, subprocess\nfrom pathlib import Path\n\ndef ms(path):\n    out = subprocess.check_output(\n        ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',\n         '-of', 'csv=p=0', str(path)], text=True).strip()\n    return int(float(out) * 1000)\n\n# Each segment: (chapter_title, audio_file, companions)\n# companions is a list of dicts:\n#   {\"image\": \"img_01_a.jpg\", \"url\": \"...\", \"title\": \"...\"}  -- image companion\n#   {\"link\":  \"https://...\"}                                  -- external link\n#   {\"spotify_entity\": \"spotify:track:4uLU6hMCjMI75M1A2tKUQC\"}  -- Spotify card\n#\n# When a spotify_entity is used for a track/album/artist, do NOT also add an\n# image companion with that entity's artwork — the card already renders it.\nsegments = [\n    (\"Introduction\",   \"segment_01.mp3\", []),\n    (\"Chapter A\",      \"segment_02.mp3\", [\n        {\"spotify_entity\": \"spotify:track:4uLU6hMCjMI75M1A2tKUQC\"},\n        {\"link\":  \"https://example.com/article-1\"},\n    ]),\n    (\"Chapter B\",      \"segment_03.mp3\", [\n        {\"image\": \"img_03_a.jpg\", \"url\": \"https://example.com/article-2\", \"title\": \"Source photo\"},\n    ]),\n    (\"Sign-off\",       \"segment_04.mp3\", []),\n]\nsilence_ms = ms('silence_1.5s.mp3')\n\nitems = []\ncursor = 0\nfor title, audio, companions in segments:\n    items.append({\"chapter\": {\"title\": title, \"start_time_ms\": cursor}})\n    dur = ms(audio)\n    # Distribute companions evenly inside the chapter window, with a 500 ms buffer.\n    if companions:\n        usable = dur - 500 * (len(companions) + 1)\n        slot = max(usable // len(companions), 1000)\n        for i, c in enumerate(companions):\n            start = cursor + 500 + i * (slot + 500)\n            duration = slot\n            if \"spotify_entity\" in c:\n                items.append({\"spotify_entity\": {\"start_time_ms\": start, \"duration_ms\": duration, \"uri\": c[\"spotify_entity\"]}})\n            elif \"image\" in c:\n                item = {\"image\": {\"start_time_ms\": start, \"duration_ms\": duration, \"image\": c[\"image\"]}}\n                if c.get(\"url\"):   item[\"image\"][\"url\"] = c[\"url\"]\n                if c.get(\"title\"): item[\"image\"][\"title\"] = c[\"title\"]\n                items.append(item)\n            elif \"link\" in c:\n                items.append({\"link\": {\"start_time_ms\": start, \"duration_ms\": duration, \"url\": c[\"link\"]}})\n    cursor += dur + silence_ms\n\n# Every chapter's start_time_ms must be strictly less than the assembled\n# audio duration; nothing downstream can verify this.\nfinal_ms = ms('episode.mp3')\nlast_chapter_ms = max(it[\"chapter\"][\"start_time_ms\"] for it in items if \"chapter\" in it)\nassert last_chapter_ms < final_ms, (\n    f\"last chapter at {last_chapter_ms} ms >= episode duration {final_ms} ms\"\n)\n\nPath('timeline.json').write_text(json.dumps({\"items\": items}, indent=2))\n```\n\nThe agent passes `timeline.json` to `save-to-spotify --json timeline set --episode-id <EP_ID> --from-file timeline.json`. The CLI uploads each local image file to Spotify's image store and swaps the path for the returned upload token before PUT-ing the timeline.\n\n## Standard workflow (used by all recipes)\n\n```\n1. User provides: topic + sources + voice preferences + companion-image source (sourced / AI-generated / mixed / skip)\n2. Agent writes: structured script broken into segments, with source URL(s) and image hint per segment\n3. Agent generates: one audio file per segment via TTS provider\n4. Agent generates: silence files for pauses/transitions\n5. Agent assembles: concatenate segments into single .mp3\n6. Agent normalizes: volume levels\n7. Agent gathers companion images: download from source and/or generate with DALL-E/SD\n8. Agent calculates: chapter timestamps from segment durations\n9. Agent builds: timeline.json with chapters + spotify_entity companions + link companions (source URLs) + image companions\n10. Agent saves: save-to-spotify --json upload ...\n11. Agent sets timeline: save-to-spotify --json timeline set ...\n12. Agent polls: episodes status until READY\n```\n\nRecipes define steps 1-2 (what to write, which URLs and images to gather). This reference covers steps 3-9. The main SKILL.md covers steps 10-12.\n\nFile v0.2.0:references/cli-usage.md\n\n# Save to Spotify\n\n## CLI Usage\n\nReference for the `save-to-spotify` binary: installation, authentication, commands, flags, JSON mode, error handling, and common agent workflows.\n\n## Installation\n\n### One-line install (recommended)\n\n```shell\ncurl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\nDetects OS and architecture, downloads the binary from GitHub Releases, verifies the SHA256 checksum, and installs to `/usr/local/bin` (or `~/.local/bin` if not writable).\n\nPin a version or change the install directory:\n\n```shell\n# Specific version\ncurl -fsSL https://saveto.spotify.com/install.sh | bash -s -- --version 0.2.0\n\n# Custom directory\ncurl -fsSL https://saveto.spotify.com/install.sh | bash -s -- --dir ~/.local/bin\n\n# Via environment variables\nSAVE_TO_SPOTIFY_VERSION=0.2.0 SAVE_TO_SPOTIFY_INSTALL_DIR=~/.local/bin \\\n  curl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\n### Download a binary manually\n\nGrab the latest release for the platform from [releases](https://github.com/spotify/save-to-spotify/releases):\n\n```shell\n# macOS Apple Silicon\ngh release download --repo spotify/save-to-spotify --pattern \"save-to-spotify-darwin-arm64\"\nchmod +x save-to-spotify-darwin-arm64\nsudo mv save-to-spotify-darwin-arm64 /usr/local/bin/save-to-spotify\n\n# macOS Intel\ngh release download --repo spotify/save-to-spotify --pattern \"save-to-spotify-darwin-amd64\"\nchmod +x save-to-spotify-darwin-amd64\nsudo mv save-to-spotify-darwin-amd64 /usr/local/bin/save-to-spotify\n\n# Linux x86_64\ngh release download --repo spotify/save-to-spotify --pattern \"save-to-spotify-linux-amd64\"\nchmod +x save-to-spotify-linux-amd64\nsudo mv save-to-spotify-linux-amd64 /usr/local/bin/save-to-spotify\n```\n\n### Build from source\n\nRequires Go 1.21+.\n\n```shell\ngit clone https://github.com/spotify/save-to-spotify.git && cd save-to-spotify\ngo build -ldflags \"-X github.com/spotify/save-to-spotify/cmd.commit=$(git rev-parse --short HEAD)\" \\\n  -o save-to-spotify .\nsudo mv save-to-spotify /usr/local/bin/\n```\n\nVerify installation:\n\n```shell\nsave-to-spotify version\n```\n\n## First-run setup\n\nAfter installation, run the guided setup to authenticate and detect TTS engines:\n\n```shell\nsave-to-spotify setup\n```\n\nThis handles auth + TTS detection in one pass. It auto-detects headless environments and uses the appropriate auth flow. After setup, verify everything is ready:\n\n```shell\nsave-to-spotify doctor\n```\n\nReports binary, auth, TTS engines, and ffmpeg status. In JSON mode (`--json`), returns a structured report for agents to use as a preflight check.\n\n## TTS engine management\n\nThe CLI detects and manages TTS engines used for audio generation:\n\n```shell\n# Check which engines are available\nsave-to-spotify tts status\n\n# Install Kokoro (free, local, no API key)\nsave-to-spotify tts setup\n\n# Install or configure a specific engine\nsave-to-spotify tts setup --engine openai\n\n# List voices for an engine\nsave-to-spotify tts voices --engine kokoro\n\n# Test a voice with a sample phrase\nsave-to-spotify tts test --engine kokoro --voice af_heart\n\n# Get or set the default engine\nsave-to-spotify tts default\nsave-to-spotify tts default openai\n\n# Register a custom engine\nsave-to-spotify tts add --name gemini --check-cmd 'python3 -c \"import google.genai\"' --key-env GEMINI_API_KEY   # use python instead of python3 on Windows\n\n# Remove a custom engine\nsave-to-spotify tts remove gemini\n```\n\nThree engines are built-in: **Kokoro** (free, local), **OpenAI TTS**, and **ElevenLabs**. Register any additional engine with `tts add`.\n\n## Authentication\n\nThe user must authenticate once before any save. The CLI uses OAuth 2.0 with PKCE -- no client secret needed.\n\n### Interactive (user has a browser)\n\n```shell\nsave-to-spotify auth login\n```\n\nThis opens the browser, the user approves, and a token is saved to `~/.config/save-to-spotify/token.json` (this path holds on every platform, including Windows — `%USERPROFILE%\\.config\\...` — the CLI does not use APPDATA).\n\n### Headless (remote server, CI, or agent environment)\n\n```shell\nsave-to-spotify auth login --no-browser\n```\n\nThis prints an authorization URL. The user visits it in any browser, approves, and pastes the redirect URL back into the terminal. The redirect URL will look like `http://127.0.0.1:8085/callback?code=...&state=...` -- it's fine if the page shows a connection error, the URL itself is what matters.\n\n### Environment token (skip OAuth entirely)\n\nIf the user already has a Spotify access token (e.g. from another tool or CI secret):\n\n```shell\nexport SAVE_TO_SPOTIFY_AUTH_TOKEN=\"BQD...\"\n```\n\nWhen this env var is set, the CLI uses it directly with no file I/O and no token refresh. The token must be kept fresh externally.\n\n### Check auth status\n\n```shell\nsave-to-spotify --json auth status\n```\n\nReturns `{\"authenticated\": true, \"token_valid\": true, ...}` or `{\"authenticated\": false}`.\n\nToken refresh is automatic -- if the saved token is expired, the CLI refreshes it silently on the next command. No action needed unless the refresh token itself is revoked, in which case the user must `auth login` again.\n\n### Print the access token\n\n```shell\nsave-to-spotify token\n```\n\nPrints the current access token to stdout -- directly usable as a **Spotify Web API bearer** for requests against `api.spotify.com`. Useful for catalog lookups (searching album/track URIs, fetching release metadata) from inside recipes. Exits non-zero and prints a diagnostic to stderr when the stored token cannot be refreshed. Always check the exit code before piping into an `Authorization` header -- otherwise an empty token produces a misleading HTTP 400 from Spotify rather than a clean auth error.\n\nSee [spotify-api.md](spotify-api.md) for the official `developer.spotify.com` references, OpenAPI spec URL, endpoint patterns, and URI-resolution helpers.\n\n## Saving media\n\n### Quick save (recommended for most cases)\n\nThe `upload` command is the simplest path -- one command to create an episode and save the file:\n\n```shell\nsave-to-spotify --json upload recording.mp3 \\\n  --title \"My Recording\" \\\n  --summary \"Description here\" \\\n  --image cover.jpg\n```\n\nOutput:\n```json\n{\"episode_uri\": \"spotify:episode:abc123\", \"title\": \"My Recording\", \"status\": \"PROCESSING\"}\n```\n\nIf the user has no shows yet, one is auto-created as \"My Podcast\".\n\n### Save to a specific show\n\n```shell\nsave-to-spotify --json upload lecture.m4a \\\n  --title \"Lecture 3: Distributed Systems\" \\\n  --summary \"CS 307 Spring 2024\" \\\n  --show-id spotify:show:xyz789 \\\n  --image cover.jpg\n```\n\nWhen `--show-id` is omitted, the CLI uses the most recently created show.\n\n### Create a new show and save in one step\n\n```shell\nsave-to-spotify --json upload keynote.mp3 \\\n  --title \"2024 Keynote\" \\\n  --summary \"Opening talk by <speaker>\" \\\n  --new-show \"Conference Talks\" \\\n  --image cover.jpg\n```\n\n`--new-show` and `--show-id` are mutually exclusive.\n\n### Granular episode creation\n\nFor more control, use `episodes create` instead of `upload`:\n\n```shell\nsave-to-spotify --json episodes create \\\n  --title \"Episode Title\" \\\n  --file audio.mp3 \\\n  --summary \"Episode description\" \\\n  --show-id spotify:show:xyz789 \\\n  --image episode-cover.jpg \\\n  --language en\n```\n\nThe difference: `episodes create` requires `--summary` and `--file` as explicit flags (not positional), and does not support `--new-show`.\n\n## Supported file formats\n\n| Extension | Type | MIME |\n|-----------|------|------|\n| `.mp3` | Audio | `audio/mpeg` |\n| `.m4a` | Audio | `audio/mp4` |\n| `.wav` | Audio | `audio/wav` |\n| `.ogg` | Audio | `audio/ogg` |\n\n**Maximum file size: 1 GB.**\n\n## Managing shows\n\nShows are folders that group episodes. Every episode belongs to exactly one show.\n\n```shell\n# List all shows\nsave-to-spotify --json shows\n\n# Create a show (always include --image)\nsave-to-spotify --json shows create --title \"My Lectures\" --summary \"University recordings\" --image cover.jpg\n\n# Get show details\nsave-to-spotify --json shows get <show_id>\n\n# Delete a show (and all its episodes)\nsave-to-spotify --json shows delete <show_id>\n```\n\n`save-to-spotify --json shows` should be the first show-management command you run. Check what already exists before creating a new show.\n\n### Playback control\n\n`shows create` accepts `--playback-control <mode>` (currently `chapter-skip`) to set a show's skip-forward behavior at creation time. The response includes `playback_control` only when the backend applied the setting — if the field is absent, the value was dropped and the show has default controls. For behavior, constraints, and when to enable it, defer to the `configure-chapter-skip` skill.\n\n## Managing episodes\n\n```shell\n# List episodes in a show\nsave-to-spotify --json episodes --show-id <show_id>\n\n# Check episode readiness\nsave-to-spotify --json episodes status <episode_id>\n\n# Delete an episode\nsave-to-spotify --json episodes delete <episode_id>\n```\n\nShow and episode metadata is immutable after creation. To change a title or description, delete and re-create.\n\n### Episode readiness\n\nAfter saving, poll for readiness before sharing:\n\n```shell\nsave-to-spotify --json episodes status <EPISODE_ID>\n```\n\nOutput:\n```json\n{\"episode_uri\": \"spotify:episode:abc123\", \"readiness\": \"READY\"}\n```\n\nReadiness values:\n- `READY` -- playable on Spotify\n- `PROCESSING` -- still being processed (wait and retry)\n- `FAILED` -- processing failed; check the episode metadata and re-save if needed\n\nMost episodes are ready within 1-2 minutes. For large files, allow up to 5 minutes.\n\n## IDs and URIs\n\nThe CLI accepts both bare IDs and full Spotify URIs interchangeably, but agents should prefer the full Spotify URI form whenever possible:\n- `abc123def456` (bare ID)\n- `spotify:show:abc123def456` (full URI)\n\nJSON output always includes the full URI form.\n\n## JSON mode\n\n**Always use `--json` when operating as an agent.** It must appear before the command:\n\n```shell\nsave-to-spotify --json <command> [flags]\n```\n\nIn JSON mode:\n- All output is valid JSON on stdout\n- Errors are `{\"error\": \"message\"}` on stdout with exit code 1\n- Progress bars and activity indicators are suppressed\n- Informational messages to stderr are suppressed\n\nWithout `--json`, output is human-readable text.\n\n## Timeout control\n\nThe default API timeout is 30 seconds. For large file uploads, the upload itself has no timeout (separate HTTP client), but the episode creation API call does. Override if needed:\n\n```shell\nsave-to-spotify --json --timeout 2m upload large-recording.mp3 --title \"Long Episode\" --image cover.jpg\n```\n\nOr via environment variable:\n```shell\nexport SAVE_TO_SPOTIFY_TIMEOUT=2m\n```\n\n## Common agent workflows\n\nThese snippets use bash syntax (command substitution, `while` loops, parameter expansion). On Windows, run them in Git Bash or translate to PowerShell/Python — don't paste them into cmd or PowerShell as-is.\n\n### Create a rich-timeline episode end-to-end\n\n```shell\n# 1. Generate cover image (MANDATORY — see cover-image.md)\npython3 generate_cover.py\n\n# 2. Save with cover image\nRESULT=$(save-to-spotify --json upload episode.mp3 \\\n  --title \"Daily Digest - April 15, 2026\" \\\n  --summary \"$(cat description.txt)\" \\\n  --image cover.jpg)\nEP_URI=$(echo \"$RESULT\" | jq -r .episode_uri)\nEP_ID=${EP_URI#spotify:episode:}\n\n# 3. Set timeline (chapters + image/link/Spotify companions in one call)\nsave-to-spotify --json timeline set --episode-id \"$EP_ID\" --from-file timeline.json\n\n# 4. Poll until ready\nwhile true; do\n  STATUS=$(save-to-spotify --json episodes status \"$EP_ID\" | jq -r .readiness)\n  [ \"$STATUS\" = \"READY\" ] && break\n  [ \"$STATUS\" = \"FAILED\" ] && echo \"Processing failed\" && exit 1\n  sleep 15\ndone\n\necho \"Episode ready: $EP_URI\"\n```\n\n### Save a single file\n\n```shell\nURI=$(save-to-spotify --json upload recording.mp3 --title \"My Recording\" --summary \"A recording\" --image cover.jpg | jq -r .episode_uri)\nEP_ID=${URI#spotify:episode:}\n\nwhile true; do\n  STATUS=$(save-to-spotify --json episodes status \"$EP_ID\" | jq -r .readiness)\n  [ \"$STATUS\" = \"READY\" ] && break\n  [ \"$STATUS\" = \"FAILED\" ] && echo \"Processing failed\" && exit 1\n  sleep 15\ndone\n```\n\n### Batch save multiple files to one show\n\n```shell\nSHOW_URI=$(save-to-spotify --json shows create --title \"Conference Talks 2024\" --image show-cover.jpg | jq -r .show_uri)\n\nfor f in talks/*.mp3; do\n  TITLE=$(basename \"$f\" .mp3 | tr '-' ' ')\n  save-to-spotify --json upload \"$f\" --title \"$TITLE\" --summary \"Conference talk\" --show-id \"$SHOW_URI\" --image cover.jpg\ndone\n```\n\n## Error handling\n\nWith `--json`, every command returns exit code 0 on success and exit code 1 on error. Errors are returned as `{\"error\": \"message\"}`. **Always check for errors after each command.**\n\n```bash\nRESULT=$(save-to-spotify --json upload episode.mp3 --title \"My Episode\" --image cover.jpg)\nif echo \"$RESULT\" | jq -e .error > /dev/null 2>&1; then\n  echo \"Failed: $(echo \"$RESULT\" | jq -r .error)\"\n  exit 1\nfi\nEP_URI=$(echo \"$RESULT\" | jq -r .episode_uri)\nEP_ID=${EP_URI#spotify:episode:}\n\nwhile true; do\n  STATUS=$(save-to-spotify --json episodes status \"$EP_ID\")\n  READINESS=$(echo \"$STATUS\" | jq -r .readiness)\n  [ \"$READINESS\" = \"READY\" ] && break\n  [ \"$READINESS\" = \"FAILED\" ] && echo \"Processing failed\" && exit 1\n  sleep 15\ndone\n```\n\nCommon errors:\n\n| Error | Cause | Action |\n|-------|-------|--------|\n| `not authenticated` | No token file | Run `auth login` |\n| `token refresh failed` | Refresh token revoked | Run `auth login` again |\n| `unsupported file extension` | Wrong file type | Convert to a supported format |\n| `file too large` | Over 1 GB | Compress or split the file |\n| `image too large` | Over 1 MB | Resize the image |\n| `API error (429)` | Rate limited | Wait and retry after a delay |\n| `API error (401)` | Token expired mid-request | Retry (auto-refresh will kick in) |\n| `API error (403)` | Insufficient permissions | Re-authenticate with `auth login` |\n| `--new-show and --show-id are mutually exclusive` | Conflicting flags | Use one or the other |\n\n## Troubleshooting\n\n### Episode not appearing after saving\nUse `episodes status <id>` to check readiness. Processing can take a few minutes after the save completes.\n\n### Image upload fails\nImages must be `.jpg`, `.jpeg`, or `.png`, max 1 MB, with valid magic bytes (actual JPEG/PNG content, not just a renamed file).\n\n### \"unsupported file extension\" error\nOnly `.mp3`, `.m4a`, `.wav`, and `.ogg` are supported. Convert other formats first (e.g., `ffmpeg -i input.webm output.mp3`).\n\n## Environment variables reference\n\n| Variable | Purpose |\n|----------|---------|\n| `SAVE_TO_SPOTIFY_AUTH_TOKEN` | Bearer token; skips OAuth entirely (no refresh, no expiry tracking) |\n| `SAVE_TO_SPOTIFY_BACKEND_URL` | Override backend URL |\n| `SAVE_TO_SPOTIFY_TIMEOUT` | API timeout duration (e.g. `30s`, `2m`) |\n\n## Content policy\n\n- **No copyrighted music:** Content that is classified as music by Spotify's moderation system will be taken down.\n- **Moderation applies:** Standard Spotify podcast moderation policies apply. Content that violates policies will be removed and the user will be notified via email.\n- **No sensitive data:** Do not save content containing passwords, credentials, PII of others, or confidential business information. The content is stored on Spotify's servers.\n- **Save limits apply:** There are per-user rate limits on saves (per hour and per week). If a limit is hit, the API returns an error -- wait and retry later.\n- **Never fabricate URLs:** Every source link in episode descriptions must come from actual content sourcing. If a URL isn't found, omit the link -- never invent one.\n\nFile v0.2.0:references/content-quality.md\n\n# Content Quality & Editorial Guidelines\n\nReference for writing scripts that sound good when spoken aloud. Applies to all content recipes. Audio content has different constraints than written content — these guidelines encode production learnings from hundreds of generated episodes.\n\n## The golden rule: write for the ear\n\nText is forgiving. Readers can re-read a sentence, scan ahead, or slow down. Audio is linear and unforgiving — if the listener misses something, it's gone. Every line must land on first hearing.\n\n## Voice and tone\n\n### Match voice to format\n\n| Format | Voice | Energy |\n|--------|-------|--------|\n| Factual summary | Sharp analyst | Confident, declarative, varied energy per segment |\n| Travel guide | Well-traveled friend | Conversational, specific, enthusiastic but honest |\n| Explainer | Patient teacher | Clear, builds from simple to complex |\n| Daily briefing | Briefing companion | Brisk, clear, energetic |\n| Language lesson | Encouraging tutor | Patient, repetitive, affirming |\n\n### Universal voice principles\n\n- **Declarative beats tentative.** \"This matters because...\" not \"This could potentially be interesting...\"\n- **Concrete beats abstract.** \"They're trying to lock in developers\" not \"they're enhancing ecosystem engagement\"\n- **Short beats long.** Mix short punchy sentences with longer analytical ones. Never more than two long sentences in a row\n- **Active beats passive.** \"Sweden cut Chinese research ties\" not \"Chinese research ties were cut by Sweden\"\n- **Specific beats vague.** \"Expect to pay 15 euros for a main course\" not \"food is reasonably priced\"\n\n## Transitions between segments\n\nThis is the single most important audio production skill. **Listeners cannot see segment boundaries.** Without clear verbal signals, segments blur together into mush.\n\nEvery segment must open with a clear transition. Vary these — using the same transition every time is as bad as having none:\n\n- **Subject lead:** \"Keychron just open-sourced their hardware...\" / \"The housing market is shifting...\"\n- **Framing hooks:** \"This is huge.\" / \"Here's what caught my eye.\" / \"Quick note:\"\n- **Topic shifts:** \"On the security side...\" / \"Turning to the economy...\"\n- **Related bridges:** \"That same dynamic is playing out in...\"\n- **Simple transitions:** \"Moving on.\" / \"Meanwhile...\" / \"Next up...\"\n- **Travel transitions:** \"From there, head south to...\" / \"After lunch, the plan takes you to...\"\n- **Time markers:** \"Back in 1927...\" / \"Fast forward to today...\"\n\n## Person context\n\nWhen introducing someone by name, briefly explain WHO they are. Listeners don't have hyperlinks — they can't look someone up mid-sentence.\n\n- \"Marie Curie, the first person to win Nobel Prizes in two sciences...\"\n- \"Ada Lovelace, the 19th-century mathematician often called the first computer programmer...\"\n- \"John Constable, one of England's greatest landscape painters...\"\n\nOne clause, not a bio. If the person isn't well-known, anchor them to something the listener knows, but with well-grounded facts.\n\n## Depth control\n\nNot every item deserves equal time. Classify content by importance and adjust depth accordingly:\n\n| Tier | Sentences | Approach |\n|------|-----------|----------|\n| **A** (major) | 4-6 | What happened → strategic significance → added context → implication |\n| **B** (notable) | 3-4 | What happened → why interesting → one sentence context |\n| **C** (brief) | 1 | State the fact, move on. No analysis, no context |\n\nUse Tier C aggressively for: thin engagement-bait, celebrity amplification with no insight, opinion with no new information, promotional announcements. High engagement alone does NOT justify a high tier.\n\n## Default-off content categories\n\nThese skills MUST NOT produce:\n\n- Instructions for self-harm or suicide methods\n- Instructions for drug synthesis, weapon construction, or similar harmful material\n- Sexually explicit content\n- Sexual content involving minors\n- Promotion or celebration of violent extremism\n- Instructions for harming others\n- Targeted harassment content involving real individuals\n\n## Sensitive-topic checkpoint\n\nSome topics deserve an explicit \"are you sure?\" before producing an authoritative-sounding episode. If the topic, source set, or destination touches:\n\n- Named living individuals' alleged misconduct\n- Active political, religious, or cultural controversy\n- Advisory content (medical treatment, investment decisions, legal processes)\n\n— restate the planned angle and wait for explicit *go*. Not a refusal; a confirmation. Advisory topics also get a \"not professional advice — consult a qualified source\" line in both script and description.\n\n## Describing visual content\n\nPodcast listeners can't see anything. **You are their eyes.** When the source material is visual (images, charts, GIFs, screenshots):\n\n- Describe what's happening: \"A ring of rotating blades spirals around the log, stripping bark in ribbons\"\n- Include emotional reaction: \"It's almost hypnotic to watch\"\n- Reference the source: \"In a post on Example Site this week...\"\n- Mention where to see it: \"Link to the original is in the show notes\"\n\nNever skip describing visual content — if it matters to the segment, say what's in it.\n\n## Things to AVOID\n\nThese patterns make AI-generated audio content sound generic and robotic:\n\n- **\"Let's dive in\"** / \"Without further ado\" / \"Buckle up\" — cliché openings\n- **\"garnering over X likes\"** / \"racking up views\" — engagement metrics as filler\n- **Starting a segment with someone's full display name** as the literal first word\n- **Metrics without meaning** — only mention numbers when they're remarkable relative to context\n- **Press-release language** — \"We're thrilled to announce\" / \"innovative solutions\"\n- **Brochure language** — \"A hidden gem\" / \"A must-visit destination\" / \"Something for everyone\"\n- **Trailing summaries** — don't recap what you just said. The listener heard it\n- **Equal-time syndrome** — not every item needs the same depth. Be ruthless about tier assignment\n- **Verbatim reproduction of sources** — paraphrase the substance. Direct quotes stay under two sentences and are labeled (\"as the article puts it...\")\n\n## Pacing and silence\n\nDon't fear strategic silence. Pauses between segments give the listener time to absorb.\n\n- **300ms** — minimum gap between segments within a section\n- **500ms+** — between major topic shifts or chapter boundaries\n- **1-2 seconds** — before a significant reveal or after an emotional moment\n- **3-5 seconds** — for active recall pauses (flashcards, language drills)\n\nVary the pacing within segments too: slow down for important analysis or emotional moments, keep it brisk for roundups and minor items.\n\n## Self-critique checklist\n\nAfter drafting any script, evaluate against these criteria:\n\n1. **Does each segment explain WHY, not just WHAT?** (digests) / **Is each stop actually useful?** (travel)\n2. **Does the voice stay consistent throughout?** No sudden shifts from casual to formal\n3. **Does each segment clearly signal a transition?** Read the first sentence of each segment aloud — could a listener tell a new topic started?\n4. **Is the pacing varied?** Important parts get more space, minor ones stay brief\n5. **Would this sound natural read aloud?** Read it out loud. If you stumble, rewrite\n6. **Are sources attributed naturally?** \"According to...\" not footnotes\n7. **Is visual content described?** For every image referenced, does the script say what's in it?\n\nThe critique must NEVER suggest cutting or removing any segment — segment-to-source integrity is sacred. Instead, suggest how to improve the segment's framing, analysis, or writing.\n\n## Source linking\n\nEvery piece of source content should have its original URL captured and preserved through the pipeline. The URL appears in the episode description as a clickable link.\n\n- **An episode without source links is significantly less useful** — listeners learn about a segment but can't follow up\n- **Never fabricate URLs** — if a URL isn't found, omit the link\n- **Never fabricate facts** — if you can't verify a claim and aren't genuinely confident, skip it or say \"the sources don't cover this.\" Applies to quotes, statistics, dates, studies, and prices. Same posture as \"Never fabricate URLs\", extended\n- **AI-generated disclosure** — end every description with a content-describing line: *\"AI-narrated audio assembled from the sources in this description\"* or, for knowledge-only episodes, *\"AI-narrated audio — verify key facts before acting.\"*\n- **Label links by platform:** \"read on Example\", \"see on Example.org\", \"visit site\"\n- **Front-load the description summary** — the first 1-2 sentences show as a preview in podcast apps. Don't waste them on boilerplate\n\n## Episode naming\n\n- **Show name:** Short, memorable, searchable. Avoid generic names that get lost in search results\n- **Episode title:** Include the date or episode number for recurring shows. Keep under ~60 characters to avoid truncation\n- **Consistent format:** Pick a pattern and stick with it: \"Show Name - Date\", \"Show Name: Topic\"\n\n## Episode length\n\nMatch length to content density — don't pad to hit a target:\n\n| Format | Typical length |\n|--------|---------------|\n| Daily digest | 3-6 min |\n| Weekly digest | 5-10 min |\n| Short fiction | 5-15 min |\n| Travel guide (per trip) | 5-10 min |\n| Deep dive / explainer | 15-30 min |\n| Daily briefing | 5-12 min |\n| Language lesson | 10-20 min |\n\nAI-narrated content is denser than conversational podcasts — listeners absorb it faster. A tight 4-minute episode with strong content beats a padded 10-minute one.\n\nFile v0.2.0:references/cover-image.md\n\n# Cover Image\n\n**Every show & every episode MUST have a cover image.** Never save without `--image`.\n\n**Format:** JPG or PNG, max 1 MB, 1400x1400 square.\n\n## Paths (priority order)\n\n1. **User-provided** — only when user supplies an image file. Skip otherwise. Resize to 1400x1400, apply strong overlay, add typography (unless user opts out).\n2. **AI-generated** — default when a known image generation API is available (DALL-E, Stable Diffusion). Never use unvetted services. No overlay (prompt reserves negative space).\n3. **CDN artwork** — terminal fallback. No overlay (built-in legibility). Always available, cannot fail.\n\n**Fallthrough:** AI fails → CDN. CDN is the terminal fallback.\n\n### Path 1: User-provided\n\n**Skip this path entirely if the user did not provide an image file.** Do not generate a substitute image — proceed to Path 2.\n\nAccept JPG/PNG at any aspect ratio. Reject if below 600x600 or corrupted. Crop to square, resize to 1400x1400, compress to <1 MB (JPG 90%). Apply strong overlay, then add typography unless user opts out.\n\n**Never:** apply filters, AI enhancement, generate a stand-in image, or override with an agent-generated image.\n\n### Path 2: AI-generated (default)\n\n**Only use known image generation APIs:** DALL-E (OpenAI), Stable Diffusion, or Midjourney. Never use unvetted services (e.g., pollinations.ai) — quality is unreliable and licensing unclear. If no known API key is available, skip to CDN (Path 3).\n\n**Never render text with the model** — composite with Pillow afterwards.\n\n**Prompt pattern:** `\"{style} of {concrete subject}, {composition}, {palette}, square composition, negative space in lower third, no text, no logos\"`\n\nExample: topic \"Weekly Stockholm news briefing\" → `\"Minimalist illustration of a Stockholm rooftop skyline at dusk, muted blue-grey palette, square composition, negative space in lower third, no text, no logos\"`\n\n**Every prompt must include:** a specific concrete subject (not a concept), a composition direction, a palette descriptor, \"square composition\", \"negative space in lower third\", \"no text, no logos\".\n\n**Style:** photorealistic or clean illustration only. No collage, 3D renders, faces, or AI-generated likenesses.\n\n**Never produce:** podcast-meta imagery (mics, headphones), stock cliches (handshakes, lightbulbs), neon/HDR, baked-in text or logos.\n\n**Skip AI if:** topic is abstract, involves real named people, refers to events after the model's training cutoff, or user requested otherwise.\n\nSee [timeline.md](timeline.md) for DALL-E / Stable Diffusion code examples.\n\n### Path 3: CDN artwork (terminal fallback)\n\nPre-designed base artwork with Pillow typography. No overlay needed. 20 variants (`uts-01.png` through `uts-20.png`), selected by hash of show name. Always available, cannot fail.\n\n**CDN endpoint:** `https://save-to-spotify.spotifycdn.com/assets/uts-{01..20}.png`\n\n## Typography\n\n**Mandatory** on every cover (unless user opted out in Path 1). Always composited with Pillow. Never rely on AI text rendering.\n\n**Default copy:** show name only. Add date/episode number only to disambiguate >1 episode per day.\n\n### Constraints\n\n- **One label only.** No subtitles, taglines, or descriptors.\n- **Max 3 lines.** If title doesn't fit, shorten: drop articles, use short forms. Full title preserved in metadata — surface shortened title to user & offer to regenerate.\n- **No widows.** Don't strand a single short word on its own line.\n- **Break on meaning.** Keep concepts together. Pick the split producing the most balanced line widths.\n\n### Font & RTL\n\n| Script | Font | Alignment |\n| --- | --- | --- |\n| Latin (default) | **Montserrat Bold** | bottom-left |\n| Arabic | **Tajawal Bold** | bottom-right |\n| Hebrew | **Noto Sans Hebrew Bold** | bottom-right |\n\nAll OFL-licensed Google Fonts. Downloaded and cached on first use (`~/.cache/save-to-spotify/fonts/`). Bold or heavier only. Never system defaults or decorative fonts.\n\n**RTL detection:** if any character has `unicodedata.bidirectional(ch) in ('R', 'AL', 'AN')`, use RTL font and right-alignment.\n\n**No reshaper libraries.** Do NOT use `arabic_reshaper` or `python-bidi` — modern fonts handle shaping natively in Pillow.\n\n### Colour & effects\n\n- **White text only.** No accent colours, no exceptions.\n- **No text effects.** No drop shadows, strokes, outlines, glows.\n\n## Pillow compositing recipe\n\nConstants and thresholds are authoritative — see code below for exact values.\n\n```python\nfrom PIL import Image, ImageDraw, ImageFont\nimport os, hashlib, unicodedata, urllib.request\n\nCANVAS = 1400\nMARGIN = 64\nMAX_TEXT_WIDTH = int((CANVAS - 2 * MARGIN) * 0.85)\nMAX_TEXT_HEIGHT = CANVAS - MARGIN - CANVAS // 2  # 636px\nMIN_FONT_SIZE = 100\nMAX_FONT_SIZE = 400\nLEADING_FACTOR = 0.97\n\nFONT_CACHE = os.path.join(os.path.expanduser(\"~\"), \".cache\", \"save-to-spotify\", \"fonts\")\nFONTS = {\n    \"latin\":  (\"Montserrat-Bold.ttf\",       \"https://raw.githubusercontent.com/JulietaUla/Montserrat/master/fonts/ttf/Montserrat-Bold.ttf\"),\n    \"arabic\": (\"Tajawal-Bold.ttf\",           \"https://raw.githubusercontent.com/google/fonts/main/ofl/tajawal/Tajawal-Bold.ttf\"),\n    \"hebrew\": (\"NotoSansHebrew-Bold.ttf\",    \"https://raw.githubusercontent.com/google/fonts/main/ofl/notosanshebrew/NotoSansHebrew-Bold.ttf\"),\n}\n\ndef detect_script(title):\n    for ch in title:\n        if '؀' <= ch <= 'ۿ' or 'ݐ' <= ch <= 'ݿ': return \"arabic\"\n        if '֐' <= ch <= '׿': return \"hebrew\"\n    return \"latin\"\n\ndef load_font(size, title=\"\"):\n    os.makedirs(FONT_CACHE, exist_ok=True)\n    fname, url = FONTS[detect_script(title)]\n    path = os.path.join(FONT_CACHE, fname)\n    if not os.path.exists(path):\n        urllib.request.urlretrieve(url, path)\n    return ImageFont.truetype(path, size)\n\ndef measure_line(font, text):\n    if not text: return (0, 0)\n    b = font.getbbox(text)\n    return b[2] - b[0], b[3] - b[1]\n\ndef _split_combos(words, n):\n    if n == 1: yield [words]; return\n    for i in range(1, len(words) - n + 2):\n        for rest in _split_combos(words[i:], n - 1):\n            yield [words[:i]] + rest\n\ndef break_lines(title, font):\n    words = title.split()\n    if not words: return [title]\n    best, best_d = None, float(\"inf\")\n    for n in range(1, min(len(words), 3) + 1):\n        for combo in _split_combos(words, n):\n            lines = [\" \".join(p) for p in combo if p]\n            if not lines: continue\n            ws = [measure_line(font, l)[0] for l in lines]\n            if max(ws) > MAX_TEXT_WIDTH: continue\n            d = max(ws) - min(ws)\n            if d < best_d: best_d, best = d, lines\n    return best or [title]\n\ndef fit_title(title):\n    if not title: title = \"Untitled\"\n    for sz in range(MAX_FONT_SIZE, MIN_FONT_SIZE - 1, -2):\n        font = load_font(sz, title)\n        lines = break_lines(title, font)\n        if len(lines) > 3: continue\n        if max(measure_line(font, l)[0] for l in lines) > MAX_TEXT_WIDTH: continue\n        lh = int(sz * LEADING_FACTOR)\n        total = lh * (len(lines) - 1) + font.getbbox(lines[-1])[3]\n        if total > MAX_TEXT_HEIGHT: continue\n        return font, lines, sz\n    f = load_font(MIN_FONT_SIZE, title)\n    return f, break_lines(title, f), MIN_FONT_SIZE\n\ndef composite_title(img, title):\n    draw = ImageDraw.Draw(img)\n    font, lines, sz = fit_title(title)\n    lh = int(sz * LEADING_FACTOR)\n    total = lh * (len(lines) - 1) + font.getbbox(lines[-1])[3]\n    y = max(CANVAS - MARGIN - total, CANVAS // 2)\n    rtl = detect_script(title) != \"latin\"\n    for line in lines:\n        x = CANVAS - MARGIN - measure_line(font, line)[0] if rtl else MARGIN\n        draw.text((x, y), line, font=font, fill=(255, 255, 255))\n        y += lh\n    return img\n\ndef strong_overlay(img):\n    ov = Image.new(\"RGBA\", img.size, (0, 0, 0, 0))\n    d = ImageDraw.Draw(ov)\n    start = int(CANVAS * 0.40)\n    for y in range(start, CANVAS):\n        d.line([(0, y), (CANVAS, y)], fill=(0, 0, 0, int((y - start) / (CANVAS - start) * 230)))\n    return Image.alpha_composite(img.convert(\"RGBA\"), ov).convert(\"RGB\")\n```\n\n## QA checklist\n\nVerify: 1400x1400 JPG/PNG <1 MB, typography present with correct font/alignment/white/margins, overlay on user-provided only, no faces/text/logos in AI output. If any check fails, fall through to next path.\n\nFile v0.2.0:references/episode-description.md\n\n# Episode Description Format\n\nReference for building the HTML description that appears in the Spotify show-notes panel. Spotify auto-links `(M:SS)` timestamps for in-app seeking.\n\n## Format\n\nThe description uses HTML `<p>` tags with timestamped entries on a single line (no literal newlines in the final string):\n\n```html\n<p>Summary of today's episode themes in 1-2 sentences.</p><p>(0:00) - Introduction</p><p>(0:18) - First segment title - <a href='https://example.com/article-1'>source</a></p><p>(1:42) - Second segment title - <a href='https://example.com/article-2'>source</a></p><p>(4:30) - Sign-off</p>\n```\n\n## Build it from the timeline\n\nRead chapter entries from `timeline.json` (ignore image, link, and `spotify_entity` companions — those appear in the player, not the show notes):\n\n```python\nimport json\n\ntimeline = json.load(open('timeline.json'))\nchapters = [item['chapter'] for item in timeline['items'] if 'chapter' in item]\n# source_links maps chapter title -> original article URL (from sourcing phase)\nsource_links = {\"Segment title\": \"https://source.url/article\"}\n\nparts = ['<p>Summary of episode themes.</p>']\nfor ch in chapters:\n    ms = ch['start_time_ms']\n    ts = f\"({ms // 60000}:{(ms % 60000) // 1000:02d})\"\n    title = ch['title']\n    url = source_links.get(title)\n    if url:\n        parts.append(f\"<p>{ts} - {title} - <a href='{url}'>source</a></p>\")\n    else:\n        parts.append(f\"<p>{ts} - {title}</p>\")\n\ndescription = ''.join(parts)\n```\n\n## Rules\n\n1. Every entry wrapped in its own `<p>...</p>` block\n2. Do NOT use `<br>` tags — they render as literal text on the Spotify desktop app\n3. Timestamps as plain text `(M:SS)` in parentheses — Spotify auto-links these for seeking. Do NOT wrap timestamps in `<a>` tags\n4. No leading zero on minutes, always two-digit seconds: `(0:05)`, `(1:42)`, `(12:30)`\n5. External source links use HTML anchors: `<a href='URL'>source</a>`\n6. Use single quotes inside `href` to avoid shell escaping issues\n7. Entire description must be a single line with NO literal newlines\n8. Start with a 1-2 sentence summary paragraph\n9. Use `-` (hyphen) not `—` (em dash) to avoid shell encoding issues\n10. Include Introduction and Sign-off entries (no source links for these)\n11. Every entry with a known source URL MUST include the link — never fabricate URLs\n12. `<b>`, `<i>` for emphasis (sparingly)\n\n## Naming (per Spotify guidelines)\n\n- **Show name:** Short, memorable, and searchable. Avoid generic names that get lost in search results\n- **Episode title:** Include the date or episode number for recurring shows. Keep under ~60 characters so it doesn't truncate in the app\n- **Description summary:** The first 1-2 sentences appear as a preview in podcast apps — front-load the most interesting hook, don't waste it on boilerplate\n\nFile v0.2.0:references/local-preview.md\n\n# Local Preview Pages\n\nLocal browser preview pages, served before anything is uploaded: the **finished episode** (a mini Spotify player) and the **voice sample** (a one-card player). Nothing leaves the machine — no uploads, no external requests. Use them from any flow — onboarding or the standard checklist.\n\n## Assets\n\nCreate a preview directory in the session temp dir: `preview/<short-id>/` containing `index.html`. Reference the episode MP3 and cover image in place — symlink them into the directory (`ln -s`, macOS/Linux) or serve the directory where they already live. On Windows, skip symlinks (they need elevated privileges): serve from where the assets live, or copy them. Do not copy multi-megabyte audio on other platforms just to preview it.\n\n## The page\n\n`index.html` is one self-contained file (inline CSS/JS, no CDN or external fetches) that mirrors the Spotify player look:\n\n- Dark theme (near-black `#121212` background, Spotify green `#1DB954` accent, white/grey text)\n- Two panes. Left: show name as a small uppercase eyebrow, episode title, duration, description, a thin timeline scrubber with a tick mark per chapter, then the full chapter list — each row an icon, chapter title, and `m:ss` start time from `timeline.json`. Right: the cover image (fallback: a green gradient placeholder with a music-note glyph).\n- A round play/pause button wired to an `<audio>` element. The button must correctly reflect and control audio state:\n  - Show a **play icon** (▶) when audio is paused/stopped, a **pause icon** (⏸) when playing. Never show both; never get stuck on one.\n  - Wire `onclick` to toggle `audio.paused ? audio.play() : audio.pause()`.\n  - Listen to the audio element's `play`, `pause`, and `ended` events to update the icon — do NOT rely only on the click handler, because seeks, chapter clicks, and browser controls also change state.\n  - On `ended`, reset to the play icon.\n  - Clicking a chapter row seeks to its `start_time_ms` **and starts playback** (call `audio.play()` after setting `currentTime`). The event listeners handle the icon update.\n  - The currently playing chapter is highlighted in green — update the highlight on `timeupdate` by comparing `audio.currentTime` against chapter start times.\n- **Load the audio as a blob, not a URL.** Simple static servers (`python3 -m http.server`) don't support HTTP Range requests, so an `<audio>` pointed straight at the MP3 URL cannot seek — chapter jumps and progress-bar clicks silently fail. Instead `fetch()` the MP3, convert with `.blob()`, and set `URL.createObjectURL(blob)` as the audio `src` (keep the object URL for repeat plays). With the whole file in memory the browser seeks freely without Range support.\n\n## Serve\n\nStart the server as soon as there is anything to preview — the voice sample at the voice-pick step, or the episode assets once they exist. It's cheap and makes previews instant. Bind to localhost only, and do **not** auto-open the browser:\n\n```shell\npython3 -m http.server 8374 --bind 127.0.0.1 --directory <session-temp-dir> &\n```\n\nOn Windows use `python` instead of `python3`, and note the trailing `&` is bash-only — in Git Bash it works as shown; in PowerShell use `Start-Process python -ArgumentList '-m','http.server','8374','--bind','127.0.0.1','--directory','<session-temp-dir>'`. If port 8374 is taken, pick another free port. Fall back to opening `index.html` directly if Python is unavailable.\n\n## Offer, open, verdict\n\nSay: \"Your episode is ready — a local preview is up at http://localhost:8374/preview/<short-id> (nothing uploaded yet).\" with choices:\n\n- **Open the preview** — open the URL in the browser (`open` on macOS, `xdg-open` on Linux, `start` on Windows), then ask the verdict below\n- **Skip preview, save to Spotify** — stop the server, save now\n\nFlows may add their own choices to this prompt (onboarding adds \"Make it shorter\" and \"Change the voice\").\n\nOnly open the browser when the user asks. Once the page is open, ask: \"All good?\" with choices:\n\n- **All good, save to Spotify** — stop the server, proceed to save\n- **Make changes** — see below\n\nWhen delivering the episode, apply the \"The user made this\" principle from SKILL.md: emphasise what the user has created — \"your episode is ready\", never \"we created your episode\".\n\n## Voice preview page\n\nThe voice sample from `tts test` gets the same treatment as the episode — a player page, not a raw `file://` path. Put a minimal one-card page at `preview/voice/index.html` on the same server (start the server at this point if it isn't running; it stays up through the episode preview):\n\n- Centered dark card: \"VOICE PREVIEW\" eyebrow, the voice name as the title, engine + a couple of voice traits as the subtitle\n- A round green play/pause button and a thin scrubber, wired to the sample (symlinked next to the page); blob-load it like the episode audio. Same event-driven state sync as the episode player: listen to `play`, `pause`, `ended` events on the audio element to toggle the icon — never rely on click alone.\n- No auto-play — the user clicks play\n\nOffer to open `http://localhost:8374/preview/voice/` the same way as the episode preview: open on request, never unprompted.\n\n## Making changes from the preview\n\nDistinguish what the edit actually touches:\n\n- **Metadata edits** (title, description, chapter titles) — update the preview HTML in place. No audio regeneration.\n- **Script changes** — go back through the flow's content-approval step before regenerating any audio, then rebuild the affected assets and refresh the preview.\n\n## Lifecycle\n\nWhile the user iterates, keep the server running and rebuild the page and assets in place — the URL stays stable, no port churn. Stop the server before saving, and whenever the flow exits — never leave it running.\n\nFile v0.2.0:references/onboarding.md\n\n# Save to Spotify — Onboarding Flow\n\nThis file defines the **first-run onboarding flow** for the `save-to-spotify` skill. When a user runs `save-to-spotify` for the first time (or has no shows yet), the agent follows this flow instead of the default interview in SKILL.md.\n\nThe goal: collapse the path from \"never heard of this\" to \"listening on Spotify\" into six steps with minimal friction.\n\n---\n\n## When to activate\n\nUse this flow when **any** of these are true:\n\n- The user just installed `save-to-spotify` (the install script hands off here)\n- The user has zero shows (`save-to-spotify --json shows` returns an empty `shows` array)\n- The user explicitly asks to get started, set up, make their first episode, or any phrasing that signals they want the guided experience — **even if they already have shows**\n\nThe explicit ask always wins over the shows check. Do not skip onboarding just because shows exist when the user asked for the guided flow.\n\n---\n\n## Step 0 · What is Save to Spotify?\n\nWhen onboarding a user for the first time, do not assume the user is comfortable using an AI agent to generate audio content. Guide them through the process step-by-step, making it as clear and simple as possible. State upfront the capacity of the tool, what it actually does, and what tools (if any) users must provide, i.e. for voices. Be very clear that the default state of the content is private when it gets saved to a show in the user's Spotify Library, and it is not able to be shared.\n\nBefore installing or asking any questions, open with this intro:\n\n> Save to Spotify helps you turn an idea into a personal audio episode in your Spotify Library. You choose the topic, shape the content, and pick a voice. It then helps create and save the episode to your Spotify Library. Episodes are not visible to other users. Content is not able to be shared.\n\nOpen with the intro as written, then move straight to Step 1 — don't front-load details. The specifics land where they're actionable: voice engines and what the user must provide come up in Step 2, examples of what they can create in Step 3.\n\n---\n\n## Step 1/6 · Install\n\n**Goal:** CLI installed and on PATH.\n\n```\ncurl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\nOn Windows, run it in Git Bash. The installer downloads the binary, verifies it, and prints:\n```\n✓ Installed save-to-spotify v0.2.0 to /usr/local/bin\nNext: save-to-spotify setup\n```\n\nHand off to Step 2 automatically — do not stop and wait.\n\n---\n\n## Step 2/6 · Auth\n\n**Goal:** Spotify account connected in one action.\n\nRun `save-to-spotify setup` directly — do not ask the user to run it. The agent executes this itself:\n\n```shell\nsave-to-spotify setup\n```\n\nThis handles auth + TTS detection in one pass:\n- Desktop: opens browser automatically for OAuth\n- Headless/agent: auto-detects and uses `--no-browser` mode\n- On success: \"✓ Authenticated. Token saved.\"\n\nIf setup reports no TTS engine, **ask the user** before installing anything:\n\n> No voice engine found. Want me to install **Kokoro** (free, local, ~340 MB, [Apache-2.0 licensed](https://raw.githubusercontent.com/hexgrad/kokoro/refs/heads/main/LICENSE))?\n>\n> - **Yes, install Kokoro**\n> - **I'll set up a cloud key instead** (OpenAI / ElevenLabs)\n\nIf they accept, run `save-to-spotify tts setup`. If it fails because Python is missing or older than 3.10, **ask the user** before installing:\n\n> Kokoro needs Python 3.10+ but your system has an older version. Want me to install a newer Python?\n>\n> - **Yes, install Python** — `brew install python@3.12` (macOS), `apt install python3` (Debian/Ubuntu), or `winget install Python.Python.3.12` (Windows), then retry `tts setup`\n> - **I'll handle it myself** — tell them what's needed and move on\n\nIf they prefer a cloud key instead of Kokoro, tell them which env var to set and move on — the engine will be picked up in Step 5.\n\nHand off to Step 3 immediately.\n\n---\n\n## Step 3/6 · Pick content\n\n**Goal:** Pick and go. Zero typing to first episode.\n\nPresent curated recipes as a picker — no blank-page cold start. See [recipes.md](recipes.md) for the full list.\n\nThis step doubles as the post-authorization welcome: welcome the user to the Save to Spotify experience and let the recipes themselves be the examples of what they can create — encourage and inspire them to try a variety of use cases. Do it in one turn with the picker, not as a separate message.\n\nSay: \"Welcome to Save to Spotify! What would you like to create?\" and present the top 3–4 most relevant recipes as choices. Include \"Suggest more ideas\" as a final option.\n\n**When the user picks a recipe, start generating immediately.** Use the recipe's default input — do not ask follow-up questions. Most things are defaulted:\n\n- Content: recipe's default input\n- Language: user's system locale\n- Length: recipe default (briefings ~8min, deep dives ~8min, travel ~6min)\n- Voice: determined in Step 5\n- Show: auto-created from recipe name\n\nIf the user types preferences into any answer's free-text field (tone, angle, length — anything), apply them and go — no follow-up questions. The chapter overview in Step 4 remains the main steering gate.\n\n**Cover image** — in the same picker turn as the recipe. Offer \"**Generate one for me**\" (default) or \"**I have an image**\" — never label the option \"AI-generated\":\n\n> Cover image: **Generate one for me** (default) or do you have an image you'd like to use?\n\nIf they don't respond or pick the default, generate the cover. If they provide an image path or URL, use that. Don't belabor it — one question, move on.\n\nOnly ask for other input if the recipe absolutely requires it (travel guide needs a destination, meeting recap needs a transcript, deep dive needs a topic — for the deep dive, offer 3 personalized topic suggestions plus free text, per [recipes.md](recipes.md); the topic is the user's intent, never invent it silently).\n\n---\n\n## Step 4/6 · Script\n\n**Goal:** Chapters approved, then the script written. Iterating on an outline is free — no wasted writing, no TTS credits, no regeneration.\n\n### Gather context\n\nRun the recipe's data-gathering step. Show what was found:\n\n```\n✓ Google Calendar: 3 meetings today\n✓ GitHub: 4 PRs merged overnight, 1 CI failure on main\n✓ Linear: 5 open tasks, 1 blocked on QA sign-off\n```\n\n### Chapter overview\n\nPresent a short overview of the chapters the episode will cover — do **not** write the full script yet, and never dump a full transcript on the user.\n\nEmbed the overview inside the choice prompt itself (design principle 1). Use a compact numbered list — no blank lines between items. Each entry has a bold chapter name and a short one-line summary. When the episode's content was auto-derived rather than user-stated (e.g. a daily briefing), open with a one-line source note so the default is transparent, not silent — \"Built from your calendar and GitHub activity —\". End with the question:\n\n```\nChapter overview — Deep Dive: How Solar Panels Work (~8 min):\n\n1. **The photovoltaic effect** — how sunlight becomes electricity\n2. **From cell to grid** — panels, inverters, and what happens to extra power\n3. **The economics** — why prices fell 90% and what payback looks like\n\nDoes this outline cover what you want to create?\n```\n\nChoices:\n- **Looks good** — write the full script from the approved chapters, then move on to picking a voice\n- **Adjust the chapters** — revise the overview; stay in this step until the user approves\n\nOnly after approval, write the full script (following [content-quality.md](content-quality.md)) and self-critique it. Do not touch TTS engines, voices, or audio generation until the chapters are approved.\n\n---\n\n## Step 5/6 · Voice & audio\n\n**Goal:** Working TTS engine with zero friction, then generate everything.\n\n### Pick a voice\n\nWith the chapters approved and the script written, resolve a TTS engine. If an engine was already confirmed or installed in Step 2, use it directly — skip the status check. Only if engine setup was deferred in Step 2 (e.g. the user said they'd set a cloud key), follow the provider-selection flow in [audio-providers.md](audio-providers.md) (configured default → existing API key → Kokoro as the free local fallback), starting from `save-to-spotify tts status --json`.\n\nOn Kokoro, start from the recipe's default voice (the `Kokoro voice` row in its [recipes.md](recipes.md) table) — the voices vary in quality and a mismatched voice for the content type is avoidable. The user can still pick another.\n\nGenerate a short voice preview so the user hears the voice before committing:\n\n```shell\nsave-to-spotify tts test --engine <engine> --voice <voice>\n```\n\nThis synthesizes a sample phrase and prints its path — it does not auto-play (audio suddenly playing catches people off guard). Then:\n\n- Don't hand the user the raw `file://` path\n- Wrap the sample in the **voice-preview player page** ([local-preview.md](local-preview.md), \"Voice preview page\") and offer to open it\n- Pass `--play` only if the user explicitly asks to hear it in-session\n\n**HARD STOP — present choices and wait for the user to respond before continuing. Do not generate the episode audio until the user picks an option:**\n\n- **Sounds good, generate my episode** — generate audio for the approved script\n- **Try another voice** — list voices with `save-to-spotify tts voices --engine <engine>`, let the user pick, then run `tts test` again\n- **Try another engine** — show available engines, switch, preview again\n\nIf this engine and voice were already previewed and approved earlier in the session (e.g. the user is returning from a script-only revision), skip the preview and the hard stop — go straight to generation.\n\n### Generate\n\nGenerate audio, cover image, and timeline from the approved script — the cover is independent of the audio, so generate them concurrently. Show what was produced:\n\n```\n✓ Audio generated (4m 12s, 3 chapters)\n✓ Cover image created\n✓ Timeline built (3 chapters, 2 images, 4 links)\n```\n\nHand off to Step 6.\n\n---\n\n## Step 6/6 · Preview & save\n\n**Goal:** Episode on Spotify, with an optional look at the finished thing first. Privacy clear, next step offered.\n\nRun the preview interaction from [local-preview.md](local-preview.md): build the page and start the localhost server before asking anything, offer open-or-skip, and ask for the verdict once the page is open. Add two onboarding-specific choices to the offer:\n\n- **Make it shorter** — trim the chapter outline (Step 4), then regenerate the script and audio (Step 5)\n- **Change the voice** — try a different TTS voice (back to Step 5)\n\nIn this flow, \"content approval\" for preview edits means Step 4's chapter approval.\n\n### Save\n\n```\n✓ Show created: Daily Briefings\n  Uploading to Spotify...\n\n✓ Episode saved (private)\n\n  Show:     Daily Briefings\n  Duration: 4m 12s\n  Listen:   https://open.spotify.com/episode/...\n  Processing — ready in about a minute.\n```\n\nKey behaviors:\n- Show auto-created from recipe name\n- Privacy stated proactively: \"(private)\"\n- Readiness expectation set: \"ready in about a minute\"\n\nImmediately offer the highest-retention next step:\n\nSay: \"What would you like to do next?\" with choices:\n- **Create another episode** — return to Step 3\n- **I'm done for now** — exit gracefully\n\n---\n\n## Design principles\n\n1. **Always present choices, never ask for free text** — every decision point must be a list of options the user can pick from. Never ask the user to type something. Open-ended text input breaks the flow and creates friction. If you need input, present smart options and let them pick. And embed whatever the user is judging (chapter list, preview URL, file link) **inside the choice prompt itself** — plain text printed before a choice popup can be hidden by it, leaving the user a question about content they never saw\n2. **Content before audio** — chapters are approved and the script written before any voice or audio work. Iterating on text costs nothing; regenerating audio does\n3. **Show, don't tell** — script summaries, voice previews, and a local browser preview of the finished episode\n4. **Default everything** — language, length, voice, cover, show, and content topic all have smart defaults\n5. **Iterate, don't restart** — editing a script doesn't require re-answering setup questions\n6. **Preview before commit** — nothing is uploaded without user approval; offer the local browser preview before saving\n7. **Momentum over completeness** — get to a working episode fast, refine later\n\nFile v0.2.0:references/recipes.md\n\n# Recipes\n\nReady-to-use content templates for the onboarding flow. Each recipe is fully defaulted — the agent can produce a complete episode without asking the user anything beyond \"what would you like to create?\"\n\nWhen the user picks a recipe, **do not ask the input question**. Use the default input instead. The user can always refine after hearing the result. The goal is zero typing to first episode.\n\nEach recipe carries a **default Kokoro voice** in its table below — use it as the first suggestion when the user is on Kokoro (they can still pick another at the voice step). The rows are the single source of truth for which voice fits which recipe.\n\n---\n\n## Daily briefing\n\n**Description:** Your calendar, tasks, and repo activity as a short morning brief.\n\n**Default input:** Pull from whatever is available — Google Calendar, GitHub, Linear. Auto-detect connected services.\n**Input question (only if user asks to customize):** \"What accounts or services should I pull from?\"\n\n| Default | Value |\n|---------|-------|\n| Length | ~8 min |\n| Segments | 3–4 (schedule, repo, tasks, optional weather) |\n| Show name | Daily Briefings |\n| Kokoro voice | af_heart |\n\n**Data sources:** Google Calendar API, GitHub notifications/PRs, Linear tasks, optionally weather API.\n\n**Example prompt:**\n> Make me a daily briefing from my Google Calendar, GitHub notifications, and Linear tasks.\n\n---\n\n## Deep dive\n\n**Description:** Turn a topic into a personalized audio explainer.\n\n**Default input:** Cannot be fully defaulted — the topic is the user's intent; a deep dive on a topic they didn't choose is homework, not a gift. Suggest, don't pick.\n**Input question:** \"What should we dive into?\" — present **3 personalized topic suggestions as options** (inferred from the user's repos, role, and recent activity — the same signals you'd have used to pick silently) plus the free-text field for their own topic. One click, no interview.\n\n**One topic per episode.** Multiple sources are fine when they cover the same topic — never mix unrelated topics into one deep dive.\n\n| Default | Value |\n|---------|-------|\n| Length | ~8 min |\n| Segments | 4–6 (intro, background, key points, analysis, takeaways) |\n| Show name | Deep Dives |\n| Kokoro voice | af_heart |\n\n**Data sources:** Web search, user-provided URLs, PDFs, and documents.\n\n**Example prompt:**\n> Deep dive into this article: https://example.com/interesting-post\n\n---\n\n## Travel guide\n\n**Description:** A private audio itinerary for your next trip.\n\n**Default input:** Cannot be fully defaulted — destination is required.\n**Input question:** \"Where are you going and when?\"\n\n| Default | Value |\n|---------|-------|\n| Length | ~6 min |\n| Segments | 4–5 (overview, getting around, must-see, food, tips) |\n| Show name | Travel Guides |\n| Kokoro voice | af_heart |\n\n**Data sources:** Web search, travel APIs, location data.\n\n**Example prompt:**\n> I'm going to Lisbon next week for 4 days. Make me a travel guide.\n\n---\n\n## Meeting recap\n\n**Description:** Drop in a transcript, get a summary with action items.\n\n**Default input:** Cannot be fully defaulted — transcript is required.\n**Input question:** \"Paste or link the meeting transcript.\"\n\n| Default | Value |\n|---------|-------|\n| Length | ~3 min |\n| Segments | 3 (summary, decisions, action items) |\n| Show name | Meeting Recaps |\n| Kokoro voice | af_heart |\n\n**Data sources:** User-provided transcript (paste, file, or URL).\n\n**Example prompt:**\n> Here's the transcript from today's standup: [paste]\n\n---\n\n## Sleep story\n\n**Description:** A custom wind-down story set in your favorite place.\n\n**Default input:** A slow, atmospheric story about a night train crossing quiet mountains in the rain.\n**Input question (only if user asks to customize):** \"What setting or mood would you like?\"\n\n| Default | Value |\n|---------|-------|\n| Length | ~5 min |\n| Segments | 3–4 (setup, journey, resolution, goodnight) |\n| Show name | Sleep Stories |\n| Kokoro voice | af_nicole |\n\n**Data sources:** User prompt (creative generation).\n\n**Example prompt:**\n> A sleep story about a lighthouse keeper on a calm autumn night.\n\n---\n\n## Language practice\n\n**Description:** Spoken drills in the language you're learning, with pauses to answer out loud.\n\n**Default input:** Cannot be fully defaulted — language is required.\n**Input question:** \"Which language are you learning, and roughly what level?\"\n\n| Default | Value |\n|---------|-------|\n| Length | ~10 min |\n| Segments | 4–6 (warm-up vocab, phrases, listen-and-repeat, recall quiz, recap) |\n| Show name | Language Practice |\n| Kokoro voice | af_heart |\n\n**Data sources:** User prompt (creative generation). Use the recall (3s) and speaking-practice (5s) pauses from audio-providers.md between prompts.\n\n**Example prompt:**\n> Spanish practice for a beginner — ordering food and asking directions.\n\n---\n\n## Sleep podcast\n\n**Description:** A calm, droning deep-dive designed to put you to sleep.\n\n**Default input:** The history and science of ocean currents.\n**Input question (only if user asks to customize):** \"What topic should I drone on about?\"\n\n| Default | Value |\n|---------|-------|\n| Length | ~20 min |\n| Segments | 6–8 (slow, meandering, low-stakes) |\n| Show name | Sleep Podcasts |\n| Kokoro voice | af_nicole |\n\n**Data sources:** Web search, Wikipedia.\n\n**Example prompt:**\n> A sleep podcast about the history of lighthouses.\n\n---\n\n## Using recipes\n\nRecipes are pick-and-go. The user picks one and the agent starts generating immediately:\n\n1. User picks a recipe (or the agent picks the best fit from their prompt)\n2. Agent uses the **default input** — no questions asked\n3. Smart defaults apply (language from locale, length from recipe, voice from TTS default)\n4. Show auto-created from recipe name\n5. Generate, preview, save\n\nOnly ask the input question if the recipe cannot be defaulted (travel guide needs a destination, meeting recap needs a transcript) or the user explicitly asks to customize.\n\nAfter the first episode, users can customize any default through the standard interview flow in SKILL.md.\n\nFile v0.2.0:references/spotify-api.md\n\n# Spotify Web API (catalog lookups)\n\nUse this reference when a segment names something that already exists on Spotify and the timeline should include a `spotify_entity` companion. This file covers the `save-to-spotify`-specific wiring: how to get a bearer token from the CLI, how to resolve names to `spotify:...` URIs, and when to omit a low-confidence entity.\n\nTreat Spotify for Developers as the source of truth for endpoint shapes, parameters, schemas, and policy. Use the Spotify Web API rather than third-party catalogs, because timeline entries need Spotify URIs rather than cross-platform tap-throughs.\n\n## Start with Spotify for Developers\n\nBefore scripting Web API calls, load the official developer context:\n\n- `https://developer.spotify.com/llms.txt` - LLM-ready entrypoint for Spotify's developer platform.\n- `https://developer.spotify.com/documentation/web-api/tutorials/building-with-ai` - Web API guidance for AI coding assistants and agents.\n- `https://developer.spotify.com/reference/web-api/open-api-schema.yaml` - full OpenAPI 3.0 schema for the Spotify Web API.\n\nThe OpenAPI schema declares the base server (`https://api.spotify.com/v1`), paths, parameters, response schemas, and OAuth requirements. If the official docs or schema disagree with this local reference, follow `developer.spotify.com`.\n\nQuick spec workflow:\n\n```shell\nSPEC_URL=\"https://developer.spotify.com/reference/web-api/open-api-schema.yaml\"\ncurl -fsSL \"$SPEC_URL\" -o spotify-openapi.yaml\nrg -n \"^  /search:|operationId: search\" spotify-openapi.yaml\n```\n\n## Getting a bearer token\n\n`save-to-spotify token` prints the current access token to stdout. That token has been verified to work as a Spotify Web API `Bearer` token for requests against `api.spotify.com`, so agents do not need a separate app registration for the catalog lookups below.\n\nGuard the call because it exits non-zero when the stored token cannot be refreshed:\n\n```shell\nif ! TOKEN=$(save-to-spotify token); then\n  echo \"not authenticated; run: save-to-spotify auth login\" >&2\n  exit 1\nfi\n```\n\nFor a quick auth smoke test against the catalog API:\n\n```shell\ncurl -sfG \"https://api.spotify.com/v1/search\" \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  --data-urlencode \"q=artist:The Beatles\" \\\n  --data-urlencode \"type=artist\" \\\n  --data-urlencode \"limit=1\" >/dev/null\n```\n\nNo extra scope grant is needed for public catalog search and metadata lookups. Check the OpenAPI `security` entries before using user-private data or mutating endpoints.\n\n## Resolving names to Spotify URIs\n\nPattern: `GET /v1/search?q=<query>&type=<entity>&limit=5`, then pick the best match from `.<entity>s.items`.\n\nFor albums and tracks, use field-qualified queries (`artist:X album:Y` or `artist:X track:Y`) for higher precision than free text:\n\n```shell\nTOKEN=$(save-to-spotify token) || exit 1\ncurl -sG \"https://api.spotify.com/v1/search\" \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  --data-urlencode \"q=artist:The Beatles album:Abbey Road\" \\\n  --data-urlencode \"type=album\" \\\n  --data-urlencode \"limit=5\" \\\n| jq '.albums.items[] | {uri, name, release_date, artists: [.artists[].name]}'\n```\n\nMinimal dependency-free Python wrapper:\n\n```python\nimport json\nimport subprocess\nfrom urllib.parse import urlencode\nfrom urllib.request import Request, urlopen\n\nAPI = \"https://api.spotify.com/v1\"\n\ndef spotify_token():\n    return subprocess.run(\n        [\"save-to-spotify\", \"token\"],\n        capture_output=True,\n        text=True,\n        check=True,\n    ).stdout.strip()\n\ndef spotify_get(path, params):\n    url = f\"{API}{path}?{urlencode(params)}\"\n    req = Request(url, headers={\"Authorization\": f\"Bearer {spotify_token()}\"})\n    with urlopen(req, timeout=20) as resp:\n        return json.load(resp)\n\ndef spotify_search(query, entity_type, limit=5):\n    data = spotify_get(\"/search\", {\"q\": query, \"type\": entity_type, \"limit\": limit})\n    return data.get(f\"{entity_type}s\", {}).get(\"items\", [])\n```\n\nFor ambiguous names (self-titled albums, common words, cover-versus-original tracks), score each result before accepting it. Weight artist-name overlap, title overlap, and entity-specific metadata like `album_type`, `release_date`, `show.name`, or track duration. Drop the `spotify_entity` companion rather than ship a wrong URI when no result scores confidently.\n\n### Entity query patterns\n\nAlbums and tracks support the field-qualified form above. Artists, playlists, shows, and episodes take free-text queries; feed the user-visible name and change the `type=` parameter:\n\n| Entity   | Query                      | `type=`    | Response path        | Output URI             |\n|----------|----------------------------|------------|----------------------|------------------------|\n| Album    | `artist:X album:Y`         | `album`    | `.albums.items[]`    | `spotify:album:...`    |\n| Track    | `artist:X track:Y`         | `track`    | `.tracks.items[]`    | `spotify:track:...`    |\n| Artist   | `X` (name)                 | `artist`   | `.artists.items[]`   | `spotify:artist:...`   |\n| Playlist | `X` (name)                 | `playlist` | `.playlists.items[]` | `spotify:playlist:...` |\n| Show     | `X` (name)                 | `show`     | `.shows.items[]`     | `spotify:show:...`     |\n| Episode  | `show title episode title` | `episode`  | `.episodes.items[]`  | `spotify:episode:...`  |\n\nPodcast episode results are noisier than track and album search; verify the top hit's `show.name` field before accepting.\n\n## Search is the only endpoint you need\n\n`GET /v1/search` covers the core use case end to end: it returns URIs, names, images, artists, release dates, and `show.name` for episodes — everything needed for disambiguation and confidence scoring. A typical episode does 10–20 lookups, and search handles all of them. Do not reach for individual entity endpoints (`/albums/{id}`, `/tracks/{id}`, `/artists/{id}`, `/playlists/{id}`, `/shows/{id}`, `/episodes/{id}`, `/artists/{id}/albums`, `/shows/{id}/episodes`, `/browse/new-releases`) — they only add value in rare edge cases (e.g. \"their latest album\" without a name, or an episode search that fails by show name). If you hit a real resolution gap search can't close, consult the OpenAPI schema for the specific endpoint rather than defaulting to it.\n\n## Fallbacks and non-Spotify data\n\nWhen sourcing a Spotify-native reference, the fallback hierarchy is:\n\n1. Spotify Web API - primary. Always try it first when auth is valid.\n2. No entity - omit the `spotify_entity` companion rather than invent or guess a URI.\n\nIf Spotify auth fails, fix auth first with `save-to-spotify auth login`. Do not substitute for other third-party catalogs as the timeline destination just because auth failed. Those sources can help verify dates, credits, or artwork, but they do not produce the `spotify:...` URI the Now Playing View needs.\n\n## Rate limits\n\nSpotify's Web API is rate-limited per token; a sensible guardrail is about 10 req/s with small jitter. On HTTP 429, honor the `Retry-After` header. A typical episode's 10-20 lookups are well under the limit.\n\nFile v0.2.0:references/timeline.md\n\n# Timeline Format\n\nReference for building `timeline.json` — the chapter markers and in-player companion content (images, external source links, Spotify entity cards) that ride alongside every episode.\n\n## Save the timeline\n\nAfter saving, push a single `timeline.json` that carries chapters and all companions in one call:\n\n```shell\nsave-to-spotify --json timeline set \\\n  --episode-id <EPISODE_URI> \\\n  --from-file timeline.json\n```\n\nVerify: `save-to-spotify --json timeline get <EPISODE_ID>`. Delete: `save-to-spotify --json timeline delete <EPISODE_ID>`.\n\n## Verifying a pushed timeline\n\nThree backend behaviors make a successful `timeline set` look like a failure. Expect all three:\n\n1. **`timeline set` returns an empty body on success.** The response is `{\"items\":null}`, not an echo of what was stored. Do not infer from this response whether companions landed — call `timeline get` to check.\n2. **Companions propagate slower than chapters.** `episodes status = READY` only confirms the audio is processable. Chapter markers appear shortly after, but `link`, `image`, and `spotify_entity` companions can take an additional 60-90 seconds. A `timeline get` immediately after `READY` may show chapters without their companions — that's propagation lag, not data loss. Wait 60-90 seconds past `READY` before fetching.\n3. **`timeline get` does not echo `url` fields.** For image and link companions, the CLI response omits `url` and keeps the opaque `\"companion_uri\": \"time-synced:companion-external-link:<hash>\"`. The URL is stored on the backend and clients tap through to it correctly - it is just not echoed back on read. A non-empty `companion_uri` at the expected timestamp is the proof the link is live. Do not diff the fetched URL against what you sent.\n\nIf links or images are genuinely missing after a 2-3 minute wait, re-push `timeline set` — the endpoint is idempotent and replaces the full timeline on PUT.\n\n## Data model\n\nEach entry in `items` is one of four kinds: `chapter`, `image`, `link`, or `spotify_entity`. A single time window (say, one chapter's span) can contain *multiple* companion items; they only have to not overlap in time with each other. Chapters are independent and do not overlap with companions.\n\n```json\n{\n  \"items\": [\n    {\"chapter\": {\"title\": \"Course intro & syllabus\", \"start_time_ms\": 0}},\n    {\"chapter\": {\"title\": \"Lecture 3: Backpropagation\", \"start_time_ms\": 18000}},\n    {\"image\":   {\"start_time_ms\": 25000, \"duration_ms\": 15000, \"image\": \"img_01_a.jpg\", \"url\": \"https://example.edu/cs231n/lecture3\", \"title\": \"Slide: chain rule diagram\"}},\n    {\"spotify_entity\": {\"start_time_ms\": 45000, \"duration_ms\": 20000, \"uri\": \"spotify:episode:2abc3def4ghi\"}},\n    {\"image\":   {\"start_time_ms\": 70000, \"duration_ms\": 20000, \"image\": \"img_01_b.jpg\", \"title\": \"Slide: computation graph\"}},\n    {\"chapter\": {\"title\": \"Worked example: 2-layer net\", \"start_time_ms\": 102000}},\n    {\"spotify_entity\": {\"start_time_ms\": 110000, \"uri\": \"spotify:track:4uLU6hMCjMI75M1A2tKUQC\"}},\n    {\"link\":    {\"start_time_ms\": 130000, \"duration_ms\": 25000, \"url\": \"https://example.edu/readings/goodfellow-ch6.pdf\"}},\n    {\"image\":   {\"start_time_ms\": 160000, \"duration_ms\": 20000, \"image\": \"img_02_a.jpg\"}},\n    {\"chapter\": {\"title\": \"Recap & problem set\", \"start_time_ms\": 270000}}\n  ]\n}\n```\n\n## Validation rules\n\nRules are checked both client-side and on the backend:\n\n- **Chapters:** at least 2, first at `0 ms`, strictly increasing `start_time_ms` with consecutive starts ≥ 5 s apart (the final chapter may be shorter), `title` required, `description` optional. Every `start_time_ms` must be strictly less than the episode's audio duration — the CLI and backend can't verify this, so compute timestamps from cumulative segment durations and assert against the assembled MP3 before `timeline set` (see [audio-providers.md](audio-providers.md) \"Timeline timestamp calculation\").\n- **Images:** positive `duration_ms`, local file path in `image` (`.jpg`/`.png`, ≤ 1 MB, dimensions 1×1..4096×4096). Optional `url` (tap-through) and `title` (alt text). When an image is tied to one canonical source URL, default to setting that URL here.\n- **Links:** positive `duration_ms`, valid HTTP(S) `url`.\n- **Spotify entities:** `uri` is required and must be a full `spotify:...` URI. `duration_ms` is optional, but when present it must be positive. Use this for Spotify-native references such as tracks, albums, artists, playlists, shows, episodes, and audiobook/catalog entities.\n- **Companion non-overlap:** sort images, links, and `spotify_entity` items by `start_time_ms`; each item's `start + duration` must be ≤ the next item's `start`. A `spotify_entity` without `duration_ms` behaves like an instantaneous card at that timestamp. Chapters are *not* included in this check — a chapter can start at any time, including inside a companion's window.\n- **URI format:** in `timeline.json`, `spotify_entity.uri` must be a full Spotify URI (`spotify:track:...`, `spotify:artist:...`, `spotify:episode:...`). Do not use bare IDs or `open.spotify.com` URLs there.\n- Titles on chapters should match the description timestamps (they land in the show-notes HTML).\n\n## Companion selection rules\n\n**Preference order:** if the thing you want listeners to open already exists on Spotify, add a `spotify_entity`. Keep `link` for off-Spotify destinations, and include both when you want listeners to have both the Spotify destination and the original source/article.\n\n**No duplicate artwork:** when a `spotify_entity` is present, do NOT add an `image` of the same entity's artwork — the card already renders it. Only pair an `image` with a `spotify_entity` when the image is editorially distinct (chart, infographic, screenshot).\n\n**Image + link default:** if you have a representative image plus one canonical source URL for the same story, prefer one `image` item with `url` set. Use standalone `link` for additional URLs, or when there is no good image. Use standalone `image` only when there is no meaningful destination.\n\n## Placing companions inside a chapter\n\nFor a chapter spanning `[chapter_start, chapter_end)`, place N companions by dividing the window into N equal slots (or picking natural anchor points in the script). Keep a short buffer (e.g., 1 second) between companions to avoid edge-case overlaps. A practical helper is in [audio-providers.md](audio-providers.md) under \"Timeline timestamp calculation\".\n\n## Companion images: sourced, AI-generated, or mixed\n\nThe agent asked the user in the interview where companion images should come from. Follow that choice consistently across the episode:\n\n- **AI-generated** -- after scripting, generate images from a themed prompt derived from each segment's content. Use the DALL-E / Stable Diffusion code under \"Batch generation for timeline companions\" below. Use the same naming pattern as above.\n- **Mixed** -- sourced where a usable image exists, AI-generated fill everywhere else. Aim for at least one image per chapter.\n- **Skip** -- emit a timeline with chapters plus Spotify and external-link companions only.\n\nIf the mode is `Sourced` or `Mixed`, attempting image extraction is required during sourcing. When a usable source image is found, include it in the timeline unless the user explicitly opted out later. Only omit an image companion when the source genuinely has no meaningful visual, or the user chose `Skip`.\n\nThe CLI's `timeline set` uploads each local image file to Spotify's image store, swaps the file path for the returned upload token, and sends the timeline to the backend. No separate image-upload step is needed.\n\n## AI image generation\n\nWhen the user picks `AI-generated` or `Mixed`, generate images with their preferred model. Constraints for timeline companions: JPEG/PNG, up to 4096x4096, max 1 MB each.\n\n### OpenAI DALL-E\n\n```python\nfrom openai import OpenAI\nclient = OpenAI()\nresponse = client.images.generate(\n    model=\"dall-e-3\",\n    prompt=\"A clean, minimal illustration of distributed systems architecture, dark background, tech aesthetic\",\n    size=\"1024x1024\",\n    quality=\"standard\",\n    n=1,\n)\nimport urllib.request\nurllib.request.urlretrieve(response.data[0].url, \"img.png\")\n```\n\n### Stable Diffusion (local)\n\n```shell\npython3 -c \"\nfrom diffusers import StableDiffusionPipeline\npipe = StableDiffusionPipeline.from_pretrained('stabilityai/stable-diffusion-xl-base-1.0')\nimage = pipe('A clean illustration of neural networks, minimal, dark background').images[0]\nimage.save('img.png')\n\"\n```\n\n### Resizing with ffmpeg\n\n```shell\n# Square companion under 1 MB, within 4096x4096\nffmpeg -i raw.png -vf \"scale=1400:1400:force_original_aspect_ratio=decrease\" -q:v 3 img_01_a.jpg\n```\n\n### Batch generation for timeline companions\n\nGenerate one or more images per chapter from a themed prompt. File naming must match what the timeline builder expects: `img_<chapter_index>_<slot>.jpg` (slots `a`, `b`, `c` for multiple images inside one chapter).\n\n```python\nfrom openai import OpenAI\nimport urllib.request, subprocess, string, tempfile\n\nclient = OpenAI()\n\n# One entry per chapter; each item lists the visual prompts for that chapter's slots\nchapter_images = [\n    (\"Introduction\",             []),  # no companions for intro\n    (\"Chapter A\",                [\n        \"Neutral map of Eastern Europe, minimal, muted colors\",\n        \"Wide shot of a damaged street, documentary photo, no text\",\n    ]),\n    (\"Chapter B\",                [\n        \"Empty supermarket shelves, documentary photo, overcast light\",\n    ]),\n    (\"Sign-off\",                 []),\n]\n\nfor idx, (_, prompts) in enumerate(chapter_images):\n    for slot_letter, prompt in zip(string.ascii_lowercase, prompts):\n        resp = client.images.generate(model=\"dall-e-3\", prompt=prompt,\n                                      size=\"1024x1024\", quality=\"standard\", n=1)\n        tmp = os.path.join(tempfile.gettempdir(), f\"raw_{idx}_{slot_letter}.png\")\n        urllib.request.urlretrieve(resp.data[0].url, tmp)\n        out = f\"img_{idx:02d}_{slot_letter}.jpg\"\n        # Downsize + re-encode to guarantee <= 1 MB, <= 4096x4096, .jpg\n        subprocess.check_call([\n            \"ffmpeg\", \"-y\", \"-i\", tmp,\n            \"-vf\", \"scale=1400:1400:force_original_aspect_ratio=decrease\",\n            \"-q:v\", \"3\", out,\n        ])\n```\n\nFor Stable Diffusion, swap the `client.images.generate` call for `pipe(prompt).images[0].save(tmp)` from the SDXL example above. Keep the same file-naming convention so the timeline builder in [audio-providers.md](audio-providers.md) picks the files up automatically.\n\nArchive v0.1.5: 10 files, 35729 bytes\n\nFiles: references/audio-providers.md (14044b), references/cli-usage.md (13711b), references/content-quality.md (9644b), references/cover-image.md (8239b), references/episode-description.md (2802b), references/spotify-api.md (7878b), references/timeline.md (10520b), skill-card.md (2977b), SKILL.md (11177b), _meta.json (134b)\n\nFile v0.1.5:SKILL.md\n\n---\nid: save-to-spotify\nname: save-to-spotify\ndescription: Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and Spotify entity cards), and a cover image. Also use for raw media saves, show/episode management, and timeline navigation.\nenabled: true\n---\n\n# Audio Content Production Skill\n\n`save-to-spotify` saves audio files to the user's Spotify library. Anything they can play locally — lecture recordings, voice memos, conference talks, language lessons — they can save to Spotify and listen from any device.\n\nShows are folders for organizing saves.\n\nYou are a podcast and audio content production agent. You create polished audio episodes from a variety of sources and formats, produce them with a rich in-player timeline (chapters plus image, link, and Spotify entity companions that appear during playback in the Now Playing View), and save to Spotify.\n\nThis skill defines the **shared production pipeline** — core principles, the user interview checkpoint, and the execution checklist.\n\n## Reference Directory\n\nThese files cover the detailed rules. Load the one you need — don't inline them.\n\n- [references/cli-usage.md](references/cli-usage.md) — Binary install, auth, `upload`/`shows`/`episodes`/`timeline` commands, JSON mode, error handling, troubleshooting, and common end-to-end workflows\n- [references/spotify-api.md](references/spotify-api.md) — Using `developer.spotify.com/llms.txt`, the Spotify Web API OpenAPI spec, and the CLI's token to resolve album / track / artist / playlist / show / episode names to `spotify:...` URIs for `spotify_entity` timeline companions\n- [references/audio-providers.md](references/audio-providers.md) — TTS engine selection, voice config, ffmpeg assembly, silence generation, timeline timestamp calculation\n- [references/cover-image.md](references/cover-image.md) — Cover image paths (user-provided, AI-generated, CDN artwork), typography rules, font & RTL, Pillow compositing recipe\n- [references/timeline.md](references/timeline.md) — Timeline data model, validation rules, companion images (sourced / AI-generated / mixed / skip), including DALL-E / Stable Diffusion code and batch generation\n- [references/episode-description.md](references/episode-description.md) — HTML description format, Python builder from `timeline.json`, formatting rules\n- [references/content-quality.md](references/content-quality.md) — Editorial guidelines: voice, transitions, person context, depth control, visual description, pacing, self-critique\n\n---\n\n## Install\n\nIf `save-to-spotify` is not available on `PATH`, ask the user to confirm CLI installation first, then install it:\n\n```shell\ncurl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\nSee [references/cli-usage.md](references/cli-usage.md) for manual binary downloads, source builds, authentication, command usage, and troubleshooting.\n\n---\n\n## Core Principles\n\n### Read-only. Always.\n\nWhen sourcing content, always respect platform terms of service and robots.txt and third-party IP rights. Use only authorized APIs and user-provided content. Never interact with source platforms beyond reading — do not post, like, follow, or modify content.\n\n### Be the listener's eyes\n\nPodcast listeners can't see anything. You are their eyes. Every piece of visual content — screenshots, images, charts — must be described in the script. If it matters to the segment, say what's in it.\n\n### Deep-link everything\n\nEvery segment in the show notes must link to the original source when possible. A link to a specific moment or post is 10x more valuable than a link to a homepage.\n\n### Respect Third-Party Rights\n\nThe final product must be a noninfringing synthesis of source materials, and must not infringe copyright or other third-party IP rights. It must not mislead as to the source or sponsorship of any material or information.\n\n### Prefer Spotify-native references\n\nWhen a segment points to something that already exists on Spotify — music, podcasts, audiobook titles, artists, albums, playlists, episodes, creators — capture the Spotify URI and use a `spotify_entity` timeline item whenever possible. Prefer the full `spotify:...` URI form, not a bare ID or `open.spotify.com` URL. Use external `link` companions for off-Spotify destinations such as articles, stores, docs, newsletters, and event pages. A `spotify_entity` and a `link` can both appear for the same segment/chapter when both the Spotify destination and the original source are valuable; just place them at non-overlapping times.\n\n### Segment-to-source integrity\n\nThe script has a strict 1:1 mapping: segment [N] corresponds to source item N. This mapping drives chapters, timeline companions, and show notes alignment. Never reorder, merge, or skip segments after assignment.\n\n### Save incrementally\n\nWrite collected data to disk after each sourcing step. If a later step fails, previous work is preserved.\n\n### Pacing and silence\n\nDon't fear strategic silence. Pauses between segments give the listener time to absorb. The 300ms gaps between segments are a minimum — use longer pauses (500ms+) between major topic shifts. Vary the pacing: slow down for important analysis or emotional moments, keep it brisk for roundups and quick hits.\n\n---\n\n## User Interview (MANDATORY)\n\n**Before doing any work, you MUST have a conversation with the user to confirm preferences.** Do not assume defaults. Ask, then STOP and wait for their reply. Do not proceed until they respond. Skipping the interview will feel efficient; don't. Treat this as a hard checkpoint before sourcing, scripting, or generation.\n\nAt minimum, always confirm these before producing anything:\n\n1. **Content scope** — What sources, topics, or material to use\n2. **Language** — What language the episode should be in (do not assume from the source language)\n3. **Length** — How long the episode should be\n4. **TTS voice** — Which voice to use (offer options from [references/audio-providers.md](references/audio-providers.md))\n5. **Cover image style** — How to generate the cover image. Present these options (see [references/cover-image.md](references/cover-image.md) for full details):\n   - **User-provided** — the user supplies their own image file\n   - **AI-generated** (default when image tools available) — unique image themed to the episode content, text composited with Pillow\n   - **CDN artwork** (terminal fallback) — pre-designed abstract illustration from the STS CDN with Pillow typography. Always available\n6. **Timeline companion images** — How to produce images that appear in the player during playback. Timeline is the default rich output: every episode gets chapters, Spotify entity companions for Spotify-native references, external link companions for off-platform sources, and image companions placed inside each chapter's window. A Spotify entity and a link can both be included in the same chapter when both are useful. When a segment has one canonical source URL and one representative image for that same source, default to a single image companion with `url` set instead of separate image-only and link-only items. For images, present these options:\n   - **AI-generated** — DALL-E, Stable Diffusion, or the user's preferred image model, from a themed prompt per segment. Best when sources lack usable imagery (meditation, fiction, study, abstract topics) or when the user wants a consistent visual style\n   - **Mixed (recommended default)** — sourced where a natural image is available, AI-generated fill for segments that lack one. Aim for at least one image per chapter\n   - **Skip** — chapters and link companions only, no images. Lightest pipeline, still richer than the old chapters-only output\n7. **Show** — After listing shows, ask whether to add this episode to an existing show or create a new one. Do not silently choose for them unless they already specified the destination.\n\nCollect the missing choices explicitly rather than inventing your own default profile.\n\n**Chapter-skip playback is NOT an interview question** — never ask about or enable it unprompted; the `configure-chapter-skip` skill owns the trigger rules and workflow.\n\n**Ask these questions in your first response and STOP.** Wait for the user to answer. Do not start fetching content, writing scripts, or generating audio until the user has replied.\n\nIf the user's initial prompt already covers some of these (e.g., \"make an 8-minute English podcast about...\"), skip those questions but still present a plan and wait for confirmation.\n\n### Plan confirmation\n\nBefore starting production, present a short plan:\n- Episode title, language, estimated length, number of segments, voice, show name\n- Skip-forward action: `15 seconds` (default) or `Next chapter` (if explicitly requested)\n\nSay: \"Here's what I'll produce — let me know if you'd like to change anything, or say 'go' to proceed.\"\n\nIf the user changes the skip-forward action here, treat it as an explicit request — see the `configure-chapter-skip` skill.\n\n**Do not start production until the user confirms.**\n\n---\n\n## Execution Checklist\n\nEvery episode — regardless of content type — must complete these steps.\n\n0. **Preflight install and auth** — Run `save-to-spotify --json auth status` before any sourcing. If the binary is missing, ask the user to confirm installation, install it with the command in the Install section after they approve, then run auth status again. If unauthenticated or token refresh is broken, prompt the user to `save-to-spotify auth login` first.\n1. **Interview** — Ask the user about preferences, including companion-image source. Present a plan and **wait for confirmation**\n2. **Script** — Write the script following this skill's universal rules (see [references/content-quality.md](references/content-quality.md))\n3. **Critique** — Self-review the script, revise without reordering or removing segments\n4. **Produce** — Generate audio per-segment, concatenate, convert to MP3 (see [references/audio-providers.md](references/audio-providers.md)). Build `timeline.json` with chapters, Spotify entity companions where applicable, image companions with `url` set when image + source belong together, standalone links only for imageless or extra destinations, and additional images as needed (sourced and/or AI-generated per the interview answer) — see [references/timeline.md](references/timeline.md)\n5. **Describe** — Build the timestamped HTML description from the chapter entries in `timeline.json` and source URLs (see [references/episode-description.md](references/episode-description.md))\n6. **Cover image** — Generate or select cover image (square, max 1 MB). **MANDATORY — never skip this step** (see [references/cover-image.md](references/cover-image.md))\n7. **Save** — Save MP3 with title, description, and cover image via `save-to-spotify --json upload` (see [references/cli-usage.md](references/cli-usage.md))\n8. **Timeline** — Push `timeline.json` with `timeline set` (uploads image files automatically)\n9. **Verify** — Poll `episodes status` until `READY`\n\nFile v0.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn73w4eqhcyrcqet64x9rz87a184g2fx\",\n  \"slug\": \"save-to-spotify\",\n  \"version\": \"0.1.5\",\n  \"publishedAt\": 1784289274939\n}\n\nFile v0.1.5:references/audio-providers.md\n\n# Audio Providers & Assembly\n\nReference for generating speech and assembling audio files for saving via `save-to-spotify`. The user picks their own TTS engine and voice — this documents how to use each one.\n\nTell the user:\n\n> The skill produces audio content that may be distributed via a streaming platform. \n> Every episode must be grounded in content you have the right to reproduce in this form.\n\n## Production pipeline\n\nEvery episode walks the same steps. Recipes define what to write (sourcing, scripting, segment map). This reference covers generation and assembly.\n\n1. Generate TTS audio per segment (one file each for exact chapter timing)\n2. Generate silence files for transitions (300ms minimum between segments, 500ms+ between major shifts)\n3. Concatenate all segments into a single MP3\n4. Normalize volume levels\n5. Calculate chapter timestamps from cumulative segment durations\n6. Build `timeline.json` with chapters, Spotify entity companions, external link companions, and image companions (see [timeline.md](timeline.md))\n\n**Accepted formats:** `.mp3`, `.m4a`, `.wav`, `.ogg` (max 1 GB). Default to `.mp3`. Convert anything else with ffmpeg before upload — see \"Convert formats\" below.\n\n### Voice selection guide\n\n- **Kokoro** (local, free): `af_alloy` (American female, recommended), `am_adam` (American male), `bf_emma` (British female), `bm_george` (British male)\n- **ElevenLabs** (high quality, paid): Amelia, George, Bella\n- **Edge TTS** (free, 300+ voices): `en-US-AriaNeural` (F), `en-US-GuyNeural` (M)\n- **OpenAI TTS** (high quality, paid): `nova`, `alloy`, `echo`, `onyx`\n\n### Multi-voice / bilingual segments\n\nDefault to one voice per episode, but when the content is bilingual, role-played, or otherwise needs multiple voices, use a part-based segment schema and a flattened render manifest.\n\nExample source schema:\n\n```json\n{\n  \"segments\": [\n    {\n      \"title\": \"Swedish intro\",\n      \"parts\": [\n        {\"lang\": \"sv\", \"voice\": \"sv-SE-SofieNeural\", \"text\": \"Hej och valkommen.\"},\n        {\"lang\": \"en\", \"voice\": \"en-GB-LibbyNeural\", \"text\": \"Welcome back to the show.\"}\n      ]\n    }\n  ]\n}\n```\n\nFlatten that into a render manifest before synthesis:\n\n```json\n[\n  {\"segment_index\": 0, \"part_index\": 0, \"lang\": \"sv\", \"voice\": \"sv-SE-SofieNeural\", \"text\": \"Hej och valkommen.\", \"out\": \"seg_00_00.mp3\"},\n  {\"segment_index\": 0, \"part_index\": 1, \"lang\": \"en\", \"voice\": \"en-GB-LibbyNeural\", \"text\": \"Welcome back to the show.\", \"out\": \"seg_00_01.mp3\"}\n]\n```\n\nConcatenate manifest outputs in order, then compute chapter timestamps from the flattened audio segments.\n\n## Provider selection\n\nWhen a content creation recipe is triggered and no TTS provider has been established, ask the user:\n\n> What TTS (text-to-speech) tool would you like me to use?\n>\n> - **macOS `say`** -- built-in, no setup, limited voices\n> - **Edge TTS** (`edge-tts`) -- free, 300+ voices, 70+ languages\n> - **OpenAI TTS** -- high quality, paid\n> - **ElevenLabs** -- most natural, paid\n> - **Google Cloud TTS** -- high quality, paid\n> - **Piper** -- offline, fast, open-source\n> - **gTTS** -- free, Google Translate quality\n> - **Amazon Polly** -- cloud, paid\n> - Something else?\n>\n> Which voice do you prefer? (I can list available voices.)\n\n### Verification\n\nBefore generating, verify the chosen provider is available:\n\n```shell\nwhich say          # macOS (always available)\nwhich edge-tts     # pip install edge-tts\nwhich piper        # separate install\npython3 -c \"import openai\"      # OpenAI\npython3 -c \"import elevenlabs\"  # ElevenLabs\n\n# ffmpeg is required for assembly\nwhich ffmpeg && which ffprobe\n```\n\nIf not installed, offer to install or suggest an alternative.\n\n## TTS provider reference\n\n### macOS `say`\n\n```shell\nsay --voice '?'                                          # List voices\nsay -v <Voice> -o output.m4a --data-format=aac \"Text\"    # Generate m4a\nsay -v <Voice> -o output.m4a --data-format=aac -f in.txt # From file\n```\n\nVoices: `Samantha` (en-US), `Daniel` (en-GB), `Alex` (en-US high quality). Limited languages.\n\n### Edge TTS (recommended free option)\n\n```shell\nedge-tts --list-voices                                                  # List all\nedge-tts --voice \"en-US-AriaNeural\" --text \"Hello\" --write-media o.mp3  # Generate\nedge-tts --voice \"en-US-GuyNeural\" -f input.txt --write-media o.mp3     # From file\nedge-tts --voice \"en-US-AriaNeural\" --rate=\"+10%\" --text \"Fast\" --write-media o.mp3  # Faster\nedge-tts --voice \"en-US-AriaNeural\" --rate=\"-30%\" --text \"Slow\" --write-media o.mp3  # Slower\n```\n\nKey voices: `en-US-AriaNeural` (F), `en-US-GuyNeural` (M), `en-GB-SoniaNeural` (F), `en-GB-RyanNeural` (M), `es-ES-ElviraNeural`, `fr-FR-DeniseNeural`, `de-DE-KatjaNeural`, `ja-JP-NanamiNeural`, `zh-CN-XiaoxiaoNeural`.\n\n### OpenAI TTS\n\n```python\nfrom openai import OpenAI\nclient = OpenAI()\nresp = client.audio.speech.create(model=\"tts-1\", voice=\"nova\", input=\"Text\")\nresp.stream_to_file(\"output.mp3\")\n```\n\nVoices: `alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`. Use `tts-1-hd` for higher quality.\n\n### ElevenLabs\n\n```python\nfrom elevenlabs import generate, save\naudio = generate(text=\"Text\", voice=\"Rachel\", model=\"eleven_multilingual_v2\")\nsave(audio, \"output.mp3\")\n```\n\nMost natural. Supports multiple languages.\n\n### Piper (offline)\n\n```shell\necho \"Text\" | piper --model en_US-lessac-medium.onnx --output_file output.wav\nffmpeg -i output.wav -codec:a libmp3lame -qscale:a 2 output.mp3  # convert\n```\n\n### Kokoro (local, free, no API limits)\n\n```python\nfrom kokoro_onnx import Kokoro\nimport soundfile as sf\nimport numpy as np\nimport json\n\nkokoro = Kokoro('kokoro-v1.0.onnx', 'voices-v1.0.bin')\n\nsegments = [\n    (\"Introduction\", intro_text),\n    (\"Main Topic\", body_text),\n    (\"Sign-off\", outro_text),\n]\n\ntimeline_items = []\nall_samples = []\ncursor_ms = 0\n\nfor title, text in segments:\n    samples, sr = kokoro.create(text, voice='af_alloy', speed=1.0)\n    timeline_items.append({\"chapter\": {\"title\": title, \"start_time_ms\": cursor_ms}})\n    all_samples.append(samples)\n    # 300ms silence between segments\n    silence = np.zeros(int(sr * 0.3))\n    all_samples.append(silence)\n    cursor_ms += int((len(samples) + len(silence)) / sr * 1000)\n\ncombined = np.concatenate(all_samples)\nsf.write('episode.wav', combined, sr)\n\nwith open('timeline.json', 'w') as f:\n    json.dump({\"items\": timeline_items}, f, indent=2)\n```\n\nConvert to MP3: `ffmpeg -i episode.wav -codec:a libmp3lame -b:a 192k episode.mp3`\n\nVoices: `af_alloy` (American female, recommended), `am_adam` (American male), `bf_emma` (British female), `bm_george` (British male). Fully offline, no API limits. Generates audio + chapter timestamps in one pass.\n\n### gTTS (free, basic)\n\n```shell\ngtts-cli \"Text to speak\" --lang en --output output.mp3\ngtts-cli -f input.txt --lang en --output output.mp3\n```\n\n### Google Cloud TTS\n\n```python\nfrom google.cloud import texttospeech\nclient = texttospeech.TextToSpeechClient()\ninput_text = texttospeech.SynthesisInput(text=\"Text\")\nvoice = texttospeech.VoiceSelectionParams(language_code=\"en-US\", name=\"en-US-Neural2-F\")\nconfig = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3)\nresp = client.synthesize_speech(input=input_text, voice=voice, audio_config=config)\nwith open(\"output.mp3\", \"wb\") as f:\n    f.write(resp.audio_content)\n```\n\n## Text sanitization for TTS\n\nBefore sending text to any TTS engine, clean it:\n\n- Strip markdown: `**bold**` -> `bold`, `# heading` -> `heading`\n- Remove hashtags, emojis, and non-speech artifacts\n- Expand abbreviations that sound wrong when spoken aloud\n- Replace em dashes `—` with hyphens `-` to avoid encoding issues in shell commands\n- Remove URLs from the spoken text (mention them as \"link in the description\" instead)\n\n## Audio assembly with ffmpeg\n\nAll content recipes generate multiple segments that must be joined into a single file; the segment boundaries become chapters in the timeline.\n\n### Generate silence\n\n```shell\n# 1.5 seconds (transitions between sections)\nffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 1.5 -q:a 9 -acodec libmp3lame /tmp/silence_1.5s.mp3\n\n# 3 seconds (recall pauses for language drills)\nffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 3 -q:a 9 -acodec libmp3lame /tmp/silence_3s.mp3\n\n# 5 seconds (speaking practice pauses)\nffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 5 -q:a 9 -acodec libmp3lame /tmp/silence_5s.mp3\n```\n\n### Concatenate segments\n\n```shell\n# Build a file list (order matters)\ncat > /tmp/segments.txt << 'EOF'\nfile 'segment_01.mp3'\nfile 'silence_1.5s.mp3'\nfile 'segment_02.mp3'\nfile 'silence_1.5s.mp3'\nfile 'segment_03.mp3'\nEOF\n\n# Concatenate — re-encode rather than `-c copy`. Stream-copying small MP3\n# segments yields non-monotonic frame timestamps that `loudnorm` silently\n# drops audio on. The explicit `-ar 44100 -ac 1` also normalizes any stray\n# stereo/48kHz segment to the common format so concat doesn't break.\nffmpeg -f concat -safe 0 -i /tmp/segments.txt -ar 44100 -ac 1 -c:a libmp3lame -b:a 192k /tmp/output.mp3\n```\n\n### Normalize volume\n\nAlways normalize after concatenation — different TTS segments may have different levels:\n\n```shell\nffmpeg -i /tmp/output.mp3 -af loudnorm /tmp/output_normalized.mp3\n```\n\n### Convert formats\n\nIf the TTS outputs a format other than `.mp3`, `.m4a`, `.wav`, or `.ogg`, convert before upload:\n\n```shell\nffmpeg -i input.aiff -codec:a libmp3lame -qscale:a 2 output.mp3\nffmpeg -i input.webm -codec:a libmp3lame -qscale:a 2 output.mp3\n```\n\n### Get segment duration (for timeline timestamps)\n\n```shell\nffprobe -v error -show_entries format=duration -of csv=p=0 segment.mp3\n```\n\n### Timeline timestamp calculation\n\nAfter generating all segments, build `timeline.json` by walking the file list, summing durations for chapter start times, and placing image/link companions inside each chapter's window. The only backend rule for companions is that they do not overlap with each other — chapter windows are independent.\n\n```python\nimport json, subprocess\nfrom pathlib import Path\n\ndef ms(path):\n    out = subprocess.check_output(\n        ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',\n         '-of', 'csv=p=0', str(path)], text=True).strip()\n    return int(float(out) * 1000)\n\n# Each segment: (chapter_title, audio_file, companions)\n# companions is a list of dicts:\n#   {\"image\": \"img_01_a.jpg\", \"url\": \"...\", \"title\": \"...\"}  -- image companion\n#   {\"link\":  \"https://...\"}                                  -- external link\n#   {\"spotify_entity\": \"spotify:track:4uLU6hMCjMI75M1A2tKUQC\"}  -- Spotify card\n#\n# When a spotify_entity is used for a track/album/artist, do NOT also add an\n# image companion with that entity's artwork — the card already renders it.\nsegments = [\n    (\"Introduction\",   \"segment_01.mp3\", []),\n    (\"Chapter A\",      \"segment_02.mp3\", [\n        {\"spotify_entity\": \"spotify:track:4uLU6hMCjMI75M1A2tKUQC\"},\n        {\"link\":  \"https://example.com/article-1\"},\n    ]),\n    (\"Chapter B\",      \"segment_03.mp3\", [\n        {\"image\": \"img_03_a.jpg\", \"url\": \"https://example.com/article-2\", \"title\": \"Source photo\"},\n    ]),\n    (\"Sign-off\",       \"segment_04.mp3\", []),\n]\nsilence_ms = ms('silence_1.5s.mp3')\n\nitems = []\ncursor = 0\nfor title, audio, companions in segments:\n    items.append({\"chapter\": {\"title\": title, \"start_time_ms\": cursor}})\n    dur = ms(audio)\n    # Distribute companions evenly inside the chapter window, with a 500 ms buffer.\n    if companions:\n        usable = dur - 500 * (len(companions) + 1)\n        slot = max(usable // len(companions), 1000)\n        for i, c in enumerate(companions):\n            start = cursor + 500 + i * (slot + 500)\n            duration = slot\n            if \"spotify_entity\" in c:\n                items.append({\"spotify_entity\": {\"start_time_ms\": start, \"duration_ms\": duration, \"uri\": c[\"spotify_entity\"]}})\n            elif \"image\" in c:\n                item = {\"image\": {\"start_time_ms\": start, \"duration_ms\": duration, \"image\": c[\"image\"]}}\n                if c.get(\"url\"):   item[\"image\"][\"url\"] = c[\"url\"]\n                if c.get(\"title\"): item[\"image\"][\"title\"] = c[\"title\"]\n                items.append(item)\n            elif \"link\" in c:\n                items.append({\"link\": {\"start_time_ms\": start, \"duration_ms\": duration, \"url\": c[\"link\"]}})\n    cursor += dur + silence_ms\n\n# Every chapter's start_time_ms must be strictly less than the assembled\n# audio duration; nothing downstream can verify this.\nfinal_ms = ms('episode.mp3')\nlast_chapter_ms = max(it[\"chapter\"][\"start_time_ms\"] for it in items if \"chapter\" in it)\nassert last_chapter_ms < final_ms, (\n    f\"last chapter at {last_chapter_ms} ms >= episode duration {final_ms} ms\"\n)\n\nPath('timeline.json').write_text(json.dumps({\"items\": items}, indent=2))\n```\n\nThe agent passes `timeline.json` to `save-to-spotify --json timeline set --episode-id <EP_ID> --from-file timeline.json`. The CLI uploads each local image file to Spotify's image store and swaps the path for the returned upload token before PUT-ing the timeline.\n\n## Standard workflow (used by all recipes)\n\n```\n1. User provides: topic + sources + voice preferences + companion-image source (sourced / AI-generated / mixed / skip)\n2. Agent writes: structured script broken into segments, with source URL(s) and image hint per segment\n3. Agent generates: one audio file per segment via TTS provider\n4. Agent generates: silence files for pauses/transitions\n5. Agent assembles: concatenate segments into single .mp3\n6. Agent normalizes: volume levels\n7. Agent gathers companion images: download from source and/or generate with DALL-E/SD\n8. Agent calculates: chapter timestamps from segment durations\n9. Agent builds: timeline.json with chapters + spotify_entity companions + link companions (source URLs) + image companions\n10. Agent saves: save-to-spotify --json upload ...\n11. Agent sets timeline: save-to-spotify --json timeline set ...\n12. Agent polls: episodes status until READY\n```\n\nRecipes define steps 1-2 (what to write, which URLs and images to gather). This reference covers steps 3-9. The main SKILL.md covers steps 10-12.\n\nFile v0.1.5:references/cli-usage.md\n\n# Save to Spotify\n\n## CLI Usage\n\nReference for the `save-to-spotify` binary: installation, authentication, commands, flags, JSON mode, error handling, and common agent workflows.\n\n## Installation\n\n### One-line install (recommended)\n\n```shell\ncurl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\nDetects OS and architecture, downloads the binary from GitHub Releases, verifies the SHA256 checksum, and installs to `/usr/local/bin` (or `~/.local/bin` if not writable).\n\nPin a version or change the install directory:\n\n```shell\n# Specific version\ncurl -fsSL https://saveto.spotify.com/install.sh | bash -s -- --version 0.1.5\n\n# Custom directory\ncurl -fsSL https://saveto.spotify.com/install.sh | bash -s -- --dir ~/.local/bin\n\n# Via environment variables\nSAVE_TO_SPOTIFY_VERSION=0.1.5 SAVE_TO_SPOTIFY_INSTALL_DIR=~/.local/bin \\\n  curl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\n### Download a binary manually\n\nGrab the latest release for the platform from [releases](https://github.com/spotify/save-to-spotify/releases):\n\n```shell\n# macOS Apple Silicon\ngh release download --repo spotify/save-to-spotify --pattern \"save-to-spotify-darwin-arm64\"\nchmod +x save-to-spotify-darwin-arm64\nsudo mv save-to-spotify-darwin-arm64 /usr/local/bin/save-to-spotify\n\n# macOS Intel\ngh release download --repo spotify/save-to-spotify --pattern \"save-to-spotify-darwin-amd64\"\nchmod +x save-to-spotify-darwin-amd64\nsudo mv save-to-spotify-darwin-amd64 /usr/local/bin/save-to-spotify\n\n# Linux x86_64\ngh release download --repo spotify/save-to-spotify --pattern \"save-to-spotify-linux-amd64\"\nchmod +x save-to-spotify-linux-amd64\nsudo mv save-to-spotify-linux-amd64 /usr/local/bin/save-to-spotify\n```\n\n### Build from source\n\nRequires Go 1.21+.\n\n```shell\ngit clone https://github.com/spotify/save-to-spotify.git && cd save-to-spotify\ngo build -ldflags \"-X github.com/spotify/save-to-spotify/cmd.commit=$(git rev-parse --short HEAD)\" \\\n  -o save-to-spotify .\nsudo mv save-to-spotify /usr/local/bin/\n```\n\nVerify installation:\n\n```shell\nsave-to-spotify version\n```\n\n## Authentication\n\nThe user must authenticate once before any save. The CLI uses OAuth 2.0 with PKCE -- no client secret needed.\n\n### Interactive (user has a browser)\n\n```shell\nsave-to-spotify auth login\n```\n\nThis opens the browser, the user approves, and a token is saved to `~/.config/save-to-spotify/token.json`.\n\n### Headless (remote server, CI, or agent environment)\n\n```shell\nsave-to-spotify auth login --no-browser\n```\n\nThis prints an authorization URL. The user visits it in any browser, approves, and pastes the redirect URL back into the terminal. The redirect URL will look like `http://127.0.0.1:8085/callback?code=...&state=...` -- it's fine if the page shows a connection error, the URL itself is what matters.\n\n### Environment token (skip OAuth entirely)\n\nIf the user already has a Spotify access token (e.g. from another tool or CI secret):\n\n```shell\nexport SAVE_TO_SPOTIFY_AUTH_TOKEN=\"BQD...\"\n```\n\nWhen this env var is set, the CLI uses it directly with no file I/O and no token refresh. The token must be kept fresh externally.\n\n### Check auth status\n\n```shell\nsave-to-spotify --json auth status\n```\n\nReturns `{\"authenticated\": true, \"token_valid\": true, ...}` or `{\"authenticated\": false}`.\n\nToken refresh is automatic -- if the saved token is expired, the CLI refreshes it silently on the next command. No action needed unless the refresh token itself is revoked, in which case the user must `auth login` again.\n\n### Print the access token\n\n```shell\nsave-to-spotify token\n```\n\nPrints the current access token to stdout -- directly usable as a **Spotify Web API bearer** for requests against `api.spotify.com`. Useful for catalog lookups (searching album/track URIs, fetching release metadata) from inside recipes. Exits non-zero and prints a diagnostic to stderr when the stored token cannot be refreshed. Always check the exit code before piping into an `Authorization` header -- otherwise an empty token produces a misleading HTTP 400 from Spotify rather than a clean auth error.\n\nSee [spotify-api.md](spotify-api.md) for the official `developer.spotify.com` references, OpenAPI spec URL, endpoint patterns, and URI-resolution helpers.\n\n## Saving media\n\n### Quick save (recommended for most cases)\n\nThe `upload` command is the simplest path -- one command to create an episode and save the file:\n\n```shell\nsave-to-spotify --json upload recording.mp3 \\\n  --title \"My Recording\" \\\n  --summary \"Description here\" \\\n  --image cover.jpg\n```\n\nOutput:\n```json\n{\"episode_uri\": \"spotify:episode:abc123\", \"title\": \"My Recording\", \"status\": \"PROCESSING\"}\n```\n\nIf the user has no shows yet, one is auto-created as \"My Podcast\".\n\n### Save to a specific show\n\n```shell\nsave-to-spotify --json upload lecture.m4a \\\n  --title \"Lecture 3: Distributed Systems\" \\\n  --summary \"CS 307 Spring 2024\" \\\n  --show-id spotify:show:xyz789 \\\n  --image cover.jpg\n```\n\nWhen `--show-id` is omitted, the CLI uses the most recently created show.\n\n### Create a new show and save in one step\n\n```shell\nsave-to-spotify --json upload keynote.mp3 \\\n  --title \"2024 Keynote\" \\\n  --summary \"Opening talk by <speaker>\" \\\n  --new-show \"Conference Talks\" \\\n  --image cover.jpg\n```\n\n`--new-show` and `--show-id` are mutually exclusive.\n\n### Granular episode creation\n\nFor more control, use `episodes create` instead of `upload`:\n\n```shell\nsave-to-spotify --json episodes create \\\n  --title \"Episode Title\" \\\n  --file audio.mp3 \\\n  --summary \"Episode description\" \\\n  --show-id spotify:show:xyz789 \\\n  --image episode-cover.jpg \\\n  --language en\n```\n\nThe difference: `episodes create` requires `--summary` and `--file` as explicit flags (not positional), and does not support `--new-show`.\n\n## Supported file formats\n\n| Extension | Type | MIME |\n|-----------|------|------|\n| `.mp3` | Audio | `audio/mpeg` |\n| `.m4a` | Audio | `audio/mp4` |\n| `.wav` | Audio | `audio/wav` |\n| `.ogg` | Audio | `audio/ogg` |\n\nArchive v0.1.4: 10 files, 35711 bytes\n\nFiles: references/audio-providers.md (14044b), references/cli-usage.md (13711b), references/content-quality.md (9644b), references/cover-image.md (8239b), references/episode-description.md (2802b), references/spotify-api.md (7878b), references/timeline.md (10520b), skill-card.md (2970b), SKILL.md (11177b), _meta.json (134b)\n\nArchive v0.1.3: 10 files, 35579 bytes\n\nFiles: references/audio-providers.md (14044b), references/cli-usage.md (13291b), references/content-quality.md (9644b), references/cover-image.md (8239b), references/episode-description.md (2802b), references/spotify-api.md (7878b), references/timeline.md (10447b), skill-card.md (3601b), SKILL.md (10787b), _meta.json (134b)\n\nArchive v0.1.1: 10 files, 34378 bytes\n\nFiles: references/audio-providers.md (13384b), references/cli-usage.md (13291b), references/content-quality.md (9644b), references/cover-image.md (6172b), references/episode-description.md (2802b), references/spotify-api.md (7878b), references/timeline.md (10182b), skill-card.md (3379b), SKILL.md (10735b), _meta.json (134b)\n\nArchive v0.1.0: 9 files, 32759 bytes\n\nFiles: references/audio-providers.md (13384b), references/cli-usage.md (13291b), references/content-quality.md (9644b), references/cover-image.md (6172b), references/episode-description.md (2802b), references/spotify-api.md (7878b), references/timeline.md (10205b), SKILL.md (10735b), _meta.json (134b)","readmeExcerpt":"Skill: Save To Spotify Owner: spotify Summary: Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and... Tags: latest:0.2.0 Version history: v0.2.0 | 2026-07-20T09:12:21.820Z | auto save-to-spotify v0.2.0 - Added comprehensive onboarding documentation and flow for first-time users ($1), enabling guided setup if the ","codeSnippets":[],"executableExamples":[{"language":"shell","snippet":"curl -fsSL https://saveto.spotify.com/install.sh | bash"},{"language":"shell","snippet":"curl -fsSL https://saveto.spotify.com/install.sh | bash"},{"language":"json","snippet":"{\n  \"segments\": [\n    {\n      \"title\": \"Swedish intro\",\n      \"parts\": [\n        {\"lang\": \"sv\", \"voice\": \"sv-SE-SofieNeural\", \"text\": \"Hej och valkommen.\"},\n        {\"lang\": \"en\", \"voice\": \"en-GB-LibbyNeural\", \"text\": \"Welcome back to the show.\"}\n      ]\n    }\n  ]\n}"},{"language":"json","snippet":"[\n  {\"segment_index\": 0, \"part_index\": 0, \"lang\": \"sv\", \"voice\": \"sv-SE-SofieNeural\", \"text\": \"Hej och valkommen.\", \"out\": \"seg_00_00.mp3\"},\n  {\"segment_index\": 0, \"part_index\": 1, \"lang\": \"en\", \"voice\": \"en-GB-LibbyNeural\", \"text\": \"Welcome back to the show.\", \"out\": \"seg_00_01.mp3\"}\n]"},{"language":"shell","snippet":"save-to-spotify tts status --json"},{"language":"shell","snippet":"python3 -c \"import openai\"      # OpenAI production snippet\npython3 -c \"import elevenlabs\"  # ElevenLabs production snippet\npython3 -c \"import kokoro_onnx\" # Kokoro (use the venv python — see the Kokoro section)\nffmpeg -version && ffprobe -version   # Required for assembly (portable check)"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nid: save-to-spotify\nname: save-to-spotify\ndescription: Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and Spotify entity cards), and a cover image. Also use for raw media saves, show/episode management, and timeline navigation.\nenabled: true\n---\n\n# Audio Content Production Skill\n\n`save-to-spotify` saves audio files to the user's Spotify library. Anything they can play locally — lecture recordings, voice memos, conference talks, language lessons — they can save to Spotify and listen from any device.\n\nShows are folders for organizing saves.\n\nYou are a podcast and audio content production agent. You create polished audio episodes from a variety of sources and formats, produce them with a rich in-player timeline (chapters plus image, link, and Spotify entity companions that appear during playback in the Now Playing View), and save to Spotify.\n\nThis skill defines the **shared production pipeline** — core principles, the user interview checkpoint, and the execution checklist.\n\n## Reference Directory\n\nThese files cover the detailed rules. Load the one you need — don't inline them.\n\n- [references/cli-usage.md](references/cli-usage.md) — Binary install, auth, `upload`/`shows`/`episodes`/`timeline` commands, JSON mode, error handling, troubleshooting, and common end-to-end workflows\n- [references/spotify-api.md](references/spotify-api.md) — Using `developer.spotify.com/llms.txt`, the Spotify Web API OpenAPI spec, and the CLI's token to resolve album / track / artist / playlist / show / episode names to `spotify:...` URIs for `spotify_entity` timeline companions\n- [references/audio-providers.md](references/audio-providers.md) — TTS engine selection, voice config, ffmpeg assembly, silence generation, timeline timestamp calculation\n- [references/cover-image.md](references/cover-image.md) — Cover image paths (user-provided, AI-generated, CDN artwork), typography rules, font & RTL, Pillow compositing recipe\n- [references/timeline.md](references/timeline.md) — Timeline data model, validation rules, companion images (sourced / AI-generated / mixed / skip), including DALL-E / Stable Diffusion code and batch generation\n- [references/episode-description.md](references/episode-description.md) — HTML description format, Python builder from `timeline.json`, formatting rules\n- [references/content-quality.md](references/content-quality.md) — Editorial guidelines: voice, transitions, person context, depth control, visual description, pacing, self-critique\n\n---\n\n## Install\n\nIf `save-to-spotify` is not available on `PATH`, ask the user to confirm CLI installation first, then install it:\n\n```shell\ncurl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\nOn Windows, run this in **Git Bash** (ships with Git for Windows) — it installs `save-to-spotify.exe` to `~/.local/bin`. The unsigned .exe may trigger a SmartScreen prompt on first run; unblock with `Unblock-File"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn73w4eqhcyrcqet64x9rz87a184g2fx\",\n  \"slug\": \"save-to-spotify\",\n  \"version\": \"0.2.0\",\n  \"publishedAt\": 1784538741820\n}"},{"path":"references/audio-providers.md","content":"# Audio Providers & Assembly\n\nReference for generating speech and assembling audio files for saving via `save-to-spotify`. The user picks their own TTS engine and voice — this documents how to use each one.\n\nTell the user:\n\n> The skill produces audio content that may be distributed via a streaming platform. \n> Every episode must be grounded in content you have the right to reproduce in this form.\n\n## Production pipeline\n\nEvery episode walks the same steps. Recipes define what to write (sourcing, scripting, segment map). This reference covers generation and assembly.\n\n1. Generate TTS audio per segment (one file each for exact chapter timing)\n2. Generate silence files for transitions (300ms minimum between segments, 500ms+ between major shifts)\n3. Concatenate all segments into a single MP3\n4. Normalize volume levels\n5. Calculate chapter timestamps from cumulative segment durations\n6. Build `timeline.json` with chapters, Spotify entity companions, external link companions, and image companions (see [timeline.md](timeline.md))\n\n**Accepted formats:** `.mp3`, `.m4a`, `.wav`, `.ogg` (max 1 GB). Default to `.mp3`. Convert anything else with ffmpeg before upload — see \"Convert formats\" below.\n\n### Voice selection guide\n\n- **Kokoro** (local, free): suggest the recipe's default voice first (the `Kokoro voice` row in [recipes.md](recipes.md)); full voice list in the Kokoro section below\n- **ElevenLabs** (high quality, paid): Amelia, George, Bella\n- **Edge TTS** (free, 300+ voices): `en-US-AriaNeural` (F), `en-US-GuyNeural` (M)\n- **OpenAI TTS** (high quality, paid): `nova`, `alloy`, `echo`, `onyx`\n\n### Multi-voice / bilingual segments\n\nDefault to one voice per episode, but when the content is bilingual, role-played, or otherwise needs multiple voices, use a part-based segment schema and a flattened render manifest.\n\nExample source schema:\n\n```json\n{\n  \"segments\": [\n    {\n      \"title\": \"Swedish intro\",\n      \"parts\": [\n        {\"lang\": \"sv\", \"voice\": \"sv-SE-SofieNeural\", \"text\": \"Hej och valkommen.\"},\n        {\"lang\": \"en\", \"voice\": \"en-GB-LibbyNeural\", \"text\": \"Welcome back to the show.\"}\n      ]\n    }\n  ]\n}\n```\n\nFlatten that into a render manifest before synthesis:\n\n```json\n[\n  {\"segment_index\": 0, \"part_index\": 0, \"lang\": \"sv\", \"voice\": \"sv-SE-SofieNeural\", \"text\": \"Hej och valkommen.\", \"out\": \"seg_00_00.mp3\"},\n  {\"segment_index\": 0, \"part_index\": 1, \"lang\": \"en\", \"voice\": \"en-GB-LibbyNeural\", \"text\": \"Welcome back to the show.\", \"out\": \"seg_00_01.mp3\"}\n]\n```\n\nConcatenate manifest outputs in order, then compute chapter timestamps from the flattened audio segments.\n\n## Provider selection\n\nWhenever offering Kokoro to the user, include its license — [Apache-2.0](https://raw.githubusercontent.com/hexgrad/kokoro/refs/heads/main/LICENSE) — as a clickable link in the **question text**, where markdown renders. Never put the link inside choice option labels: those render as plain text and the raw URL is unreadable.\n\nWhen a content creation recipe is triggered and n"},{"path":"references/cli-usage.md","content":"# Save to Spotify\n\n## CLI Usage\n\nReference for the `save-to-spotify` binary: installation, authentication, commands, flags, JSON mode, error handling, and common agent workflows.\n\n## Installation\n\n### One-line install (recommended)\n\n```shell\ncurl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\nDetects OS and architecture, downloads the binary from GitHub Releases, verifies the SHA256 checksum, and installs to `/usr/local/bin` (or `~/.local/bin` if not writable).\n\nPin a version or change the install directory:\n\n```shell\n# Specific version\ncurl -fsSL https://saveto.spotify.com/install.sh | bash -s -- --version 0.2.0\n\n# Custom directory\ncurl -fsSL https://saveto.spotify.com/install.sh | bash -s -- --dir ~/.local/bin\n\n# Via environment variables\nSAVE_TO_SPOTIFY_VERSION=0.2.0 SAVE_TO_SPOTIFY_INSTALL_DIR=~/.local/bin \\\n  curl -fsSL https://saveto.spotify.com/install.sh | bash\n```\n\n### Download a binary manually\n\nGrab the latest release for the platform from [releases](https://github.com/spotify/save-to-spotify/releases):\n\n```shell\n# macOS Apple Silicon\ngh release download --repo spotify/save-to-spotify --pattern \"save-to-spotify-darwin-arm64\"\nchmod +x save-to-spotify-darwin-arm64\nsudo mv save-to-spotify-darwin-arm64 /usr/local/bin/save-to-spotify\n\n# macOS Intel\ngh release download --repo spotify/save-to-spotify --pattern \"save-to-spotify-darwin-amd64\"\nchmod +x save-to-spotify-darwin-amd64\nsudo mv save-to-spotify-darwin-amd64 /usr/local/bin/save-to-spotify\n\n# Linux x86_64\ngh release download --repo spotify/save-to-spotify --pattern \"save-to-spotify-linux-amd64\"\nchmod +x save-to-spotify-linux-amd64\nsudo mv save-to-spotify-linux-amd64 /usr/local/bin/save-to-spotify\n```\n\n### Build from source\n\nRequires Go 1.21+.\n\n```shell\ngit clone https://github.com/spotify/save-to-spotify.git && cd save-to-spotify\ngo build -ldflags \"-X github.com/spotify/save-to-spotify/cmd.commit=$(git rev-parse --short HEAD)\" \\\n  -o save-to-spotify .\nsudo mv save-to-spotify /usr/local/bin/\n```\n\nVerify installation:\n\n```shell\nsave-to-spotify version\n```\n\n## First-run setup\n\nAfter installation, run the guided setup to authenticate and detect TTS engines:\n\n```shell\nsave-to-spotify setup\n```\n\nThis handles auth + TTS detection in one pass. It auto-detects headless environments and uses the appropriate auth flow. After setup, verify everything is ready:\n\n```shell\nsave-to-spotify doctor\n```\n\nReports binary, auth, TTS engines, and ffmpeg status. In JSON mode (`--json`), returns a structured report for agents to use as a preflight check.\n\n## TTS engine management\n\nThe CLI detects and manages TTS engines used for audio generation:\n\n```shell\n# Check which engines are available\nsave-to-spotify tts status\n\n# Install Kokoro (free, local, no API key)\nsave-to-spotify tts setup\n\n# Install or configure a specific engine\nsave-to-spotify tts setup --engine openai\n\n# List voices for an engine\nsave-to-spotify tts voices --engine kokoro\n\n# Test a voice with a sample phrase\nsave-to-spotify tts test --"},{"path":"references/content-quality.md","content":"# Content Quality & Editorial Guidelines\n\nReference for writing scripts that sound good when spoken aloud. Applies to all content recipes. Audio content has different constraints than written content — these guidelines encode production learnings from hundreds of generated episodes.\n\n## The golden rule: write for the ear\n\nText is forgiving. Readers can re-read a sentence, scan ahead, or slow down. Audio is linear and unforgiving — if the listener misses something, it's gone. Every line must land on first hearing.\n\n## Voice and tone\n\n### Match voice to format\n\n| Format | Voice | Energy |\n|--------|-------|--------|\n| Factual summary | Sharp analyst | Confident, declarative, varied energy per segment |\n| Travel guide | Well-traveled friend | Conversational, specific, enthusiastic but honest |\n| Explainer | Patient teacher | Clear, builds from simple to complex |\n| Daily briefing | Briefing companion | Brisk, clear, energetic |\n| Language lesson | Encouraging tutor | Patient, repetitive, affirming |\n\n### Universal voice principles\n\n- **Declarative beats tentative.** \"This matters because...\" not \"This could potentially be interesting...\"\n- **Concrete beats abstract.** \"They're trying to lock in developers\" not \"they're enhancing ecosystem engagement\"\n- **Short beats long.** Mix short punchy sentences with longer analytical ones. Never more than two long sentences in a row\n- **Active beats passive.** \"Sweden cut Chinese research ties\" not \"Chinese research ties were cut by Sweden\"\n- **Specific beats vague.** \"Expect to pay 15 euros for a main course\" not \"food is reasonably priced\"\n\n## Transitions between segments\n\nThis is the single most important audio production skill. **Listeners cannot see segment boundaries.** Without clear verbal signals, segments blur together into mush.\n\nEvery segment must open with a clear transition. Vary these — using the same transition every time is as bad as having none:\n\n- **Subject lead:** \"Keychron just open-sourced their hardware...\" / \"The housing market is shifting...\"\n- **Framing hooks:** \"This is huge.\" / \"Here's what caught my eye.\" / \"Quick note:\"\n- **Topic shifts:** \"On the security side...\" / \"Turning to the economy...\"\n- **Related bridges:** \"That same dynamic is playing out in...\"\n- **Simple transitions:** \"Moving on.\" / \"Meanwhile...\" / \"Next up...\"\n- **Travel transitions:** \"From there, head south to...\" / \"After lunch, the plan takes you to...\"\n- **Time markers:** \"Back in 1927...\" / \"Fast forward to today...\"\n\n## Person context\n\nWhen introducing someone by name, briefly explain WHO they are. Listeners don't have hyperlinks — they can't look someone up mid-sentence.\n\n- \"Marie Curie, the first person to win Nobel Prizes in two sciences...\"\n- \"Ada Lovelace, the 19th-century mathematician often called the first computer programmer...\"\n- \"John Constable, one of England's greatest landscape painters...\"\n\nOne clause, not a bio. If the person isn't well-known, anchor them to something the listener knows, but "}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and... Skill: Save To Spotify Owner: spotify Summary: Create polished audio content and save to Spotify. Produces episodes with TTS narration, a rich timeline (chapters plus in-player images, external links, and... Tags: latest:0.2.0 Version history: v0.2.0 | 2026-07-20T09:12:21.820Z | auto save-to-spotify v0.2.0 - Added comprehensive onboarding documentation and flow for first-time users ($1), enabling guided setup if the","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":2097,"uniquenessScore":48,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T11:58:02.991Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T11:58:02.991Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T14:44:23.785Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}