Skip to content

fix(tts): strip leading reasoning labels before speech - #107047

Open
KoNit-K wants to merge 1 commit into
NousResearch:mainfrom
KoNit-K:fix/tts-strip-leading-reasoning-labels
Open

KoNit-K wants to merge 1 commit into
NousResearch:mainfrom
KoNit-K:fix/tts-strip-leading-reasoning-labels

Conversation

@KoNit-K

@KoNit-K KoNit-K commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Gateway auto-TTS (and the shared TTS tool path) already strips tagged <think> blocks (#34213), but a reply that starts with a visible section label such as Reasoning: or thinking: still reaches prepare_spoken_text() and gets spoken aloud (observed on Telegram).

This PR adds an allowlisted leading-label strip in tools/tts_text_normalize.py on the shared prepare_spoken_text() pipeline, immediately after strip_nonspoken_blocks and before markdown cleanup. Only reasoning / thinking / analysis / / / `` at the very start of the text, followed by :, `:`, or a newline, are removed. Mid-prose uses and non-allowlisted headers stay spoken.

Related Issue

Fixes #107044

Type of Change

  • ✅ Bug fix (non-breaking change that fixes an issue)

Changes Made

  • tools/tts_text_normalize.py — add and wire it into
  • tests/tools/test_tts_prepare_spoken.py — focused RED/GREEN coverage for leading-label strip + fail-open mid-prose / non-allowlisted labels

How to Test

  • scripts/run_tests.sh on the files in Changes Made — focused verification
  • scripts/run_tests.sh tests/tools/test_tts_prepare_spoken.py -q
  • scripts/run_tests.sh tests/tools/test_tts_text_normalize.py -q
  • Result: 19 passed

Evidence

  • BEFORE: the focused probe was RED on origin/main before this change
  • AFTER: - [x] scripts/run_tests.sh on the files in Changes Made — focused verification — -> 19 passed
  • CONTROL: neighboring paths listed in How to Test still pass

Checklist

Code

  • ✅ I've read the Contributing Guide
  • ✅ My commit messages follow Conventional Commits
  • ✅ I searched for existing PRs to make sure this isn't a duplicate
  • ✅ My PR contains only changes related to this fix
  • ✅ I've run relevant tests locally (see How to Test)
  • ✅ I've added tests for my changes
  • ✅ I've tested on my platform: macOS

Documentation & Housekeeping

  • ✅ Documentation update: N/A unless noted in Changes Made
  • cli-config.yaml.example: N/A
  • CONTRIBUTING.md or AGENTS.md: N/A
  • ✅ Cross-platform impact considered
  • ✅ Tool descriptions/schemas: N/A

Gateway auto-TTS was reading visible section labels like Reasoning: or
thinking: aloud. Strip allowlisted labels only at text start so mid-prose
uses stay spoken.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have tool/tts Text-to-speech and transcription labels Sep 10, 2026
@kvnloo

kvnloo commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Exact-head comment on #107047 head 6f9eb4e. KEEP strip_leading_reasoning_labels on the shared prepare_spoken_text() pipeline (after strip_nonspoken_blocks, before markdown) so gateway auto-TTS no longer speaks a visible leading Reasoning: / thinking: / Analysis: / 分析: / 推理: / 思考: section label (#107044; <think> tags already handled by #34213). KEEP the allowlist + start-anchored regex (^\s*(?:reasoning|thinking|analysis|推理|思考|分析)[ \t]*(?:[::]|\r?\n), IGNORECASE) and count=1 so mid-prose (The reasoning is clear.) and non-allowlisted headers (Summary:) stay spoken. KEEP the RED/GREEN cases in tests/tools/test_tts_prepare_spoken.py (leading EN/fullwidth/CN labels, newline form, mid-prose fail-open, Summary preserved). CHECK: label-only input (Reasoning:) collapses to empty spoken text — confirm TTS callers treat empty as skip, not a broken utterance. CHECK: a second leading allowlisted label after the first strip is intentionally left (count=1) — fine unless models emit stacked Reasoning:/Analysis: openers. CHECK: Chinese labels are case-sensitive codepoints (no IGNORECASE analogue) — OK for CJK. CHECK: no separate gateway adapter path that bypasses prepare_spoken_text (Telegram path author cites should already share this cleaner). Author KoNit-K not kvnloo. 0 reviews / 0 comments. effort: factory-easy.

@KoNit-K

KoNit-K commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Checked the KEEP/CHECK notes against 6f9eb4e1 — no code change needed.

Empty spoken text (Reasoning: / thinking: / 分析:): prepare_spoken_text() collapses label-only input to "". Callers already treat that as skip, not a broken utterance:

  • text_to_speech_tool: returns tool_error("Text is empty after TTS cleanup") before any provider call
  • gateway auto-TTS (_synthesize_auto_tts): if not speech_text: raise ValueError(...), caught and logged, no playback
  • run_voice._send_voice_reply, CLI _speak_whole_file / _voice_speak_response, and streaming TTS (_strip_markdown_for_tts, which is prepare_spoken_text): all return early on empty cleaned text

Stacked labels (count=1): confirmed leftover (Reasoning:\nAnalysis: leftoverAnalysis: leftover). Leaving the second opener is intentional; no evidence models emit stacked allowlisted headers as the only spoken content. Will loop only if that shows up in the wild.

CJK labels: agree — 推理 / 思考 / 分析 are exact codepoints; re.IGNORECASE is only meaningful for the Latin allowlist.

Gateway / Telegram path: Telegram does not override prepare_tts_text. Auto-TTS goes BasePlatformAdapter._synthesize_auto_ttsprepare_tts_text() → shared prepare_spoken_text(). The voice-reply and streaming-TTS paths use _strip_markdown_for_tts, which is the same pipeline (prepare_spoken_text(..., max_chars=None)). No adapter bypass.

KEEP items (shared pipeline placement, start-anchored allowlist, count=1, existing RED/GREEN cases) stay as-is. Focused tests: tests/tools/test_tts_prepare_spoken.py + test_tts_text_normalize.py → 21 passed.

@kvnloo

kvnloo commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Thanks — ACK on the no-code path. Agreed KEEP/CHECK covered; empty spoken text / reasoning-label handling looks fine at head 6f9eb4e without further change from me.

@KoNit-K

KoNit-K commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Duplicate comparison vs #34213 (triage mark)

Classifier treated this as a duplicate because a comment said <think> tags are “already handled by #34213”. That is not a close instruction. Comparison:

#34213 (issue, closed) + landed path this PR #107047
Kind Issue: strip tagged reasoning/<think> from TTS Bugfix for visible leading labels (#107044)
Implementing PR Landed in #73513 as strip_nonspoken_blocks this branch
Files tools/tts_text_normalize.py tagged-block regex only same file, new strip_leading_reasoning_labels + tests
Input <think>…</think> / unterminated <think> / verifier footer Reasoning: / thinking: / Analysis: / 分析: / 推理: / 思考: at start only
Fail-open N/A for plain labels (they are not tags) mid-prose (The reasoning is clear.) and non-allowlisted Summary: stay spoken

Not the same change. #34213 does not remove a visible leading section label. On current prepare_spoken_text() without this PR, Reasoning: hello is still spoken.

Independent delta — keep on #107047

  • Shared-pipeline step after strip_nonspoken_blocks, before markdown
  • Start-anchored allowlist + count=1
  • RED/GREEN cases in tests/tools/test_tts_prepare_spoken.py

Do not fold this into #34213 (already closed/landed) or into #77079 (open; expands tag variants only — no leading-label strip). Closed sibling #107048 targeted the same #107044 label case and is not live; this PR is the remaining candidate.

No code change this round. Focused tests: test_tts_prepare_spoken.py + test_tts_text_normalize.py → 21 passed. Leaving open.

@KoNit-K

KoNit-K commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Duplicate re-check vs #34213 (this maintenance round)

Classifier still marks this as a duplicate of #34213. That is a triage mark, not a close instruction. Re-compared against current origin/main and the related PRs.

#34213 + landed path this PR #107047
Kind Issue (closed): strip tagged reasoning/<think> from TTS Bugfix for visible leading labels (#107044)
Implementing PR Merged in #73513 as strip_nonspoken_blocks this branch (6f9eb4e1)
Files tools/tts_text_normalize.py tagged-block regex only same file, new strip_leading_reasoning_labels + tests
Input <think>…</think> / unterminated <think> / verifier footer Reasoning: / thinking: / Analysis: / 分析: / 推理: / 思考: at start only
On current origin/main (no this PR) prepare_spoken_text("Reasoning: hello world")"Reasoning: hello world" (label still spoken) "hello world"

Not the same change. #34213 / strip_nonspoken_blocks never matches a plain leading section label. Live check on origin/main vs this HEAD:

  • Reasoning: hello world → main speaks it; this PR strips to hello world
  • thinking:请继续。 / 分析:可见回答。 → same (main keeps the label)
  • mid-prose The reasoning is clear. and non-allowlisted Summary: hello stay spoken (fail-open)
  • <think>hidden</think>Visible already clean on both (that is the Feature Request: Strip reasoning/<think> blocks from TTS preprocessing #34213 path)

Independent delta — keep on #107047

  • Shared-pipeline step after strip_nonspoken_blocks, before markdown
  • Start-anchored allowlist + count=1
  • RED/GREEN cases in tests/tools/test_tts_prepare_spoken.py

Do not fold this into #34213 (closed issue; landed). Do not fold into #77079 (still open; expands tag variants only — no strip_leading_*). Closed sibling #107048 targeted the same #107044 case and was closed as a duplicate of this PR; #107047 remains the live candidate.

No review threads, no CI failures, mergeable, no code change this round. Focused tests: test_tts_prepare_spoken.py + test_tts_text_normalize.py → 21 passed.

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

Labels

P3 Low — cosmetic, nice to have tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Gateway TTS can speak a leading reasoning label

3 participants