Skip to content

feat: experimental real-time streaming transcription (fixes #320) - #387

Open
jatinkrmalik wants to merge 9 commits into
mainfrom
fix/issue-320-streaming-transcription
Open

feat: experimental real-time streaming transcription (fixes #320)#387
jatinkrmalik wants to merge 9 commits into
mainfrom
fix/issue-320-streaming-transcription

Conversation

@jatinkrmalik

@jatinkrmalik jatinkrmalik commented Apr 6, 2026

Copy link
Copy Markdown
Member

Summary

Implements real-time streaming transcription as an experimental feature (off by default) that shows text as you speak, rather than waiting until you stop recording.

Fixes #320
Related discussions: #382, #94

Changes

Phase 1: Config & Engine Plumbing

  • config_manager.py: Added experimental_streaming, streaming_chunk_duration_ms, streaming_overlap_ms to DEFAULT_CONFIG (all off/default)
  • common_types.py: Added StreamingCallbackProtocol and extended SpeechRecognitionManagerProtocol with streaming methods
  • main.py: Passes streaming config from settings to SpeechRecognitionManager constructor

Phase 2: TranscriptBuffer (LA-2 Dedup)

  • transcript_buffer.py: New file implementing Local Agreement (LA-2) policy for word-level dedup across overlapping audio segments. Prevents flickering and duplicate text injection.

Phase 3: Streaming Engine

  • recognition_manager.py:
    • Timer-based audio chunking with configurable overlap
    • Vosk path: uses native PartialResult() API
    • Whisper path: uses TranscriptBuffer with sliding window
    • add_streaming_callback / remove_streaming_callback for external consumers
    • reconfigure accepts streaming params with validation

Phase 4: UI & Integration

  • settings_dialog.py: Added streaming toggle and chunk duration spin in Recognition Settings
  • tray_indicator.py: Registers streaming callback for live text injection

Tests

  • test_streaming.py: 23 unit tests covering TranscriptBuffer logic (basic, flush, reset, overlap, case-insensitive), config defaults, and config manager round-trip integration
  • test_main.py: Updated for new constructor kwargs

Architecture

┌─────────────┐    timer    ┌──────────────────┐    callback        ┌──────────────┐
│ Recording   │───enqueue──▶│ Streaming Engine │───(text,is_final)─▶│ TrayIndicator│
│ Loop        │  (overlap)  │ (Vosk/Whisper)   │                    │ → inject_text│
└─────────────┘             └──────────────────┘                    └──────────────┘
                                     │
                            ┌────────┴────────┐
                            │ TranscriptBuffer│
                            │ (LA-2 dedup)    │
                            └─────────────────┘

Testing

  • ✅ 23 new streaming tests pass
  • ✅ 1455 existing tests pass (8 pre-existing failures in xkb_layout/ibus unrelated to this PR)
  • ✅ All changed files formatted with black --line-length 100
  • flake8 clean on all changed files
  • ✅ All files compile without syntax errors

How to Test

  1. Open Settings → Recognition Settings
  2. Enable "Real-time Streaming" toggle
  3. Start dictation — text should appear incrementally as you speak
  4. Adjust "Chunk Duration" (0.2s–5.0s) to balance latency vs accuracy

Notes

  • Feature is OFF by default behind experimental_streaming config key
  • Vosk streaming uses native PartialResult() — fast and well-tested
  • Whisper streaming uses a sliding window with TranscriptBuffer dedup — experimental and may be less accurate
  • The TranscriptBuffer LA-2 policy ensures words are only committed when seen in consecutive passes, preventing duplicates from overlapping audio segments

@codecov

codecov Bot commented Apr 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.02204% with 58 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.64%. Comparing base (0556d16) to head (6944303).
⚠️ Report is 23 commits behind head on main.

Files with missing lines Patch % Lines
...ocalinux/speech_recognition/recognition_manager.py 80.81% 38 Missing and 9 partials ⚠️
.../vocalinux/speech_recognition/transcript_buffer.py 94.17% 3 Missing and 3 partials ⚠️
src/vocalinux/common_types.py 50.00% 2 Missing and 1 partial ⚠️
src/vocalinux/main.py 66.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #387      +/-   ##
==========================================
+ Coverage   79.91%   80.64%   +0.73%     
==========================================
  Files          30       31       +1     
  Lines        4615     4955     +340     
  Branches      699      770      +71     
==========================================
+ Hits         3688     3996     +308     
- Misses        779      799      +20     
- Partials      148      160      +12     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jatinkrmalik jatinkrmalik added the stale The pull request is stale and/or has merge conflicts. label May 7, 2026
saschabuehrle and others added 7 commits May 6, 2026 23:18
- Add real_time_streaming config option (default: true)
- Add streaming_chunk_duration config option (default: 3.0s)
- Modify recognition loop to process audio chunks at regular intervals
- Keep existing silence-based chunking when streaming is disabled
- Stream audio every 3 seconds instead of waiting for silence pauses

This enables true real-time transcription as users speak, rather than
waiting for silence periods to process entire utterances.
Implement streaming transcription as an experimental feature gated behind
an 'experimental_streaming' config toggle (off by default).

- Add TranscriptBuffer with LA-2 dedup for overlapping segment deduplication
- Add streaming engine integration for Vosk (PartialResult API) and Whisper
  (sliding window with transcript buffer)
- Add streaming config to settings dialog with toggle and chunk duration spin
- Register streaming callback in tray indicator for live text injection
- Add StreamingCallbackProtocol and extend SpeechRecognitionManagerProtocol
- Add 23 unit tests covering buffer logic, config defaults, and integration
- Update existing test_main.py for new constructor kwargs
…- fix flaky streaming config tests by using robust temp config paths\n- prevent duplicate text injection by removing tray-level final injection\n- make TranscriptBuffer.flush emit deltas only; avoid re-emitting committed prefixes\n- align streaming Whisper inference with non-streaming settings and model locking\n- avoid Vosk overlap replay and add partial/final duplicate suppression\n- fix settings dialog read path for VAD/silence values from speech_recognition section\n- document streaming as experimental/WIP in README and docs\n\nUltraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)\n\nCo-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
test_recognition_manager.py had stacked patches on os.makedirs with
incorrect LIFO tearDown ordering, leaving os.makedirs as a MagicMock
for subsequent test modules.  Fix tearDown ordering and harden
TestStreamingConfigIntegration to restore real stdlib functions before
use.
Cover streaming-specific code paths in recognition_manager.py:
- Callback registration (add/remove/multiple)
- _emit_text with voice commands, exceptions, edge cases
- _enqueue_streaming_segment (overlap, queue full, final flag)
- _process_streaming_segment routing (vosk/whisper/whisper_cpp)
- _process_streaming_vosk (partial/final, dedup, JSON errors)
- _process_streaming_whisper (transcribe, auto-language, cpp delegate)
- _perform_recognition streaming segment handling
- Init streaming attributes and state reset
- TranscriptBuffer edge cases and TrayIndicator callback
- CommonTypes protocol coverage

Improves patch coverage from ~30% to ~50% for recognition_manager.py,
transcript_buffer.py to 98%, common_types.py to 71%.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@jatinkrmalik
jatinkrmalik force-pushed the fix/issue-320-streaming-transcription branch from 34a639f to cb6b6bd Compare May 7, 2026 06:24
@jatinkrmalik jatinkrmalik added enhancement New feature or request and removed stale The pull request is stale and/or has merge conflicts. labels May 7, 2026

Copy link
Copy Markdown
Member Author

A note from local testing/investigation:

The streaming behavior is much more straightforward for Vosk than for Whisper/whisper.cpp. Vosk has a native incremental recognizer API: we can feed audio continuously via AcceptWaveform(), show PartialResult() as a live hypothesis, and commit Result() / FinalResult() when the recognizer endpoints. That model maps pretty naturally to dictation.

Whisper is different. It is fundamentally a windowed transcription model, not a true streaming decoder. For the experimental mode here we are repeatedly transcribing short overlapping chunks and then trying to decide what text is stable enough to inject. That creates a few practical problems:

  • Very short chunks improve latency but make punctuation and phrase boundaries worse.
  • Overlapping chunks often repeat boundary words, e.g. are... are, of... of, so we need tail/head deduping.
  • Chunks can be sequential rather than revisions of the same phrase, so a strict Local Agreement policy can accidentally hold all text until stop.
  • Injecting partial text directly into arbitrary apps is risky because replacing previous partials reliably across IBus/X11/Wayland/clipboard paths is a much bigger UI/input problem than just recognizing speech.

So my current read is: keeping this as an opt-in experimental mode is the right shape. Vosk streaming can be treated as closer to "native streaming". Whisper/whisper.cpp streaming should be framed as a best-effort low-latency chunking mode, where users may need to tune chunk duration upward for better punctuation and flow.

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

Labels

enhancement New feature or request stale The pull request is stale and/or has merge conflicts.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Real Time Transcription doesn't work.

2 participants