meeting-noter 1.0.0__py3-none-any.whl → 1.2.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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """Meeting Noter - Offline meeting transcription with virtual audio devices."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "1.2.0"
meeting_noter/cli.py CHANGED
@@ -90,24 +90,46 @@ class SuggestCommand(click.Command):
90
90
  SuggestGroup.command_class = SuggestCommand
91
91
 
92
92
 
93
- def _launch_gui_background():
94
- """Launch the GUI in background and return immediately."""
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
- subprocess.Popen(
99
- [sys.executable, "-m", "meeting_noter.gui"],
100
- stdout=subprocess.DEVNULL,
101
- stderr=subprocess.DEVNULL,
102
- start_new_session=True,
103
- )
104
- click.echo("Meeting Noter GUI launched.")
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.version_option(version=__version__)
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 or menubar_running:
277
- mode = "watcher" if watcher_running else "menubar"
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, menubar, GUI)."""
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
  }
meeting_noter/config.py CHANGED
@@ -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.0.0
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"
@@ -1,22 +1,15 @@
1
- meeting_noter/__init__.py,sha256=bLOErRC3sfnQ4a4RyZUzUljZEikXy7zOiYYUz5GytPg,103
1
+ meeting_noter/__init__.py,sha256=AFUfCIr09uvvLIrY-GT15IaqogAMm4zSwZCpcZWzk9s,103
2
2
  meeting_noter/__main__.py,sha256=6sSOqH1o3jvgvkVzsVKmF6-xVGcUAbNVQkRl2CrygdE,120
3
- meeting_noter/cli.py,sha256=brLJ_2kuqEi5wq868hFaMGtKPSkyBpD-OS1u27rJb1k,32859
4
- meeting_noter/config.py,sha256=41LFBNp5o0IojYS5Hf0FJVIr7GNn7B5O1TJDE8SQkkk,5977
3
+ meeting_noter/cli.py,sha256=jEdiOBEG0BP43Z05HSRv5lI9kuj30p-XFCFWZWlxLjc,31439
4
+ meeting_noter/config.py,sha256=_Jy-gZDTCN3TwH5fiLEWk-qFhoSGISO2xDQmACV_zRc,6334
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
8
+ meeting_noter/update_checker.py,sha256=sMmIiiZJL6K7wqLWE64Aj4hS8uspjUOirr6BG_IlL1I,1701
9
9
  meeting_noter/audio/__init__.py,sha256=O7PU8CxHSHxMeHbc9Jdwt9kePLQzsPh81GQU7VHCtBY,44
10
10
  meeting_noter/audio/capture.py,sha256=fDrT5oXfva8vdFlht9cv60NviKbksw2QeJ8eOtI19uE,6469
11
11
  meeting_noter/audio/encoder.py,sha256=OBsgUmlZPz-YZQZ7Rp8MAlMRaQxTsccjuTgCtvRebmc,6573
12
12
  meeting_noter/audio/system_audio.py,sha256=jbHGjNCerI19weXap0a90Ik17lVTCT1hCEgRKYke-p8,13016
13
- meeting_noter/gui/__init__.py,sha256=z5GxxaeXyjqyEa9ox0dQxuL5u_BART0bi7cI6rfntEI,103
14
- meeting_noter/gui/__main__.py,sha256=A2HWdYod0bTgjQQIi21O7XpmgxLH36e_X0aygEUZLls,146
15
- meeting_noter/gui/app.py,sha256=COUAWu_dR5HriNYxbE86CVGS1eGYqyteH2oUFN_YtYQ,1370
16
- meeting_noter/gui/main_window.py,sha256=vSvNO86CHMgJf9Pem8AOdrqKyTV9ITp3W4nCoqIuAmI,1667
17
- meeting_noter/gui/meetings_tab.py,sha256=pqXqMv5YvCj8H6yR_TF3SMzsIDMAxyLe2otbENbM2SY,12315
18
- meeting_noter/gui/recording_tab.py,sha256=UlrPkUiOmkGgOKqVrfAVp4EyiY5sq86W49Y-YAhYexY,12521
19
- meeting_noter/gui/settings_tab.py,sha256=NUQVKDdSpyNp_MVxPLw2dB93wxaD5VeBKiDtGS4CyoU,8446
20
13
  meeting_noter/install/__init__.py,sha256=SX5vLFMrV8aBDEGW18jhaqBqJqnRXaeo0Ct7QVGDgvE,38
21
14
  meeting_noter/install/macos.py,sha256=dO-86zbNKRtt0l4D8naVn7kFWjzI8TufWLWE3FRLHQ8,3400
22
15
  meeting_noter/output/__init__.py,sha256=F7xPlOrqweZcPbZtDrhved1stBI59vnWnLYfGwdu6oY,31
@@ -33,8 +26,8 @@ meeting_noter/resources/icon_64.png,sha256=TqG7Awx3kK8YdiX1e_z1odZonosZyQI2trlkN
33
26
  meeting_noter/transcription/__init__.py,sha256=7GY9diP06DzFyoli41wddbrPv5bVDzH35bmnWlIJev4,29
34
27
  meeting_noter/transcription/engine.py,sha256=G9NcSS6Q-UhW7PlQ0E85hQXn6BWao64nIvyw4NR2yxI,7208
35
28
  meeting_noter/transcription/live_transcription.py,sha256=AslB1T1_gxu7eSp7xc79_2SdfGrNJq7L_8bA1t6YoU4,9277
36
- meeting_noter-1.0.0.dist-info/METADATA,sha256=m7Pi8_-haGOHX0DbA7YXTc1KMpMWAxLHHgbE3kB3-FM,6995
37
- meeting_noter-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
38
- meeting_noter-1.0.0.dist-info/entry_points.txt,sha256=osZoOmm-UBPCJ4b6DGH6JOAm7mofM2fK06eK6blplmg,83
39
- meeting_noter-1.0.0.dist-info/top_level.txt,sha256=9Tuq04_0SXM0OXOHVbOHkHkB5tG3fqkrMrfzCMpbLpY,14
40
- meeting_noter-1.0.0.dist-info/RECORD,,
29
+ meeting_noter-1.2.0.dist-info/METADATA,sha256=ieb-BY1DCzl4joYd7wDc3JOPqxAxdww7Rb-goDknKcs,6939
30
+ meeting_noter-1.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
31
+ meeting_noter-1.2.0.dist-info/entry_points.txt,sha256=osZoOmm-UBPCJ4b6DGH6JOAm7mofM2fK06eK6blplmg,83
32
+ meeting_noter-1.2.0.dist-info/top_level.txt,sha256=9Tuq04_0SXM0OXOHVbOHkHkB5tG3fqkrMrfzCMpbLpY,14
33
+ meeting_noter-1.2.0.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- """Desktop GUI for Meeting Noter."""
2
-
3
- from meeting_noter.gui.app import run_gui
4
-
5
- __all__ = ["run_gui"]
@@ -1,6 +0,0 @@
1
- """Allow running gui as a module: python -m meeting_noter.gui"""
2
-
3
- from meeting_noter.gui import run_gui
4
-
5
- if __name__ == "__main__":
6
- run_gui()
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()
@@ -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()