Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f7045a1
feat: add markdown-aware TTS text normalization (#1)
Fieldnote-Echo Feb 21, 2026
d882577
security: pin CUDA base images by digest in bake config
Fieldnote-Echo Jul 16, 2026
cb514d2
security: hardened GHCR release workflow with supply-chain protections
Fieldnote-Echo Feb 21, 2026
b9c8645
ci: add Dependabot for GitHub Actions updates
Fieldnote-Echo Feb 21, 2026
f7b18f4
docs: add fork notice with delta from upstream
Fieldnote-Echo Feb 21, 2026
780f689
test(md): relocate markdown normalizer tests into api/tests
Fieldnote-Echo Jul 18, 2026
e727c37
fix(md): let emphasis span soft-wrap newlines but not blank lines
Fieldnote-Echo Jul 18, 2026
f7a727f
fix(md): treat unclosed code fences as extending to end of input
Fieldnote-Echo Jul 18, 2026
7e28739
fix(md): tolerate balanced parens in link and image URLs
Fieldnote-Echo Jul 18, 2026
733f74f
fix(md): preserve segment boundary spaces around custom phonemes
Fieldnote-Echo Jul 18, 2026
551488d
fix(md): apply markdown normalization for all languages
Fieldnote-Echo Jul 18, 2026
84c7e9a
fix(md): batch of minor markdown normalization fixes
Fieldnote-Echo Jul 18, 2026
50c863a
fix(md): normalize CRLF, bound emphasis scan, blockquoted fences, esc…
Fieldnote-Echo Jul 18, 2026
ff9dc91
fix(md): eliminate remaining quadratic scans in markdown pass
Fieldnote-Echo Jul 18, 2026
db2d699
fix(md): eliminate quadratic scans in list markers, URL/email, acrony…
Fieldnote-Echo Jul 18, 2026
7d2cfe0
docs: add fork roadmap, north star, and narration strategy
Fieldnote-Echo Jul 18, 2026
043098a
fix(md): address bot review — URL regex, borderless tables, test flak…
Fieldnote-Echo Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Dependabot — GitHub Actions only.
# Python deps are managed by upstream; we stay in sync via git merge.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
groups:
ci-actions:
patterns:
- "*"
372 changes: 112 additions & 260 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

158 changes: 158 additions & 0 deletions NARRATION_STRATEGY.md

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions NORTH_STAR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# North Star: Correct, Open, Tunable Narration

*Distilled from the AI-singing/TTS landscape research (see `NARRATION_STRATEGY.md`).
This supersedes "singing" as the goal. Singing was the demo that taught us where
the real gap is; it stays a horizon toy, off the critical path.*

---

## The one sentence

**A pronunciation-and-prosody control layer that lets an author — or the LLM
drafting the script — guarantee *how* an open TTS says something, not just hope
it guesses right; correct by default, tunable when it isn't, and open enough to
give back.**

## What it is

- **Correct.** Jargon, acronyms, numbers, units, and names come out right the
first time. Wrong pronunciation in training content isn't a polish issue — it's
a correctness bug, and we treat it like one (tested, gated, regressible).
- **Tunable.** When the default is wrong, the author fixes it *once*, in a
versioned lexicon, in the same phonetic language (IPA) the model already speaks
through its G2P — not by misspelling words to trick the voice.
- **Open.** Built on Apache-2.0 Kokoro, self-hostable, no per-minute meter, data
stays in-house. And the correctness layer itself is contributed back, not hoarded.

## What it is NOT

- **Not singing.** Vocoder-retune singing is physics-capped (V/UV boundary errors,
formant–pitch coupling, degrades past ~150ms notes). Fun demo, permanent horizon.
- **Not a naturalness play.** We will lose a blind naturalness A/B to ElevenLabs
and that's fine. Modern large TTS already infers good prosody from raw text; we
don't out-scale them and won't try.
- **Not markup-for-its-own-sake.** Reserve explicit control for what scale
provably *fails* at — pronunciation of jargon/acronyms/numbers and deliberate
emphasis. Everything else, let the model do its job.

## Why this is the right long-horizon goal

The market wedge that survived adversarial scrutiny: **correctness + control +
cost, not naturalness.** Incumbents (ElevenLabs, Murf, WellSaid) patch
pronunciation *post-hoc, per word,* behind closed metered APIs. The dominant
authoring tool (Articulate Storyline) has **no pronunciation dictionary at all.**
No one lets the content-generation step emit the pronunciation/prosody markup as
part of drafting. That closed loop — LLM + phoneme-native open model + authoring
pipeline, owned end-to-end and open — is the defensible position, and it's most
valuable precisely for technical training content where a mispronounced acronym
is unacceptable.

## The good-faith open contribution

Kokoro today has no lexicon / pronunciation-control layer. The parts of our work
that *every* Kokoro user benefits from should go **upstream** (to Kokoro-FastAPI
or the misaki G2P project), not stay in our fork:

| Contribute upstream (good-faith) | Keep as our product |
|---|---|
| The Prosody-Annotated Phoneme IR (a clean token contract) | LMS/authoring integration (rippling-lms) |
| A pronunciation **lexicon resolver** + versioned lexicon format | Our domain lexicon (Oxford House jargon) |
| LLM→IPA **validation gate** (reject off-inventory phonemes) | The script-generation prompt chain |
| ReDoS/normalizer hardening *(already PR'd: remsky#489)* | The closed generation→narration loop |

The ReDoS fix we already sent upstream is the template: find a real gap in a
solid project, fix it cleanly and in isolation, give it back. The pronunciation
layer is the same move at larger scale — it makes Kokoro better for everyone and
makes our product possible at the same time.

## The shape of the contribution (concrete)

A **Prosody-Annotated Phoneme IR** as the contract between stages:

```
Token { text, phonemes|None, stress:-2..+2, emphasis:bool,
break_after_ms:int, source: {g2p_default|lexicon|llm_authored|human_override} }
```

Pipeline (each stage swappable, upstreamable pieces marked *):
1. Structure/markdown normalizer ← our md work, already built & hardened
2. Number/acronym/unit expander* (rule-based; where big TTS fails)
3. Lexicon resolver* (human > domain > rules > LLM > G2P default)
4. LLM IPA/prosody annotator + validation gate* (naive LLM→IPA is 31.6% PER;
validated against Kokoro's phoneme inventory → ~5.8%)
5. Kokoro adapter (emit `[word](/ipa/)` + `[word](+N)`, chunk <40k, `speed` callable)

Graded by an **ASR-based eval harness** (WER against intended script) plus a
pronunciation unit-test corpus — build this first; it's what lets us *prove*
"correct," not just claim it.

## Hard-won constraints (don't relearn these)

- Kokoro stress markup `[word](+N)` works but is **subtle**; emotion markup
**never** works (not in its training data) — don't build emotion on Kokoro.
- **Validate every LLM-authored IPA** against the phoneme inventory before
injection; the failure mode is silent symbol inconsistency, not obvious garbage.
- Punctuation / respelling are the *reliable* prosody levers; SSML-style tags are
widely ignored by modern open TTS.
- Phoneme injection breaks ~40k chars → chunk. Word-level timestamps are free
forced alignment. `speed` takes a callable (unused pacing lever).
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
> **Fieldnote-Echo fork** of [remsky/Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) (currently based on `v0.6.0`)
>
> **Goal: correct, open, tunable narration.** Kokoro can *say* words but can't yet
> *guarantee* it says them correctly, and no open TTS gives authors real control over
> pronunciation. This fork is building that layer — correct-by-default pronunciation
> and prosody, tunable when it isn't — aimed at technical/e-learning narration. Not a
> naturalness contest, not singing. See [`ROADMAP.md`](./ROADMAP.md) and [`NORTH_STAR.md`](./NORTH_STAR.md).
>
> **Shipping today:**
> - **Markdown-aware TTS normalization** — strips `**bold**`, `[links](url)`, headings, code blocks, tables, etc. before synthesis so LLM output sounds natural (toggle: `markdown_normalization` in `NormalizationOptions`, default on)
> - **Supply-chain hardened CI** — CUDA base images pinned by digest (`docker-bake.hcl` + release workflow), all GitHub Actions SHA-pinned, Sigstore build provenance, Trivy scan, CycloneDX SBOM per release
> - **Normalizer ReDoS hardening** — bounded the URL/email/markdown patterns that let a single request pin the event loop (also upstreamed: [remsky#489](https://github.com/remsky/Kokoro-FastAPI/pull/489))
> - **GHCR images** — `ghcr.io/fieldnote-echo/kokoro-fastapi-gpu` (GPU, amd64)
>
> Generally-useful fixes are contributed back upstream; we rebase on `remsky/master` periodically.

<p align="center">
<img src="githubbanner.png" alt="Kokoro TTS Banner">
</p>
Expand Down
56 changes: 56 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Fork Roadmap

*Why this fork exists, and where it's going. Full rationale in [`NORTH_STAR.md`](./NORTH_STAR.md); market/technical research in [`NARRATION_STRATEGY.md`](./NARRATION_STRATEGY.md).*

## North star

**Correct, open, tunable narration.** Kokoro can *say* words; it can't yet
*guarantee* it says them correctly, and no open TTS gives authors real control
over pronunciation. This fork's purpose is to close that gap — pronunciation and
prosody an author (or the LLM drafting the script) can guarantee, correct by
default and tunable when it isn't — and to contribute the generally-useful parts
back upstream.

**Not** a naturalness contest (large TTS wins that; fine) and **not** singing
(a physics-capped demo, permanently off the critical path). The wedge is
**correctness + control + cost.**

## Good-faith upstreaming

Parts that help every Kokoro user go to `remsky/Kokoro-FastAPI`; the product
(LMS integration, domain lexicons, the generation→narration loop) stays here.
The ReDoS fix ([remsky#489](https://github.com/remsky/Kokoro-FastAPI/pull/489))
is the template: find a real gap, fix it cleanly in isolation, give it back.

## Status

| # | Milestone | State |
|---|---|---|
| 0 | Upstream sync to v0.6.0 | ✅ done |
| 0 | Supply-chain hardening (digest pins, SHA-pinned actions, provenance, SBOM, Trivy) | ✅ done |
| 0 | Normalizer ReDoS hardening | ✅ done · upstream PR #489 |
| 1 | Markdown-aware normalization (LLM output → speakable) | ✅ done |
| 2 | **ASR-based eval harness + pronunciation test corpus** | ⏭️ next |
| 3 | Deterministic number/acronym/unit expander | planned |
| 4 | Versioned pronunciation **lexicon** + resolver | planned |
| 5 | Prosody-Annotated Phoneme **IR** (the token contract) | planned |
| 6 | LLM IPA/prosody annotator **behind a validation gate** | planned |
| 7 | Blind A/B vs raw Kokoro + one commercial baseline | planned |
| H | *(horizon)* speech-to-singing WORLD retune — demo only | spike done |

## Milestone 2 first (why)

You can't claim "correct" or "less robotic" without measuring it. The harness
(WER via ASR against the intended script + a jargon/acronym unit-test corpus)
grades everything downstream and instantly catches pronunciation regressions.
It's the smallest, highest-leverage piece and it unblocks every later A/B claim.

## Constraints banked (don't relearn)

- Kokoro stress markup `[word](+N)` works but is **subtle**; emotion markup
**never** works (not in its training data).
- **Validate LLM-authored IPA** against Kokoro's phoneme inventory before
injection — naive LLM→IPA is ~31.6% phoneme-error; validated ~5.8%.
- Punctuation/respelling are the reliable prosody levers; SSML-style tags are
widely ignored. Phoneme injection breaks ~40k chars → chunk. Word timestamps
are free forced alignment. `speed` takes a callable (unused pacing lever).
Loading