test
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
# STT Status Bar App Usage
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The STT (Speech-to-Text) status bar app provides a convenient macOS menu bar interface for controlling speech-to-text functionality using wake word activation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Start/Stop/Pause/Resume**: Full control over STT recording
|
||||||
|
- **Status Indicators**: Visual status in menu bar (🎙️🔴 recording, 🎙️⏸️ paused, 🎙️⚫ stopped)
|
||||||
|
- **Configurable Settings**: Change wake words and models on the fly
|
||||||
|
- **File Output**: Save transcriptions to a file
|
||||||
|
- **Notifications**: Real-time notifications for transcriptions and status changes
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Starting the Status Bar App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch the status bar app
|
||||||
|
tooling stt statusbar
|
||||||
|
|
||||||
|
# Or using the full CLI path
|
||||||
|
python -m tooling.cli stt statusbar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Menu Options
|
||||||
|
|
||||||
|
#### Main Controls
|
||||||
|
- **Start STT**: Begin speech-to-text with current settings
|
||||||
|
- **Stop STT**: Stop speech-to-text completely
|
||||||
|
- **Pause**: Temporarily pause recognition (keeps recorder alive)
|
||||||
|
- **Resume**: Resume recognition from pause
|
||||||
|
|
||||||
|
#### Settings
|
||||||
|
- **Wake Word**: Choose from predefined options:
|
||||||
|
- jarvis (default)
|
||||||
|
- alexa
|
||||||
|
- hey google
|
||||||
|
- hey siri
|
||||||
|
- computer
|
||||||
|
|
||||||
|
- **Model**: Select Whisper model:
|
||||||
|
- tiny (fastest, least accurate)
|
||||||
|
- base (default, good balance)
|
||||||
|
- small (more accurate)
|
||||||
|
- medium (most accurate, slower)
|
||||||
|
|
||||||
|
#### File Management
|
||||||
|
- **Show Recent Transcriptions**: Display last 10 transcription entries
|
||||||
|
- **Save to File...**: Set output file for saving transcriptions
|
||||||
|
|
||||||
|
### Status Indicators
|
||||||
|
|
||||||
|
| Icon | Status | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| 🎙️⚫ | Stopped | STT is not running |
|
||||||
|
| 🎙️🔴 | Recording | STT is active and listening |
|
||||||
|
| 🎙️⏸️ | Paused | STT is paused but can be resumed |
|
||||||
|
|
||||||
|
### Workflow Example
|
||||||
|
|
||||||
|
1. **Launch the app**: `tooling stt statusbar`
|
||||||
|
2. **Look for the 🎙️ icon** in your macOS menu bar
|
||||||
|
3. **Click the icon** to open the menu
|
||||||
|
4. **Set your preferences**:
|
||||||
|
- Choose wake word from Settings > Wake Word
|
||||||
|
- Select model from Settings > Model
|
||||||
|
- Optionally set output file with "Save to File..."
|
||||||
|
5. **Click "Start STT"** to begin
|
||||||
|
6. **Say your wake word** (e.g., "jarvis") to trigger recording
|
||||||
|
7. **Speak clearly** after the wake word is detected
|
||||||
|
8. **Get notifications** with your transcribed text
|
||||||
|
9. **Use Pause/Resume** as needed
|
||||||
|
10. **Click "Stop STT"** when done
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
The app provides notifications for:
|
||||||
|
- STT started/stopped/paused/resumed
|
||||||
|
- Transcribed speech (shows first 100 characters)
|
||||||
|
- Settings changes (when STT is running)
|
||||||
|
- File operations
|
||||||
|
|
||||||
|
### File Output
|
||||||
|
|
||||||
|
When you set an output file:
|
||||||
|
- Transcriptions are saved with timestamps
|
||||||
|
- Sessions are marked with start/end timestamps
|
||||||
|
- File is automatically created in ~/Documents/ by default
|
||||||
|
- Each transcription includes: `[HH:MM:SS] transcribed text`
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- macOS (rumps requires macOS and PyObjC)
|
||||||
|
- RealtimeSTT library
|
||||||
|
- Working microphone
|
||||||
|
- Python 3.11+
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
- **No menu bar icon**: Make sure you're running on macOS and rumps is installed
|
||||||
|
- **No transcriptions**: Check microphone permissions and try speaking louder
|
||||||
|
- **Wake word not detected**: Try adjusting sensitivity or use a different wake word
|
||||||
|
- **High CPU usage**: Consider using the "tiny" model for better performance
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
The status bar app uses sensible defaults, but you can modify the underlying configuration by editing the STTStatusBarApp class in `src/tooling/stt_cli.py`.
|
||||||
|
|
||||||
|
Default settings:
|
||||||
|
- Wake word: "jarvis"
|
||||||
|
- Model: "base"
|
||||||
|
- Sensitivity: 0.6
|
||||||
|
- Device: auto-detect (CUDA if available, otherwise CPU)
|
||||||
|
- Realtime display: enabled
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
2025-07-22 22:10:54.507 - RealTimeSTT: realtimestt - INFO - Starting RealTimeSTT
|
||||||
|
2025-07-22 22:10:54.520 - RealTimeSTT: realtimestt - INFO - Initializing audio recording (creating pyAudio input stream, sample rate: 16000 buffer size: 512
|
||||||
|
2025-07-22 22:10:54.523 - RealTimeSTT: realtimestt - INFO - Initializing faster_whisper realtime transcription model tiny, default device: cpu, compute type: default, device index: 0, download root: None
|
||||||
|
2025-07-22 22:10:55.181 - RealTimeSTT: realtimestt - DEBUG - Faster_whisper realtime speech to text transcription model initialized successfully
|
||||||
|
2025-07-22 22:10:55.181 - RealTimeSTT: realtimestt - ERROR - Wakeword engine unknown/unsupported or wake_words not specified. Please specify one of: pvporcupine, openwakeword.
|
||||||
|
NoneType: None
|
||||||
|
2025-07-22 22:10:55.181 - RealTimeSTT: realtimestt - INFO - Initializing WebRTC voice with Sensitivity 3
|
||||||
|
2025-07-22 22:10:55.181 - RealTimeSTT: realtimestt - DEBUG - WebRTC VAD voice activity detection engine initialized successfully
|
||||||
|
2025-07-22 22:10:55.838 - RealTimeSTT: realtimestt - DEBUG - Silero VAD voice activity detection engine initialized successfully
|
||||||
|
2025-07-22 22:10:55.838 - RealTimeSTT: realtimestt - DEBUG - Starting realtime worker
|
||||||
|
2025-07-22 22:10:55.838 - RealTimeSTT: realtimestt - DEBUG - Waiting for main transcription model to start
|
||||||
|
2025-07-22 22:11:01.946 - RealTimeSTT: realtimestt - DEBUG - Main transcription model ready
|
||||||
|
2025-07-22 22:11:01.946 - RealTimeSTT: realtimestt - DEBUG - RealtimeSTT initialization completed successfully
|
||||||
|
2025-07-22 22:11:01.946 - RealTimeSTT: realtimestt - INFO - Setting listen time
|
||||||
|
2025-07-22 22:11:01.946 - RealTimeSTT: realtimestt - INFO - State changed from 'inactive' to 'listening'
|
||||||
|
2025-07-22 22:11:01.947 - RealTimeSTT: realtimestt - DEBUG - Waiting for recording start
|
||||||
|
2025-07-22 22:11:01.981 - RealTimeSTT: realtimestt - INFO - State changed from 'listening' to 'wakeword'
|
||||||
+285
-285
@@ -96,300 +96,300 @@ if RUMPS_AVAILABLE:
|
|||||||
|
|
||||||
def __init__(self, name="STT", **kwargs):
|
def __init__(self, name="STT", **kwargs):
|
||||||
super().__init__(name, **kwargs)
|
super().__init__(name, **kwargs)
|
||||||
|
|
||||||
# Initialize state
|
|
||||||
self.recorder = None
|
|
||||||
self.is_running = False
|
|
||||||
self.is_paused = False
|
|
||||||
self.output_file = None
|
|
||||||
self.transcription_thread = None
|
|
||||||
self.stop_event = threading.Event()
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
self.wake_word = "jarvis"
|
|
||||||
self.model = "base"
|
|
||||||
self.language = ""
|
|
||||||
self.realtime = True
|
|
||||||
self.sensitivity = 0.6
|
|
||||||
self.device = "auto"
|
|
||||||
self.save_to_file = None
|
|
||||||
|
|
||||||
# Menu setup
|
|
||||||
self.menu = [
|
|
||||||
rumps.MenuItem("Start STT", callback=self.start_stt),
|
|
||||||
rumps.MenuItem("Stop STT", callback=self.stop_stt),
|
|
||||||
rumps.separator,
|
|
||||||
rumps.MenuItem("Pause", callback=self.pause_stt),
|
|
||||||
rumps.MenuItem("Resume", callback=self.resume_stt),
|
|
||||||
rumps.separator,
|
|
||||||
{
|
|
||||||
"Settings": {
|
|
||||||
"Wake Word": {
|
|
||||||
"jarvis": rumps.MenuItem("jarvis", callback=self.set_wake_word),
|
|
||||||
"alexa": rumps.MenuItem("alexa", callback=self.set_wake_word),
|
|
||||||
"hey google": rumps.MenuItem("hey google", callback=self.set_wake_word),
|
|
||||||
"hey siri": rumps.MenuItem("hey siri", callback=self.set_wake_word),
|
|
||||||
"computer": rumps.MenuItem("computer", callback=self.set_wake_word),
|
|
||||||
},
|
|
||||||
"Model": {
|
|
||||||
"tiny": rumps.MenuItem("tiny", callback=self.set_model),
|
|
||||||
"base": rumps.MenuItem("base", callback=self.set_model),
|
|
||||||
"small": rumps.MenuItem("small", callback=self.set_model),
|
|
||||||
"medium": rumps.MenuItem("medium", callback=self.set_model),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rumps.separator,
|
|
||||||
rumps.MenuItem("Show Recent Transcriptions", callback=self.show_transcriptions),
|
|
||||||
rumps.MenuItem("Save to File...", callback=self.select_output_file),
|
|
||||||
rumps.separator,
|
|
||||||
rumps.MenuItem("Quit", callback=rumps.quit_application),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Set initial menu states
|
|
||||||
self.update_menu_states()
|
|
||||||
self.set_current_wake_word()
|
|
||||||
self.set_current_model()
|
|
||||||
|
|
||||||
def update_menu_states(self):
|
|
||||||
"""Update menu item states based on current status."""
|
|
||||||
if self.is_running:
|
|
||||||
if self.is_paused:
|
|
||||||
self.title = "🎙️⏸️"
|
|
||||||
self.menu["Start STT"].set_callback(None)
|
|
||||||
self.menu["Stop STT"].set_callback(self.stop_stt)
|
|
||||||
self.menu["Pause"].set_callback(None)
|
|
||||||
self.menu["Resume"].set_callback(self.resume_stt)
|
|
||||||
else:
|
|
||||||
self.title = "🎙️🔴"
|
|
||||||
self.menu["Start STT"].set_callback(None)
|
|
||||||
self.menu["Stop STT"].set_callback(self.stop_stt)
|
|
||||||
self.menu["Pause"].set_callback(self.pause_stt)
|
|
||||||
self.menu["Resume"].set_callback(None)
|
|
||||||
else:
|
|
||||||
self.title = "🎙️⚫"
|
|
||||||
self.menu["Start STT"].set_callback(self.start_stt)
|
|
||||||
self.menu["Stop STT"].set_callback(None)
|
|
||||||
self.menu["Pause"].set_callback(None)
|
|
||||||
self.menu["Resume"].set_callback(None)
|
|
||||||
|
|
||||||
def set_current_wake_word(self):
|
|
||||||
"""Set checkmark for current wake word."""
|
|
||||||
for word in ["jarvis", "alexa", "hey google", "hey siri", "computer"]:
|
|
||||||
self.menu["Settings"]["Wake Word"][word].state = (word == self.wake_word)
|
|
||||||
|
|
||||||
def set_current_model(self):
|
|
||||||
"""Set checkmark for current model."""
|
|
||||||
for model in ["tiny", "base", "small", "medium"]:
|
|
||||||
self.menu["Settings"]["Model"][model].state = (model == self.model)
|
|
||||||
|
|
||||||
def set_wake_word(self, sender):
|
|
||||||
"""Set the wake word from menu selection."""
|
|
||||||
self.wake_word = sender.title
|
|
||||||
self.set_current_wake_word()
|
|
||||||
if self.is_running:
|
|
||||||
rumps.notification("STT Settings", "Wake Word Changed", f"Restart STT to use '{self.wake_word}'")
|
|
||||||
|
|
||||||
def set_model(self, sender):
|
|
||||||
"""Set the model from menu selection."""
|
|
||||||
self.model = sender.title
|
|
||||||
self.set_current_model()
|
|
||||||
if self.is_running:
|
|
||||||
rumps.notification("STT Settings", "Model Changed", f"Restart STT to use '{self.model}' model")
|
|
||||||
|
|
||||||
def start_stt(self, _):
|
|
||||||
"""Start STT functionality."""
|
|
||||||
if self.is_running:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
from RealtimeSTT import AudioToTextRecorder
|
|
||||||
except ImportError:
|
|
||||||
rumps.alert("RealtimeSTT not installed", "Please install with: pip install RealtimeSTT")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Determine device
|
|
||||||
if self.device == "auto":
|
|
||||||
try:
|
|
||||||
import torch
|
|
||||||
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
||||||
except ImportError:
|
|
||||||
self.device = "cpu"
|
|
||||||
|
|
||||||
# Setup output file if specified
|
# Initialize state
|
||||||
if self.save_to_file:
|
self.recorder = None
|
||||||
self.save_to_file.parent.mkdir(parents=True, exist_ok=True)
|
self.is_running = False
|
||||||
self.output_file = open(self.save_to_file, 'a', encoding='utf-8')
|
|
||||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
self.output_file.write(f"\n=== STT Session Started: {timestamp} ===\n")
|
|
||||||
self.output_file.flush()
|
|
||||||
|
|
||||||
# Configure recorder
|
|
||||||
recorder_config = {
|
|
||||||
"model": self.model,
|
|
||||||
"wake_words": self.wake_word,
|
|
||||||
"wake_words_sensitivity": self.sensitivity,
|
|
||||||
"device": self.device,
|
|
||||||
"on_recording_start": self.on_recording_start,
|
|
||||||
"on_recording_stop": self.on_recording_stop,
|
|
||||||
"on_wakeword_detected": self.on_wakeword_detected,
|
|
||||||
"on_wakeword_timeout": self.on_wakeword_timeout,
|
|
||||||
"on_wakeword_detection_start": self.on_wakeword_detection_start,
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.language:
|
|
||||||
recorder_config["language"] = self.language
|
|
||||||
|
|
||||||
if self.realtime:
|
|
||||||
recorder_config.update({
|
|
||||||
"enable_realtime_transcription": True,
|
|
||||||
"on_realtime_transcription_update": self.on_realtime_transcription,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Initialize recorder
|
|
||||||
self.recorder = AudioToTextRecorder(**recorder_config)
|
|
||||||
|
|
||||||
# Start transcription thread
|
|
||||||
self.is_running = True
|
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
self.stop_event.clear()
|
self.output_file = None
|
||||||
self.transcription_thread = threading.Thread(target=self.transcription_loop, daemon=True)
|
self.transcription_thread = None
|
||||||
self.transcription_thread.start()
|
self.stop_event = threading.Event()
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
self.wake_word = "jarvis"
|
||||||
|
self.model = "base"
|
||||||
|
self.language = ""
|
||||||
|
self.realtime = True
|
||||||
|
self.sensitivity = 0.6
|
||||||
|
self.device = "auto"
|
||||||
|
self.save_to_file = None
|
||||||
|
|
||||||
|
# Menu setup
|
||||||
|
self.menu = [
|
||||||
|
rumps.MenuItem("Start STT", callback=self.start_stt),
|
||||||
|
rumps.MenuItem("Stop STT", callback=self.stop_stt),
|
||||||
|
rumps.separator,
|
||||||
|
rumps.MenuItem("Pause", callback=self.pause_stt),
|
||||||
|
rumps.MenuItem("Resume", callback=self.resume_stt),
|
||||||
|
rumps.separator,
|
||||||
|
{
|
||||||
|
"Settings": {
|
||||||
|
"Wake Word": {
|
||||||
|
"jarvis": rumps.MenuItem("jarvis", callback=self.set_wake_word),
|
||||||
|
"alexa": rumps.MenuItem("alexa", callback=self.set_wake_word),
|
||||||
|
"hey google": rumps.MenuItem("hey google", callback=self.set_wake_word),
|
||||||
|
"hey siri": rumps.MenuItem("hey siri", callback=self.set_wake_word),
|
||||||
|
"computer": rumps.MenuItem("computer", callback=self.set_wake_word),
|
||||||
|
},
|
||||||
|
"Model": {
|
||||||
|
"tiny": rumps.MenuItem("tiny", callback=self.set_model),
|
||||||
|
"base": rumps.MenuItem("base", callback=self.set_model),
|
||||||
|
"small": rumps.MenuItem("small", callback=self.set_model),
|
||||||
|
"medium": rumps.MenuItem("medium", callback=self.set_model),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rumps.separator,
|
||||||
|
rumps.MenuItem("Show Recent Transcriptions", callback=self.show_transcriptions),
|
||||||
|
rumps.MenuItem("Save to File...", callback=self.select_output_file),
|
||||||
|
rumps.separator,
|
||||||
|
rumps.MenuItem("Quit", callback=rumps.quit_application),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Set initial menu states
|
||||||
|
self.update_menu_states()
|
||||||
|
self.set_current_wake_word()
|
||||||
|
self.set_current_model()
|
||||||
|
|
||||||
|
def update_menu_states(self):
|
||||||
|
"""Update menu item states based on current status."""
|
||||||
|
if self.is_running:
|
||||||
|
if self.is_paused:
|
||||||
|
self.title = "🎙️⏸️"
|
||||||
|
self.menu["Start STT"].set_callback(None)
|
||||||
|
self.menu["Stop STT"].set_callback(self.stop_stt)
|
||||||
|
self.menu["Pause"].set_callback(None)
|
||||||
|
self.menu["Resume"].set_callback(self.resume_stt)
|
||||||
|
else:
|
||||||
|
self.title = "🎙️🔴"
|
||||||
|
self.menu["Start STT"].set_callback(None)
|
||||||
|
self.menu["Stop STT"].set_callback(self.stop_stt)
|
||||||
|
self.menu["Pause"].set_callback(self.pause_stt)
|
||||||
|
self.menu["Resume"].set_callback(None)
|
||||||
|
else:
|
||||||
|
self.title = "🎙️⚫"
|
||||||
|
self.menu["Start STT"].set_callback(self.start_stt)
|
||||||
|
self.menu["Stop STT"].set_callback(None)
|
||||||
|
self.menu["Pause"].set_callback(None)
|
||||||
|
self.menu["Resume"].set_callback(None)
|
||||||
|
|
||||||
|
def set_current_wake_word(self):
|
||||||
|
"""Set checkmark for current wake word."""
|
||||||
|
for word in ["jarvis", "alexa", "hey google", "hey siri", "computer"]:
|
||||||
|
self.menu["Settings"]["Wake Word"][word].state = (word == self.wake_word)
|
||||||
|
|
||||||
|
def set_current_model(self):
|
||||||
|
"""Set checkmark for current model."""
|
||||||
|
for model in ["tiny", "base", "small", "medium"]:
|
||||||
|
self.menu["Settings"]["Model"][model].state = (model == self.model)
|
||||||
|
|
||||||
|
def set_wake_word(self, sender):
|
||||||
|
"""Set the wake word from menu selection."""
|
||||||
|
self.wake_word = sender.title
|
||||||
|
self.set_current_wake_word()
|
||||||
|
if self.is_running:
|
||||||
|
rumps.notification("STT Settings", "Wake Word Changed", f"Restart STT to use '{self.wake_word}'")
|
||||||
|
|
||||||
|
def set_model(self, sender):
|
||||||
|
"""Set the model from menu selection."""
|
||||||
|
self.model = sender.title
|
||||||
|
self.set_current_model()
|
||||||
|
if self.is_running:
|
||||||
|
rumps.notification("STT Settings", "Model Changed", f"Restart STT to use '{self.model}' model")
|
||||||
|
|
||||||
|
def start_stt(self, _):
|
||||||
|
"""Start STT functionality."""
|
||||||
|
if self.is_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from RealtimeSTT import AudioToTextRecorder
|
||||||
|
except ImportError:
|
||||||
|
rumps.alert("RealtimeSTT not installed", "Please install with: pip install RealtimeSTT")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determine device
|
||||||
|
if self.device == "auto":
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
except ImportError:
|
||||||
|
self.device = "cpu"
|
||||||
|
|
||||||
|
# Setup output file if specified
|
||||||
|
if self.save_to_file:
|
||||||
|
self.save_to_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.output_file = open(self.save_to_file, 'a', encoding='utf-8')
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
self.output_file.write(f"\n=== STT Session Started: {timestamp} ===\n")
|
||||||
|
self.output_file.flush()
|
||||||
|
|
||||||
|
# Configure recorder
|
||||||
|
recorder_config = {
|
||||||
|
"model": self.model,
|
||||||
|
"wake_words": self.wake_word,
|
||||||
|
"wake_words_sensitivity": self.sensitivity,
|
||||||
|
"device": self.device,
|
||||||
|
"on_recording_start": self.on_recording_start,
|
||||||
|
"on_recording_stop": self.on_recording_stop,
|
||||||
|
"on_wakeword_detected": self.on_wakeword_detected,
|
||||||
|
"on_wakeword_timeout": self.on_wakeword_timeout,
|
||||||
|
"on_wakeword_detection_start": self.on_wakeword_detection_start,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.language:
|
||||||
|
recorder_config["language"] = self.language
|
||||||
|
|
||||||
|
if self.realtime:
|
||||||
|
recorder_config.update({
|
||||||
|
"enable_realtime_transcription": True,
|
||||||
|
"on_realtime_transcription_update": self.on_realtime_transcription,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Initialize recorder
|
||||||
|
self.recorder = AudioToTextRecorder(**recorder_config)
|
||||||
|
|
||||||
|
# Start transcription thread
|
||||||
|
self.is_running = True
|
||||||
|
self.is_paused = False
|
||||||
|
self.stop_event.clear()
|
||||||
|
self.transcription_thread = threading.Thread(target=self.transcription_loop, daemon=True)
|
||||||
|
self.transcription_thread.start()
|
||||||
|
|
||||||
|
self.update_menu_states()
|
||||||
|
rumps.notification("STT Started", f"Wake word: {self.wake_word}", f"Model: {self.model} | Device: {self.device}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
rumps.alert("STT Error", f"Failed to start STT: {str(e)}")
|
||||||
|
|
||||||
|
def stop_stt(self, _):
|
||||||
|
"""Stop STT functionality."""
|
||||||
|
if not self.is_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.is_running = False
|
||||||
|
self.is_paused = False
|
||||||
|
self.stop_event.set()
|
||||||
|
|
||||||
|
if self.recorder:
|
||||||
|
try:
|
||||||
|
self.recorder.shutdown()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.recorder = None
|
||||||
|
|
||||||
|
if self.output_file:
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
self.output_file.write(f"=== STT Session Ended: {timestamp} ===\n\n")
|
||||||
|
self.output_file.close()
|
||||||
|
self.output_file = None
|
||||||
|
|
||||||
self.update_menu_states()
|
self.update_menu_states()
|
||||||
rumps.notification("STT Started", f"Wake word: {self.wake_word}", f"Model: {self.model} | Device: {self.device}")
|
rumps.notification("STT Stopped", "Speech-to-text has been stopped", "")
|
||||||
|
|
||||||
|
def pause_stt(self, _):
|
||||||
|
"""Pause STT functionality."""
|
||||||
|
if not self.is_running or self.is_paused:
|
||||||
|
return
|
||||||
|
|
||||||
except Exception as e:
|
self.is_paused = True
|
||||||
rumps.alert("STT Error", f"Failed to start STT: {str(e)}")
|
self.update_menu_states()
|
||||||
|
rumps.notification("STT Paused", "Speech recognition paused", "Resume from menu")
|
||||||
def stop_stt(self, _):
|
|
||||||
"""Stop STT functionality."""
|
|
||||||
if not self.is_running:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.is_running = False
|
def resume_stt(self, _):
|
||||||
self.is_paused = False
|
"""Resume STT functionality."""
|
||||||
self.stop_event.set()
|
if not self.is_running or not self.is_paused:
|
||||||
|
return
|
||||||
if self.recorder:
|
|
||||||
try:
|
|
||||||
self.recorder.shutdown()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.recorder = None
|
|
||||||
|
|
||||||
if self.output_file:
|
|
||||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
self.output_file.write(f"=== STT Session Ended: {timestamp} ===\n\n")
|
|
||||||
self.output_file.close()
|
|
||||||
self.output_file = None
|
|
||||||
|
|
||||||
self.update_menu_states()
|
|
||||||
rumps.notification("STT Stopped", "Speech-to-text has been stopped", "")
|
|
||||||
|
|
||||||
def pause_stt(self, _):
|
|
||||||
"""Pause STT functionality."""
|
|
||||||
if not self.is_running or self.is_paused:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.is_paused = True
|
|
||||||
self.update_menu_states()
|
|
||||||
rumps.notification("STT Paused", "Speech recognition paused", "Resume from menu")
|
|
||||||
|
|
||||||
def resume_stt(self, _):
|
|
||||||
"""Resume STT functionality."""
|
|
||||||
if not self.is_running or not self.is_paused:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.is_paused = False
|
|
||||||
self.update_menu_states()
|
|
||||||
rumps.notification("STT Resumed", "Speech recognition resumed", f"Listening for '{self.wake_word}'")
|
|
||||||
|
|
||||||
def transcription_loop(self):
|
|
||||||
"""Main transcription loop running in background thread."""
|
|
||||||
while self.is_running and not self.stop_event.is_set():
|
|
||||||
try:
|
|
||||||
if not self.is_paused and self.recorder:
|
|
||||||
text = self.recorder.text()
|
|
||||||
if text and text.strip():
|
|
||||||
self.on_transcription_complete(text)
|
|
||||||
else:
|
|
||||||
time.sleep(0.1) # Small delay when paused
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Transcription error: {e}")
|
|
||||||
time.sleep(1) # Longer delay on error
|
|
||||||
|
|
||||||
def on_realtime_transcription(self, text: str):
|
|
||||||
"""Handle real-time transcription updates."""
|
|
||||||
# Could update a notification or log
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_transcription_complete(self, text: str):
|
|
||||||
"""Handle completed transcriptions."""
|
|
||||||
if text.strip():
|
|
||||||
# Show notification with transcription
|
|
||||||
rumps.notification("Transcription", "Speech detected:", text[:100] + ("..." if len(text) > 100 else ""))
|
|
||||||
|
|
||||||
# Save to file if specified
|
self.is_paused = False
|
||||||
if self.output_file:
|
self.update_menu_states()
|
||||||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
rumps.notification("STT Resumed", "Speech recognition resumed", f"Listening for '{self.wake_word}'")
|
||||||
self.output_file.write(f"[{timestamp}] {text}\n")
|
|
||||||
self.output_file.flush()
|
def transcription_loop(self):
|
||||||
|
"""Main transcription loop running in background thread."""
|
||||||
def on_recording_start(self):
|
while self.is_running and not self.stop_event.is_set():
|
||||||
"""Called when recording starts."""
|
try:
|
||||||
pass
|
if not self.is_paused and self.recorder:
|
||||||
|
text = self.recorder.text()
|
||||||
def on_recording_stop(self):
|
if text and text.strip():
|
||||||
"""Called when recording stops."""
|
self.on_transcription_complete(text)
|
||||||
pass
|
|
||||||
|
|
||||||
def on_wakeword_detected(self):
|
|
||||||
"""Called when wake word is detected."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_wakeword_timeout(self):
|
|
||||||
"""Called when wake word times out."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_wakeword_detection_start(self):
|
|
||||||
"""Called when starting to listen for wake words."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def show_transcriptions(self, _):
|
|
||||||
"""Show recent transcriptions in an alert."""
|
|
||||||
if self.output_file and self.save_to_file and self.save_to_file.exists():
|
|
||||||
try:
|
|
||||||
with open(self.save_to_file, 'r', encoding='utf-8') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
recent = ''.join(lines[-10:]) # Last 10 lines
|
|
||||||
if recent.strip():
|
|
||||||
rumps.alert("Recent Transcriptions", recent)
|
|
||||||
else:
|
else:
|
||||||
rumps.alert("No Transcriptions", "No transcriptions found yet.")
|
time.sleep(0.1) # Small delay when paused
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Transcription error: {e}")
|
||||||
|
time.sleep(1) # Longer delay on error
|
||||||
|
|
||||||
|
def on_realtime_transcription(self, text: str):
|
||||||
|
"""Handle real-time transcription updates."""
|
||||||
|
# Could update a notification or log
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_transcription_complete(self, text: str):
|
||||||
|
"""Handle completed transcriptions."""
|
||||||
|
if text.strip():
|
||||||
|
# Show notification with transcription
|
||||||
|
rumps.notification("Transcription", "Speech detected:", text[:100] + ("..." if len(text) > 100 else ""))
|
||||||
|
|
||||||
|
# Save to file if specified
|
||||||
|
if self.output_file:
|
||||||
|
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||||||
|
self.output_file.write(f"[{timestamp}] {text}\n")
|
||||||
|
self.output_file.flush()
|
||||||
|
|
||||||
|
def on_recording_start(self):
|
||||||
|
"""Called when recording starts."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_recording_stop(self):
|
||||||
|
"""Called when recording stops."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_wakeword_detected(self):
|
||||||
|
"""Called when wake word is detected."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_wakeword_timeout(self):
|
||||||
|
"""Called when wake word times out."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_wakeword_detection_start(self):
|
||||||
|
"""Called when starting to listen for wake words."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def show_transcriptions(self, _):
|
||||||
|
"""Show recent transcriptions in an alert."""
|
||||||
|
if self.output_file and self.save_to_file and self.save_to_file.exists():
|
||||||
|
try:
|
||||||
|
with open(self.save_to_file, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
recent = ''.join(lines[-10:]) # Last 10 lines
|
||||||
|
if recent.strip():
|
||||||
|
rumps.alert("Recent Transcriptions", recent)
|
||||||
|
else:
|
||||||
|
rumps.alert("No Transcriptions", "No transcriptions found yet.")
|
||||||
|
except Exception as e:
|
||||||
|
rumps.alert("Error", f"Could not read transcriptions: {e}")
|
||||||
|
else:
|
||||||
|
rumps.alert("No File", "No output file configured. Use 'Save to File...' to set one.")
|
||||||
|
|
||||||
|
def select_output_file(self, _):
|
||||||
|
"""Select output file for saving transcriptions."""
|
||||||
|
try:
|
||||||
|
# Simple file selection using input dialog
|
||||||
|
response = rumps.Window(
|
||||||
|
message="Enter filename for transcriptions:",
|
||||||
|
title="Save Transcriptions",
|
||||||
|
default_text="stt_transcriptions.txt",
|
||||||
|
ok="Save",
|
||||||
|
cancel="Cancel"
|
||||||
|
).run()
|
||||||
|
|
||||||
|
if response.clicked and response.text:
|
||||||
|
self.save_to_file = Path.home() / "Documents" / response.text
|
||||||
|
rumps.notification("File Set", "Transcriptions will be saved to:", str(self.save_to_file))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
rumps.alert("Error", f"Could not read transcriptions: {e}")
|
rumps.alert("Error", f"Could not set output file: {e}")
|
||||||
else:
|
|
||||||
rumps.alert("No File", "No output file configured. Use 'Save to File...' to set one.")
|
|
||||||
|
|
||||||
def select_output_file(self, _):
|
|
||||||
"""Select output file for saving transcriptions."""
|
|
||||||
try:
|
|
||||||
# Simple file selection using input dialog
|
|
||||||
response = rumps.Window(
|
|
||||||
message="Enter filename for transcriptions:",
|
|
||||||
title="Save Transcriptions",
|
|
||||||
default_text="stt_transcriptions.txt",
|
|
||||||
ok="Save",
|
|
||||||
cancel="Cancel"
|
|
||||||
).run()
|
|
||||||
|
|
||||||
if response.clicked and response.text:
|
|
||||||
self.save_to_file = Path.home() / "Documents" / response.text
|
|
||||||
rumps.notification("File Set", "Transcriptions will be saved to:", str(self.save_to_file))
|
|
||||||
except Exception as e:
|
|
||||||
rumps.alert("Error", f"Could not set output file: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@stt_app.command("listen")
|
@stt_app.command("listen")
|
||||||
|
|||||||
Reference in New Issue
Block a user