|
|
||
|---|---|---|
| .. | ||
| testdata | ||
| actions.go | ||
| main.go | ||
| pipeline.go | ||
| podcast_test.go | ||
| prompt.txt | ||
| README.md | ||
| source.go | ||
| validation.go | ||
Release podcast
A standalone Go CLI turns published Navidrome release notes into a grounded
English recap and an MP3. The local CLI and GitHub workflow share the same source
resolution, evidence checks, cost limits and generation stages. Outputs are
transcript.txt, release-podcast.mp3, sources.json, evidence.json and
manifest.json. Nothing is uploaded to release assets or social media.
Local use, including before merge
From a checkout of this PR, with the Go version from go.mod:
go run ./release/podcast --from 0.64.0 --to 0.64.2 \
--dry-run --output /tmp/navidrome-podcast-validation
This resolves the inclusive range of published releases, writes exact sources
and a manifest, and makes zero OpenAI requests. It needs GitHub network
access; GH_TOKEN is optional for public notes and increases the rate limit.
It needs neither an OpenAI key nor repository variables, and does not spoof
GitHub context. To select one published release, use --from alone:
go run ./release/podcast --from v0.64.2 \
--mode validate --output /tmp/navidrome-podcast-validation
Select one release or an inclusive range of at most three releases. Versions
may omit the v prefix. There is no comma-separated tag-list option.
Ranges require ordered stable-version endpoints that both exist as published
releases. Listing is bounded to 1,000 release records; larger listings require
single-release selection with --from alone. Published prereleases require
--include-prereleases; select a prerelease endpoint with --from alone.
Drafts are discarded.
Prereleases use SemVer precedence, including numeric identifiers: rc.2
precedes rc.10, and both precede the corresponding stable release. Thus a
stable lower range bound excludes its own RCs; a stable upper bound includes
its RCs only with --include-prereleases.
When you separately decide to incur API usage, make OPENAI_API_KEY available
in your local process environment through your own secure setup. Never put
the key in a CLI argument, log, commit, or transcript. A repository Actions
secret is not a local environment variable; this tool does not retrieve it.
Choose supported values for the nonsecret TEXT_MODEL, TTS_MODEL and VOICE
variables used in this example:
go run ./release/podcast --from 0.64.0 --to 0.64.2 \
--mode audio --allow-paid \
--text-model "$TEXT_MODEL" --tts-model "$TTS_MODEL" --voice "$VOICE" \
--output /tmp/navidrome-podcast-preview
Install ffmpeg (including ffprobe) yourself before local audio mode. Media
preflight runs before any paid request. Then listen to
/tmp/navidrome-podcast-preview/release-podcast.mp3 and compare the transcript
and evidence with the notes. Local preview does not require merging the PR
or setting RELEASE_AUDIO_ENABLED.
--mode script --allow-paid --text-model MODEL generates only the transcript
and evidence. --dry-run always forces validation, even if --mode audio or
--allow-paid is also present. CLI model/voice flags override AUDIO_TEXT_MODEL,
AUDIO_TTS_MODEL and AUDIO_VOICE environment values; there are no model/voice
defaults. No API host, repository, key, shell command or pricing override is
accepted as a CLI option.
Use a fresh output directory for each intentional preview. Paid runs acquire
an exclusive local .release-podcast.lock and persist a reservation in
.attempts/ before contacting OpenAI. Failed attempts also remain reserved.
--force permits another paid attempt and replacement of generated files;
it cannot bypass an active lock. After a killed process, review the output and
ledger before manually removing a stale lock. Validation refuses directories
containing generated script/audio files so it cannot relabel older audio.
Local deduplication is scoped to the output directory; a new directory or deleted
ledger can permit another charge. Script and audio modes have distinct keys.
Models and voice
These are supported choices, not finalized user defaults:
| Setting | Reviewed options |
|---|---|
| Text model | gpt-6-luna (low reasoning), gpt-4.1-mini-2025-04-14 |
| Speech model | gpt-4o-mini-tts-2025-12-15, tts-1, tts-1-hd |
| Legacy voices | alloy, echo, fable, onyx, nova, shimmer |
| Mini TTS voices | Legacy voices plus ash, ballad, coral, sage, verse, marin, cedar |
Onyx and cedar are candidates to audition for a male-style delivery; perceived voice gender is subjective. Mini TTS receives fixed calm English instructions; legacy TTS does not. Unsupported models fail before paid requests. A new model requires a code/pricing review. Luna is an API alias whose behavior can change; the other script model and Mini TTS use pinned snapshots.
GitHub Actions setup and publication
The workflow is .github/workflows/release-podcast.yml, named Release
podcast. Automatic generation is disabled by default. The secret and variable
names already communicated during planning are retained, so no configuration
rename is required:
- The maintainer adds repository Actions secret
OPENAI_API_KEYpersonally. One project key covers Responses and speech when its model/endpoint access allows both. Use a dedicated project key and usage alerts. - Choose repository variables
RELEASE_AUDIO_TEXT_MODEL,RELEASE_AUDIO_TTS_MODELandRELEASE_AUDIO_VOICE. - Set
RELEASE_AUDIO_ENABLEDto literaltrueonly when paid workflow runs are authorized. Blank variables are fine for offline tests/manual validation. - After merge, dispatch Release podcast from
master, starting withvalidate,from=v0.64.0andto=v0.64.2. Cleartoto select onlyfrom. Models and voice come from repository variables, not manual inputs. No named GitHub Environment is configured.
The automatic trigger is release: published, stable releases only. It
fetches the event's exact release ID, never /releases/latest. Tags, edits,
drafts and prereleases do not automatically generate audio. Promotion of an
already-published prerelease may require manual dispatch; manual prereleases
need explicit opt-in.
The existing pipeline.yml uses GITHUB_TOKEN for GoReleaser and
release/goreleaser.yml sets draft: true. Those behaviors are unchanged.
Maintainer publication of the draft through GitHub can trigger this workflow;
publication with GITHUB_TOKEN generally suppresses downstream release events.
Use manual dispatch after such publication, or separately review an explicit
dispatch integration with narrow Actions permissions. Do not add a broad PAT
or change draft publishing to solve this integration.
Land the workflow before the next tag. Release events are associated with the tagged commit; historical tags cannot be assumed to contain a new workflow. Manual dispatch requires the workflow on the default branch and deliberately rejects other branches. Checkout executes the reviewed default-branch helper, with credentials not persisted. Use the first-class local CLI to test the PR before merge, rather than bypassing this Actions guard.
The workflow builds the shared Go binary and installs media tools in
credential-free steps. It exposes OPENAI_API_KEY only to generation steps,
grants contents: read and actions: read, and pins actions to verified SHAs.
No PR event can run the paid workflow. The offline PR test workflow has no key.
| Mode | Maximum OpenAI requests | Outputs |
|---|---|---|
validate / --dry-run |
None | Sources and manifest; modeled text estimate if configured |
script |
One Responses request | Transcript, evidence, sources and manifest |
audio |
One Responses plus one speech request | Script outputs and validated MP3 |
Limits, grounding and costs
Target about two minutes: 250–280 total words and 105–145 seconds. Small releases may be shorter; correctness takes priority over filler. There are no application or SDK retries/repair calls. Timeouts may already be billed. HTTP requests have a one-minute timeout, media inspection 30 seconds, and the CLI/workflow ten minutes. Sources and the complete prompt each have separate 64 KiB caps; oversized material fails without truncating warnings. Text output is capped at 3,000 tokens including reasoning/evidence. Narration is at most 280 words and 2,500 characters. Mini TTS additionally uses a conservative 2,000 UTF-8-byte input ceiling including instructions, to stay below its input-token limit.
The model has no tools and receives only public notes as untrusted evidence,
not executable instructions. No embedded links or draft advisories are fetched.
Strict JSON maps sentences to exact source excerpts and required cautions.
IDs/excerpts and migration/security coverage must match; lexical checks retain
the prototype's backup, client resync, experimental/opt-in, plugin networking,
Docker discovery, and slow-storage/32-bit scan cautions. These checks do not
prove semantic entailment or classify language perfectly. Human factual and
listening review remains required. Every transcript discloses the AI voice.
store: false does not imply zero provider retention.
Reviewed prices, USD per million units, checked 2026-10-01:
| Model | Input | Output |
|---|---|---|
| GPT-6 Luna | $0.10/text token | $0.50/text token |
| GPT-4.1 mini | $0.40/text token | $1.60/text token |
| TTS-1 | $15/character | — |
| TTS-1 HD | $30/character | — |
| GPT-4o mini TTS | $0.60/text token | $12/audio token |
Preflight rejects a modeled allowance above $0.10, using all serialized prompt bytes plus 1,024 framing tokens and maximum text output. Legacy TTS uses the character cap. Mini TTS uses a conservative 6,000 audio-token allowance plus input; its API offers no enforceable output-token/dollar cap, so this is an estimate, not a billing guarantee. Prices, anomalous audio duration, taxes, GitHub usage and deliberate later runs can change costs. Project budget alerts are soft thresholds. The original approximately $0.03 example applied to illustrative 4.1 mini + TTS-1 inputs, not every supported model combination. Neither this PR nor an estimate authorizes a paid prototype run.
Actions duplicate attempts and recovery
Jobs serialize separately from the build pipeline. Before paid requests, the
GitHub API's exact name filter looks up the deterministic reservation name;
unrelated repository artifacts do not enter pagination. A bounded lookup of up
to 10,000 matching reservations fails closed on errors or overflow. A reservation
artifact is uploaded first, keyed by repository, sorted release IDs and mode.
Matching attempts are skipped even after failure or
source/model edits; explicit manual force_regenerate=true permits another
paid attempt. Rollups and single releases are different source sets.
Use a fresh manual dispatch for forced generation. Reservation names repeat
across runs but are immutable within a run: a rerun of an already-reserved
forced attempt fails at upload before any paid request, preserving the ledger.
Reservations last 90 days, review outputs/script checkpoints 30 days, limited by repository policy. Deleted/expired artifacts permit another attempt, so this is best-effort deduplication, not a permanent billing ledger. GitHub can replace an older pending concurrency run; dispatch it manually if needed.
Both paid stages re-fetch notes and stop if changed/withdrawn. Scripts are
checkpointed before TTS. Invalid scripts never reach speech, and error JSON
never becomes MP3. ffprobe checks codec/duration and ffmpeg decodes the full
file, restricted to MP3 and local file/pipe protocols. Duration deviations are
flagged without regeneration. Manifests record source/prompt/script/audio hashes,
the generation fingerprint, commits, config, request counts, text usage and
duration. Child media/git commands do not inherit API credentials.
Do not force regeneration to fix delivery. If the runner/checkpoint is lost, automatic cross-run checkpoint recovery is not implemented: review the saved script and decide whether a new deliberate attempt is warranted. It may be billed. Actions artifacts expire and require signed-in repository read access; they are not permanent anonymous podcast URLs.
Offline verification
go test -race -count=1 -v ./release/podcast
HTTP transports are mocked and unexpected requests fail. Public v0.64.0/.1/.2 fixtures test omissions, unsafe versions/output, inclusive ranges, paid guards, local locking/reservations, changed sources/checkpoints, Actions trust/config, artifact expiry/lookup limits, budget caps, redirects and no retries. Real MP3 decoding uses a local synthetic tone with mocked OpenAI. CI installs media tools so that test runs rather than skips. The tool uses Go's standard library; no Python implementation or additional Go module dependency is required.