meeting-noter 1.0.0__tar.gz → 1.2.0__tar.gz
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-1.0.0 → meeting_noter-1.2.0}/PKG-INFO +1 -3
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/pyproject.toml +1 -5
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/__init__.py +1 -1
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/cli.py +42 -95
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/config.py +11 -0
- meeting_noter-1.2.0/src/meeting_noter/update_checker.py +65 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter.egg-info/PKG-INFO +1 -3
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter.egg-info/SOURCES.txt +1 -8
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter.egg-info/requires.txt +0 -2
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/tests/test_cli.py +1 -52
- meeting_noter-1.0.0/src/meeting_noter/gui/__init__.py +0 -5
- meeting_noter-1.0.0/src/meeting_noter/gui/__main__.py +0 -6
- meeting_noter-1.0.0/src/meeting_noter/gui/app.py +0 -53
- meeting_noter-1.0.0/src/meeting_noter/gui/main_window.py +0 -50
- meeting_noter-1.0.0/src/meeting_noter/gui/meetings_tab.py +0 -348
- meeting_noter-1.0.0/src/meeting_noter/gui/recording_tab.py +0 -358
- meeting_noter-1.0.0/src/meeting_noter/gui/settings_tab.py +0 -249
- meeting_noter-1.0.0/src/meeting_noter/menubar.py +0 -411
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/README.md +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/setup.cfg +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/__main__.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/audio/__init__.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/audio/capture.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/audio/encoder.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/audio/system_audio.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/daemon.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/install/__init__.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/install/macos.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/meeting_detector.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/mic_monitor.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/output/__init__.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/output/writer.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/resources/__init__.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/resources/icon.icns +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/resources/icon.png +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/resources/icon_128.png +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/resources/icon_16.png +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/resources/icon_256.png +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/resources/icon_32.png +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/resources/icon_512.png +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/resources/icon_64.png +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/transcription/__init__.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/transcription/engine.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter/transcription/live_transcription.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter.egg-info/dependency_links.txt +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter.egg-info/entry_points.txt +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/src/meeting_noter.egg-info/top_level.txt +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/tests/test_config.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/tests/test_daemon.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/tests/test_meeting_detector.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/tests/test_mic_monitor.py +0 -0
- {meeting_noter-1.0.0 → meeting_noter-1.2.0}/tests/test_output_writer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meeting-noter
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meeting-noter"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "Offline meeting transcription for macOS with automatic meeting detection"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -33,8 +33,6 @@ dependencies = [
|
|
|
33
33
|
"sounddevice>=0.4.6",
|
|
34
34
|
"numpy>=1.24",
|
|
35
35
|
"faster-whisper>=1.0.0",
|
|
36
|
-
"rumps>=0.4.0",
|
|
37
|
-
"PyQt6>=6.5.0",
|
|
38
36
|
"imageio-ffmpeg>=0.4.9", # Bundles ffmpeg binary for MP3 encoding
|
|
39
37
|
"pyobjc-framework-Cocoa>=9.0; sys_platform == 'darwin'",
|
|
40
38
|
"pyobjc-framework-Quartz>=9.0; sys_platform == 'darwin'",
|
|
@@ -98,8 +96,6 @@ filterwarnings = [
|
|
|
98
96
|
source = ["src/meeting_noter"]
|
|
99
97
|
branch = true
|
|
100
98
|
omit = [
|
|
101
|
-
"*/gui/*",
|
|
102
|
-
"*/menubar.py",
|
|
103
99
|
"*/__main__.py",
|
|
104
100
|
"*/audio/system_audio.py", # ScreenCaptureKit-based, requires macOS runtime
|
|
105
101
|
]
|
|
@@ -90,24 +90,46 @@ class SuggestCommand(click.Command):
|
|
|
90
90
|
SuggestGroup.command_class = SuggestCommand
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
def
|
|
94
|
-
"""
|
|
93
|
+
def _handle_version():
|
|
94
|
+
"""Handle --version: show version, check for updates, auto-update if enabled."""
|
|
95
95
|
import subprocess
|
|
96
|
-
import sys
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
from meeting_noter.update_checker import check_for_update
|
|
98
|
+
|
|
99
|
+
config = get_config()
|
|
100
|
+
|
|
101
|
+
click.echo(f"meeting-noter {__version__}")
|
|
102
|
+
|
|
103
|
+
# Check for updates
|
|
104
|
+
click.echo("Checking for updates...", nl=False)
|
|
105
|
+
new_version = check_for_update()
|
|
106
|
+
|
|
107
|
+
if new_version:
|
|
108
|
+
click.echo(f" update available: {new_version}")
|
|
109
|
+
|
|
110
|
+
if config.auto_update:
|
|
111
|
+
click.echo(f"Auto-updating to {new_version}...")
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
["pipx", "upgrade", "meeting-noter"],
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
)
|
|
117
|
+
if result.returncode == 0:
|
|
118
|
+
click.echo(click.style(f"Updated to {new_version}", fg="green"))
|
|
119
|
+
else:
|
|
120
|
+
click.echo(click.style("Update failed. Run manually:", fg="yellow"))
|
|
121
|
+
click.echo(" pipx upgrade meeting-noter")
|
|
122
|
+
else:
|
|
123
|
+
click.echo("Run to update: pipx upgrade meeting-noter")
|
|
124
|
+
click.echo("Or enable auto-update: mn config auto-update true")
|
|
125
|
+
else:
|
|
126
|
+
click.echo(" up to date")
|
|
105
127
|
|
|
106
128
|
|
|
107
129
|
@click.group(cls=SuggestGroup, invoke_without_command=True)
|
|
108
|
-
@click.
|
|
130
|
+
@click.option("--version", "-V", is_flag=True, help="Show version and check for updates")
|
|
109
131
|
@click.pass_context
|
|
110
|
-
def cli(ctx):
|
|
132
|
+
def cli(ctx, version):
|
|
111
133
|
"""Meeting Noter - Offline meeting transcription.
|
|
112
134
|
|
|
113
135
|
\b
|
|
@@ -124,6 +146,10 @@ def cli(ctx):
|
|
|
124
146
|
meeting-noter config whisper-model base.en Set transcription model
|
|
125
147
|
meeting-noter config auto-transcribe false Disable auto-transcribe
|
|
126
148
|
"""
|
|
149
|
+
if version:
|
|
150
|
+
_handle_version()
|
|
151
|
+
ctx.exit(0)
|
|
152
|
+
|
|
127
153
|
if ctx.invoked_subcommand is None:
|
|
128
154
|
# No subcommand - start background watcher
|
|
129
155
|
ctx.invoke(watcher)
|
|
@@ -250,17 +276,6 @@ def status():
|
|
|
250
276
|
except (ProcessLookupError, ValueError, FileNotFoundError):
|
|
251
277
|
pass
|
|
252
278
|
|
|
253
|
-
# Check menubar
|
|
254
|
-
menubar_running = False
|
|
255
|
-
menubar_pid_file = Path.home() / ".meeting-noter-menubar.pid"
|
|
256
|
-
if menubar_pid_file.exists():
|
|
257
|
-
try:
|
|
258
|
-
pid = int(menubar_pid_file.read_text().strip())
|
|
259
|
-
os.kill(pid, 0)
|
|
260
|
-
menubar_running = True
|
|
261
|
-
except (ProcessLookupError, ValueError, FileNotFoundError):
|
|
262
|
-
pass
|
|
263
|
-
|
|
264
279
|
# Check daemon (recording)
|
|
265
280
|
daemon_running = False
|
|
266
281
|
daemon_pid = read_pid_file(DEFAULT_PID_FILE)
|
|
@@ -273,9 +288,8 @@ def status():
|
|
|
273
288
|
# Get current recording name from log
|
|
274
289
|
recording_name = _get_current_recording_name()
|
|
275
290
|
click.echo(f"🔴 Recording: {recording_name or 'In progress'}")
|
|
276
|
-
elif watcher_running
|
|
277
|
-
|
|
278
|
-
click.echo(f"👀 Ready to record ({mode} active)")
|
|
291
|
+
elif watcher_running:
|
|
292
|
+
click.echo("👀 Ready to record (watcher active)")
|
|
279
293
|
else:
|
|
280
294
|
click.echo("⏹️ Stopped (run 'meeting-noter' to start)")
|
|
281
295
|
|
|
@@ -284,7 +298,6 @@ def status():
|
|
|
284
298
|
# Show details
|
|
285
299
|
click.echo("Components:")
|
|
286
300
|
click.echo(f" Watcher: {'running' if watcher_running else 'stopped'}")
|
|
287
|
-
click.echo(f" Menubar: {'running' if menubar_running else 'stopped'}")
|
|
288
301
|
click.echo(f" Recorder: {'recording' if daemon_running else 'idle'}")
|
|
289
302
|
click.echo()
|
|
290
303
|
|
|
@@ -318,7 +331,7 @@ def _get_current_recording_name() -> str | None:
|
|
|
318
331
|
|
|
319
332
|
@cli.command()
|
|
320
333
|
def shutdown():
|
|
321
|
-
"""Stop all Meeting Noter processes (daemon, watcher
|
|
334
|
+
"""Stop all Meeting Noter processes (daemon, watcher)."""
|
|
322
335
|
import subprocess
|
|
323
336
|
import os
|
|
324
337
|
import signal
|
|
@@ -341,17 +354,6 @@ def shutdown():
|
|
|
341
354
|
except (ProcessLookupError, ValueError):
|
|
342
355
|
WATCHER_PID_FILE.unlink(missing_ok=True)
|
|
343
356
|
|
|
344
|
-
# Stop menubar
|
|
345
|
-
menubar_pid_file = Path.home() / ".meeting-noter-menubar.pid"
|
|
346
|
-
if menubar_pid_file.exists():
|
|
347
|
-
try:
|
|
348
|
-
pid = int(menubar_pid_file.read_text().strip())
|
|
349
|
-
os.kill(pid, signal.SIGTERM)
|
|
350
|
-
menubar_pid_file.unlink()
|
|
351
|
-
stopped.append("menubar")
|
|
352
|
-
except (ProcessLookupError, ValueError):
|
|
353
|
-
menubar_pid_file.unlink(missing_ok=True)
|
|
354
|
-
|
|
355
357
|
# Kill any remaining meeting-noter processes
|
|
356
358
|
result = subprocess.run(
|
|
357
359
|
["pkill", "-f", "meeting_noter"],
|
|
@@ -544,68 +546,13 @@ def live():
|
|
|
544
546
|
click.echo("\n" + click.style("Stopped watching.", fg="cyan"))
|
|
545
547
|
|
|
546
548
|
|
|
547
|
-
@cli.command()
|
|
548
|
-
@click.option(
|
|
549
|
-
"--foreground", "-f",
|
|
550
|
-
is_flag=True,
|
|
551
|
-
help="Run in foreground instead of background",
|
|
552
|
-
)
|
|
553
|
-
@require_setup
|
|
554
|
-
def menubar(foreground: bool):
|
|
555
|
-
"""Launch menu bar app for daemon control.
|
|
556
|
-
|
|
557
|
-
Adds a menu bar icon for one-click start/stop of the recording daemon.
|
|
558
|
-
The icon shows "MN" when idle and "MN [filename]" when recording.
|
|
559
|
-
|
|
560
|
-
By default, runs in background. Use -f for foreground (debugging).
|
|
561
|
-
"""
|
|
562
|
-
import subprocess
|
|
563
|
-
import sys
|
|
564
|
-
|
|
565
|
-
if foreground:
|
|
566
|
-
from meeting_noter.menubar import run_menubar
|
|
567
|
-
run_menubar()
|
|
568
|
-
else:
|
|
569
|
-
# Spawn as background process
|
|
570
|
-
subprocess.Popen(
|
|
571
|
-
[sys.executable, "-m", "meeting_noter.menubar"],
|
|
572
|
-
stdout=subprocess.DEVNULL,
|
|
573
|
-
stderr=subprocess.DEVNULL,
|
|
574
|
-
start_new_session=True,
|
|
575
|
-
)
|
|
576
|
-
click.echo("Menu bar app started in background.")
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
@cli.command()
|
|
580
|
-
@click.option(
|
|
581
|
-
"--foreground", "-f",
|
|
582
|
-
is_flag=True,
|
|
583
|
-
help="Run in foreground instead of background",
|
|
584
|
-
)
|
|
585
|
-
@require_setup
|
|
586
|
-
def gui(foreground: bool):
|
|
587
|
-
"""Launch the desktop GUI application.
|
|
588
|
-
|
|
589
|
-
Opens a window with tabs for:
|
|
590
|
-
- Recording: Start/stop recordings with meeting names
|
|
591
|
-
- Meetings: Browse, play, and manage recordings
|
|
592
|
-
- Settings: Configure directories, models, and preferences
|
|
593
|
-
|
|
594
|
-
By default runs in background. Use -f for foreground.
|
|
595
|
-
"""
|
|
596
|
-
if foreground:
|
|
597
|
-
from meeting_noter.gui import run_gui
|
|
598
|
-
run_gui()
|
|
599
|
-
else:
|
|
600
|
-
_launch_gui_background()
|
|
601
|
-
|
|
602
|
-
|
|
603
549
|
# Config key mappings (CLI name -> config attribute)
|
|
604
550
|
CONFIG_KEYS = {
|
|
605
551
|
"recordings-dir": ("recordings_dir", "path", "Directory for audio recordings"),
|
|
606
552
|
"transcripts-dir": ("transcripts_dir", "path", "Directory for transcripts"),
|
|
607
553
|
"whisper-model": ("whisper_model", "choice:tiny.en,base.en,small.en,medium.en,large-v3", "Whisper model for transcription"),
|
|
608
554
|
"auto-transcribe": ("auto_transcribe", "bool", "Auto-transcribe after recording"),
|
|
555
|
+
"auto-update": ("auto_update", "bool", "Auto-update when running --version"),
|
|
609
556
|
"silence-timeout": ("silence_timeout", "int", "Minutes of silence before auto-stop"),
|
|
610
557
|
"capture-system-audio": ("capture_system_audio", "bool", "Capture meeting participants via ScreenCaptureKit"),
|
|
611
558
|
}
|
|
@@ -32,6 +32,7 @@ DEFAULT_CONFIG = {
|
|
|
32
32
|
"transcripts_dir": str(Path.home() / "meetings"),
|
|
33
33
|
"whisper_model": "tiny.en",
|
|
34
34
|
"auto_transcribe": True,
|
|
35
|
+
"auto_update": True, # Auto-update when running --version
|
|
35
36
|
"silence_timeout": 5, # Minutes of silence before stopping recording
|
|
36
37
|
"capture_system_audio": True, # Capture other participants via ScreenCaptureKit
|
|
37
38
|
"show_menubar": False,
|
|
@@ -139,6 +140,16 @@ class Config:
|
|
|
139
140
|
"""Set show menubar setting."""
|
|
140
141
|
self._data["show_menubar"] = value
|
|
141
142
|
|
|
143
|
+
@property
|
|
144
|
+
def auto_update(self) -> bool:
|
|
145
|
+
"""Get auto-update setting."""
|
|
146
|
+
return self._data.get("auto_update", True)
|
|
147
|
+
|
|
148
|
+
@auto_update.setter
|
|
149
|
+
def auto_update(self, value: bool) -> None:
|
|
150
|
+
"""Set auto-update setting."""
|
|
151
|
+
self._data["auto_update"] = value
|
|
152
|
+
|
|
142
153
|
@property
|
|
143
154
|
def setup_complete(self) -> bool:
|
|
144
155
|
"""Check if setup has been completed."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Check for updates from PyPI."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import urllib.request
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from meeting_noter import __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PYPI_URL = "https://pypi.org/pypi/meeting-noter/json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_version(version: str) -> Tuple[int, ...]:
|
|
15
|
+
"""Parse version string into tuple for comparison."""
|
|
16
|
+
try:
|
|
17
|
+
return tuple(int(x) for x in version.split("."))
|
|
18
|
+
except ValueError:
|
|
19
|
+
return (0, 0, 0)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_latest_version() -> Optional[str]:
|
|
23
|
+
"""Fetch the latest version from PyPI."""
|
|
24
|
+
try:
|
|
25
|
+
req = urllib.request.Request(
|
|
26
|
+
PYPI_URL,
|
|
27
|
+
headers={"Accept": "application/json", "User-Agent": "meeting-noter"}
|
|
28
|
+
)
|
|
29
|
+
with urllib.request.urlopen(req, timeout=5) as response:
|
|
30
|
+
data = json.loads(response.read().decode())
|
|
31
|
+
return data.get("info", {}).get("version")
|
|
32
|
+
except Exception:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_for_update() -> Optional[str]:
|
|
37
|
+
"""Check if an update is available.
|
|
38
|
+
|
|
39
|
+
Returns the new version string if an update is available, None otherwise.
|
|
40
|
+
"""
|
|
41
|
+
latest = get_latest_version()
|
|
42
|
+
if not latest:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
current = parse_version(__version__)
|
|
46
|
+
latest_parsed = parse_version(latest)
|
|
47
|
+
|
|
48
|
+
if latest_parsed > current:
|
|
49
|
+
return latest
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_for_update_async(callback):
|
|
54
|
+
"""Check for updates in a background thread.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
callback: Function to call with the new version string (or None if no update).
|
|
58
|
+
"""
|
|
59
|
+
def _check():
|
|
60
|
+
new_version = check_for_update()
|
|
61
|
+
if new_version:
|
|
62
|
+
callback(new_version)
|
|
63
|
+
|
|
64
|
+
thread = threading.Thread(target=_check, daemon=True)
|
|
65
|
+
thread.start()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meeting-noter
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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"
|
|
@@ -6,8 +6,8 @@ src/meeting_noter/cli.py
|
|
|
6
6
|
src/meeting_noter/config.py
|
|
7
7
|
src/meeting_noter/daemon.py
|
|
8
8
|
src/meeting_noter/meeting_detector.py
|
|
9
|
-
src/meeting_noter/menubar.py
|
|
10
9
|
src/meeting_noter/mic_monitor.py
|
|
10
|
+
src/meeting_noter/update_checker.py
|
|
11
11
|
src/meeting_noter.egg-info/PKG-INFO
|
|
12
12
|
src/meeting_noter.egg-info/SOURCES.txt
|
|
13
13
|
src/meeting_noter.egg-info/dependency_links.txt
|
|
@@ -18,13 +18,6 @@ src/meeting_noter/audio/__init__.py
|
|
|
18
18
|
src/meeting_noter/audio/capture.py
|
|
19
19
|
src/meeting_noter/audio/encoder.py
|
|
20
20
|
src/meeting_noter/audio/system_audio.py
|
|
21
|
-
src/meeting_noter/gui/__init__.py
|
|
22
|
-
src/meeting_noter/gui/__main__.py
|
|
23
|
-
src/meeting_noter/gui/app.py
|
|
24
|
-
src/meeting_noter/gui/main_window.py
|
|
25
|
-
src/meeting_noter/gui/meetings_tab.py
|
|
26
|
-
src/meeting_noter/gui/recording_tab.py
|
|
27
|
-
src/meeting_noter/gui/settings_tab.py
|
|
28
21
|
src/meeting_noter/install/__init__.py
|
|
29
22
|
src/meeting_noter/install/macos.py
|
|
30
23
|
src/meeting_noter/output/__init__.py
|
|
@@ -28,7 +28,7 @@ class TestCliHelp:
|
|
|
28
28
|
result = cli_runner.invoke(cli, ["--version"])
|
|
29
29
|
|
|
30
30
|
assert result.exit_code == 0
|
|
31
|
-
assert "
|
|
31
|
+
assert "meeting-noter" in result.output.lower()
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class TestStartCommand:
|
|
@@ -271,33 +271,6 @@ class TestSuggestCommand:
|
|
|
271
271
|
# Should mention available options or suggest
|
|
272
272
|
|
|
273
273
|
|
|
274
|
-
class TestMenubarCommand:
|
|
275
|
-
"""Tests for the menubar command."""
|
|
276
|
-
|
|
277
|
-
def test_menubar_background(self, cli_runner: CliRunner, mock_config, mocker):
|
|
278
|
-
"""menubar should launch in background by default."""
|
|
279
|
-
mock_popen = mocker.patch("subprocess.Popen")
|
|
280
|
-
|
|
281
|
-
result = cli_runner.invoke(cli, ["menubar"])
|
|
282
|
-
|
|
283
|
-
assert result.exit_code == 0
|
|
284
|
-
assert mock_popen.called
|
|
285
|
-
assert "background" in result.output.lower()
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
class TestGuiCommand:
|
|
289
|
-
"""Tests for the gui command."""
|
|
290
|
-
|
|
291
|
-
def test_gui_background(self, cli_runner: CliRunner, mock_config, mocker):
|
|
292
|
-
"""gui should launch in background by default."""
|
|
293
|
-
mock_popen = mocker.patch("subprocess.Popen")
|
|
294
|
-
|
|
295
|
-
result = cli_runner.invoke(cli, ["gui"])
|
|
296
|
-
|
|
297
|
-
assert result.exit_code == 0
|
|
298
|
-
assert mock_popen.called
|
|
299
|
-
|
|
300
|
-
|
|
301
274
|
class TestWatcherCommand:
|
|
302
275
|
"""Tests for the watcher command."""
|
|
303
276
|
|
|
@@ -749,30 +722,6 @@ class TestOpenCommandExtended:
|
|
|
749
722
|
assert new_dir.exists()
|
|
750
723
|
|
|
751
724
|
|
|
752
|
-
class TestMenubarForeground:
|
|
753
|
-
"""Tests for menubar command in foreground mode."""
|
|
754
|
-
|
|
755
|
-
def test_menubar_foreground(self, cli_runner: CliRunner, mock_config, mocker):
|
|
756
|
-
"""menubar --foreground should run in foreground."""
|
|
757
|
-
mock_run_menubar = mocker.patch("meeting_noter.menubar.run_menubar")
|
|
758
|
-
|
|
759
|
-
result = cli_runner.invoke(cli, ["menubar", "--foreground"])
|
|
760
|
-
|
|
761
|
-
mock_run_menubar.assert_called_once()
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
class TestGuiForeground:
|
|
765
|
-
"""Tests for gui command in foreground mode."""
|
|
766
|
-
|
|
767
|
-
def test_gui_foreground(self, cli_runner: CliRunner, mock_config, mocker):
|
|
768
|
-
"""gui --foreground should run in foreground."""
|
|
769
|
-
mock_run_gui = mocker.patch("meeting_noter.gui.run_gui")
|
|
770
|
-
|
|
771
|
-
result = cli_runner.invoke(cli, ["gui", "--foreground"])
|
|
772
|
-
|
|
773
|
-
mock_run_gui.assert_called_once()
|
|
774
|
-
|
|
775
|
-
|
|
776
725
|
class TestConfigInvalidPath:
|
|
777
726
|
"""Tests for config command with invalid paths."""
|
|
778
727
|
|
|
@@ -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()
|
|
@@ -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()
|