meeting-noter 1.1.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of meeting-noter might be problematic. Click here for more details.

@@ -1,348 +0,0 @@
1
- """Meetings list tab for browsing and managing recordings."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- import subprocess
7
- from datetime import datetime
8
- from pathlib import Path
9
- from typing import Optional
10
-
11
- from PyQt6.QtCore import Qt, QUrl
12
- from PyQt6.QtWidgets import (
13
- QWidget,
14
- QVBoxLayout,
15
- QHBoxLayout,
16
- QTableWidget,
17
- QTableWidgetItem,
18
- QPushButton,
19
- QHeaderView,
20
- QDialog,
21
- QTextEdit,
22
- QLineEdit,
23
- QLabel,
24
- QMessageBox,
25
- QDialogButtonBox,
26
- )
27
- from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
28
-
29
- from meeting_noter.config import get_config
30
-
31
-
32
- class TranscriptDialog(QDialog):
33
- """Dialog for viewing a transcript."""
34
-
35
- def __init__(self, title: str, content: str, parent=None):
36
- super().__init__(parent)
37
- self.setWindowTitle(f"Transcript: {title}")
38
- self.setMinimumSize(600, 400)
39
-
40
- layout = QVBoxLayout(self)
41
-
42
- self.text_edit = QTextEdit()
43
- self.text_edit.setReadOnly(True)
44
- self.text_edit.setPlainText(content)
45
- layout.addWidget(self.text_edit)
46
-
47
- button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
48
- button_box.rejected.connect(self.close)
49
- layout.addWidget(button_box)
50
-
51
-
52
- class RenameDialog(QDialog):
53
- """Dialog for renaming a meeting."""
54
-
55
- def __init__(self, current_name: str, parent=None):
56
- super().__init__(parent)
57
- self.setWindowTitle("Rename Meeting")
58
- self.setMinimumWidth(400)
59
-
60
- layout = QVBoxLayout(self)
61
-
62
- layout.addWidget(QLabel("New name:"))
63
-
64
- self.name_input = QLineEdit()
65
- self.name_input.setText(current_name)
66
- self.name_input.selectAll()
67
- layout.addWidget(self.name_input)
68
-
69
- button_box = QDialogButtonBox(
70
- QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
71
- )
72
- button_box.accepted.connect(self.accept)
73
- button_box.rejected.connect(self.reject)
74
- layout.addWidget(button_box)
75
-
76
- def get_name(self) -> str:
77
- """Get the entered name."""
78
- return self.name_input.text().strip()
79
-
80
-
81
- class MeetingsTab(QWidget):
82
- """Tab for browsing and managing meeting recordings."""
83
-
84
- def __init__(self):
85
- super().__init__()
86
- self.config = get_config()
87
- self.player: Optional[QMediaPlayer] = None
88
- self.audio_output: Optional[QAudioOutput] = None
89
- self.current_playing: Optional[Path] = None
90
-
91
- self._setup_ui()
92
- self.refresh()
93
-
94
- def _setup_ui(self):
95
- """Set up the user interface."""
96
- layout = QVBoxLayout(self)
97
-
98
- # Table for meetings
99
- self.table = QTableWidget()
100
- self.table.setColumnCount(5)
101
- self.table.setHorizontalHeaderLabels(["Date", "Name", "Duration", "Transcript", "Actions"])
102
- self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
103
- self.table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
104
- self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
105
- self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
106
- self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
107
- self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
108
- self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
109
- layout.addWidget(self.table)
110
-
111
- # Bottom buttons
112
- button_layout = QHBoxLayout()
113
-
114
- self.refresh_button = QPushButton("Refresh")
115
- self.refresh_button.clicked.connect(self.refresh)
116
- button_layout.addWidget(self.refresh_button)
117
-
118
- self.open_folder_button = QPushButton("Open Folder")
119
- self.open_folder_button.clicked.connect(self._open_folder)
120
- button_layout.addWidget(self.open_folder_button)
121
-
122
- button_layout.addStretch()
123
-
124
- layout.addLayout(button_layout)
125
-
126
- def refresh(self):
127
- """Refresh the meetings list."""
128
- self.table.setRowCount(0)
129
-
130
- recordings_dir = self.config.recordings_dir
131
- if not recordings_dir.exists():
132
- return
133
-
134
- # Find all MP3 files
135
- mp3_files = sorted(recordings_dir.glob("*.mp3"), reverse=True)
136
-
137
- for mp3_path in mp3_files:
138
- self._add_meeting_row(mp3_path)
139
-
140
- def _get_transcript_path(self, mp3_path: Path) -> Path:
141
- """Get the transcript path for an audio file."""
142
- transcripts_dir = self.config.transcripts_dir
143
- return transcripts_dir / mp3_path.with_suffix(".txt").name
144
-
145
- def _add_meeting_row(self, mp3_path: Path):
146
- """Add a row for a meeting recording."""
147
- row = self.table.rowCount()
148
- self.table.insertRow(row)
149
-
150
- # Parse filename
151
- name = mp3_path.stem
152
- parts = name.split("_", 2)
153
-
154
- if len(parts) >= 2:
155
- # Format: YYYY-MM-DD_HHMMSS or YYYY-MM-DD_HHMMSS_name
156
- try:
157
- date_str = parts[0]
158
- time_str = parts[1]
159
- date_obj = datetime.strptime(f"{date_str}_{time_str}", "%Y-%m-%d_%H%M%S")
160
- display_date = date_obj.strftime("%Y-%m-%d %H:%M")
161
- except ValueError:
162
- display_date = name
163
- meeting_name = parts[2] if len(parts) > 2 else ""
164
- else:
165
- display_date = name
166
- meeting_name = ""
167
-
168
- # Date column
169
- date_item = QTableWidgetItem(display_date)
170
- date_item.setData(Qt.ItemDataRole.UserRole, str(mp3_path))
171
- self.table.setItem(row, 0, date_item)
172
-
173
- # Name column
174
- name_item = QTableWidgetItem(meeting_name)
175
- self.table.setItem(row, 1, name_item)
176
-
177
- # Duration column
178
- duration = self._get_duration(mp3_path)
179
- duration_item = QTableWidgetItem(duration)
180
- self.table.setItem(row, 2, duration_item)
181
-
182
- # Transcript column
183
- transcript_path = self._get_transcript_path(mp3_path)
184
- has_transcript = transcript_path.exists()
185
- transcript_item = QTableWidgetItem("Yes" if has_transcript else "No")
186
- self.table.setItem(row, 3, transcript_item)
187
-
188
- # Actions column - widget with buttons
189
- actions_widget = QWidget()
190
- actions_layout = QHBoxLayout(actions_widget)
191
- actions_layout.setContentsMargins(4, 2, 4, 2)
192
- actions_layout.setSpacing(4)
193
-
194
- play_btn = QPushButton("Play")
195
- play_btn.setFixedWidth(50)
196
- play_btn.clicked.connect(lambda checked, p=mp3_path: self._play_audio(p))
197
- actions_layout.addWidget(play_btn)
198
-
199
- if has_transcript:
200
- view_btn = QPushButton("View")
201
- view_btn.setFixedWidth(50)
202
- tp = transcript_path # Capture for lambda
203
- view_btn.clicked.connect(lambda checked, p=tp: self._view_transcript(p))
204
- actions_layout.addWidget(view_btn)
205
- else:
206
- transcribe_btn = QPushButton("Transcribe")
207
- transcribe_btn.setFixedWidth(70)
208
- transcribe_btn.clicked.connect(lambda checked, p=mp3_path: self._transcribe_recording(p))
209
- actions_layout.addWidget(transcribe_btn)
210
-
211
- rename_btn = QPushButton("Rename")
212
- rename_btn.setFixedWidth(60)
213
- rename_btn.clicked.connect(lambda checked, p=mp3_path, r=row: self._rename_meeting(p, r))
214
- actions_layout.addWidget(rename_btn)
215
-
216
- self.table.setCellWidget(row, 4, actions_widget)
217
-
218
- def _get_duration(self, mp3_path: Path) -> str:
219
- """Get the duration of an MP3 file."""
220
- try:
221
- # Use file size as rough estimate (128kbps = 16KB/s)
222
- size_bytes = mp3_path.stat().st_size
223
- duration_secs = size_bytes / (128 * 1000 / 8)
224
- mins, secs = divmod(int(duration_secs), 60)
225
- return f"{mins:02d}:{secs:02d}"
226
- except Exception:
227
- return "--:--"
228
-
229
- def _play_audio(self, mp3_path: Path):
230
- """Play or stop audio playback."""
231
- # Stop current playback if any
232
- if self.player and self.current_playing == mp3_path:
233
- self.player.stop()
234
- self.player = None
235
- self.audio_output = None
236
- self.current_playing = None
237
- return
238
-
239
- # Stop any existing playback
240
- if self.player:
241
- self.player.stop()
242
-
243
- # Create new player
244
- self.audio_output = QAudioOutput()
245
- self.player = QMediaPlayer()
246
- self.player.setAudioOutput(self.audio_output)
247
- self.player.setSource(QUrl.fromLocalFile(str(mp3_path)))
248
- self.player.play()
249
- self.current_playing = mp3_path
250
-
251
- def _view_transcript(self, transcript_path: Path):
252
- """View a transcript in a dialog."""
253
- try:
254
- content = transcript_path.read_text()
255
- dialog = TranscriptDialog(transcript_path.stem, content, self)
256
- dialog.exec()
257
- except Exception as e:
258
- QMessageBox.warning(self, "Error", f"Could not read transcript: {e}")
259
-
260
- def _rename_meeting(self, mp3_path: Path, row: int):
261
- """Rename a meeting (both MP3 and transcript files)."""
262
- # Extract current name
263
- name = mp3_path.stem
264
- parts = name.split("_", 2)
265
- current_name = parts[2] if len(parts) > 2 else ""
266
-
267
- dialog = RenameDialog(current_name, self)
268
- if dialog.exec() != QDialog.DialogCode.Accepted:
269
- return
270
-
271
- new_name = dialog.get_name()
272
- if not new_name:
273
- return
274
-
275
- # Sanitize new name
276
- import re
277
- sanitized = new_name.replace(" ", "_")
278
- sanitized = re.sub(r"[^\w\-]", "", sanitized)
279
- if len(sanitized) > 50:
280
- sanitized = sanitized[:50].rstrip("_-")
281
-
282
- # Build new filename
283
- if len(parts) >= 2:
284
- new_stem = f"{parts[0]}_{parts[1]}_{sanitized}"
285
- else:
286
- new_stem = f"{name}_{sanitized}"
287
-
288
- new_mp3_path = mp3_path.with_stem(new_stem)
289
- transcript_path = self._get_transcript_path(mp3_path)
290
- new_transcript_path = self._get_transcript_path(new_mp3_path)
291
-
292
- try:
293
- # Rename MP3
294
- mp3_path.rename(new_mp3_path)
295
-
296
- # Rename transcript if exists
297
- if transcript_path.exists():
298
- transcript_path.rename(new_transcript_path)
299
-
300
- self.refresh()
301
- except Exception as e:
302
- QMessageBox.warning(self, "Error", f"Could not rename files: {e}")
303
-
304
- def _transcribe_recording(self, mp3_path: Path):
305
- """Transcribe a recording that doesn't have a transcript."""
306
- from PyQt6.QtCore import QThread, pyqtSignal
307
-
308
- class TranscribeWorker(QThread):
309
- finished = pyqtSignal(bool, str)
310
-
311
- def __init__(self, audio_path, config):
312
- super().__init__()
313
- self.audio_path = audio_path
314
- self.config = config
315
-
316
- def run(self):
317
- try:
318
- from meeting_noter.transcription.engine import transcribe_file
319
- transcribe_file(
320
- str(self.audio_path),
321
- self.config.recordings_dir,
322
- self.config.whisper_model,
323
- self.config.transcripts_dir,
324
- )
325
- self.finished.emit(True, "")
326
- except Exception as e:
327
- self.finished.emit(False, str(e))
328
-
329
- def on_finished(success, error):
330
- self._transcribe_worker = None
331
- if success:
332
- QMessageBox.information(self, "Success", f"Transcription complete: {mp3_path.name}")
333
- self.refresh()
334
- else:
335
- QMessageBox.warning(self, "Error", f"Transcription failed: {error}")
336
-
337
- # Show progress
338
- QMessageBox.information(self, "Transcribing", f"Transcribing {mp3_path.name}...\nThis may take a while.")
339
-
340
- self._transcribe_worker = TranscribeWorker(mp3_path, self.config)
341
- self._transcribe_worker.finished.connect(on_finished)
342
- self._transcribe_worker.start()
343
-
344
- def _open_folder(self):
345
- """Open the recordings folder in Finder."""
346
- recordings_dir = self.config.recordings_dir
347
- recordings_dir.mkdir(parents=True, exist_ok=True)
348
- subprocess.run(["open", str(recordings_dir)])
@@ -1,358 +0,0 @@
1
- """Recording tab for starting/stopping meeting recordings."""
2
-
3
- from __future__ import annotations
4
-
5
- from pathlib import Path
6
- from typing import Optional
7
-
8
- from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
9
- from PyQt6.QtWidgets import (
10
- QWidget,
11
- QVBoxLayout,
12
- QHBoxLayout,
13
- QLabel,
14
- QLineEdit,
15
- QPushButton,
16
- QTextEdit,
17
- QGroupBox,
18
- )
19
-
20
- from meeting_noter.config import get_config, generate_meeting_name, is_default_meeting_name
21
-
22
-
23
- class RecordingWorker(QThread):
24
- """Background worker thread for audio recording."""
25
-
26
- log_message = pyqtSignal(str)
27
- recording_started = pyqtSignal(str) # filepath
28
- recording_stopped = pyqtSignal(str, float) # filepath, duration
29
- silence_stopped = pyqtSignal() # stopped due to silence
30
- error = pyqtSignal(str)
31
-
32
- def __init__(self, output_dir: Path, meeting_name: str, silence_timeout_minutes: int = 5):
33
- super().__init__()
34
- self.output_dir = output_dir
35
- self.meeting_name = meeting_name
36
- self.silence_timeout_minutes = silence_timeout_minutes
37
- self._stop_requested = False
38
- self.saved_filepath: Optional[Path] = None
39
-
40
- def run(self):
41
- """Run the recording in background thread."""
42
- # Import audio modules in thread
43
- try:
44
- from meeting_noter.audio.capture import SilenceDetector
45
- from meeting_noter.audio.system_audio import CombinedAudioCapture
46
- from meeting_noter.audio.encoder import RecordingSession
47
- except ImportError as e:
48
- self.error.emit(f"Import error: {e}")
49
- return
50
-
51
- from meeting_noter.daemon import check_audio_available
52
- if not check_audio_available():
53
- self.error.emit("No audio input device found. Please check your microphone.")
54
- return
55
-
56
- try:
57
- # Use CombinedAudioCapture for mic + system audio
58
- capture = CombinedAudioCapture()
59
- capture.start()
60
- except Exception as e:
61
- self.error.emit(f"Audio capture error: {e}")
62
- return
63
-
64
- session = RecordingSession(
65
- self.output_dir,
66
- sample_rate=capture.sample_rate,
67
- channels=capture.channels,
68
- meeting_name=self.meeting_name,
69
- )
70
-
71
- # Silence detection
72
- silence_detector = SilenceDetector(
73
- threshold=0.01,
74
- silence_duration=self.silence_timeout_minutes * 60.0,
75
- sample_rate=capture.sample_rate,
76
- )
77
- stopped_by_silence = False
78
-
79
- # Log capture mode
80
- if capture.has_system_audio:
81
- self.log_message.emit("Capturing: microphone + system audio")
82
- else:
83
- self.log_message.emit("Capturing: microphone only")
84
-
85
- try:
86
- filepath = session.start()
87
- self.recording_started.emit(str(filepath))
88
- self.log_message.emit(f"Will stop after {self.silence_timeout_minutes} min of silence")
89
-
90
- while not self._stop_requested:
91
- audio = capture.get_audio(timeout=0.5)
92
- if audio is None:
93
- continue
94
-
95
- if audio.ndim > 1:
96
- audio = audio.flatten()
97
-
98
- session.write(audio)
99
-
100
- # Check for extended silence
101
- if silence_detector.update(audio):
102
- self.log_message.emit("Stopped: silence timeout reached")
103
- stopped_by_silence = True
104
- break
105
-
106
- except Exception as e:
107
- self.error.emit(f"Recording error: {e}")
108
- finally:
109
- capture.stop()
110
-
111
- if session.is_active:
112
- filepath, duration = session.stop()
113
- if filepath:
114
- self.saved_filepath = filepath
115
- self.recording_stopped.emit(str(filepath), duration)
116
- if stopped_by_silence:
117
- self.silence_stopped.emit()
118
- else:
119
- self.log_message.emit("Recording discarded (too short)")
120
-
121
- def stop(self):
122
- """Request the recording to stop."""
123
- self._stop_requested = True
124
-
125
-
126
- class RecordingTab(QWidget):
127
- """Tab for recording meetings."""
128
-
129
- recording_saved = pyqtSignal()
130
-
131
- def __init__(self):
132
- super().__init__()
133
- self.config = get_config()
134
- self.worker: Optional[RecordingWorker] = None
135
- self.is_recording = False
136
-
137
- self._setup_ui()
138
- self._setup_meeting_detection()
139
-
140
- def _setup_ui(self):
141
- """Set up the user interface."""
142
- layout = QVBoxLayout(self)
143
- layout.setSpacing(16)
144
-
145
- # Meeting name input
146
- name_group = QGroupBox("Meeting Name")
147
- name_layout = QHBoxLayout(name_group)
148
-
149
- self.name_input = QLineEdit()
150
- self.name_input.setPlaceholderText("Enter meeting name or leave blank to auto-detect")
151
- self.name_input.returnPressed.connect(self._on_start_clicked)
152
- name_layout.addWidget(self.name_input)
153
-
154
- self.detect_button = QPushButton("Detect")
155
- self.detect_button.setMaximumWidth(80)
156
- self.detect_button.clicked.connect(self._detect_meeting)
157
- name_layout.addWidget(self.detect_button)
158
-
159
- layout.addWidget(name_group)
160
-
161
- # Control buttons
162
- button_layout = QHBoxLayout()
163
-
164
- self.start_button = QPushButton("Start Recording")
165
- self.start_button.setMinimumHeight(50)
166
- self.start_button.clicked.connect(self._on_start_clicked)
167
- button_layout.addWidget(self.start_button)
168
-
169
- self.stop_button = QPushButton("Stop Recording")
170
- self.stop_button.setMinimumHeight(50)
171
- self.stop_button.setEnabled(False)
172
- self.stop_button.clicked.connect(self._on_stop_clicked)
173
- button_layout.addWidget(self.stop_button)
174
-
175
- layout.addLayout(button_layout)
176
-
177
- # Status
178
- self.status_label = QLabel("Ready to record")
179
- self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
180
- self.status_label.setStyleSheet("font-size: 14px; padding: 10px;")
181
- layout.addWidget(self.status_label)
182
-
183
- # Log output
184
- log_group = QGroupBox("Log")
185
- log_layout = QVBoxLayout(log_group)
186
-
187
- self.log_output = QTextEdit()
188
- self.log_output.setReadOnly(True)
189
- self.log_output.setMaximumHeight(200)
190
- log_layout.addWidget(self.log_output)
191
-
192
- layout.addWidget(log_group)
193
-
194
- # Stretch to push everything up
195
- layout.addStretch()
196
-
197
- def _setup_meeting_detection(self):
198
- """Set up periodic meeting detection."""
199
- self._detection_timer = QTimer()
200
- self._detection_timer.timeout.connect(self._check_for_meeting)
201
- self._detection_timer.start(3000) # Check every 3 seconds
202
-
203
- # Initial check
204
- self._detect_meeting()
205
-
206
- def _check_for_meeting(self):
207
- """Check for active meeting and update UI if not recording."""
208
- if self.is_recording:
209
- return
210
-
211
- # Only auto-update if field is empty or has default timestamp
212
- current = self.name_input.text().strip()
213
- if current and not is_default_meeting_name(current):
214
- return
215
-
216
- try:
217
- from meeting_noter.meeting_detector import detect_active_meeting
218
- meeting = detect_active_meeting()
219
- if meeting and meeting.meeting_name:
220
- self.name_input.setText(meeting.meeting_name)
221
- self.status_label.setText(f"Meeting detected: {meeting.app_name}")
222
- except Exception:
223
- pass
224
-
225
- def _detect_meeting(self):
226
- """Manually detect current meeting."""
227
- try:
228
- from meeting_noter.meeting_detector import detect_active_meeting
229
- meeting = detect_active_meeting()
230
- if meeting:
231
- name = meeting.meeting_name or meeting.app_name
232
- self.name_input.setText(name)
233
- self._log(f"Detected: {meeting.app_name} - {name}")
234
- else:
235
- self.name_input.setText(generate_meeting_name())
236
- self._log("No meeting detected, using timestamp")
237
- except Exception as e:
238
- self._log(f"Detection error: {e}")
239
- self.name_input.setText(generate_meeting_name())
240
-
241
- def _on_start_clicked(self):
242
- """Handle start button click."""
243
- meeting_name = self.name_input.text().strip()
244
-
245
- # Auto-detect or generate name if empty
246
- if not meeting_name:
247
- try:
248
- from meeting_noter.meeting_detector import detect_active_meeting
249
- meeting = detect_active_meeting()
250
- if meeting and meeting.meeting_name:
251
- meeting_name = meeting.meeting_name
252
- else:
253
- meeting_name = generate_meeting_name()
254
- except Exception:
255
- meeting_name = generate_meeting_name()
256
- self.name_input.setText(meeting_name)
257
-
258
- self.start_recording(meeting_name)
259
-
260
- def _on_stop_clicked(self):
261
- """Handle stop button click."""
262
- self.stop_recording()
263
-
264
- def start_recording(self, meeting_name: str):
265
- """Start a new recording."""
266
- if self.is_recording:
267
- return
268
-
269
- self.is_recording = True
270
- self.start_button.setEnabled(False)
271
- self.stop_button.setEnabled(True)
272
- self.name_input.setEnabled(False)
273
- self.detect_button.setEnabled(False)
274
-
275
- self.status_label.setText(f"Recording: {meeting_name}")
276
- self.status_label.setStyleSheet(
277
- "font-size: 14px; padding: 10px; color: white; background-color: #c0392b;"
278
- )
279
-
280
- self.log_output.clear()
281
- self._log(f"Starting recording: {meeting_name}")
282
-
283
- # Start worker thread
284
- output_dir = self.config.recordings_dir
285
- output_dir.mkdir(parents=True, exist_ok=True)
286
-
287
- self.worker = RecordingWorker(
288
- output_dir,
289
- meeting_name,
290
- silence_timeout_minutes=self.config.silence_timeout,
291
- )
292
- self.worker.log_message.connect(self._log)
293
- self.worker.recording_started.connect(self._on_recording_started)
294
- self.worker.recording_stopped.connect(self._on_recording_stopped)
295
- self.worker.error.connect(self._on_error)
296
- self.worker.finished.connect(self._on_worker_finished)
297
- self.worker.start()
298
-
299
- def stop_recording(self):
300
- """Stop the current recording."""
301
- if not self.is_recording or not self.worker:
302
- return
303
-
304
- self._log("Stopping recording...")
305
- self.worker.stop()
306
- self.worker.wait(5000) # Wait up to 5 seconds
307
-
308
- def _on_recording_started(self, filepath: str):
309
- """Handle recording started."""
310
- self._log(f"Recording to: {Path(filepath).name}")
311
-
312
- def _on_recording_stopped(self, filepath: str, duration: float):
313
- """Handle recording stopped."""
314
- mins, secs = divmod(int(duration), 60)
315
- self._log(f"Saved: {Path(filepath).name} ({mins:02d}:{secs:02d})")
316
-
317
- # Auto-transcribe if enabled
318
- if self.config.auto_transcribe:
319
- self._log("Auto-transcribing...")
320
- try:
321
- from meeting_noter.transcription.engine import transcribe_file
322
- transcribe_file(
323
- filepath,
324
- self.config.recordings_dir,
325
- self.config.whisper_model,
326
- self.config.transcripts_dir,
327
- )
328
- self._log("Transcription complete")
329
- except Exception as e:
330
- self._log(f"Transcription error: {e}")
331
-
332
- self.recording_saved.emit()
333
-
334
- def _on_error(self, message: str):
335
- """Handle error from worker."""
336
- self._log(f"Error: {message}")
337
- self.status_label.setText(f"Error: {message}")
338
- self.status_label.setStyleSheet("font-size: 14px; padding: 10px; color: red;")
339
-
340
- def _on_worker_finished(self):
341
- """Handle worker thread finished."""
342
- self.is_recording = False
343
- self.start_button.setEnabled(True)
344
- self.stop_button.setEnabled(False)
345
- self.name_input.setEnabled(True)
346
- self.detect_button.setEnabled(True)
347
-
348
- # Clear the name field for next recording
349
- self.name_input.clear()
350
-
351
- self.status_label.setText("Ready to record")
352
- self.status_label.setStyleSheet("font-size: 14px; padding: 10px;")
353
-
354
- self.worker = None
355
-
356
- def _log(self, message: str):
357
- """Add a message to the log output."""
358
- self.log_output.append(message)