Moonshine Voice uses an event-driven architecture to notify your application of transcription updates. This page documents the event types and the listener interface.
TranscriptEventListener
Protocol (interface) for receiving transcription events.
from moonshine_voice import TranscriptEventListener
class MyListener(TranscriptEventListener):
def on_line_started(self, event: LineStarted):
pass
def on_line_updated(self, event: LineUpdated):
pass
def on_line_text_changed(self, event: LineTextChanged):
pass
def on_line_completed(self, event: LineCompleted):
pass
def on_error(self, event: Error):
pass
All methods are optional. Implement only the events you need.
Event Types
All event types have these common fields:
The transcript line this event refers to
The stream that generated this event (useful when using multiple streams)
LineStarted
Fired when a new speech segment begins.
from moonshine_voice import LineStarted
def on_line_started(self, event: LineStarted):
print(f"New line started: ID {event.line.line_id}")
print(f"Initial text: {event.line.text}")
Guarantees:
- Called exactly once per line
- Always called before any updates or completion
event.line.is_new is True
event.line.is_complete is False
event.line.text may be empty or contain initial recognition
LineUpdated
Fired when any property of a line changes.
from moonshine_voice import LineUpdated
def on_line_updated(self, event: LineUpdated):
print(f"Line {event.line.line_id} updated")
print(f"Duration: {event.line.duration:.2f}s")
Guarantees:
- Only called between
LineStarted and LineCompleted
event.line.is_updated is True
- May be called zero or more times (depends on
update_interval)
- Called when duration, speaker ID, or text changes
LineTextChanged
Fired when the text of a line changes (subset of LineUpdated).
from moonshine_voice import LineTextChanged
def on_line_text_changed(self, event: LineTextChanged):
# Update UI to show new text
print(f"\r{event.line.text}", end="", flush=True)
Guarantees:
- Only called when
event.line.text changes
event.line.has_text_changed is True
event.line.is_updated is also True
- If text changes, this is called in addition to
LineUpdated
LineCompleted
Fired when a speech segment ends (user pauses).
from moonshine_voice import LineCompleted
def on_line_completed(self, event: LineCompleted):
print(f"\nFinal: {event.line.text}")
# Save to database, trigger actions, etc.
Guarantees:
- Called exactly once per line
- Always called after
LineStarted
event.line.is_complete is True
- This is the last event for this line
- The line data never changes after this event
- If
stop() is called, any active line gets this event
Error
Fired when an error occurs during processing.
from moonshine_voice import Error
def on_error(self, event: Error):
print(f"Error on stream {event.stream_handle}: {event.error}")
# event.line may be None
The exception that occurred
The line being processed when the error occurred, or None
Event Flow Guarantees
Moonshine provides these guarantees about event ordering:
- Exactly one
LineStarted per segment
- Zero or more updates (
LineUpdated, LineTextChanged) per segment
- Exactly one
LineCompleted per segment
- Updates only between start and completion
- Sequential processing - only one line is active at a time per stream
- Stable line IDs -
line_id never changes for a line
- Immutable completed lines - data never changes after
LineCompleted
LineStarted
↓
[LineUpdated] ← May happen 0+ times
[LineTextChanged] ← May happen 0+ times
↓
LineCompleted
Example: Basic Listener
from moonshine_voice import (
Transcriber,
TranscriptEventListener,
LineStarted,
LineTextChanged,
LineCompleted
)
class SimpleListener(TranscriptEventListener):
def on_line_started(self, event: LineStarted):
print("\n[Listening...]", end="", flush=True)
def on_line_text_changed(self, event: LineTextChanged):
# Show updates inline
print(f"\r{event.line.text}", end="", flush=True)
def on_line_completed(self, event: LineCompleted):
# Print final version
print(f"\n✓ {event.line.text}")
transcriber = Transcriber(
model_path="/path/to/models"
)
transcriber.add_listener(SimpleListener())
Example: Multi-Speaker Listener
class SpeakerListener(TranscriptEventListener):
def __init__(self):
self.speaker_names = {}
self.colors = ['\033[91m', '\033[92m', '\033[94m', '\033[95m']
self.reset = '\033[0m'
def get_speaker_label(self, line):
if not line.has_speaker_id:
return "Speaker ?"
if line.speaker_id not in self.speaker_names:
# Assign name and color
idx = len(self.speaker_names)
self.speaker_names[line.speaker_id] = f"Speaker {idx + 1}"
return self.speaker_names[line.speaker_id]
def on_line_completed(self, event: LineCompleted):
speaker = self.get_speaker_label(event.line)
color = self.colors[event.line.speaker_index % len(self.colors)]
print(f"{color}{speaker}:{self.reset} {event.line.text}")
transcriber.add_listener(SpeakerListener())
Example: Saving to Database
import sqlite3
from datetime import datetime
class DatabaseListener(TranscriptEventListener):
def __init__(self, db_path):
self.conn = sqlite3.connect(db_path)
self.conn.execute('''
CREATE TABLE IF NOT EXISTS transcripts (
id INTEGER PRIMARY KEY,
line_id INTEGER,
timestamp TEXT,
speaker_id INTEGER,
text TEXT,
duration REAL
)
''')
def on_line_completed(self, event: LineCompleted):
self.conn.execute(
'INSERT INTO transcripts (line_id, timestamp, speaker_id, text, duration) VALUES (?, ?, ?, ?, ?)',
(
event.line.line_id,
datetime.now().isoformat(),
event.line.speaker_id if event.line.has_speaker_id else None,
event.line.text,
event.line.duration
)
)
self.conn.commit()
transcriber.add_listener(DatabaseListener('transcripts.db'))
Example: Multiple Listeners
You can attach multiple listeners to handle different aspects:
class UIListener(TranscriptEventListener):
"""Update the user interface"""
def on_line_text_changed(self, event):
update_ui(event.line.text)
class LogListener(TranscriptEventListener):
"""Log to file"""
def on_line_completed(self, event):
with open('transcript.log', 'a') as f:
f.write(f"{event.line.start_time:.2f}s: {event.line.text}\n")
class CommandListener(TranscriptEventListener):
"""Detect commands"""
def on_line_completed(self, event):
if 'stop' in event.line.text.lower():
stop_playback()
transcriber.add_listener(UIListener())
transcriber.add_listener(LogListener())
transcriber.add_listener(CommandListener())
Thread Safety
Event listeners are called from the same thread that calls add_audio() or update_transcription().
If your listener does heavy work, consider using a queue to process events asynchronously.
import queue
import threading
class AsyncListener(TranscriptEventListener):
def __init__(self):
self.queue = queue.Queue()
self.thread = threading.Thread(target=self._process_queue)
self.thread.daemon = True
self.thread.start()
def on_line_completed(self, event):
# Quick: just queue it
self.queue.put(event.line.text)
def _process_queue(self):
while True:
text = self.queue.get()
# Do slow work here
process_text(text)
See Also