Skip to content

Summary mode: fix content padding, add expressive delivery, fix map-reduce threshold gap#21

Merged
MKS-01 merged 13 commits into
mainfrom
fix/summary-padding-short-articles
Jul 2, 2026
Merged

Summary mode: fix content padding, add expressive delivery, fix map-reduce threshold gap#21
MKS-01 merged 13 commits into
mainfrom
fix/summary-padding-short-articles

Conversation

@MKS-01

@MKS-01 MKS-01 commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix Summary mode padding short articles with invented, ungrounded filler — anchors the LLM prompt to the source's actual word count instead of an abstract "don't pad" instruction.
  • Add content-driven expression: CSM delivery temperature now nudges per synthesis chunk from punctuation (!/?/dense commas) instead of staying flat for the whole read.
  • Randomize chunk boundaries within a range instead of a fixed cap, for a less mechanical breath cadence (also sharpens the expression feature's granularity).
  • Merge in summary_max_chars 16K→60K (this branch had diverged from main before that fix landed) — closes a real gap where medium-long articles were needlessly map-reducing.
  • Switch to _MAX_CHARS: 200 for better intonation; keep precision: bf16 (no audible quality loss vs fp32, so no reason to pay the ~6% fp32 cost).

Verified

  • Real before/after on a reported slow read ("Chasing a Phantom Jump", 3,446 words): 140.4s → 97.0s total conversion time (31% faster), map-reduce ceiling overshoot 403 → 313 words (better, not fully fixed — tracked as a follow-up in docs/ROADMAP.md).
  • Padding fix: same article re-verified live, 243 → 188 words with zero invented claims; long-form coverage unaffected (5,240-word article still produces a near-ceiling 280-word summary).
  • Expression feature: real Synthesizer (no stub) produces genuinely different temperatures across chunks; played back with afplay for a live A/B.
  • Chunk randomization: same input text produces different chunk counts/boundaries across repeated calls; existing tests only assert bounds, not exact boundaries, so unaffected — ran 20x to rule out flakiness.
  • Full pytest suite: 38/38 pass throughout.

Test plan

  • Live read (Summary mode) on a short article — confirm no padding/invented content
  • Live read on a long article (>16K, <60K chars) — confirm single-pass, not map-reduce
  • Live read with the CLI, listen for delivery variation on content with mixed punctuation
  • pytest

🤖 Generated with Claude Code

MKS-01 and others added 13 commits July 2, 2026 01:28
The configured summary LLM (Qwen3.5-9B) has a 262K-token context, so the old
16K-char single-pass threshold forced most long-form articles through 3-4
sequential LLM calls when one pass would do. Verified live: a 33K-char
article's Summary-mode conversion time dropped from 102.2s to 72.0s (~30%
faster); summarize alone dropped 51.3s to 13.4s. Map-reduce still triggers
correctly for genuinely huge inputs (confirmed with a 226K-char synthetic
doc). Full pytest suite passes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Reviewing a real library read surfaced the bug: a 189-word source produced a
243-word "summary" — longer than the article — padded with generic wrap-up
sentences not grounded in the source (e.g. "a significant shift toward
industry-wide safety standards", "malicious applications" — neither in the
original text). The tone prompts said "don't pad" but gave the model no sense
of what "short" meant for a given source, just an upper bound it drifted
toward regardless of input length.

_summarize_once now spells out the source's word count as a concrete anchor,
and both tone prompts target roughly half that count, framing 250 words as a
ceiling for genuinely long sources rather than a default. Verified: the same
article now summarizes to 188 words with zero invented claims, while a
5,240-word Wikipedia article still produces a 280-word summary near the
ceiling (long-form coverage unaffected). Full pytest suite passes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
CSM has no direct emotion/prosody control API — sampling temperature is the
only delivery lever it exposes, and it was previously fixed once per read
from the reading tone. Added _expressive_temperature (speak.py), which nudges
the tone's base temperature per chunk from its punctuation (! -> livelier,
? -> questioning, comma-dense with neither -> more measured), so delivery
shifts with content instead of staying flat for the whole read. Also nudged
both tone prompts (tones.py) to write with natural spoken rhythm instead of a
flat, even-register list of facts.

Verified with a real Synthesizer (no stub): a mixed-punctuation paragraph
produced genuinely different temperatures across its two chunks (0.88 vs 0.77
from a 0.8 base). Granularity is per ~400-char chunk, not per sentence — a
documented tradeoff against the existing chunk-size speed/quality knob, not a
bug. Full pytest suite passes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…nks)

User asked to pick the best version for voice quality and summary accuracy,
explicitly accepting a slighter delay. Switches to the preset already
documented in config.yaml's speed/quality guide: precision fp32 (max
fidelity) and _MAX_CHARS 200 (best intonation), previously left off in favor
of the Fast preset (bf16 / 400).

Smaller chunks also sharpen the per-chunk expressive-temperature feature: a
dramatic test paragraph went from 2 chunks (a calm sentence dragged into the
same chunk/temperature as the exclamation after it) to 5 chunks, each getting
its own content-appropriate temperature. Verified live with afplay A/B and a
real Synthesizer. Full pytest suite passes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
A uniform _MAX_CHARS cap makes every chunk close to the same length, which
reads with a mechanical, same-every-time breath cadence. chunk_text now draws
each chunk's cap fresh from [_MIN_CHUNK_CHARS(120), _MAX_CHARS(200)] via
_next_chunk_cap(), redrawn per chunk, so consecutive chunks vary in length.
The over-long-sentence comma-split safety net still checks against the fixed
_MAX_CHARS, not the random cap.

This also compounds with the per-chunk expressive-temperature feature: a
shorter random cap is more likely to isolate a single sentence (and its own
temperature) instead of merging it with a differently-toned neighbor.

Verified: same input text produces genuinely different chunk counts/lengths
across repeated calls. Existing chunking tests only assert bounds and
paragraph-splitting, not exact boundaries, so they hold regardless of which
cap is drawn; ran them 20x to rule out flakiness. Full pytest suite passes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
User asked to stick with bf16 after trying the fp32 Max-quality preset — bf16
has no audible quality loss at normal listening per the engine's own docs, so
there's no real quality cost to reverting it, just the ~6% speed saving back.
The 200-char / randomized chunk-boundary changes (the actual driver of the
requested voice-quality/expression improvements) are unaffected.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…-padding-short-articles

# Conflicts:
#	docs/PLAN.md
Confirmed the merge actually fixes the reported 140.4s conversion time: same
article, full pipeline, before vs after the merge — 140.4s -> 97.0s (31%
faster), and the map-reduce path's 250-word-ceiling overshoot (403 words)
improves to 313 (better, not yet fully fixed).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…iling

Tracked for next session: the word-count-anchor fix helps (403 -> 313 words
on the reported slow article) but doesn't fully hold the 250-word hard limit
on longer single-pass summaries. Also folds the padding/expression/chunking
work from this branch into the Recently shipped list.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…ast chunk band, fragment drop

- chunk band [120,200] -> [280,400]: the low band measured 2.5-3x the chunks
  (= CSM prefills = time) of fixed 400; randomization itself is free, so the
  cadence variation is kept at the fast end
- enforce the 250-word summary ceiling in code (_trim_to_word_ceiling,
  sentence-boundary clip at SUMMARY_WORD_CEILING) instead of prompt-only
- thread the original source word count through map-reduce so the reduce
  step's length anchor stops measuring the compressed digests
- never drop a sub-_MIN_CHARS fragment mid-paragraph ("Wow!" was lost ~21%
  of runs under low random caps); space-split comma-free runs > _MAX_CHARS
- dedupe the tone prompts' length policy into _LENGTH_RULES (one source of truth)
- tests: 38 -> 44 (fragment retention, comma-free hard split, 4x ceiling trim)

Co-Authored-By: Claude Fable 5 <[email protected]>
Reviewing a live read showed the summary faithfully reproducing the blog's
closing acknowledgements and 'share your feedback' invite — grounded in the
source, but zero-information listening. _LENGTH_RULES now orders the model to
omit acknowledgements, community thanks, calls to action, and contact invites
entirely and end on a content point. A softer 'skip them' phrasing was not
enough (the tail still appeared, paraphrased); the imperative form verified
clean on two live regenerations of the same article.

Co-Authored-By: Claude Fable 5 <[email protected]>
… in the player

The reading pace felt slow; measurement showed the speech itself is 170-187 wpm
(normal) so pace is listener taste, and CSM has no speed control — the right
altitude is playback. afplay -r RATE -q 1 gives pitch-preserving rate scaling:
+/- in the player steps 0.1x (0.5-2x) live, restarting at the current position
via the existing seek-slice mechanism; /speed <x> sets it from the input
screen; rate shows beside the progress bar when not 1x and persists to
~/.readback/cli.json. elapsed now advances at rate x wall time (audio
position) so seek and the synced transcript stay aligned at any rate.

Verified via tmux-driven CLI: set/step/persist, pause/resume/seek/transcript
at non-1x rates, no orphaned afplay on quit.

Co-Authored-By: Claude Fable 5 <[email protected]>
Summary quality/delivery fixes (padding, ceiling enforcement, map-reduce
anchor, expressive chunking) plus the CLI playback speed controller land
in this release. Bumped all four anchors (pyproject.toml,
src/readback/__init__.py, src/cli/package.json, src/dashboard/package.json)
and refreshed the CLAUDE.md Version section + ARCHITECTURE.md header +
JOURNEY.md stack snapshot. CLI binary rebuilt to pick up the new banner.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@MKS-01 MKS-01 marked this pull request as ready for review July 2, 2026 16:40
@MKS-01 MKS-01 merged commit 92c6250 into main Jul 2, 2026
4 checks passed
@MKS-01 MKS-01 deleted the fix/summary-padding-short-articles branch July 2, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant