meeting-noter 1.3.0__py3-none-any.whl → 3.0.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.
- meeting_noter/__init__.py +1 -1
- meeting_noter/cli.py +103 -0
- meeting_noter/daemon.py +38 -0
- meeting_noter/gui/__init__.py +51 -0
- meeting_noter/gui/main_window.py +219 -0
- meeting_noter/gui/menubar.py +248 -0
- meeting_noter/gui/screens/__init__.py +17 -0
- meeting_noter/gui/screens/dashboard.py +262 -0
- meeting_noter/gui/screens/logs.py +184 -0
- meeting_noter/gui/screens/recordings.py +279 -0
- meeting_noter/gui/screens/search.py +229 -0
- meeting_noter/gui/screens/settings.py +232 -0
- meeting_noter/gui/screens/viewer.py +140 -0
- meeting_noter/gui/theme/__init__.py +5 -0
- meeting_noter/gui/theme/dark_theme.py +53 -0
- meeting_noter/gui/theme/styles.qss +504 -0
- meeting_noter/gui/utils/__init__.py +15 -0
- meeting_noter/gui/utils/signals.py +82 -0
- meeting_noter/gui/utils/workers.py +258 -0
- meeting_noter/gui/widgets/__init__.py +6 -0
- meeting_noter/gui/widgets/sidebar.py +210 -0
- meeting_noter/gui/widgets/status_indicator.py +108 -0
- meeting_noter/mic_monitor.py +29 -1
- meeting_noter/ui/__init__.py +5 -0
- meeting_noter/ui/app.py +68 -0
- meeting_noter/ui/screens/__init__.py +17 -0
- meeting_noter/ui/screens/logs.py +166 -0
- meeting_noter/ui/screens/main.py +346 -0
- meeting_noter/ui/screens/recordings.py +241 -0
- meeting_noter/ui/screens/search.py +191 -0
- meeting_noter/ui/screens/settings.py +184 -0
- meeting_noter/ui/screens/viewer.py +116 -0
- meeting_noter/ui/styles/app.tcss +257 -0
- meeting_noter/ui/widgets/__init__.py +1 -0
- {meeting_noter-1.3.0.dist-info → meeting_noter-3.0.0.dist-info}/METADATA +4 -1
- meeting_noter-3.0.0.dist-info/RECORD +65 -0
- meeting_noter-1.3.0.dist-info/RECORD +0 -35
- {meeting_noter-1.3.0.dist-info → meeting_noter-3.0.0.dist-info}/WHEEL +0 -0
- {meeting_noter-1.3.0.dist-info → meeting_noter-3.0.0.dist-info}/entry_points.txt +0 -0
- {meeting_noter-1.3.0.dist-info → meeting_noter-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Menu bar (system tray) integration for Meeting Noter GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import QTimer
|
|
9
|
+
from PySide6.QtGui import QAction, QIcon, QPixmap, QPainter, QColor, QFont
|
|
10
|
+
from PySide6.QtWidgets import QMenu, QSystemTrayIcon
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from meeting_noter.gui.main_window import MainWindow
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MenuBarIcon(QSystemTrayIcon):
|
|
17
|
+
"""System tray / menu bar icon for Meeting Noter."""
|
|
18
|
+
|
|
19
|
+
# Status states
|
|
20
|
+
STATUS_STOPPED = "stopped"
|
|
21
|
+
STATUS_READY = "ready"
|
|
22
|
+
STATUS_RECORDING = "recording"
|
|
23
|
+
|
|
24
|
+
def __init__(self, main_window: "MainWindow"):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self._main_window = main_window
|
|
27
|
+
self._current_status = self.STATUS_STOPPED
|
|
28
|
+
self._recording_name = ""
|
|
29
|
+
|
|
30
|
+
self._setup_icons()
|
|
31
|
+
self._setup_menu()
|
|
32
|
+
self._setup_polling()
|
|
33
|
+
|
|
34
|
+
# Set initial icon
|
|
35
|
+
self._update_icon()
|
|
36
|
+
|
|
37
|
+
# Connect signals
|
|
38
|
+
self.activated.connect(self._on_activated)
|
|
39
|
+
|
|
40
|
+
def _setup_icons(self):
|
|
41
|
+
"""Create status icons programmatically."""
|
|
42
|
+
self._icons = {}
|
|
43
|
+
|
|
44
|
+
# Create icons for each status
|
|
45
|
+
for status, color in [
|
|
46
|
+
(self.STATUS_STOPPED, "#5a5a5a"), # Gray
|
|
47
|
+
(self.STATUS_READY, "#4ec9b0"), # Green/Cyan
|
|
48
|
+
(self.STATUS_RECORDING, "#f14c4c"), # Red
|
|
49
|
+
]:
|
|
50
|
+
self._icons[status] = self._create_icon(color)
|
|
51
|
+
|
|
52
|
+
def _create_icon(self, color: str) -> QIcon:
|
|
53
|
+
"""Create a simple colored circle icon."""
|
|
54
|
+
size = 22
|
|
55
|
+
pixmap = QPixmap(size, size)
|
|
56
|
+
pixmap.fill(QColor(0, 0, 0, 0)) # Transparent background
|
|
57
|
+
|
|
58
|
+
painter = QPainter(pixmap)
|
|
59
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
60
|
+
|
|
61
|
+
# Draw filled circle
|
|
62
|
+
painter.setBrush(QColor(color))
|
|
63
|
+
painter.setPen(QColor(color))
|
|
64
|
+
margin = 3
|
|
65
|
+
painter.drawEllipse(margin, margin, size - margin * 2, size - margin * 2)
|
|
66
|
+
|
|
67
|
+
painter.end()
|
|
68
|
+
return QIcon(pixmap)
|
|
69
|
+
|
|
70
|
+
def _setup_menu(self):
|
|
71
|
+
"""Set up the context menu."""
|
|
72
|
+
self._menu = QMenu()
|
|
73
|
+
self._menu.setStyleSheet("""
|
|
74
|
+
QMenu {
|
|
75
|
+
background-color: #1a1a1a;
|
|
76
|
+
border: 1px solid #2a2a2a;
|
|
77
|
+
border-radius: 6px;
|
|
78
|
+
padding: 5px;
|
|
79
|
+
}
|
|
80
|
+
QMenu::item {
|
|
81
|
+
color: #d4d4d4;
|
|
82
|
+
padding: 8px 20px;
|
|
83
|
+
border-radius: 4px;
|
|
84
|
+
}
|
|
85
|
+
QMenu::item:selected {
|
|
86
|
+
background-color: #2a2a2a;
|
|
87
|
+
}
|
|
88
|
+
QMenu::separator {
|
|
89
|
+
height: 1px;
|
|
90
|
+
background-color: #2a2a2a;
|
|
91
|
+
margin: 5px 10px;
|
|
92
|
+
}
|
|
93
|
+
""")
|
|
94
|
+
|
|
95
|
+
# Status label (non-clickable)
|
|
96
|
+
self._status_action = QAction("● Stopped")
|
|
97
|
+
self._status_action.setEnabled(False)
|
|
98
|
+
self._menu.addAction(self._status_action)
|
|
99
|
+
|
|
100
|
+
self._menu.addSeparator()
|
|
101
|
+
|
|
102
|
+
# Open GUI
|
|
103
|
+
self._open_action = QAction("Open Meeting Noter")
|
|
104
|
+
self._open_action.triggered.connect(self._show_main_window)
|
|
105
|
+
self._menu.addAction(self._open_action)
|
|
106
|
+
|
|
107
|
+
self._menu.addSeparator()
|
|
108
|
+
|
|
109
|
+
# Quit
|
|
110
|
+
self._quit_action = QAction("Quit Meeting Noter")
|
|
111
|
+
self._quit_action.triggered.connect(self._quit_app)
|
|
112
|
+
self._menu.addAction(self._quit_action)
|
|
113
|
+
|
|
114
|
+
self.setContextMenu(self._menu)
|
|
115
|
+
|
|
116
|
+
def _setup_polling(self):
|
|
117
|
+
"""Set up status polling timer."""
|
|
118
|
+
self._poll_timer = QTimer()
|
|
119
|
+
self._poll_timer.timeout.connect(self._poll_status)
|
|
120
|
+
self._poll_timer.start(2000) # Poll every 2 seconds
|
|
121
|
+
|
|
122
|
+
def _poll_status(self):
|
|
123
|
+
"""Poll for status updates."""
|
|
124
|
+
import os
|
|
125
|
+
from meeting_noter.daemon import is_process_running, read_pid_file
|
|
126
|
+
|
|
127
|
+
DEFAULT_PID_FILE = Path.home() / ".meeting-noter.pid"
|
|
128
|
+
WATCHER_PID_FILE = Path.home() / ".meeting-noter-watcher.pid"
|
|
129
|
+
|
|
130
|
+
# Check watcher
|
|
131
|
+
watcher_running = False
|
|
132
|
+
if WATCHER_PID_FILE.exists():
|
|
133
|
+
try:
|
|
134
|
+
pid = int(WATCHER_PID_FILE.read_text().strip())
|
|
135
|
+
os.kill(pid, 0)
|
|
136
|
+
watcher_running = True
|
|
137
|
+
except (ProcessLookupError, ValueError, FileNotFoundError, PermissionError):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
# Check daemon
|
|
141
|
+
daemon_running = False
|
|
142
|
+
daemon_pid = read_pid_file(DEFAULT_PID_FILE)
|
|
143
|
+
if daemon_pid and is_process_running(daemon_pid):
|
|
144
|
+
daemon_running = True
|
|
145
|
+
|
|
146
|
+
# Determine status
|
|
147
|
+
if daemon_running:
|
|
148
|
+
new_status = self.STATUS_RECORDING
|
|
149
|
+
# Get recording name
|
|
150
|
+
recording_name = self._get_recording_name()
|
|
151
|
+
self._recording_name = recording_name or "Recording..."
|
|
152
|
+
elif watcher_running:
|
|
153
|
+
new_status = self.STATUS_READY
|
|
154
|
+
self._recording_name = ""
|
|
155
|
+
else:
|
|
156
|
+
new_status = self.STATUS_STOPPED
|
|
157
|
+
self._recording_name = ""
|
|
158
|
+
|
|
159
|
+
if new_status != self._current_status:
|
|
160
|
+
self._current_status = new_status
|
|
161
|
+
self._update_icon()
|
|
162
|
+
self._update_status_text()
|
|
163
|
+
|
|
164
|
+
def _get_recording_name(self) -> str:
|
|
165
|
+
"""Get the current recording name from logs."""
|
|
166
|
+
log_path = Path.home() / ".meeting-noter.log"
|
|
167
|
+
if not log_path.exists():
|
|
168
|
+
return ""
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
with open(log_path, "r") as f:
|
|
172
|
+
lines = f.readlines()
|
|
173
|
+
|
|
174
|
+
for line in reversed(lines[-50:]):
|
|
175
|
+
if "Recording started:" in line:
|
|
176
|
+
parts = line.split("Recording started:")
|
|
177
|
+
if len(parts) > 1:
|
|
178
|
+
filename = parts[1].strip().replace(".mp3", "")
|
|
179
|
+
name_parts = filename.split("_", 2)
|
|
180
|
+
if len(name_parts) >= 3:
|
|
181
|
+
return name_parts[2]
|
|
182
|
+
return filename
|
|
183
|
+
elif "Recording saved:" in line or "Recording discarded" in line:
|
|
184
|
+
break
|
|
185
|
+
return ""
|
|
186
|
+
except Exception:
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
def _update_icon(self):
|
|
190
|
+
"""Update the tray icon based on status."""
|
|
191
|
+
self.setIcon(self._icons[self._current_status])
|
|
192
|
+
|
|
193
|
+
def _update_status_text(self):
|
|
194
|
+
"""Update the status text in menu."""
|
|
195
|
+
if self._current_status == self.STATUS_RECORDING:
|
|
196
|
+
text = f"● Recording: {self._recording_name}"
|
|
197
|
+
elif self._current_status == self.STATUS_READY:
|
|
198
|
+
text = "● Ready"
|
|
199
|
+
else:
|
|
200
|
+
text = "● Stopped"
|
|
201
|
+
|
|
202
|
+
self._status_action.setText(text)
|
|
203
|
+
|
|
204
|
+
# Update tooltip
|
|
205
|
+
self.setToolTip(f"Meeting Noter - {text}")
|
|
206
|
+
|
|
207
|
+
def _on_activated(self, reason):
|
|
208
|
+
"""Handle tray icon activation."""
|
|
209
|
+
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
|
210
|
+
# Single click - toggle window visibility
|
|
211
|
+
self._toggle_main_window()
|
|
212
|
+
elif reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
|
213
|
+
# Double click - show window
|
|
214
|
+
self._show_main_window()
|
|
215
|
+
|
|
216
|
+
def _toggle_main_window(self):
|
|
217
|
+
"""Toggle main window visibility."""
|
|
218
|
+
if self._main_window.isVisible():
|
|
219
|
+
self._main_window.hide()
|
|
220
|
+
else:
|
|
221
|
+
self._show_main_window()
|
|
222
|
+
|
|
223
|
+
def _show_main_window(self):
|
|
224
|
+
"""Show and raise the main window."""
|
|
225
|
+
self._main_window.show() # This also shows in dock
|
|
226
|
+
self._main_window.raise_()
|
|
227
|
+
self._main_window.activateWindow()
|
|
228
|
+
|
|
229
|
+
def _quit_app(self):
|
|
230
|
+
"""Quit the application completely."""
|
|
231
|
+
self._poll_timer.stop()
|
|
232
|
+
from PySide6.QtWidgets import QApplication
|
|
233
|
+
QApplication.instance().quit()
|
|
234
|
+
|
|
235
|
+
def update_status(self, watcher_running: bool, daemon_running: bool, meeting_name: str):
|
|
236
|
+
"""Update status from external source."""
|
|
237
|
+
if daemon_running:
|
|
238
|
+
self._current_status = self.STATUS_RECORDING
|
|
239
|
+
self._recording_name = meeting_name or "Recording..."
|
|
240
|
+
elif watcher_running:
|
|
241
|
+
self._current_status = self.STATUS_READY
|
|
242
|
+
self._recording_name = ""
|
|
243
|
+
else:
|
|
244
|
+
self._current_status = self.STATUS_STOPPED
|
|
245
|
+
self._recording_name = ""
|
|
246
|
+
|
|
247
|
+
self._update_icon()
|
|
248
|
+
self._update_status_text()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Screen modules for Meeting Noter GUI."""
|
|
2
|
+
|
|
3
|
+
from meeting_noter.gui.screens.dashboard import DashboardScreen
|
|
4
|
+
from meeting_noter.gui.screens.logs import LogsScreen
|
|
5
|
+
from meeting_noter.gui.screens.recordings import RecordingsScreen
|
|
6
|
+
from meeting_noter.gui.screens.search import SearchScreen
|
|
7
|
+
from meeting_noter.gui.screens.settings import SettingsScreen
|
|
8
|
+
from meeting_noter.gui.screens.viewer import ViewerScreen
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"DashboardScreen",
|
|
12
|
+
"RecordingsScreen",
|
|
13
|
+
"SearchScreen",
|
|
14
|
+
"ViewerScreen",
|
|
15
|
+
"SettingsScreen",
|
|
16
|
+
"LogsScreen",
|
|
17
|
+
]
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Dashboard screen for Meeting Noter GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtGui import QFont
|
|
13
|
+
from PySide6.QtWidgets import (
|
|
14
|
+
QCheckBox,
|
|
15
|
+
QFrame,
|
|
16
|
+
QHBoxLayout,
|
|
17
|
+
QLabel,
|
|
18
|
+
QLineEdit,
|
|
19
|
+
QPushButton,
|
|
20
|
+
QTextEdit,
|
|
21
|
+
QVBoxLayout,
|
|
22
|
+
QWidget,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from meeting_noter.config import generate_meeting_name, get_config
|
|
26
|
+
from meeting_noter.daemon import is_process_running, read_pid_file
|
|
27
|
+
from meeting_noter.gui.utils.workers import LiveTranscriptWatcher
|
|
28
|
+
from meeting_noter.gui.widgets.status_indicator import StatusIndicator
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DEFAULT_PID_FILE = Path.home() / ".meeting-noter.pid"
|
|
32
|
+
WATCHER_PID_FILE = Path.home() / ".meeting-noter-watcher.pid"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DashboardScreen(QWidget):
|
|
36
|
+
"""Main dashboard screen with recording controls."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, parent=None):
|
|
39
|
+
super().__init__(parent)
|
|
40
|
+
self._setup_ui()
|
|
41
|
+
self._setup_workers()
|
|
42
|
+
self._connect_signals()
|
|
43
|
+
|
|
44
|
+
def _setup_ui(self):
|
|
45
|
+
"""Set up the dashboard UI."""
|
|
46
|
+
mono_font = QFont("Menlo, Monaco, Courier New, monospace")
|
|
47
|
+
|
|
48
|
+
layout = QVBoxLayout(self)
|
|
49
|
+
layout.setContentsMargins(30, 30, 30, 30)
|
|
50
|
+
layout.setSpacing(20)
|
|
51
|
+
|
|
52
|
+
# Title
|
|
53
|
+
title = QLabel("Dashboard")
|
|
54
|
+
title.setProperty("class", "heading")
|
|
55
|
+
title_font = QFont("Menlo, Monaco, Courier New, monospace", 22)
|
|
56
|
+
title_font.setBold(True)
|
|
57
|
+
title.setFont(title_font)
|
|
58
|
+
title.setStyleSheet("color: #ffffff;")
|
|
59
|
+
layout.addWidget(title)
|
|
60
|
+
|
|
61
|
+
# Status indicator
|
|
62
|
+
self._status_indicator = StatusIndicator()
|
|
63
|
+
layout.addWidget(self._status_indicator)
|
|
64
|
+
|
|
65
|
+
# Recording controls panel
|
|
66
|
+
controls_panel = QFrame()
|
|
67
|
+
controls_panel.setProperty("class", "panel")
|
|
68
|
+
controls_panel.setStyleSheet(
|
|
69
|
+
"background-color: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 8px; padding: 20px;"
|
|
70
|
+
)
|
|
71
|
+
controls_layout = QVBoxLayout(controls_panel)
|
|
72
|
+
controls_layout.setSpacing(15)
|
|
73
|
+
|
|
74
|
+
# Meeting name input
|
|
75
|
+
name_row = QHBoxLayout()
|
|
76
|
+
name_label = QLabel("Meeting name:")
|
|
77
|
+
name_label.setFont(mono_font)
|
|
78
|
+
name_label.setFixedWidth(140)
|
|
79
|
+
self._name_input = QLineEdit()
|
|
80
|
+
self._name_input.setPlaceholderText(generate_meeting_name())
|
|
81
|
+
self._name_input.setFont(mono_font)
|
|
82
|
+
name_row.addWidget(name_label)
|
|
83
|
+
name_row.addWidget(self._name_input)
|
|
84
|
+
controls_layout.addLayout(name_row)
|
|
85
|
+
|
|
86
|
+
# Live transcription toggle
|
|
87
|
+
toggle_row = QHBoxLayout()
|
|
88
|
+
toggle_label = QLabel("Live transcription:")
|
|
89
|
+
toggle_label.setFont(mono_font)
|
|
90
|
+
toggle_label.setFixedWidth(140)
|
|
91
|
+
self._live_toggle = QCheckBox("Show live transcript")
|
|
92
|
+
self._live_toggle.setFont(mono_font)
|
|
93
|
+
self._live_toggle.setChecked(True)
|
|
94
|
+
toggle_row.addWidget(toggle_label)
|
|
95
|
+
toggle_row.addWidget(self._live_toggle)
|
|
96
|
+
toggle_row.addStretch()
|
|
97
|
+
controls_layout.addLayout(toggle_row)
|
|
98
|
+
|
|
99
|
+
# Buttons
|
|
100
|
+
button_row = QHBoxLayout()
|
|
101
|
+
button_row.setSpacing(12)
|
|
102
|
+
|
|
103
|
+
self._start_btn = QPushButton("▶ Start Recording")
|
|
104
|
+
self._start_btn.setProperty("class", "success")
|
|
105
|
+
self._start_btn.setFont(mono_font)
|
|
106
|
+
self._start_btn.setStyleSheet(
|
|
107
|
+
"background-color: #4ec9b0; color: #0d0d0d; min-width: 160px; padding: 12px 24px; font-weight: 600;"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self._stop_btn = QPushButton("■ Stop")
|
|
111
|
+
self._stop_btn.setProperty("class", "danger")
|
|
112
|
+
self._stop_btn.setFont(mono_font)
|
|
113
|
+
self._stop_btn.setStyleSheet(
|
|
114
|
+
"background-color: #f14c4c; color: #ffffff; min-width: 100px; padding: 12px 24px; font-weight: 600;"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
button_row.addWidget(self._start_btn)
|
|
118
|
+
button_row.addWidget(self._stop_btn)
|
|
119
|
+
button_row.addStretch()
|
|
120
|
+
controls_layout.addLayout(button_row)
|
|
121
|
+
|
|
122
|
+
layout.addWidget(controls_panel)
|
|
123
|
+
|
|
124
|
+
# Live transcription display
|
|
125
|
+
transcript_label = QLabel("LIVE TRANSCRIPTION")
|
|
126
|
+
transcript_label.setFont(mono_font)
|
|
127
|
+
transcript_label.setStyleSheet("font-size: 11px; font-weight: 600; color: #5a5a5a; letter-spacing: 2px;")
|
|
128
|
+
layout.addWidget(transcript_label)
|
|
129
|
+
|
|
130
|
+
self._transcript_display = QTextEdit()
|
|
131
|
+
self._transcript_display.setReadOnly(True)
|
|
132
|
+
self._transcript_display.setPlaceholderText("Waiting for transcription...")
|
|
133
|
+
self._transcript_display.setMinimumHeight(200)
|
|
134
|
+
self._transcript_display.setFont(mono_font)
|
|
135
|
+
self._transcript_display.setStyleSheet(
|
|
136
|
+
"background-color: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 6px; "
|
|
137
|
+
"color: #4ec9b0; padding: 12px; font-size: 13px;"
|
|
138
|
+
)
|
|
139
|
+
layout.addWidget(self._transcript_display, 1) # Stretch factor
|
|
140
|
+
|
|
141
|
+
def _setup_workers(self):
|
|
142
|
+
"""Set up background workers."""
|
|
143
|
+
self._transcript_watcher = LiveTranscriptWatcher()
|
|
144
|
+
self._transcript_watcher.new_content.connect(self._on_new_transcript)
|
|
145
|
+
self._transcript_watcher.file_changed.connect(self._on_transcript_file_changed)
|
|
146
|
+
self._transcript_watcher.recording_ended.connect(self._on_recording_ended)
|
|
147
|
+
self._transcript_watcher.start()
|
|
148
|
+
|
|
149
|
+
def _connect_signals(self):
|
|
150
|
+
"""Connect button signals."""
|
|
151
|
+
self._start_btn.clicked.connect(self._start_recording)
|
|
152
|
+
self._stop_btn.clicked.connect(self._stop_recording)
|
|
153
|
+
self._live_toggle.toggled.connect(self._on_live_toggle)
|
|
154
|
+
|
|
155
|
+
def _start_recording(self):
|
|
156
|
+
"""Start a new recording."""
|
|
157
|
+
# Check if already recording
|
|
158
|
+
daemon_pid = read_pid_file(DEFAULT_PID_FILE)
|
|
159
|
+
if daemon_pid and is_process_running(daemon_pid):
|
|
160
|
+
self._show_message("Already recording. Stop first.")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# Get meeting name
|
|
164
|
+
meeting_name = self._name_input.text().strip() or generate_meeting_name()
|
|
165
|
+
|
|
166
|
+
# Start recording daemon
|
|
167
|
+
subprocess.Popen(
|
|
168
|
+
[sys.executable, "-m", "meeting_noter.cli", "daemon", "--name", meeting_name],
|
|
169
|
+
stdout=subprocess.DEVNULL,
|
|
170
|
+
stderr=subprocess.DEVNULL,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Clear input and transcript
|
|
174
|
+
self._name_input.clear()
|
|
175
|
+
self._name_input.setPlaceholderText(generate_meeting_name())
|
|
176
|
+
self._transcript_display.clear()
|
|
177
|
+
self._transcript_display.append(f"Starting recording: {meeting_name}\n")
|
|
178
|
+
|
|
179
|
+
def _stop_recording(self):
|
|
180
|
+
"""Stop recording."""
|
|
181
|
+
daemon_pid = read_pid_file(DEFAULT_PID_FILE)
|
|
182
|
+
watcher_pid = None
|
|
183
|
+
|
|
184
|
+
if WATCHER_PID_FILE.exists():
|
|
185
|
+
try:
|
|
186
|
+
watcher_pid = int(WATCHER_PID_FILE.read_text().strip())
|
|
187
|
+
except (ValueError, FileNotFoundError):
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
stopped = []
|
|
191
|
+
|
|
192
|
+
# Stop daemon gracefully
|
|
193
|
+
if daemon_pid and is_process_running(daemon_pid):
|
|
194
|
+
try:
|
|
195
|
+
os.kill(daemon_pid, signal.SIGTERM)
|
|
196
|
+
stopped.append("recording")
|
|
197
|
+
except ProcessLookupError:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
# Stop watcher
|
|
201
|
+
if watcher_pid:
|
|
202
|
+
try:
|
|
203
|
+
os.kill(watcher_pid, signal.SIGTERM)
|
|
204
|
+
stopped.append("watcher")
|
|
205
|
+
except ProcessLookupError:
|
|
206
|
+
pass
|
|
207
|
+
WATCHER_PID_FILE.unlink(missing_ok=True)
|
|
208
|
+
|
|
209
|
+
if not stopped:
|
|
210
|
+
self._show_message("Nothing running")
|
|
211
|
+
else:
|
|
212
|
+
self._show_message(f"Stopped: {', '.join(stopped)}")
|
|
213
|
+
|
|
214
|
+
def _on_live_toggle(self, checked: bool):
|
|
215
|
+
"""Handle live transcription toggle."""
|
|
216
|
+
if checked:
|
|
217
|
+
self._transcript_watcher.start()
|
|
218
|
+
else:
|
|
219
|
+
self._transcript_watcher.stop()
|
|
220
|
+
|
|
221
|
+
def _on_new_transcript(self, text: str):
|
|
222
|
+
"""Handle new transcript content."""
|
|
223
|
+
if self._live_toggle.isChecked():
|
|
224
|
+
# Format timestamps in cyan
|
|
225
|
+
lines = text.splitlines()
|
|
226
|
+
for line in lines:
|
|
227
|
+
if line.strip() and line.startswith("["):
|
|
228
|
+
self._transcript_display.append(line)
|
|
229
|
+
# Auto-scroll to bottom
|
|
230
|
+
scrollbar = self._transcript_display.verticalScrollBar()
|
|
231
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
232
|
+
|
|
233
|
+
def _on_transcript_file_changed(self, meeting_name: str):
|
|
234
|
+
"""Handle new transcript file."""
|
|
235
|
+
self._transcript_display.clear()
|
|
236
|
+
self._transcript_display.append(f"Transcribing: {meeting_name}\n")
|
|
237
|
+
|
|
238
|
+
def _on_recording_ended(self):
|
|
239
|
+
"""Handle recording end."""
|
|
240
|
+
self._transcript_display.append("\n--- Recording ended ---")
|
|
241
|
+
|
|
242
|
+
def _show_message(self, message: str):
|
|
243
|
+
"""Show a message in the transcript area."""
|
|
244
|
+
self._transcript_display.append(f"\n{message}")
|
|
245
|
+
|
|
246
|
+
def update_status(self, watcher_running: bool, daemon_running: bool, meeting_name: str):
|
|
247
|
+
"""Update status indicator."""
|
|
248
|
+
self._status_indicator.update_status(watcher_running, daemon_running, meeting_name)
|
|
249
|
+
|
|
250
|
+
# Update button states
|
|
251
|
+
self._start_btn.setEnabled(not daemon_running)
|
|
252
|
+
self._stop_btn.setEnabled(daemon_running or watcher_running)
|
|
253
|
+
|
|
254
|
+
def refresh(self):
|
|
255
|
+
"""Refresh the dashboard."""
|
|
256
|
+
# Update placeholder with new timestamp
|
|
257
|
+
self._name_input.setPlaceholderText(generate_meeting_name())
|
|
258
|
+
|
|
259
|
+
def stop_workers(self):
|
|
260
|
+
"""Stop background workers."""
|
|
261
|
+
if hasattr(self, "_transcript_watcher"):
|
|
262
|
+
self._transcript_watcher.stop()
|