Skip to content

feat: MockTTS — emit audio_output_end on delay for speak_dialog(wait=True)#102

Merged
JarbasAl merged 3 commits into
devfrom
fix/mocktts-del-shared-playback
Jun 29, 2026
Merged

feat: MockTTS — emit audio_output_end on delay for speak_dialog(wait=True)#102
JarbasAl merged 3 commits into
devfrom
fix/mocktts-del-shared-playback

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Jun 29, 2026

Copy link
Copy Markdown
Member

Skills calling speak_dialog(..., wait=True) block on recognizer_loop:audio_output_end via SessionManager.wait_while_speaking. Without a real TTS the handler thread blocks for 15+s, tripping the §8.3 10s handler backstop and producing spurious handler.error.

MockTTS schedules audio_output_end on a 0.1s Timer from the speak handler. Uses bus.ee.emit (not bus.emit) to bypass FakeBus namespace-migration and on_message side effects so the synthetic event is invisible to test captures.

Fixes the test_adapt_match and test_fallback_match timeouts in ovos-core's end2end suite.

Summary by CodeRabbit

  • Bug Fixes
    • Improved audio playback reliability in test and harness scenarios.
    • Prevented premature shutdown of speech playback during repeated runs, helping audio completion events fire consistently.
    • Added safeguards so successive audio sessions continue working after cleanup and garbage collection.

JarbasAl and others added 2 commits June 28, 2026 00:34
TTS.playback is a class-level attribute shared by every TTS instance in the
process. The inherited TTS.__del__ chains into TTS.stop() -> TTS.playback.stop(),
so when an earlier PlaybackServiceHarness's MockTTS is garbage-collected its
destructor terminated whatever PlaybackThread was *currently* registered there
— which by then belongs to a later, still-running harness. The victim thread
had _terminated set and exited its loop, so its queued speak never played and
ovos.audio.output.ended was never emitted, hanging the next speak() until
timeout. GC timing made this a flaky TimeoutError that surfaced only after
several harness create/destroy cycles (e.g. mid-file in a consumer's
test/end2end suite).

Override MockTTS.__del__ as a no-op: the harness already manages playback-thread
lifecycle explicitly via PlaybackService.shutdown() on context exit, so a mock
instance must never tear down the shared thread on collection.

Add regression tests: a deterministic guard that fires a stale mock's
destructor while a later harness owns TTS.playback and asserts the live
thread is neither terminated nor unable to keep speaking, plus a
many-sequential-harnesses smoke test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…True)

Skills calling speak_dialog(..., wait=True) block on
recognizer_loop:audio_output_end via SessionManager.wait_while_speaking.
Without a real TTS the handler thread blocks for 15+s, tripping the §8.3
10s handler backstop and spurious handler.error.

MockTTS schedules audio_output_end on a 0.1s Timer from the speak handler.
Uses bus.ee.emit (not bus.emit) to bypass FakeBus namespace-migration and
on_message side effects so the synthetic event is invisible to test captures.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99d03c50-c8c2-4cdb-8dc4-67b60a619919

📥 Commits

Reviewing files that changed from the base of the PR and between 3075b6a and a4c8efa.

📒 Files selected for processing (3)
  • ovoscope/__init__.py
  • ovoscope/audio.py
  • test/unittests/test_audio_harness.py

📝 Walkthrough

Walkthrough

MockTTS gains a no-op __del__ to prevent the inherited TTS.__del__ from terminating the shared class-level playback thread during garbage collection. MiniCroft.__init__ registers a _mock_tts handler on SpecMessage.SPEAK that schedules a synthetic recognizer_loop:audio_output_end event via bus.ee.emit. Two regression tests cover both the GC destructor race and sequential harness lifecycle scenarios.

MockTTS GC fix and synthetic TTS completion signal

Layer / File(s) Summary
MockTTS no-op __del__
ovoscope/audio.py
MockTTS overrides __del__ as a no-op, blocking TTS.__del__ from stopping the shared class-level playback thread when a MockTTS instance is garbage-collected.
MiniCroft synthetic audio_output_end on SPEAK
ovoscope/__init__.py
Imports SpecMessage and registers _mock_tts on SpecMessage.SPEAK; the handler uses threading.Timer to emit recognizer_loop:audio_output_end with session context via bus.ee.emit, bypassing FakeBus capture/reset side effects.
Isolation regression tests
test/unittests/test_audio_harness.py
Adds TestPlaybackServiceHarnessIsolation with tests for sequential harness GC cycling and explicit stale MockTTS.__del__() invocation, asserting the live harness playback_thread._terminated stays false and speak() completes correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • OpenVoiceOS/ovoscope#75: Extends PlaybackServiceHarness to inject and use MockTTS, directly related to the playback thread lifecycle being fixed here.
  • OpenVoiceOS/ovoscope#100: Also overrides MockTTS.__del__ and adds TestPlaybackServiceHarnessIsolation regression tests for the same GC/destructor race condition.

Suggested labels

fix

🐇 A rabbit once said with a wiggle of ears,
"No destructor shall stop what the playback thread clears!"
With a no-op __del__ and a timer so neat,
The synthetic end-signal completes every beat.
🎵 hop hop — the thread lives on! 🥕

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mocktts-del-shared-playback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

Ping! I've got your results right here. 🛎️

I've aggregated the results of the automated checks for this PR below.

🏷️ Release Preview

I've generated a preview of the upcoming changes. 🎬

Current: 1.0.2a1Next: 1.1.0a1

Signal Value
Label feature
PR title feat: MockTTS — emit audio_output_end on delay for speak_dialog(wait=True)
Bump minor

✅ PR title follows conventional commit format.


🚀 Release Channel Compatibility

Predicted next version: 1.1.0a1

Channel Status Note Current Constraint
Stable Not in channel -
Testing Too new (must be <1.0.0) ovoscope>=0.7.2,<1.0.0
Alpha Compatible ovoscope>=1.0.2a1

🔒 Security (pip-audit)

I've scanned the dependencies for any hidden surprises. 🔍

✅ No known vulnerabilities found (78 packages scanned).

📋 Repo Health

Ensuring the repo's heart is beating steady (aka main branch). ❤️

✅ All required files present.

Latest Version: 1.0.2a1

ovoscope/version.py — Version file
README.md — README
LICENSE — License file
pyproject.toml — pyproject.toml
⚠️ setup.py — setup.py
CHANGELOG.md — Changelog
ovoscope/version.py has valid version block markers

⚖️ License Check

A detailed legal audit of your PR. 📖

✅ No license violations found.

Policy: Apache 2.0 (universal donor). StrongCopyleft / NetworkCopyleft / WeakCopyleft / Other / Error categories fail. MPL allowed.

🔍 Lint

I've double-checked the data for any anomalies. 🔍

ruff: issues found — see job log

🔨 Build Tests

The build report has been filed and is ready. 📁

✅ All versions pass

Python Build Install Tests
3.10
3.11
3.12
3.13
3.14

📊 Coverage

A comprehensive review of our code coverage. 📖

53.2% total coverage

Files below 80% coverage (17 files)
File Coverage Missing lines
ovoscope/simple_listener.py 0.0% 55
ovoscope/tts_intelligibility.py 0.0% 190
ovoscope/version.py 0.0% 5
ovoscope/intent_cases.py 14.0% 141
ovoscope/classic_listener.py 18.2% 117
ovoscope/pipeline.py 31.0% 78
ovoscope/ocp.py 31.8% 58
ovoscope/cli.py 32.8% 162
ovoscope/e2e.py 34.9% 99
ovoscope/pytest_plugin.py 41.4% 222
ovoscope/listener.py 56.2% 123
ovoscope/media.py 57.2% 101
ovoscope/voice_loop.py 58.3% 115
ovoscope/__init__.py 58.9% 310
ovoscope/coverage.py 63.3% 69
ovoscope/audio.py 64.2% 112
ovoscope/media_provider.py 64.9% 20

Full report: download the coverage-report artifact.


Helping you build the future of voice, one check at a time. 🎙️

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JarbasAl JarbasAl marked this pull request as ready for review June 29, 2026 01:57
@JarbasAl JarbasAl merged commit 0256086 into dev Jun 29, 2026
13 checks passed
@JarbasAl JarbasAl deleted the fix/mocktts-del-shared-playback branch June 29, 2026 01:57
@github-actions github-actions Bot added feature and removed feature labels Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant