Skip to content

fix(voice): stop treating the word "no" as the abbreviation "No." - #927

Open
philluiz2323 wants to merge 2 commits into
GeniePod:mainfrom
philluiz2323:fix/voice-no-is-not-an-abbreviation
Open

fix(voice): stop treating the word "no" as the abbreviation "No."#927
philluiz2323 wants to merge 2 commits into
GeniePod:mainfrom
philluiz2323:fix/voice-no-is-not-an-abbreviation

Conversation

@philluiz2323

@philluiz2323 philluiz2323 commented Jul 31, 2026

Copy link
Copy Markdown

Summary

Closes #925. ABBREVIATIONS is matched case-insensitively and carried "no"
(for the number sign No.) and "fig" (for Fig.). Both are also ordinary
English words that really do end sentences, so any clause closing on one never
confirmed its boundary.

ends_with_abbreviation is shared with voice::streaming::SentenceStreamer, so
the damage was two-sided.

Truncation. for_voice caps voice replies at 3 sentences — unless the first
clause ends in "no":

input before after
The answer is no. The lights are off. I checked the kitchen. And the hall. all four sentences first three
The answer is yes. The lights are off. I checked the kitchen. And the hall. first three first three

The only difference between those two lines is no vs yes.

Streaming. Feeding "The answer is no. The lights are off." emitted nothing
at the no. boundary — the first sentence was withheld from Piper until the
next terminator arrived. That is first-audio latency paid for a false
abbreviation, on one of the most common short answers a home assistant gives.

A sentence-initial No. masks this in casual testing: a 3-char clause is
below MIN_SENTENCE_CHARS in both callers (5 in format, 8 in streaming) and
merges into the next clause anyway. The bug only surfaces when the word ends a
clause long enough to stand alone.

Changes

  • Split the two colliding entries out of ABBREVIATIONS into a new
    CAPITALIZED_ABBREVIATIONS matched case-sensitively in their canonical
    form. No. / Fig. stay abbreviations; no. / fig. / NO. are words and
    close the sentence.
  • Deleting them outright was the other option and is wrong: See No. 5 below.
    would then split mid-reference. There is a test guarding exactly that.
  • Every other entry keeps its case-insensitive match, so Dr. / dr. / p.m.
    are unaffected.
  • Regression tests in voice::format (helper + both for_voice behaviours) and
    in voice::streaming (the shared-helper latency path).

Real Behavior Proof

  • I have built and run the affected code locally (or noted why I could not).
  • I have verified the change end-to-end on Jetson hardware.
  • I have NOT verified on Jetson hardware, and I explain the equivalent verification path or validation gap below.

Tested profile / hardware (check all that apply):

  • jetson
  • raspberry_pi
  • portable_sbc
  • laptop
  • mac
  • CI-only / docs-only
  • Not run locally

What I ran

x86_64 Linux laptop (Ubuntu 22.04 LTS, kernel 6.8.0-136-generic,
rustc 1.96.0 / cargo 1.96.0). No Jetson and no Piper/audio device available to
me — see the validation gap below.

cargo test -p genie-core
cargo test -p genie-core --no-default-features
cargo clippy -p genie-core --all-targets
cargo clippy -p genie-core --all-targets --no-default-features
cargo fmt -p genie-core -- --check

Plus a temporary probe calling for_voice directly on main over the
paired "no"/"yes" inputs, so the difference was observed rather than inferred.
The probe was removed before committing; only the permanent tests remain.

What I observed

Probe output on main — same sentence, one word different, two different
truncation results:

IN : The answer is no. The lights are off. I checked the kitchen. And the hall.
OUT: The answer is no. The lights are off. I checked the kitchen. And the hall.

IN : The answer is yes. The lights are off. I checked the kitchen. And the hall.
OUT: The answer is yes. The lights are off. I checked the kitchen.

The "no" line returns all four sentences; the "yes" line correctly stops at
three. On this branch both stop at three. Controls on main behaved correctly
in the same probe run (Ask Dr. Smith about it. and It costs 3.50 dollars.
both truncated to three), which is what isolates the abbreviation list as the
cause rather than the terminator logic.

All four new tests pass here and fail on main. Full suite:
1005 lib tests + all integration suites green on default features and on
--no-default-features; clippy clean on both; cargo fmt --check clean.

Validation gap

I could not verify on Jetson hardware or with a real Piper voice. The
equivalent verification path: both changed call sites are pure text→text (a
&str predicate and a String transform) with no I/O, no async, no audio
dependency and no model involvement. The streaming test drives the real
SentenceStreamer through feed() and asserts which sentences are handed to
the TTS channel, which is precisely the boundary the Jetson path would exercise
— what happens downstream of that channel is unchanged by this diff.

Test plan

  1. Cherry-pick the tests onto main alone — the_word_no_is_not_an_abbreviation
    and splits_at_a_clause_ending_in_the_word_no fail.
  2. Check out this branch, rerun — all pass.
  3. On a device, ask a yes/no question that yields a multi-sentence answer
    beginning "The answer is no." and confirm audio starts at the first clause
    rather than after the second.

Notes for reviewers

  • Fig is included because it is the same defect, not scope creep: fig is a
    lowercase English noun in exactly the way no is, and it sits in the same
    list behind the same case-insensitive compare.
  • No prompt growth, no new dependencies. The 4096-token Jetson context contract
    is untouched.

Summary by CodeRabbit

  • Bug Fixes

    • Improved sentence detection for “no.” and “fig.” so capitalization is handled correctly.
    • Preserved phrases such as “No. 5” without prematurely ending the sentence.
    • Improved streaming responses by ending sentences promptly when lowercase sentence-ending forms are used.
  • Tests

    • Added regression coverage for abbreviation detection, sentence truncation, and streaming behavior.

ABBREVIATIONS is matched case-insensitively, and it carried "no" (for
the number sign "No.") and "fig" (for "Fig."). Both are also ordinary
English words that really do end sentences, so any clause closing on one
never confirmed its boundary.

Both voice paths share ends_with_abbreviation, so the damage was
two-sided:

  * format::truncate_for_voice silently over-ran its 3-sentence cap.
    "The answer is no. The lights are off. I checked the kitchen. And
    the hall." emitted all four sentences, while the identical text with
    "yes" correctly stopped at three.

  * SentenceStreamer withheld the clause from Piper until the *next*
    terminator arrived, so first-audio latency was paid for a false
    abbreviation on one of the most common short answers a home
    assistant gives.

Deleting the entries outright would regress the real abbreviation
("See No. 5 below." would split mid-reference), so match these
case-sensitively in their canonical capitalized form instead: "No."/
"Fig." stay abbreviations, "no."/"fig."/"NO." are words and close the
sentence. Every other entry keeps its case-insensitive match, so
"Dr."/"dr."/"p.m." are unaffected.

A sentence-initial "No." is still not split out on its own, but that is
MIN_SENTENCE_CHARS doing the work — a 3-char clause is below the floor
in both callers (5 here, 8 in the streamer) and merges into the next
clause regardless, exactly as before.

Closes GeniePod#925
@github-actions github-actions Bot added the bug Something isn't working label Jul 31, 2026
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6891248e-fe40-4a10-b02e-2810120e9f5b

📥 Commits

Reviewing files that changed from the base of the PR and between f766cfa and af26afc.

📒 Files selected for processing (1)
  • crates/genie-core/src/voice/format.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/genie-core/src/voice/format.rs

📝 Walkthrough

Walkthrough

The voice formatter now treats only capitalized No. and Fig. as abbreviations. Other case variants end sentences. Formatter and streaming tests cover sentence caps and No. 5 handling.

Changes

Voice abbreviation boundaries

Layer / File(s) Summary
Abbreviation matching rules
crates/genie-core/src/voice/format.rs
The formatter checks canonical No and Fig abbreviations case-sensitively before applying case-insensitive matching to other abbreviations.
Sentence boundary regression coverage
crates/genie-core/src/voice/format.rs, crates/genie-core/src/voice/streaming.rs
Tests verify sentence caps and streaming boundaries for no., No. 5, fig., and related forms.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the fix for incorrect treatment of "no" as the abbreviation "No.".
Linked Issues check ✅ Passed The changes address the linked issue by fixing casing behavior for No. and Fig., sentence limits, streaming boundaries, and regression coverage.
Out of Scope Changes check ✅ Passed The changes are limited to the shared voice formatter behavior, streaming behavior, and focused regression tests required by issue #925.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/genie-core/src/voice/format.rs`:
- Around line 358-362: Add an assertion in the ends_with_abbreviation test cases
covering the uppercase “FIG.” sentence ending, alongside the existing lowercase
“fig.” and uppercase “NO.” assertions, and verify it is treated as a sentence
terminator.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 669d2699-cb04-4f7d-b0f5-43ee67e43660

📥 Commits

Reviewing files that changed from the base of the PR and between 02a577d and f766cfa.

📒 Files selected for processing (2)
  • crates/genie-core/src/voice/format.rs
  • crates/genie-core/src/voice/streaming.rs

Comment thread crates/genie-core/src/voice/format.rs
Follow-up to the review on GeniePod#927.

The case-sensitive list is matched verbatim, so every non-canonical
casing is the ordinary word and must close the sentence. "NO." was
asserted; "FIG." and the mixed-case "nO." were not, even though they run
through the same compare. Adding them pins the whole rule rather than
one instance of it.

Tests only — no behavior change.
@philluiz2323

Copy link
Copy Markdown
Author

Addressed in the latest commit. Added FIG. alongside the existing NO. assertion, plus a mixed-case nO. case — the case-sensitive list is matched verbatim, so every non-canonical casing is the ordinary word, and pinning all three covers the rule rather than one instance of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] voice: the word "no" is treated as the abbreviation "No.", so a clause ending in it never closes

1 participant