Skip to content

Commit 847f49a

Browse files
Refactor TTS handling in MessageBubble; improve thread cleanup and add volume control in OfflineTTSWorker; update settings.json for offline STT model path
1 parent 187c48a commit 847f49a

5 files changed

Lines changed: 81 additions & 46 deletions

File tree

app/chatbot.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,9 @@ def _handle_speak(self):
512512
return
513513

514514
try:
515+
# Note: This method handles Text-to-Speech (TTS), not Speech-to-Text (STT)
516+
# For offline STT implementation, see the VoiceWorker.record() method
517+
515518
# Cancel any existing TTS operation
516519
if self.tts_thread is not None and self.tts_thread.isRunning():
517520
if hasattr(self, 'tts_worker') and hasattr(self.tts_worker, 'cancel'):
@@ -573,7 +576,7 @@ def _handle_speak(self):
573576
"TTS Error",
574577
f"Error initializing text-to-speech: {str(e)}\n\nPlease check the settings."
575578
)
576-
579+
577580
def _play_audio(self, audio_path, status_msg=None):
578581
"""Play the generated audio."""
579582
try:
@@ -621,7 +624,7 @@ def _play_audio(self, audio_path, status_msg=None):
621624
layout.addWidget(status_label)
622625

623626
# Connect player signals
624-
player.errorOccurred.connect(lambda error, errorString:
627+
player.errorOccurred.connect(lambda _error, errorString:
625628
status_label.setText(f"Player error: {errorString}"))
626629

627630
player.playbackStateChanged.connect(lambda state:
@@ -699,7 +702,7 @@ def _play_audio(self, audio_path, status_msg=None):
699702

700703
except Exception as e:
701704
self.chat_window.update_status(f"Error playing audio: {str(e)}")
702-
705+
703706
def _export_audio(self, audio_path):
704707
"""Export the generated audio file."""
705708
if not audio_path or not os.path.exists(audio_path):
@@ -781,19 +784,31 @@ def _on_tts_thread_finished(self):
781784
"""Handle cleanup when TTS thread finishes."""
782785
if hasattr(self, 'tts_worker'):
783786
del self.tts_worker
784-
785-
def closeEvent(self, event):
786-
"""Properly clean up threads before the widget is closed."""
787+
self.tts_thread = None
788+
789+
def __del__(self):
790+
"""Destructor to ensure threads are cleaned up when object is garbage collected."""
787791
self._cleanup_threads()
788-
super().closeEvent(event)
789792

790793
def _cleanup_threads(self):
791794
"""Ensure all threads are properly terminated before object destruction."""
792-
if self.tts_thread is not None and self.tts_thread.isRunning():
793-
if hasattr(self, 'tts_worker') and hasattr(self.tts_worker, 'cancel'):
794-
self.tts_worker.cancel()
795+
try:
796+
if hasattr(self, 'tts_thread') and self.tts_thread is not None and self.tts_thread.isRunning():
797+
if hasattr(self, 'tts_worker') and hasattr(self.tts_worker, 'cancel'):
798+
self.tts_worker.cancel()
795799
self.tts_thread.quit()
796-
self.tts_thread.wait(1000) # Wait up to 1 second for thread to finish
800+
# Use longer timeout and check if thread actually finished
801+
if not self.tts_thread.wait(3000): # Wait up to 3 seconds for thread to finish
802+
self.tts_thread.terminate() # Force termination if necessary
803+
self.tts_thread.wait() # Wait for termination to complete
804+
except RuntimeError:
805+
# Handle case where thread might already be deleted
806+
pass
807+
self.tts_thread.quit()
808+
# Use longer timeout and check if thread actually finished
809+
if not self.tts_thread.wait(3000): # Wait up to 3 seconds for thread to finish
810+
self.tts_thread.terminate() # Force termination if necessary
811+
self.tts_thread.wait() # Wait for termination to complete
797812

798813
class VoiceWorker(QObject):
799814
"""Worker class for voice recording and speech recognition"""

app/tts_worker.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
from PyQt6.QtCore import QObject, pyqtSignal
22
import os
33
import tempfile
4-
import time
5-
import wave
6-
import array
74
import pyttsx3 # For offline TTS
8-
import requests # For online TTS
9-
import json
10-
import io
5+
# The following imports are kept for the OnlineTTSWorker implementation
6+
# Will be used when the API implementation is completed
7+
import requests # For online TTS - unused for now but kept for future implementation # noqa
8+
import json # For online TTS - unused for now but kept for future implementation # noqa
119

1210
class TTSWorkerBase(QObject):
1311
# Base class remains unchanged
@@ -30,12 +28,12 @@ def cancel(self):
3028
class OfflineTTSWorker(TTSWorkerBase):
3129
"""Worker for offline text-to-speech processing"""
3230

33-
def __init__(self, text, model_path, speech_rate=1.0):
34-
super().__init__(text, None, speech_rate)
35-
self.model_path = model_path
31+
def __init__(self, text, voice_id=None, speech_rate=1.0, volume=1.0):
32+
super().__init__(text, voice_id, speech_rate)
33+
self.volume = volume
3634

3735
def generate_speech(self):
38-
"""Generate speech using offline TTS model"""
36+
"""Generate speech using offline TTS engine"""
3937
try:
4038
# Create temporary file for audio output
4139
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
@@ -44,16 +42,30 @@ def generate_speech(self):
4442

4543
self.progress.emit(10)
4644

47-
# Use pyttsx3 for offline TTS
45+
# Initialize pyttsx3 engine
4846
engine = pyttsx3.init()
49-
engine.setProperty('rate', int(engine.getProperty('rate') * self.speech_rate))
50-
51-
# Set voice if available
52-
voices = engine.getProperty('voices')
53-
if voices and len(voices) > 0:
54-
engine.setProperty('voice', voices[0].id)
5547

56-
self.progress.emit(30)
48+
# Configure voice properties
49+
engine.setProperty('rate', int(engine.getProperty('rate') * self.speech_rate))
50+
engine.setProperty('volume', self.volume)
51+
52+
# Set specific voice if requested
53+
if self.voice_id:
54+
voices = engine.getProperty('voices')
55+
for voice in voices:
56+
if self.voice_id.lower() in voice.id.lower():
57+
engine.setProperty('voice', voice.id)
58+
break
59+
# Otherwise use default voice
60+
else:
61+
voices = engine.getProperty('voices')
62+
if voices:
63+
engine.setProperty('voice', voices[0].id)
64+
65+
self.progress.emit(40)
66+
67+
if self.is_cancelled:
68+
raise Exception("TTS generation cancelled")
5769

5870
# Save to file
5971
engine.save_to_file(self.text, output_path)
@@ -63,6 +75,11 @@ def generate_speech(self):
6375
# Wait for file generation to complete
6476
engine.runAndWait()
6577

78+
if self.is_cancelled:
79+
if os.path.exists(output_path):
80+
os.remove(output_path)
81+
raise Exception("TTS generation cancelled")
82+
6683
self.progress.emit(100)
6784
self.speech_ready.emit(output_path)
6885

@@ -87,21 +104,23 @@ def generate_speech(self):
87104

88105
self.progress.emit(10)
89106

90-
# Example using a generic TTS service API (replace with your preferred service)
91107
# This is a placeholder - you'll need to implement the specific API calls
92108
# for your chosen service (Google, AWS, Azure, etc.)
93109

94-
headers = {
110+
# These variables are prepared for future API implementation
111+
# and will be used when the API call is uncommented
112+
headers = { # Unused for now - will be used with actual API implementation # noqa
95113
"Content-Type": "application/json",
96114
"Authorization": f"Bearer {self.api_key}"
97115
}
98116

99-
data = {
117+
data = { # Unused for now - will be used with actual API implementation # noqa
100118
"text": self.text,
101119
"voice": self.voice_id,
102120
"rate": self.speech_rate
103121
}
104122

123+
105124
self.progress.emit(30)
106125

107126
# This is a placeholder for the API call

chat_history.db

0 Bytes
Binary file not shown.

models/tiny-whisper.bin

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
MOCK_MODEL_DATA

settings.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
{
2-
"theme": "dark",
3-
"default_model": "llama3.2:1b",
4-
"streaming": false,
5-
"text_to_speech": true,
6-
"system_prompt": "",
7-
"max_history": 50,
8-
"font_size": 16,
9-
"api_url": "http://localhost:11434",
10-
"temperature": 100,
11-
"user_name": "Madick Ange C\u00e9sar",
12-
"language": "en",
13-
"use_offline_voice": false,
14-
"offline_stt_model": "",
15-
"offline_tts_model": ""
2+
"theme": "dark",
3+
"default_model": "llama3.2:1b",
4+
"streaming": false,
5+
"text_to_speech": true,
6+
"system_prompt": "",
7+
"max_history": 50,
8+
"font_size": 16,
9+
"api_url": "http://localhost:11434",
10+
"temperature": 100,
11+
"user_name": "Madick Ange C\u00e9sar",
12+
"language": "en",
13+
"use_offline_voice": false,
14+
"offline_stt_model": "C:\\Users\\user\\Desktop\\Projects\\3- In Progress\\chatbot\\app\\../models\\tiny-whisper.bin",
15+
"offline_tts_model": ""
1616
}

0 commit comments

Comments
 (0)