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.
- meeting_noter/__init__.py +1 -1
- meeting_noter/cli.py +131 -107
- meeting_noter/config.py +34 -0
- meeting_noter/output/favorites.py +189 -0
- meeting_noter/output/searcher.py +218 -0
- meeting_noter/transcription/live_transcription.py +17 -13
- {meeting_noter-1.1.0.dist-info → meeting_noter-1.3.0.dist-info}/METADATA +1 -3
- {meeting_noter-1.1.0.dist-info → meeting_noter-1.3.0.dist-info}/RECORD +11 -17
- meeting_noter/gui/__init__.py +0 -5
- meeting_noter/gui/__main__.py +0 -6
- meeting_noter/gui/app.py +0 -53
- meeting_noter/gui/main_window.py +0 -50
- meeting_noter/gui/meetings_tab.py +0 -348
- meeting_noter/gui/recording_tab.py +0 -358
- meeting_noter/gui/settings_tab.py +0 -249
- meeting_noter/menubar.py +0 -411
- {meeting_noter-1.1.0.dist-info → meeting_noter-1.3.0.dist-info}/WHEEL +0 -0
- {meeting_noter-1.1.0.dist-info → meeting_noter-1.3.0.dist-info}/entry_points.txt +0 -0
- {meeting_noter-1.1.0.dist-info → meeting_noter-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Search functionality for meeting transcripts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import click
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SearchMatch:
|
|
14
|
+
"""A single match within a transcript file."""
|
|
15
|
+
|
|
16
|
+
line_number: int
|
|
17
|
+
line: str
|
|
18
|
+
timestamp: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class FileSearchResult:
|
|
23
|
+
"""Search results for a single transcript file."""
|
|
24
|
+
|
|
25
|
+
filepath: Path
|
|
26
|
+
matches: list[SearchMatch]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def match_count(self) -> int:
|
|
30
|
+
return len(self.matches)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_timestamp(line: str) -> Optional[str]:
|
|
34
|
+
"""Extract timestamp from line if present (e.g., [05:32] or [01:23:45])."""
|
|
35
|
+
match = re.match(r"^\[(\d{1,2}:\d{2}(?::\d{2})?)\]", line.strip())
|
|
36
|
+
if match:
|
|
37
|
+
return match.group(1)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _truncate_line(line: str, max_length: int = 80) -> str:
|
|
42
|
+
"""Truncate line to max length with ellipsis."""
|
|
43
|
+
line = line.strip()
|
|
44
|
+
if len(line) <= max_length:
|
|
45
|
+
return line
|
|
46
|
+
return line[: max_length - 3] + "..."
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _highlight_match(line: str, query: str, case_sensitive: bool) -> str:
|
|
50
|
+
"""Highlight matching text in the line."""
|
|
51
|
+
if case_sensitive:
|
|
52
|
+
pattern = re.escape(query)
|
|
53
|
+
else:
|
|
54
|
+
pattern = re.compile(re.escape(query), re.IGNORECASE)
|
|
55
|
+
|
|
56
|
+
def replace_with_highlight(match):
|
|
57
|
+
return click.style(match.group(0), bold=True, fg="yellow")
|
|
58
|
+
|
|
59
|
+
if case_sensitive:
|
|
60
|
+
return re.sub(pattern, replace_with_highlight, line)
|
|
61
|
+
else:
|
|
62
|
+
return pattern.sub(replace_with_highlight, line)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _search_file(
|
|
66
|
+
filepath: Path,
|
|
67
|
+
query: str,
|
|
68
|
+
case_sensitive: bool,
|
|
69
|
+
context_lines: int = 1,
|
|
70
|
+
) -> Optional[FileSearchResult]:
|
|
71
|
+
"""Search a single file for the query.
|
|
72
|
+
|
|
73
|
+
Returns FileSearchResult if matches found, None otherwise.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
content = filepath.read_text(encoding="utf-8", errors="ignore")
|
|
77
|
+
except Exception:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
lines = content.splitlines()
|
|
81
|
+
matches: list[SearchMatch] = []
|
|
82
|
+
|
|
83
|
+
search_query = query if case_sensitive else query.lower()
|
|
84
|
+
|
|
85
|
+
for i, line in enumerate(lines):
|
|
86
|
+
search_line = line if case_sensitive else line.lower()
|
|
87
|
+
|
|
88
|
+
if search_query in search_line:
|
|
89
|
+
timestamp = _extract_timestamp(line)
|
|
90
|
+
matches.append(
|
|
91
|
+
SearchMatch(
|
|
92
|
+
line_number=i + 1,
|
|
93
|
+
line=line,
|
|
94
|
+
timestamp=timestamp,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if matches:
|
|
99
|
+
return FileSearchResult(filepath=filepath, matches=matches)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def search_transcripts(
|
|
104
|
+
transcripts_dir: Path,
|
|
105
|
+
query: str,
|
|
106
|
+
case_sensitive: bool = False,
|
|
107
|
+
limit: int = 20,
|
|
108
|
+
context_lines: int = 1,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Search across all meeting transcripts.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
transcripts_dir: Directory containing transcript files
|
|
114
|
+
query: Search query string
|
|
115
|
+
case_sensitive: Whether to perform case-sensitive search
|
|
116
|
+
limit: Maximum number of matches to display
|
|
117
|
+
context_lines: Number of context lines around matches (not yet implemented)
|
|
118
|
+
"""
|
|
119
|
+
if not query.strip():
|
|
120
|
+
click.echo(click.style("Error: Search query cannot be empty.", fg="red"))
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if not transcripts_dir.exists():
|
|
124
|
+
click.echo(click.style(f"Directory not found: {transcripts_dir}", fg="red"))
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Find all transcript files
|
|
128
|
+
txt_files = sorted(
|
|
129
|
+
transcripts_dir.glob("*.txt"),
|
|
130
|
+
key=lambda p: p.stat().st_mtime,
|
|
131
|
+
reverse=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not txt_files:
|
|
135
|
+
click.echo(click.style("No transcripts found.", fg="yellow"))
|
|
136
|
+
click.echo(f"\nTranscripts directory: {transcripts_dir}")
|
|
137
|
+
click.echo("Record and transcribe meetings to search them.")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Search all files
|
|
141
|
+
results: list[FileSearchResult] = []
|
|
142
|
+
for txt_file in txt_files:
|
|
143
|
+
result = _search_file(txt_file, query, case_sensitive, context_lines)
|
|
144
|
+
if result:
|
|
145
|
+
results.append(result)
|
|
146
|
+
|
|
147
|
+
if not results:
|
|
148
|
+
click.echo(click.style(f'No results found for "{query}"', fg="yellow"))
|
|
149
|
+
click.echo(f"\nSearched {len(txt_files)} transcripts in {transcripts_dir}")
|
|
150
|
+
if not case_sensitive:
|
|
151
|
+
click.echo("Tip: Use --case-sensitive for exact matching.")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# Sort by match count (most matches first)
|
|
155
|
+
results.sort(key=lambda r: r.match_count, reverse=True)
|
|
156
|
+
|
|
157
|
+
# Count total matches
|
|
158
|
+
total_matches = sum(r.match_count for r in results)
|
|
159
|
+
total_files = len(results)
|
|
160
|
+
|
|
161
|
+
# Display header
|
|
162
|
+
click.echo()
|
|
163
|
+
matches_word = "match" if total_matches == 1 else "matches"
|
|
164
|
+
files_word = "transcript" if total_files == 1 else "transcripts"
|
|
165
|
+
click.echo(
|
|
166
|
+
click.style(
|
|
167
|
+
f"Found {total_matches} {matches_word} in {total_files} {files_word}:",
|
|
168
|
+
bold=True,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
click.echo()
|
|
172
|
+
|
|
173
|
+
# Display results
|
|
174
|
+
matches_shown = 0
|
|
175
|
+
limit_reached = False
|
|
176
|
+
for result in results:
|
|
177
|
+
if matches_shown >= limit:
|
|
178
|
+
limit_reached = True
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
# File header
|
|
182
|
+
match_word = "match" if result.match_count == 1 else "matches"
|
|
183
|
+
click.echo(
|
|
184
|
+
click.style(f"{result.filepath.name}", fg="green", bold=True)
|
|
185
|
+
+ f" ({result.match_count} {match_word})"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Show matches (limited)
|
|
189
|
+
for match in result.matches:
|
|
190
|
+
if matches_shown >= limit:
|
|
191
|
+
limit_reached = True
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
# Format the line
|
|
195
|
+
prefix = f" [{match.timestamp}] " if match.timestamp else " "
|
|
196
|
+
line_text = match.line
|
|
197
|
+
if match.timestamp:
|
|
198
|
+
# Remove timestamp from line since we're showing it in prefix
|
|
199
|
+
line_text = re.sub(r"^\[\d{1,2}:\d{2}(?::\d{2})?\]\s*", "", line_text)
|
|
200
|
+
|
|
201
|
+
truncated = _truncate_line(line_text, 70)
|
|
202
|
+
highlighted = _highlight_match(truncated, query, case_sensitive)
|
|
203
|
+
|
|
204
|
+
click.echo(f"{prefix}...{highlighted}...")
|
|
205
|
+
matches_shown += 1
|
|
206
|
+
|
|
207
|
+
click.echo()
|
|
208
|
+
|
|
209
|
+
# Show remaining count if limit was reached
|
|
210
|
+
if limit_reached and matches_shown < total_matches:
|
|
211
|
+
remaining = total_matches - matches_shown
|
|
212
|
+
click.echo(
|
|
213
|
+
click.style(f"... and {remaining} more matches", fg="cyan")
|
|
214
|
+
)
|
|
215
|
+
click.echo()
|
|
216
|
+
|
|
217
|
+
# Footer
|
|
218
|
+
click.echo(f"Searched {len(txt_files)} transcripts in {transcripts_dir}")
|
|
@@ -4,7 +4,7 @@ Buffers audio chunks and transcribes them in a background thread,
|
|
|
4
4
|
writing segments to a .live.txt file that can be tailed by the CLI.
|
|
5
5
|
|
|
6
6
|
Uses overlapping windows for lower latency: keeps a 5-second context window
|
|
7
|
-
but transcribes every
|
|
7
|
+
but transcribes every 1 second, only outputting new content.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -24,7 +24,7 @@ class LiveTranscriber:
|
|
|
24
24
|
|
|
25
25
|
Uses overlapping windows approach:
|
|
26
26
|
- Maintains a rolling window of audio (default 5 seconds)
|
|
27
|
-
- Transcribes every `slide_seconds` (default
|
|
27
|
+
- Transcribes every `slide_seconds` (default 1 second)
|
|
28
28
|
- Only outputs new segments to avoid duplicates
|
|
29
29
|
"""
|
|
30
30
|
|
|
@@ -34,7 +34,7 @@ class LiveTranscriber:
|
|
|
34
34
|
sample_rate: int = 48000,
|
|
35
35
|
channels: int = 2,
|
|
36
36
|
window_seconds: float = 5.0,
|
|
37
|
-
slide_seconds: float =
|
|
37
|
+
slide_seconds: float = 1.0,
|
|
38
38
|
model_size: str = "tiny.en",
|
|
39
39
|
):
|
|
40
40
|
"""Initialize the live transcriber.
|
|
@@ -115,15 +115,20 @@ class LiveTranscriber:
|
|
|
115
115
|
except ImportError:
|
|
116
116
|
pass
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
# Try GPU acceleration first, fall back to CPU if not supported
|
|
119
|
+
model_path = str(bundled_path) if (bundled_path and self.model_size == "tiny.en") else self.model_size
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
# Try GPU with float16 first
|
|
119
123
|
self._model = WhisperModel(
|
|
120
|
-
|
|
121
|
-
device="
|
|
122
|
-
compute_type="
|
|
124
|
+
model_path,
|
|
125
|
+
device="cuda",
|
|
126
|
+
compute_type="float16",
|
|
123
127
|
)
|
|
124
|
-
|
|
128
|
+
except Exception:
|
|
129
|
+
# Fall back to CPU with int8 (fastest CPU option)
|
|
125
130
|
self._model = WhisperModel(
|
|
126
|
-
|
|
131
|
+
model_path,
|
|
127
132
|
device="cpu",
|
|
128
133
|
compute_type="int8",
|
|
129
134
|
)
|
|
@@ -150,11 +155,10 @@ class LiveTranscriber:
|
|
|
150
155
|
try:
|
|
151
156
|
# Collect audio chunks
|
|
152
157
|
try:
|
|
153
|
-
chunk = self._audio_queue.get(timeout=0.
|
|
158
|
+
chunk = self._audio_queue.get(timeout=0.1)
|
|
154
159
|
|
|
155
|
-
# Add samples to rolling buffer
|
|
156
|
-
|
|
157
|
-
rolling_buffer.append(sample)
|
|
160
|
+
# Add samples to rolling buffer (batch extend is faster than per-sample append)
|
|
161
|
+
rolling_buffer.extend(chunk)
|
|
158
162
|
|
|
159
163
|
samples_since_last_transcribe += len(chunk)
|
|
160
164
|
self._recording_offset += len(chunk) / self.sample_rate
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meeting-noter
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Offline meeting transcription for macOS with automatic meeting detection
|
|
5
5
|
Author: Victor
|
|
6
6
|
License: MIT
|
|
@@ -26,8 +26,6 @@ Requires-Dist: click>=8.0
|
|
|
26
26
|
Requires-Dist: sounddevice>=0.4.6
|
|
27
27
|
Requires-Dist: numpy>=1.24
|
|
28
28
|
Requires-Dist: faster-whisper>=1.0.0
|
|
29
|
-
Requires-Dist: rumps>=0.4.0
|
|
30
|
-
Requires-Dist: PyQt6>=6.5.0
|
|
31
29
|
Requires-Dist: imageio-ffmpeg>=0.4.9
|
|
32
30
|
Requires-Dist: pyobjc-framework-Cocoa>=9.0; sys_platform == "darwin"
|
|
33
31
|
Requires-Dist: pyobjc-framework-Quartz>=9.0; sys_platform == "darwin"
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
meeting_noter/__init__.py,sha256=
|
|
1
|
+
meeting_noter/__init__.py,sha256=6WLmaMLX2bfnCWqF2vzOnwjKo728hFUrDHXgUjykajQ,103
|
|
2
2
|
meeting_noter/__main__.py,sha256=6sSOqH1o3jvgvkVzsVKmF6-xVGcUAbNVQkRl2CrygdE,120
|
|
3
|
-
meeting_noter/cli.py,sha256=
|
|
4
|
-
meeting_noter/config.py,sha256=
|
|
3
|
+
meeting_noter/cli.py,sha256=XMTDUpXpmfvy8E0KqXAtS3GD382xKJSzHBpZdvPfbwE,34771
|
|
4
|
+
meeting_noter/config.py,sha256=vdnTh6W6-DUPJCj0ekFu1Q1m87O7gx0QD3E5DrRGpXk,7537
|
|
5
5
|
meeting_noter/daemon.py,sha256=u9VrYe94o3lxabuIS9MDVPHSH7MqKqzTqGTuA7TNAIc,19767
|
|
6
6
|
meeting_noter/meeting_detector.py,sha256=St0qoMkvUERP4BaxnXO1M6fZDJpWqBf9In7z2SgWcWg,10564
|
|
7
|
-
meeting_noter/menubar.py,sha256=Gn6p8y5jA_HCWf1T3ademxH-vndpONHkf9vUlKs6XEo,14379
|
|
8
7
|
meeting_noter/mic_monitor.py,sha256=P8vF4qaZcGrEzzJyVos78Vuf38NXHGNRREDsD-HyBHc,16211
|
|
9
8
|
meeting_noter/update_checker.py,sha256=sMmIiiZJL6K7wqLWE64Aj4hS8uspjUOirr6BG_IlL1I,1701
|
|
10
9
|
meeting_noter/audio/__init__.py,sha256=O7PU8CxHSHxMeHbc9Jdwt9kePLQzsPh81GQU7VHCtBY,44
|
|
11
10
|
meeting_noter/audio/capture.py,sha256=fDrT5oXfva8vdFlht9cv60NviKbksw2QeJ8eOtI19uE,6469
|
|
12
11
|
meeting_noter/audio/encoder.py,sha256=OBsgUmlZPz-YZQZ7Rp8MAlMRaQxTsccjuTgCtvRebmc,6573
|
|
13
12
|
meeting_noter/audio/system_audio.py,sha256=jbHGjNCerI19weXap0a90Ik17lVTCT1hCEgRKYke-p8,13016
|
|
14
|
-
meeting_noter/gui/__init__.py,sha256=z5GxxaeXyjqyEa9ox0dQxuL5u_BART0bi7cI6rfntEI,103
|
|
15
|
-
meeting_noter/gui/__main__.py,sha256=A2HWdYod0bTgjQQIi21O7XpmgxLH36e_X0aygEUZLls,146
|
|
16
|
-
meeting_noter/gui/app.py,sha256=COUAWu_dR5HriNYxbE86CVGS1eGYqyteH2oUFN_YtYQ,1370
|
|
17
|
-
meeting_noter/gui/main_window.py,sha256=vSvNO86CHMgJf9Pem8AOdrqKyTV9ITp3W4nCoqIuAmI,1667
|
|
18
|
-
meeting_noter/gui/meetings_tab.py,sha256=pqXqMv5YvCj8H6yR_TF3SMzsIDMAxyLe2otbENbM2SY,12315
|
|
19
|
-
meeting_noter/gui/recording_tab.py,sha256=UlrPkUiOmkGgOKqVrfAVp4EyiY5sq86W49Y-YAhYexY,12521
|
|
20
|
-
meeting_noter/gui/settings_tab.py,sha256=NUQVKDdSpyNp_MVxPLw2dB93wxaD5VeBKiDtGS4CyoU,8446
|
|
21
13
|
meeting_noter/install/__init__.py,sha256=SX5vLFMrV8aBDEGW18jhaqBqJqnRXaeo0Ct7QVGDgvE,38
|
|
22
14
|
meeting_noter/install/macos.py,sha256=dO-86zbNKRtt0l4D8naVn7kFWjzI8TufWLWE3FRLHQ8,3400
|
|
23
15
|
meeting_noter/output/__init__.py,sha256=F7xPlOrqweZcPbZtDrhved1stBI59vnWnLYfGwdu6oY,31
|
|
16
|
+
meeting_noter/output/favorites.py,sha256=kYtEshq5E5xxaqjG36JMY5a-r6C2w98iqmKsGsxpgag,5764
|
|
17
|
+
meeting_noter/output/searcher.py,sha256=ZGbEewodNuqq5mM4XvVtxELv_6UQByjs-LmEwodc-Ug,6448
|
|
24
18
|
meeting_noter/output/writer.py,sha256=zO8y6FAFUAp0EEtALY-M5e2Ja5P-hgV38JjcKW7c-bA,3017
|
|
25
19
|
meeting_noter/resources/__init__.py,sha256=yzHNxgypkuVDFZWv6xZjUygOVB_Equ9NNX_HGRvN7VM,43
|
|
26
20
|
meeting_noter/resources/icon.icns,sha256=zMWqXCq7pI5acS0tbekFgFDvLt66EKUBP5-5IgztwPM,35146
|
|
@@ -33,9 +27,9 @@ meeting_noter/resources/icon_512.png,sha256=o7X3ngYcppcIAAk9AcfPx94MUmrsPRp0qBTp
|
|
|
33
27
|
meeting_noter/resources/icon_64.png,sha256=TqG7Awx3kK8YdiX1e_z1odZonosZyQI2trlkNZCzUoI,607
|
|
34
28
|
meeting_noter/transcription/__init__.py,sha256=7GY9diP06DzFyoli41wddbrPv5bVDzH35bmnWlIJev4,29
|
|
35
29
|
meeting_noter/transcription/engine.py,sha256=G9NcSS6Q-UhW7PlQ0E85hQXn6BWao64nIvyw4NR2yxI,7208
|
|
36
|
-
meeting_noter/transcription/live_transcription.py,sha256=
|
|
37
|
-
meeting_noter-1.
|
|
38
|
-
meeting_noter-1.
|
|
39
|
-
meeting_noter-1.
|
|
40
|
-
meeting_noter-1.
|
|
41
|
-
meeting_noter-1.
|
|
30
|
+
meeting_noter/transcription/live_transcription.py,sha256=YfojFWv4h3Lp-pK5tjIauvimCWAmDhj6pj5gUmyBxr4,9539
|
|
31
|
+
meeting_noter-1.3.0.dist-info/METADATA,sha256=Y8UXDVyKjcA_F4Ck8czm9GlNpsAjUcbFnLJLAAAdEdw,6939
|
|
32
|
+
meeting_noter-1.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
33
|
+
meeting_noter-1.3.0.dist-info/entry_points.txt,sha256=osZoOmm-UBPCJ4b6DGH6JOAm7mofM2fK06eK6blplmg,83
|
|
34
|
+
meeting_noter-1.3.0.dist-info/top_level.txt,sha256=9Tuq04_0SXM0OXOHVbOHkHkB5tG3fqkrMrfzCMpbLpY,14
|
|
35
|
+
meeting_noter-1.3.0.dist-info/RECORD,,
|
meeting_noter/gui/__init__.py
DELETED
meeting_noter/gui/__main__.py
DELETED
meeting_noter/gui/app.py
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"""PyQt6 application entry point."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from PyQt6.QtGui import QIcon
|
|
9
|
-
from PyQt6.QtWidgets import QApplication
|
|
10
|
-
|
|
11
|
-
from meeting_noter.gui.main_window import MainWindow
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _set_macos_dock_icon(icon_path: Path):
|
|
15
|
-
"""Set the macOS dock icon using AppKit."""
|
|
16
|
-
try:
|
|
17
|
-
from AppKit import NSApplication, NSImage
|
|
18
|
-
ns_app = NSApplication.sharedApplication()
|
|
19
|
-
icon = NSImage.alloc().initWithContentsOfFile_(str(icon_path))
|
|
20
|
-
if icon:
|
|
21
|
-
ns_app.setApplicationIconImage_(icon)
|
|
22
|
-
except ImportError:
|
|
23
|
-
pass # pyobjc not installed
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def run_gui():
|
|
27
|
-
"""Launch the Meeting Noter GUI application."""
|
|
28
|
-
resources = Path(__file__).parent.parent / "resources"
|
|
29
|
-
|
|
30
|
-
app = QApplication(sys.argv)
|
|
31
|
-
app.setApplicationName("Meeting Noter")
|
|
32
|
-
app.setOrganizationName("Meeting Noter")
|
|
33
|
-
|
|
34
|
-
# Set window icon
|
|
35
|
-
icon_path = resources / "icon.png"
|
|
36
|
-
if icon_path.exists():
|
|
37
|
-
app.setWindowIcon(QIcon(str(icon_path)))
|
|
38
|
-
|
|
39
|
-
window = MainWindow()
|
|
40
|
-
window.show()
|
|
41
|
-
|
|
42
|
-
# Set macOS dock icon AFTER window is shown
|
|
43
|
-
if sys.platform == "darwin":
|
|
44
|
-
icns_path = resources / "icon.icns"
|
|
45
|
-
if icns_path.exists():
|
|
46
|
-
_set_macos_dock_icon(icns_path)
|
|
47
|
-
app.processEvents()
|
|
48
|
-
|
|
49
|
-
sys.exit(app.exec())
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if __name__ == "__main__":
|
|
53
|
-
run_gui()
|
meeting_noter/gui/main_window.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Main window with tab interface."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget, QVBoxLayout
|
|
6
|
-
|
|
7
|
-
from meeting_noter.gui.recording_tab import RecordingTab
|
|
8
|
-
from meeting_noter.gui.meetings_tab import MeetingsTab
|
|
9
|
-
from meeting_noter.gui.settings_tab import SettingsTab
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class MainWindow(QMainWindow):
|
|
13
|
-
"""Main application window with tabbed interface."""
|
|
14
|
-
|
|
15
|
-
def __init__(self):
|
|
16
|
-
super().__init__()
|
|
17
|
-
self.setWindowTitle("Meeting Noter")
|
|
18
|
-
self.setMinimumSize(800, 600)
|
|
19
|
-
|
|
20
|
-
# Create central widget with tabs
|
|
21
|
-
central_widget = QWidget()
|
|
22
|
-
self.setCentralWidget(central_widget)
|
|
23
|
-
|
|
24
|
-
layout = QVBoxLayout(central_widget)
|
|
25
|
-
layout.setContentsMargins(0, 0, 0, 0)
|
|
26
|
-
|
|
27
|
-
# Create tab widget
|
|
28
|
-
self.tabs = QTabWidget()
|
|
29
|
-
layout.addWidget(self.tabs)
|
|
30
|
-
|
|
31
|
-
# Create tabs
|
|
32
|
-
self.recording_tab = RecordingTab()
|
|
33
|
-
self.meetings_tab = MeetingsTab()
|
|
34
|
-
self.settings_tab = SettingsTab()
|
|
35
|
-
|
|
36
|
-
self.tabs.addTab(self.recording_tab, "Record")
|
|
37
|
-
self.tabs.addTab(self.meetings_tab, "Meetings")
|
|
38
|
-
self.tabs.addTab(self.settings_tab, "Settings")
|
|
39
|
-
|
|
40
|
-
# Connect settings changes to refresh meetings list
|
|
41
|
-
self.settings_tab.settings_saved.connect(self.meetings_tab.refresh)
|
|
42
|
-
|
|
43
|
-
# Connect recording completion to refresh meetings list
|
|
44
|
-
self.recording_tab.recording_saved.connect(self.meetings_tab.refresh)
|
|
45
|
-
|
|
46
|
-
def closeEvent(self, event):
|
|
47
|
-
"""Handle window close - stop any active recording."""
|
|
48
|
-
if self.recording_tab.is_recording:
|
|
49
|
-
self.recording_tab.stop_recording()
|
|
50
|
-
event.accept()
|