Skip to content

Commit fc94f38

Browse files
authored
Development (#14)
* Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload
1 parent ba7b6ec commit fc94f38

File tree

16 files changed

+607
-270
lines changed

16 files changed

+607
-270
lines changed

config/manager.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
"""
2-
Centralized configuration management for the Whisper transcriber.
3-
"""
41
from __future__ import annotations
52

6-
import logging
73
from pathlib import Path
84
from typing import Any, Dict, Optional
95

106
import yaml
117

128
from utils import get_resource_path
9+
from core.logging_config import get_logger
10+
from core.exceptions import ConfigurationError
1311

14-
logger = logging.getLogger(__name__)
12+
logger = get_logger(__name__)
1513

1614

1715
class ConfigManager:
@@ -25,7 +23,8 @@ class ConfigManager:
2523
"supported_quantizations": {
2624
"cpu": [],
2725
"cuda": []
28-
}
26+
},
27+
"curate_transcription": True
2928
}
3029

3130
def __init__(self):
@@ -51,6 +50,9 @@ def _load_from_file(self) -> Dict[str, Any]:
5150
except yaml.YAMLError as e:
5251
logger.error(f"Error parsing config file: {e}")
5352
config = {}
53+
except Exception as e:
54+
logger.error(f"Unexpected error loading config: {e}")
55+
config = {}
5456

5557
merged_config = self.DEFAULT_CONFIG.copy()
5658
self._deep_update(merged_config, config)
@@ -68,7 +70,7 @@ def save_config(self, config: Dict[str, Any]) -> None:
6870

6971
except Exception as e:
7072
logger.error(f"Failed to save configuration: {e}")
71-
raise
73+
raise ConfigurationError(f"Failed to save configuration: {e}") from e
7274

7375
def update_config(self, updates: Dict[str, Any]) -> None:
7476
config = self.load_config()

core/audio/manager.py

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1-
# core/audio/manager.py
1+
from __future__ import annotations
2+
23
from typing import Optional
3-
from pathlib import Path
4-
import tempfile
54
import wave
5+
66
from PySide6.QtCore import QObject, Signal, Slot
7-
from .recording import RecordingThread
7+
8+
from core.audio.recording import RecordingThread
9+
from core.temp_file_manager import temp_file_manager
10+
from core.logging_config import get_logger
11+
from core.exceptions import AudioSaveError
12+
13+
logger = get_logger(__name__)
14+
815

916
class AudioManager(QObject):
1017
recording_started = Signal()
1118
recording_stopped = Signal()
12-
audio_ready = Signal(str) # file path
19+
audio_ready = Signal(str)
1320
audio_error = Signal(str)
1421

1522
def __init__(self, samplerate: int = 44_100, channels: int = 1, dtype: str = "int16"):
@@ -18,55 +25,89 @@ def __init__(self, samplerate: int = 44_100, channels: int = 1, dtype: str = "in
1825
self.channels = channels
1926
self.dtype = dtype
2027
self._recording_thread: Optional[RecordingThread] = None
28+
self._current_temp_file: Optional[str] = None
2129

2230
def start_recording(self) -> bool:
23-
"""Start audio recording."""
2431
if self._recording_thread and self._recording_thread.isRunning():
32+
logger.warning("Attempted to start recording while already recording")
33+
return False
34+
35+
try:
36+
self._recording_thread = RecordingThread(
37+
self.samplerate, self.channels, self.dtype
38+
)
39+
self._recording_thread.recording_error.connect(self._on_recording_error)
40+
self._recording_thread.recording_finished.connect(self._on_recording_finished)
41+
self._recording_thread.start()
42+
self.recording_started.emit()
43+
logger.info("Recording started")
44+
return True
45+
except Exception as e:
46+
logger.exception("Failed to start recording")
47+
self.audio_error.emit(f"Failed to start recording: {e}")
2548
return False
26-
27-
self._recording_thread = RecordingThread(
28-
self.samplerate, self.channels, self.dtype
29-
)
30-
self._recording_thread.recording_error.connect(self.audio_error)
31-
self._recording_thread.recording_finished.connect(self._on_recording_finished)
32-
self._recording_thread.start()
33-
self.recording_started.emit()
34-
return True
3549

3650
def stop_recording(self) -> None:
37-
"""Stop audio recording."""
3851
if self._recording_thread and self._recording_thread.isRunning():
3952
self._recording_thread.stop()
53+
logger.info("Recording stop requested")
4054
self.recording_stopped.emit()
4155

56+
@Slot(str)
57+
def _on_recording_error(self, error: str) -> None:
58+
logger.error(f"Recording error: {error}")
59+
self.audio_error.emit(error)
60+
4261
@Slot()
4362
def _on_recording_finished(self) -> None:
44-
"""Handle recording completion and save to file."""
4563
try:
46-
audio_file = self._save_recording_to_file()
64+
if self._recording_thread is None:
65+
raise AudioSaveError("No recording thread available")
66+
67+
buffer_contents = self._recording_thread.get_buffer_contents()
68+
if not buffer_contents:
69+
raise AudioSaveError("No audio data recorded")
70+
71+
audio_file = self._save_recording_to_file(buffer_contents)
72+
self._current_temp_file = str(audio_file)
73+
logger.info(f"Audio saved to: {audio_file}")
4774
self.audio_ready.emit(str(audio_file))
75+
76+
except AudioSaveError as e:
77+
logger.error(f"Audio save error: {e}")
78+
self.audio_error.emit(str(e))
4879
except Exception as e:
80+
logger.exception("Unexpected error saving audio")
4981
self.audio_error.emit(f"Failed to save audio: {e}")
5082

51-
def _save_recording_to_file(self) -> Path:
52-
"""Save recorded audio to temporary WAV file."""
53-
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tf:
54-
path = Path(tf.name)
55-
56-
with wave.open(str(path), "wb") as wf:
57-
wf.setnchannels(self.channels)
58-
wf.setsampwidth(self._sample_width())
59-
wf.setframerate(self.samplerate)
60-
while not self._recording_thread.buffer.empty():
61-
wf.writeframes(self._recording_thread.buffer.get().tobytes())
83+
def _save_recording_to_file(self, buffer_contents: list):
84+
path = temp_file_manager.create_temp_wav()
6285

63-
return path
86+
try:
87+
with wave.open(str(path), "wb") as wf:
88+
wf.setnchannels(self.channels)
89+
wf.setsampwidth(self._sample_width())
90+
wf.setframerate(self.samplerate)
91+
for chunk in buffer_contents:
92+
wf.writeframes(chunk.tobytes())
93+
return path
94+
except Exception as e:
95+
temp_file_manager.release(path)
96+
raise AudioSaveError(f"Failed to write WAV file: {e}") from e
6497

6598
def _sample_width(self) -> int:
6699
return {"int16": 2, "int32": 4, "float32": 4}.get(self.dtype, 2)
67100

68101
def cleanup(self) -> None:
69-
"""Clean up audio resources."""
70102
if self._recording_thread and self._recording_thread.isRunning():
71103
self._recording_thread.stop()
72-
self._recording_thread.wait()
104+
self._recording_thread.wait(5000)
105+
if self._recording_thread.isRunning():
106+
logger.warning("Recording thread did not stop in time")
107+
self._recording_thread.terminate()
108+
109+
if self._current_temp_file:
110+
temp_file_manager.release(self._current_temp_file)
111+
self._current_temp_file = None
112+
113+
logger.debug("AudioManager cleanup complete")

core/audio/recording.py

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
1-
"""
2-
Audio-capture thread and tiny WAV helpers.
3-
4-
Placed in: myapp/core/audio/recording.py
5-
"""
61
from __future__ import annotations
72

8-
import logging
93
import queue
10-
import tempfile
114
import threading
12-
import wave
135
from contextlib import contextmanager
14-
from pathlib import Path
156
from typing import Iterator
167

178
import sounddevice as sd
189
from PySide6.QtCore import QThread, Signal
1910

11+
from core.logging_config import get_logger
12+
from core.exceptions import AudioRecordingError
2013

21-
logger = logging.getLogger(__name__)
14+
logger = get_logger(__name__)
2215

2316

2417
class RecordingThread(QThread):
@@ -33,35 +26,53 @@ def __init__(self, samplerate: int = 44_100, channels: int = 1, dtype: str = "in
3326
self.channels = channels
3427
self.dtype = dtype
3528
self.buffer: queue.Queue = queue.Queue()
29+
self._stream_error: str | None = None
3630

3731
@contextmanager
3832
def _audio_stream(self) -> Iterator[None]:
39-
stream = sd.InputStream(
40-
samplerate=self.samplerate,
41-
channels=self.channels,
42-
dtype=self.dtype,
43-
callback=self._audio_callback,
44-
)
33+
stream = None
4534
try:
35+
stream = sd.InputStream(
36+
samplerate=self.samplerate,
37+
channels=self.channels,
38+
dtype=self.dtype,
39+
callback=self._audio_callback,
40+
)
4641
with stream:
4742
yield
43+
except sd.PortAudioError as e:
44+
logger.error(f"Audio device error: {e}")
45+
raise AudioRecordingError(f"Audio device error: {e}") from e
46+
except Exception as e:
47+
logger.error(f"Failed to create audio stream: {e}")
48+
raise AudioRecordingError(f"Failed to create audio stream: {e}") from e
4849
finally:
49-
stream.close()
50+
if stream is not None:
51+
try:
52+
stream.close()
53+
except Exception as e:
54+
logger.warning(f"Error closing audio stream: {e}")
5055

51-
def _audio_callback(self, indata, frames, timestamp, status) -> None: # noqa: D401, N802
56+
def _audio_callback(self, indata, frames, timestamp, status) -> None:
5257
if status:
53-
logger.warning(status)
58+
logger.warning(f"Audio callback status: {status}")
59+
self._stream_error = str(status)
5460
self.buffer.put(indata.copy())
5561

56-
def run(self) -> None: # noqa: D401
62+
def run(self) -> None:
5763
self.update_status_signal.emit("Recording.")
5864
try:
5965
with self._audio_stream():
6066
gate = threading.Event()
6167
while not self.isInterruptionRequested():
62-
gate.wait(timeout=1.0)
63-
except Exception as exc: # pragma: no cover
64-
self.recording_error.emit(f"Recording error: {exc}")
68+
gate.wait(timeout=0.1)
69+
if self._stream_error:
70+
logger.warning(f"Stream error during recording: {self._stream_error}")
71+
except AudioRecordingError as e:
72+
self.recording_error.emit(str(e))
73+
except Exception as e:
74+
logger.exception("Unexpected recording error")
75+
self.recording_error.emit(f"Recording error: {e}")
6576
finally:
6677
self.recording_finished.emit()
6778

@@ -72,20 +83,18 @@ def stop(self) -> None:
7283
def _sample_width_from_dtype(dtype: str) -> int:
7384
return {"int16": 2, "int32": 4, "float32": 4}.get(dtype, 2)
7485

75-
def dump_to_wav(self, outfile: str | Path) -> Path:
76-
77-
outfile = Path(outfile)
78-
with wave.open(str(outfile), "wb") as wf:
79-
wf.setnchannels(self.channels)
80-
wf.setsampwidth(self._sample_width_from_dtype(self.dtype))
81-
wf.setframerate(self.samplerate)
82-
83-
while not self.buffer.empty():
84-
wf.writeframes(self.buffer.get().tobytes())
85-
return outfile
86-
87-
def dump_to_temp_wav(self) -> Path:
88-
89-
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
90-
tmp.close()
91-
return self.dump_to_wav(tmp.name)
86+
def get_buffer_contents(self) -> list:
87+
contents = []
88+
while not self.buffer.empty():
89+
try:
90+
contents.append(self.buffer.get_nowait())
91+
except queue.Empty:
92+
break
93+
return contents
94+
95+
def clear_buffer(self) -> None:
96+
while not self.buffer.empty():
97+
try:
98+
self.buffer.get_nowait()
99+
except queue.Empty:
100+
break

0 commit comments

Comments
 (0)