meeting-noter 1.1.0__tar.gz → 1.3.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.1.0 → meeting_noter-1.3.0}/PKG-INFO +1 -3
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/pyproject.toml +1 -5
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/__init__.py +1 -1
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/cli.py +131 -107
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/config.py +34 -0
- meeting_noter-1.3.0/src/meeting_noter/output/favorites.py +189 -0
- meeting_noter-1.3.0/src/meeting_noter/output/searcher.py +218 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/transcription/live_transcription.py +17 -13
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter.egg-info/PKG-INFO +1 -3
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter.egg-info/SOURCES.txt +5 -9
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter.egg-info/requires.txt +0 -2
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/tests/test_cli.py +196 -52
- meeting_noter-1.3.0/tests/test_favorites.py +255 -0
- meeting_noter-1.3.0/tests/test_searcher.py +327 -0
- meeting_noter-1.1.0/src/meeting_noter/gui/__init__.py +0 -5
- meeting_noter-1.1.0/src/meeting_noter/gui/__main__.py +0 -6
- meeting_noter-1.1.0/src/meeting_noter/gui/app.py +0 -53
- meeting_noter-1.1.0/src/meeting_noter/gui/main_window.py +0 -50
- meeting_noter-1.1.0/src/meeting_noter/gui/meetings_tab.py +0 -348
- meeting_noter-1.1.0/src/meeting_noter/gui/recording_tab.py +0 -358
- meeting_noter-1.1.0/src/meeting_noter/gui/settings_tab.py +0 -249
- meeting_noter-1.1.0/src/meeting_noter/menubar.py +0 -411
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/README.md +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/setup.cfg +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/__main__.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/audio/__init__.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/audio/capture.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/audio/encoder.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/audio/system_audio.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/daemon.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/install/__init__.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/install/macos.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/meeting_detector.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/mic_monitor.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/output/__init__.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/output/writer.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/resources/__init__.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/resources/icon.icns +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/resources/icon.png +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/resources/icon_128.png +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/resources/icon_16.png +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/resources/icon_256.png +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/resources/icon_32.png +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/resources/icon_512.png +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/resources/icon_64.png +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/transcription/__init__.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/transcription/engine.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter/update_checker.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter.egg-info/dependency_links.txt +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter.egg-info/entry_points.txt +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/src/meeting_noter.egg-info/top_level.txt +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/tests/test_config.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/tests/test_daemon.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/tests/test_meeting_detector.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.0}/tests/test_mic_monitor.py +0 -0
- {meeting_noter-1.1.0 → meeting_noter-1.3.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.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"
|
|
@@ -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.3.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,20 +90,6 @@ 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."""
|
|
95
|
-
import subprocess
|
|
96
|
-
import sys
|
|
97
|
-
|
|
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.")
|
|
105
|
-
|
|
106
|
-
|
|
107
93
|
def _handle_version():
|
|
108
94
|
"""Handle --version: show version, check for updates, auto-update if enabled."""
|
|
109
95
|
import subprocess
|
|
@@ -276,7 +262,12 @@ def daemon(output_dir: Optional[str], foreground: bool, name: Optional[str]):
|
|
|
276
262
|
|
|
277
263
|
@cli.command()
|
|
278
264
|
def status():
|
|
279
|
-
"""Show Meeting Noter status.
|
|
265
|
+
"""Show Meeting Noter status.
|
|
266
|
+
|
|
267
|
+
\b
|
|
268
|
+
Examples:
|
|
269
|
+
meeting-noter status # Check if recording or watching
|
|
270
|
+
"""
|
|
280
271
|
import os
|
|
281
272
|
from meeting_noter.daemon import read_pid_file, is_process_running
|
|
282
273
|
|
|
@@ -290,17 +281,6 @@ def status():
|
|
|
290
281
|
except (ProcessLookupError, ValueError, FileNotFoundError):
|
|
291
282
|
pass
|
|
292
283
|
|
|
293
|
-
# Check menubar
|
|
294
|
-
menubar_running = False
|
|
295
|
-
menubar_pid_file = Path.home() / ".meeting-noter-menubar.pid"
|
|
296
|
-
if menubar_pid_file.exists():
|
|
297
|
-
try:
|
|
298
|
-
pid = int(menubar_pid_file.read_text().strip())
|
|
299
|
-
os.kill(pid, 0)
|
|
300
|
-
menubar_running = True
|
|
301
|
-
except (ProcessLookupError, ValueError, FileNotFoundError):
|
|
302
|
-
pass
|
|
303
|
-
|
|
304
284
|
# Check daemon (recording)
|
|
305
285
|
daemon_running = False
|
|
306
286
|
daemon_pid = read_pid_file(DEFAULT_PID_FILE)
|
|
@@ -313,9 +293,8 @@ def status():
|
|
|
313
293
|
# Get current recording name from log
|
|
314
294
|
recording_name = _get_current_recording_name()
|
|
315
295
|
click.echo(f"🔴 Recording: {recording_name or 'In progress'}")
|
|
316
|
-
elif watcher_running
|
|
317
|
-
|
|
318
|
-
click.echo(f"👀 Ready to record ({mode} active)")
|
|
296
|
+
elif watcher_running:
|
|
297
|
+
click.echo("👀 Ready to record (watcher active)")
|
|
319
298
|
else:
|
|
320
299
|
click.echo("⏹️ Stopped (run 'meeting-noter' to start)")
|
|
321
300
|
|
|
@@ -324,7 +303,6 @@ def status():
|
|
|
324
303
|
# Show details
|
|
325
304
|
click.echo("Components:")
|
|
326
305
|
click.echo(f" Watcher: {'running' if watcher_running else 'stopped'}")
|
|
327
|
-
click.echo(f" Menubar: {'running' if menubar_running else 'stopped'}")
|
|
328
306
|
click.echo(f" Recorder: {'recording' if daemon_running else 'idle'}")
|
|
329
307
|
click.echo()
|
|
330
308
|
|
|
@@ -358,7 +336,12 @@ def _get_current_recording_name() -> str | None:
|
|
|
358
336
|
|
|
359
337
|
@cli.command()
|
|
360
338
|
def shutdown():
|
|
361
|
-
"""Stop all Meeting Noter processes (daemon, watcher
|
|
339
|
+
"""Stop all Meeting Noter processes (daemon, watcher).
|
|
340
|
+
|
|
341
|
+
\b
|
|
342
|
+
Examples:
|
|
343
|
+
meeting-noter shutdown # Stop recording and watcher
|
|
344
|
+
"""
|
|
362
345
|
import subprocess
|
|
363
346
|
import os
|
|
364
347
|
import signal
|
|
@@ -381,17 +364,6 @@ def shutdown():
|
|
|
381
364
|
except (ProcessLookupError, ValueError):
|
|
382
365
|
WATCHER_PID_FILE.unlink(missing_ok=True)
|
|
383
366
|
|
|
384
|
-
# Stop menubar
|
|
385
|
-
menubar_pid_file = Path.home() / ".meeting-noter-menubar.pid"
|
|
386
|
-
if menubar_pid_file.exists():
|
|
387
|
-
try:
|
|
388
|
-
pid = int(menubar_pid_file.read_text().strip())
|
|
389
|
-
os.kill(pid, signal.SIGTERM)
|
|
390
|
-
menubar_pid_file.unlink()
|
|
391
|
-
stopped.append("menubar")
|
|
392
|
-
except (ProcessLookupError, ValueError):
|
|
393
|
-
menubar_pid_file.unlink(missing_ok=True)
|
|
394
|
-
|
|
395
367
|
# Kill any remaining meeting-noter processes
|
|
396
368
|
result = subprocess.run(
|
|
397
369
|
["pkill", "-f", "meeting_noter"],
|
|
@@ -410,7 +382,14 @@ def shutdown():
|
|
|
410
382
|
@click.option("--follow", "-f", is_flag=True, help="Follow log output (like tail -f)")
|
|
411
383
|
@click.option("--lines", "-n", default=50, help="Number of lines to show")
|
|
412
384
|
def logs(follow: bool, lines: int):
|
|
413
|
-
"""View Meeting Noter logs.
|
|
385
|
+
"""View Meeting Noter logs.
|
|
386
|
+
|
|
387
|
+
\b
|
|
388
|
+
Examples:
|
|
389
|
+
meeting-noter logs # Show last 50 lines
|
|
390
|
+
meeting-noter logs -n 100 # Show last 100 lines
|
|
391
|
+
meeting-noter logs -f # Follow log output (Ctrl+C to stop)
|
|
392
|
+
"""
|
|
414
393
|
import subprocess
|
|
415
394
|
|
|
416
395
|
log_file = Path.home() / ".meeting-noter.log"
|
|
@@ -430,6 +409,11 @@ def logs(follow: bool, lines: int):
|
|
|
430
409
|
|
|
431
410
|
|
|
432
411
|
@cli.command("list")
|
|
412
|
+
@click.option(
|
|
413
|
+
"--transcripts", "-t",
|
|
414
|
+
is_flag=True,
|
|
415
|
+
help="List transcripts instead of recordings",
|
|
416
|
+
)
|
|
433
417
|
@click.option(
|
|
434
418
|
"--output-dir", "-o",
|
|
435
419
|
type=click.Path(exists=True),
|
|
@@ -440,22 +424,118 @@ def logs(follow: bool, lines: int):
|
|
|
440
424
|
"--limit", "-n",
|
|
441
425
|
type=int,
|
|
442
426
|
default=10,
|
|
443
|
-
help="Number of
|
|
427
|
+
help="Number of items to show",
|
|
428
|
+
)
|
|
429
|
+
@require_setup
|
|
430
|
+
def list_recordings(transcripts: bool, output_dir: Optional[str], limit: int):
|
|
431
|
+
"""List recent meeting recordings or transcripts.
|
|
432
|
+
|
|
433
|
+
\b
|
|
434
|
+
Examples:
|
|
435
|
+
meeting-noter list # Show last 10 recordings
|
|
436
|
+
meeting-noter list -t # Show last 10 transcripts
|
|
437
|
+
meeting-noter list -t -n 20 # Show last 20 transcripts
|
|
438
|
+
"""
|
|
439
|
+
config = get_config()
|
|
440
|
+
|
|
441
|
+
if transcripts:
|
|
442
|
+
from meeting_noter.output.favorites import list_transcripts_with_favorites
|
|
443
|
+
path = Path(output_dir) if output_dir else config.transcripts_dir
|
|
444
|
+
list_transcripts_with_favorites(path, limit)
|
|
445
|
+
else:
|
|
446
|
+
from meeting_noter.output.writer import list_recordings as _list_recordings
|
|
447
|
+
path = Path(output_dir) if output_dir else config.recordings_dir
|
|
448
|
+
_list_recordings(path, limit)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@cli.command("search")
|
|
452
|
+
@click.argument("query")
|
|
453
|
+
@click.option(
|
|
454
|
+
"--case-sensitive", "-c",
|
|
455
|
+
is_flag=True,
|
|
456
|
+
help="Case-sensitive search",
|
|
457
|
+
)
|
|
458
|
+
@click.option(
|
|
459
|
+
"--limit", "-n",
|
|
460
|
+
type=int,
|
|
461
|
+
default=20,
|
|
462
|
+
help="Max results to show",
|
|
463
|
+
)
|
|
464
|
+
@click.option(
|
|
465
|
+
"--transcripts-dir", "-d",
|
|
466
|
+
type=click.Path(),
|
|
467
|
+
default=None,
|
|
468
|
+
help="Override transcripts directory",
|
|
444
469
|
)
|
|
445
470
|
@require_setup
|
|
446
|
-
def
|
|
447
|
-
"""
|
|
471
|
+
def search(query: str, case_sensitive: bool, limit: int, transcripts_dir: Optional[str]):
|
|
472
|
+
"""Search across all meeting transcripts.
|
|
448
473
|
|
|
449
474
|
\b
|
|
450
475
|
Examples:
|
|
451
|
-
meeting-noter
|
|
452
|
-
meeting-noter
|
|
476
|
+
meeting-noter search "action items"
|
|
477
|
+
meeting-noter search "API" --case-sensitive
|
|
478
|
+
meeting-noter search "standup" -n 5
|
|
453
479
|
"""
|
|
454
|
-
from meeting_noter.output.
|
|
480
|
+
from meeting_noter.output.searcher import search_transcripts
|
|
455
481
|
|
|
456
482
|
config = get_config()
|
|
457
|
-
path = Path(
|
|
458
|
-
|
|
483
|
+
path = Path(transcripts_dir) if transcripts_dir else config.transcripts_dir
|
|
484
|
+
search_transcripts(path, query, case_sensitive, limit)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@cli.group("favorites", invoke_without_command=True)
|
|
488
|
+
@click.pass_context
|
|
489
|
+
@require_setup
|
|
490
|
+
def favorites(ctx):
|
|
491
|
+
"""Manage favorite transcripts.
|
|
492
|
+
|
|
493
|
+
\b
|
|
494
|
+
Examples:
|
|
495
|
+
meeting-noter favorites # List all favorites
|
|
496
|
+
meeting-noter favorites add file.txt # Add to favorites
|
|
497
|
+
meeting-noter favorites add --latest # Add most recent transcript
|
|
498
|
+
meeting-noter favorites remove file # Remove from favorites
|
|
499
|
+
"""
|
|
500
|
+
if ctx.invoked_subcommand is None:
|
|
501
|
+
# Default: list favorites
|
|
502
|
+
from meeting_noter.output.favorites import list_favorites
|
|
503
|
+
|
|
504
|
+
config = get_config()
|
|
505
|
+
list_favorites(config.transcripts_dir)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@favorites.command("add")
|
|
509
|
+
@click.argument("filename", required=False)
|
|
510
|
+
@click.option("--latest", "-l", is_flag=True, help="Add the most recent transcript")
|
|
511
|
+
@require_setup
|
|
512
|
+
def favorites_add(filename: Optional[str], latest: bool):
|
|
513
|
+
"""Add a transcript to favorites.
|
|
514
|
+
|
|
515
|
+
\b
|
|
516
|
+
Examples:
|
|
517
|
+
meeting-noter favorites add meeting.txt
|
|
518
|
+
meeting-noter favorites add --latest
|
|
519
|
+
"""
|
|
520
|
+
from meeting_noter.output.favorites import add_favorite
|
|
521
|
+
|
|
522
|
+
config = get_config()
|
|
523
|
+
add_favorite(config.transcripts_dir, filename, latest)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@favorites.command("remove")
|
|
527
|
+
@click.argument("filename")
|
|
528
|
+
@require_setup
|
|
529
|
+
def favorites_remove(filename: str):
|
|
530
|
+
"""Remove a transcript from favorites.
|
|
531
|
+
|
|
532
|
+
\b
|
|
533
|
+
Examples:
|
|
534
|
+
meeting-noter favorites remove meeting.txt
|
|
535
|
+
"""
|
|
536
|
+
from meeting_noter.output.favorites import remove_favorite
|
|
537
|
+
|
|
538
|
+
remove_favorite(filename)
|
|
459
539
|
|
|
460
540
|
|
|
461
541
|
@cli.command()
|
|
@@ -584,62 +664,6 @@ def live():
|
|
|
584
664
|
click.echo("\n" + click.style("Stopped watching.", fg="cyan"))
|
|
585
665
|
|
|
586
666
|
|
|
587
|
-
@cli.command()
|
|
588
|
-
@click.option(
|
|
589
|
-
"--foreground", "-f",
|
|
590
|
-
is_flag=True,
|
|
591
|
-
help="Run in foreground instead of background",
|
|
592
|
-
)
|
|
593
|
-
@require_setup
|
|
594
|
-
def menubar(foreground: bool):
|
|
595
|
-
"""Launch menu bar app for daemon control.
|
|
596
|
-
|
|
597
|
-
Adds a menu bar icon for one-click start/stop of the recording daemon.
|
|
598
|
-
The icon shows "MN" when idle and "MN [filename]" when recording.
|
|
599
|
-
|
|
600
|
-
By default, runs in background. Use -f for foreground (debugging).
|
|
601
|
-
"""
|
|
602
|
-
import subprocess
|
|
603
|
-
import sys
|
|
604
|
-
|
|
605
|
-
if foreground:
|
|
606
|
-
from meeting_noter.menubar import run_menubar
|
|
607
|
-
run_menubar()
|
|
608
|
-
else:
|
|
609
|
-
# Spawn as background process
|
|
610
|
-
subprocess.Popen(
|
|
611
|
-
[sys.executable, "-m", "meeting_noter.menubar"],
|
|
612
|
-
stdout=subprocess.DEVNULL,
|
|
613
|
-
stderr=subprocess.DEVNULL,
|
|
614
|
-
start_new_session=True,
|
|
615
|
-
)
|
|
616
|
-
click.echo("Menu bar app started in background.")
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
@cli.command()
|
|
620
|
-
@click.option(
|
|
621
|
-
"--foreground", "-f",
|
|
622
|
-
is_flag=True,
|
|
623
|
-
help="Run in foreground instead of background",
|
|
624
|
-
)
|
|
625
|
-
@require_setup
|
|
626
|
-
def gui(foreground: bool):
|
|
627
|
-
"""Launch the desktop GUI application.
|
|
628
|
-
|
|
629
|
-
Opens a window with tabs for:
|
|
630
|
-
- Recording: Start/stop recordings with meeting names
|
|
631
|
-
- Meetings: Browse, play, and manage recordings
|
|
632
|
-
- Settings: Configure directories, models, and preferences
|
|
633
|
-
|
|
634
|
-
By default runs in background. Use -f for foreground.
|
|
635
|
-
"""
|
|
636
|
-
if foreground:
|
|
637
|
-
from meeting_noter.gui import run_gui
|
|
638
|
-
run_gui()
|
|
639
|
-
else:
|
|
640
|
-
_launch_gui_background()
|
|
641
|
-
|
|
642
|
-
|
|
643
667
|
# Config key mappings (CLI name -> config attribute)
|
|
644
668
|
CONFIG_KEYS = {
|
|
645
669
|
"recordings-dir": ("recordings_dir", "path", "Directory for audio recordings"),
|
|
@@ -160,6 +160,40 @@ class Config:
|
|
|
160
160
|
"""Set setup completion status."""
|
|
161
161
|
self._data["setup_complete"] = value
|
|
162
162
|
|
|
163
|
+
@property
|
|
164
|
+
def favorites(self) -> list[str]:
|
|
165
|
+
"""Get list of favorite transcript filenames."""
|
|
166
|
+
return self._data.get("favorites", [])
|
|
167
|
+
|
|
168
|
+
@favorites.setter
|
|
169
|
+
def favorites(self, value: list[str]) -> None:
|
|
170
|
+
"""Set list of favorite transcript filenames."""
|
|
171
|
+
self._data["favorites"] = value
|
|
172
|
+
|
|
173
|
+
def add_favorite(self, filename: str) -> bool:
|
|
174
|
+
"""Add a transcript to favorites. Returns True if added, False if already exists."""
|
|
175
|
+
favorites = self.favorites
|
|
176
|
+
if filename not in favorites:
|
|
177
|
+
favorites.append(filename)
|
|
178
|
+
self.favorites = favorites
|
|
179
|
+
self.save()
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def remove_favorite(self, filename: str) -> bool:
|
|
184
|
+
"""Remove a transcript from favorites. Returns True if removed, False if not found."""
|
|
185
|
+
favorites = self.favorites
|
|
186
|
+
if filename in favorites:
|
|
187
|
+
favorites.remove(filename)
|
|
188
|
+
self.favorites = favorites
|
|
189
|
+
self.save()
|
|
190
|
+
return True
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def is_favorite(self, filename: str) -> bool:
|
|
194
|
+
"""Check if a transcript is a favorite."""
|
|
195
|
+
return filename in self.favorites
|
|
196
|
+
|
|
163
197
|
def __getitem__(self, key: str) -> Any:
|
|
164
198
|
"""Get config value by key."""
|
|
165
199
|
return self._data.get(key, DEFAULT_CONFIG.get(key))
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Favorites management for meeting transcripts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from meeting_noter.config import get_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def list_favorites(transcripts_dir: Path) -> None:
|
|
14
|
+
"""List all favorite transcripts.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
transcripts_dir: Directory containing transcript files
|
|
18
|
+
"""
|
|
19
|
+
config = get_config()
|
|
20
|
+
favorites = config.favorites
|
|
21
|
+
|
|
22
|
+
if not favorites:
|
|
23
|
+
click.echo(click.style("No favorites yet.", fg="yellow"))
|
|
24
|
+
click.echo("\nAdd favorites with: meeting-noter favorites add <filename>")
|
|
25
|
+
click.echo("Or use: meeting-noter favorites add --latest")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
click.echo()
|
|
29
|
+
click.echo(click.style("Favorite Transcripts", bold=True))
|
|
30
|
+
click.echo("=" * 50)
|
|
31
|
+
click.echo()
|
|
32
|
+
|
|
33
|
+
found_count = 0
|
|
34
|
+
missing_count = 0
|
|
35
|
+
|
|
36
|
+
for idx, filename in enumerate(favorites, 1):
|
|
37
|
+
filepath = transcripts_dir / filename
|
|
38
|
+
if filepath.exists():
|
|
39
|
+
found_count += 1
|
|
40
|
+
# Get file info
|
|
41
|
+
stat = filepath.stat()
|
|
42
|
+
mod_time = datetime.fromtimestamp(stat.st_mtime)
|
|
43
|
+
date_str = mod_time.strftime("%Y-%m-%d %H:%M")
|
|
44
|
+
|
|
45
|
+
# Display: index, filename prominently, then date
|
|
46
|
+
click.echo(
|
|
47
|
+
click.style(f" {idx}. ", fg="cyan")
|
|
48
|
+
+ click.style(filename, fg="green", bold=True)
|
|
49
|
+
)
|
|
50
|
+
click.echo(f" ★ {date_str}")
|
|
51
|
+
else:
|
|
52
|
+
missing_count += 1
|
|
53
|
+
click.echo(
|
|
54
|
+
click.style(f" {idx}. ", fg="cyan")
|
|
55
|
+
+ click.style(filename, fg="red", strikethrough=True)
|
|
56
|
+
)
|
|
57
|
+
click.echo(click.style(" (file not found)", fg="red"))
|
|
58
|
+
|
|
59
|
+
click.echo()
|
|
60
|
+
click.echo(f"Total: {found_count} favorites")
|
|
61
|
+
if missing_count > 0:
|
|
62
|
+
click.echo(
|
|
63
|
+
click.style(
|
|
64
|
+
f"Warning: {missing_count} favorite(s) no longer exist",
|
|
65
|
+
fg="yellow"
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
click.echo()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def add_favorite(
|
|
72
|
+
transcripts_dir: Path,
|
|
73
|
+
filename: Optional[str] = None,
|
|
74
|
+
latest: bool = False
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Add a transcript to favorites.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
transcripts_dir: Directory containing transcript files
|
|
80
|
+
filename: Name of transcript file to add
|
|
81
|
+
latest: If True, add the most recent transcript
|
|
82
|
+
"""
|
|
83
|
+
config = get_config()
|
|
84
|
+
|
|
85
|
+
if latest:
|
|
86
|
+
# Find the most recent transcript
|
|
87
|
+
txt_files = sorted(
|
|
88
|
+
transcripts_dir.glob("*.txt"),
|
|
89
|
+
key=lambda p: p.stat().st_mtime,
|
|
90
|
+
reverse=True,
|
|
91
|
+
)
|
|
92
|
+
if not txt_files:
|
|
93
|
+
click.echo(click.style("No transcripts found.", fg="red"))
|
|
94
|
+
return
|
|
95
|
+
filename = txt_files[0].name
|
|
96
|
+
|
|
97
|
+
if not filename:
|
|
98
|
+
click.echo(click.style("Error: Specify a filename or use --latest", fg="red"))
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# Check if file exists
|
|
102
|
+
filepath = transcripts_dir / filename
|
|
103
|
+
if not filepath.exists():
|
|
104
|
+
# Try adding .txt extension
|
|
105
|
+
if not filename.endswith(".txt"):
|
|
106
|
+
filepath = transcripts_dir / f"{filename}.txt"
|
|
107
|
+
if filepath.exists():
|
|
108
|
+
filename = f"{filename}.txt"
|
|
109
|
+
else:
|
|
110
|
+
click.echo(click.style(f"File not found: {filename}", fg="red"))
|
|
111
|
+
click.echo(f"Looking in: {transcripts_dir}")
|
|
112
|
+
return
|
|
113
|
+
else:
|
|
114
|
+
click.echo(click.style(f"File not found: {filename}", fg="red"))
|
|
115
|
+
click.echo(f"Looking in: {transcripts_dir}")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
if config.add_favorite(filename):
|
|
119
|
+
click.echo(
|
|
120
|
+
click.style("★ ", fg="yellow")
|
|
121
|
+
+ f"Added to favorites: "
|
|
122
|
+
+ click.style(filename, fg="green")
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
click.echo(click.style(f"Already a favorite: {filename}", fg="yellow"))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def remove_favorite(filename: str) -> None:
|
|
129
|
+
"""Remove a transcript from favorites.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
filename: Name of transcript file to remove
|
|
133
|
+
"""
|
|
134
|
+
config = get_config()
|
|
135
|
+
|
|
136
|
+
# Handle with or without .txt extension
|
|
137
|
+
if not filename.endswith(".txt"):
|
|
138
|
+
if f"{filename}.txt" in config.favorites:
|
|
139
|
+
filename = f"{filename}.txt"
|
|
140
|
+
|
|
141
|
+
if config.remove_favorite(filename):
|
|
142
|
+
click.echo(
|
|
143
|
+
click.style("☆ ", fg="cyan")
|
|
144
|
+
+ f"Removed from favorites: "
|
|
145
|
+
+ click.style(filename, fg="green")
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
click.echo(click.style(f"Not a favorite: {filename}", fg="yellow"))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def list_transcripts_with_favorites(transcripts_dir: Path, limit: int = 10) -> None:
|
|
152
|
+
"""List transcripts with favorite status indicated.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
transcripts_dir: Directory containing transcript files
|
|
156
|
+
limit: Maximum number of transcripts to show
|
|
157
|
+
"""
|
|
158
|
+
config = get_config()
|
|
159
|
+
|
|
160
|
+
if not transcripts_dir.exists():
|
|
161
|
+
click.echo(click.style(f"Directory not found: {transcripts_dir}", fg="red"))
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
txt_files = sorted(
|
|
165
|
+
transcripts_dir.glob("*.txt"),
|
|
166
|
+
key=lambda p: p.stat().st_mtime,
|
|
167
|
+
reverse=True,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if not txt_files:
|
|
171
|
+
click.echo(click.style("No transcripts found.", fg="yellow"))
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
click.echo(f"\nTranscripts in {transcripts_dir}:\n")
|
|
175
|
+
|
|
176
|
+
for txt_file in txt_files[:limit]:
|
|
177
|
+
stat = txt_file.stat()
|
|
178
|
+
mod_time = datetime.fromtimestamp(stat.st_mtime)
|
|
179
|
+
date_str = mod_time.strftime("%Y-%m-%d %H:%M")
|
|
180
|
+
|
|
181
|
+
is_fav = config.is_favorite(txt_file.name)
|
|
182
|
+
star = click.style("★ ", fg="yellow") if is_fav else " "
|
|
183
|
+
|
|
184
|
+
click.echo(f"{star}{date_str} {txt_file.name}")
|
|
185
|
+
|
|
186
|
+
if len(txt_files) > limit:
|
|
187
|
+
click.echo(f"\n ... and {len(txt_files) - limit} more")
|
|
188
|
+
|
|
189
|
+
click.echo(f"\nTotal: {len(txt_files)} transcripts")
|