meeting-noter 0.5.1__py3-none-any.whl → 0.6.1__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.

@@ -1,37 +1,21 @@
1
- """MP3 encoding for audio recordings."""
1
+ """MP3 encoding for audio recordings using ffmpeg."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
- import sys
6
+ import subprocess
7
7
  import numpy as np
8
8
  from pathlib import Path
9
9
  from datetime import datetime
10
10
  from typing import Optional, Tuple
11
11
 
12
- # Try to import lameenc, provide helpful error if not available
12
+ # Get bundled ffmpeg binary path
13
13
  try:
14
- import lameenc
15
- LAMEENC_AVAILABLE = True
14
+ import imageio_ffmpeg
15
+ FFMPEG_PATH = imageio_ffmpeg.get_ffmpeg_exe()
16
16
  except ImportError:
17
- LAMEENC_AVAILABLE = False
18
- lameenc = None
19
-
20
-
21
- def _check_lameenc():
22
- """Check if lameenc is available, raise helpful error if not."""
23
- if not LAMEENC_AVAILABLE:
24
- print("\n" + "=" * 60, file=sys.stderr)
25
- print("ERROR: MP3 encoding requires the 'lame' library", file=sys.stderr)
26
- print("=" * 60, file=sys.stderr)
27
- print("\nTo fix this, run:", file=sys.stderr)
28
- print("\n brew install lame", file=sys.stderr)
29
- print("\nThen reinstall meeting-noter:", file=sys.stderr)
30
- print("\n pip install --force-reinstall meeting-noter", file=sys.stderr)
31
- print("\n" + "=" * 60 + "\n", file=sys.stderr)
32
- raise ImportError(
33
- "lameenc not available. Install LAME first: brew install lame"
34
- )
17
+ # Fallback to system ffmpeg
18
+ FFMPEG_PATH = "ffmpeg"
35
19
 
36
20
 
37
21
  def _sanitize_filename(name: str, max_length: int = 50) -> str:
@@ -62,24 +46,42 @@ def _is_timestamp_name(name: str) -> bool:
62
46
 
63
47
 
64
48
  class MP3Encoder:
65
- """Encodes audio data to MP3 format."""
49
+ """Encodes audio data to MP3 format using ffmpeg."""
66
50
 
67
51
  def __init__(
68
52
  self,
53
+ output_path: Path,
69
54
  sample_rate: int = 16000,
70
55
  channels: int = 1,
71
56
  bitrate: int = 128,
72
- quality: int = 2,
73
57
  ):
74
- _check_lameenc()
58
+ self.output_path = output_path
75
59
  self.sample_rate = sample_rate
76
60
  self.channels = channels
77
- self.encoder = lameenc.Encoder()
78
- self.encoder.set_bit_rate(bitrate)
79
- self.encoder.set_in_sample_rate(sample_rate)
80
- self.encoder.set_channels(channels)
81
- self.encoder.set_quality(quality) # 2 = high quality
82
- self._buffer = bytearray()
61
+ self.bitrate = bitrate
62
+ self._process: Optional[subprocess.Popen] = None
63
+ self._start_ffmpeg()
64
+
65
+ def _start_ffmpeg(self):
66
+ """Start ffmpeg process for encoding."""
67
+ cmd = [
68
+ FFMPEG_PATH,
69
+ "-y", # Overwrite output
70
+ "-f", "s16le", # Input format: signed 16-bit little-endian PCM
71
+ "-ar", str(self.sample_rate), # Sample rate
72
+ "-ac", str(self.channels), # Channels
73
+ "-i", "pipe:0", # Read from stdin
74
+ "-codec:a", "libmp3lame", # MP3 encoder
75
+ "-b:a", f"{self.bitrate}k", # Bitrate
76
+ "-f", "mp3", # Output format
77
+ str(self.output_path),
78
+ ]
79
+ self._process = subprocess.Popen(
80
+ cmd,
81
+ stdin=subprocess.PIPE,
82
+ stdout=subprocess.DEVNULL,
83
+ stderr=subprocess.DEVNULL,
84
+ )
83
85
 
84
86
  def encode_chunk(self, audio: np.ndarray) -> bytes:
85
87
  """Encode a chunk of audio data.
@@ -88,16 +90,29 @@ class MP3Encoder:
88
90
  audio: Float32 audio data, values between -1 and 1
89
91
 
90
92
  Returns:
91
- MP3 encoded bytes
93
+ Empty bytes (ffmpeg writes directly to file)
92
94
  """
95
+ if self._process is None or self._process.stdin is None:
96
+ return b""
97
+
93
98
  # Convert float32 to int16
94
99
  int_data = (audio * 32767).astype(np.int16)
95
- mp3_data = self.encoder.encode(int_data.tobytes())
96
- return mp3_data
100
+ try:
101
+ self._process.stdin.write(int_data.tobytes())
102
+ except BrokenPipeError:
103
+ pass
104
+ return b"" # ffmpeg writes to file, not returning data
97
105
 
98
106
  def finalize(self) -> bytes:
99
- """Finalize encoding and return remaining data."""
100
- return self.encoder.flush()
107
+ """Finalize encoding."""
108
+ if self._process is not None and self._process.stdin is not None:
109
+ try:
110
+ self._process.stdin.close()
111
+ except Exception:
112
+ pass
113
+ self._process.wait()
114
+ self._process = None
115
+ return b""
101
116
 
102
117
 
103
118
  class RecordingSession:
@@ -115,7 +130,6 @@ class RecordingSession:
115
130
  self.channels = channels
116
131
  self.meeting_name = meeting_name
117
132
  self.encoder: Optional[MP3Encoder] = None
118
- self.file_handle = None
119
133
  self.filepath: Optional[Path] = None
120
134
  self.start_time: Optional[datetime] = None
121
135
  self.total_samples = 0
@@ -139,22 +153,20 @@ class RecordingSession:
139
153
  self.filepath = self.output_dir / filename
140
154
 
141
155
  self.encoder = MP3Encoder(
156
+ output_path=self.filepath,
142
157
  sample_rate=self.sample_rate,
143
158
  channels=self.channels,
144
159
  )
145
- self.file_handle = open(self.filepath, "wb")
146
160
  self.total_samples = 0
147
161
 
148
162
  return self.filepath
149
163
 
150
164
  def write(self, audio: np.ndarray):
151
165
  """Write audio data to the recording."""
152
- if self.encoder is None or self.file_handle is None:
166
+ if self.encoder is None:
153
167
  raise RuntimeError("Recording session not started")
154
168
 
155
- mp3_data = self.encoder.encode_chunk(audio)
156
- if mp3_data:
157
- self.file_handle.write(mp3_data)
169
+ self.encoder.encode_chunk(audio)
158
170
  self.total_samples += len(audio)
159
171
 
160
172
  def stop(self) -> Tuple[Optional[Path], float]:
@@ -166,13 +178,9 @@ class RecordingSession:
166
178
  duration = 0.0
167
179
  filepath = self.filepath
168
180
 
169
- if self.encoder and self.file_handle:
170
- # Write final data
171
- final_data = self.encoder.finalize()
172
- if final_data:
173
- self.file_handle.write(final_data)
174
- self.file_handle.close()
175
-
181
+ if self.encoder:
182
+ # Finalize encoding
183
+ self.encoder.finalize()
176
184
  duration = self.total_samples / self.sample_rate
177
185
 
178
186
  # Delete if too short (less than 5 seconds)
@@ -181,7 +189,6 @@ class RecordingSession:
181
189
  filepath = None
182
190
 
183
191
  self.encoder = None
184
- self.file_handle = None
185
192
  self.filepath = None
186
193
  self.start_time = None
187
194
  self.total_samples = 0
@@ -8,13 +8,39 @@ from datetime import datetime
8
8
  from typing import Optional
9
9
 
10
10
 
11
+ def _get_bundled_model_path() -> Optional[Path]:
12
+ """Check if bundled model is available via meeting-noter-models package."""
13
+ try:
14
+ from meeting_noter_models import get_model_path, MODEL_NAME
15
+ model_path = get_model_path()
16
+ if model_path.exists() and (model_path / "model.bin").exists():
17
+ return model_path
18
+ except ImportError:
19
+ pass
20
+ return None
21
+
22
+
11
23
  def get_model(model_size: str = "tiny.en"):
12
24
  """Load the Whisper model.
13
25
 
14
- Models are cached after first download (~75MB for tiny.en).
26
+ Priority:
27
+ 1. Bundled model from meeting-noter-models package (offline)
28
+ 2. Download from Hugging Face Hub (cached after first download)
15
29
  """
16
30
  from faster_whisper import WhisperModel
17
31
 
32
+ # Check for bundled model first (for offline use)
33
+ bundled_path = _get_bundled_model_path()
34
+ if bundled_path and model_size == "tiny.en":
35
+ click.echo(f"Loading bundled model '{model_size}'...")
36
+ model = WhisperModel(
37
+ str(bundled_path),
38
+ device="cpu",
39
+ compute_type="int8",
40
+ )
41
+ return model
42
+
43
+ # Fall back to Hugging Face download
18
44
  click.echo(f"Loading model '{model_size}'...")
19
45
 
20
46
  # Use INT8 for CPU efficiency
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meeting-noter
3
- Version: 0.5.1
3
+ Version: 0.6.1
4
4
  Summary: Offline meeting transcription for macOS with automatic meeting detection
5
5
  Author: Victor
6
6
  License: MIT
@@ -28,14 +28,15 @@ Requires-Dist: numpy>=1.24
28
28
  Requires-Dist: faster-whisper>=1.0.0
29
29
  Requires-Dist: rumps>=0.4.0
30
30
  Requires-Dist: PyQt6>=6.5.0
31
+ Requires-Dist: imageio-ffmpeg>=0.4.9
31
32
  Requires-Dist: pyobjc-framework-Cocoa>=9.0; sys_platform == "darwin"
32
33
  Requires-Dist: pyobjc-framework-Quartz>=9.0; sys_platform == "darwin"
33
34
  Requires-Dist: pyobjc-framework-ScreenCaptureKit>=9.0; sys_platform == "darwin"
34
35
  Requires-Dist: pyobjc-framework-AVFoundation>=9.0; sys_platform == "darwin"
35
36
  Requires-Dist: pyobjc-framework-CoreMedia>=9.0; sys_platform == "darwin"
36
37
  Requires-Dist: pyobjc-framework-libdispatch>=9.0; sys_platform == "darwin"
37
- Provides-Extra: mp3
38
- Requires-Dist: lameenc>=1.5.0; extra == "mp3"
38
+ Provides-Extra: offline
39
+ Requires-Dist: meeting-noter-models>=0.1.0; extra == "offline"
39
40
  Provides-Extra: dev
40
41
  Requires-Dist: pytest>=7.0; extra == "dev"
41
42
  Requires-Dist: pytest-cov; extra == "dev"
@@ -59,11 +60,7 @@ Offline meeting transcription tool for macOS. Captures both your voice and meeti
59
60
  ## Installation
60
61
 
61
62
  ```bash
62
- # Install dependencies
63
- brew install ffmpeg lame pkg-config python@3.12
64
-
65
- # Install meeting-noter (use Python 3.12 for best compatibility)
66
- pipx install meeting-noter --python /opt/homebrew/bin/python3.12
63
+ pipx install meeting-noter
67
64
  ```
68
65
 
69
66
  Or with pip:
@@ -71,6 +68,8 @@ Or with pip:
71
68
  pip install meeting-noter
72
69
  ```
73
70
 
71
+ No system dependencies required - ffmpeg is bundled automatically.
72
+
74
73
  ## Quick Start
75
74
 
76
75
  **Menu Bar App** (recommended):
@@ -208,10 +207,7 @@ Config file: `~/.config/meeting-noter/config.json`
208
207
  ## Requirements
209
208
 
210
209
  - macOS 12.3+ (for ScreenCaptureKit)
211
- - Python 3.9+ (3.12 recommended for best compatibility)
212
- - FFmpeg (`brew install ffmpeg`) - required for audio processing
213
- - LAME (`brew install lame`) - required for MP3 encoding
214
- - pkg-config (`brew install pkg-config`) - required for building dependencies
210
+ - Python 3.9+
215
211
 
216
212
  ## License
217
213
 
@@ -8,7 +8,7 @@ meeting_noter/menubar.py,sha256=Gn6p8y5jA_HCWf1T3ademxH-vndpONHkf9vUlKs6XEo,1437
8
8
  meeting_noter/mic_monitor.py,sha256=Dzt7RZT7-X5US7mT2I247UguR-uLWK0BZDnjiehLD-A,13634
9
9
  meeting_noter/audio/__init__.py,sha256=O7PU8CxHSHxMeHbc9Jdwt9kePLQzsPh81GQU7VHCtBY,44
10
10
  meeting_noter/audio/capture.py,sha256=fDrT5oXfva8vdFlht9cv60NviKbksw2QeJ8eOtI19uE,6469
11
- meeting_noter/audio/encoder.py,sha256=C-lGn6S-1KmvfeXMaP032XhifDNMe6sa0RKDZHoZlio,6374
11
+ meeting_noter/audio/encoder.py,sha256=6UgEYLFACSQEIx2nhH1Qq-cBh3qPJziMGkrm39k6Nz8,6401
12
12
  meeting_noter/audio/system_audio.py,sha256=jbHGjNCerI19weXap0a90Ik17lVTCT1hCEgRKYke-p8,13016
13
13
  meeting_noter/gui/__init__.py,sha256=z5GxxaeXyjqyEa9ox0dQxuL5u_BART0bi7cI6rfntEI,103
14
14
  meeting_noter/gui/__main__.py,sha256=A2HWdYod0bTgjQQIi21O7XpmgxLH36e_X0aygEUZLls,146
@@ -31,9 +31,9 @@ meeting_noter/resources/icon_32.png,sha256=Bw6hJXqkd-d1OfDpFv2om7Stex7daedxnpXt3
31
31
  meeting_noter/resources/icon_512.png,sha256=o7X3ngYcppcIAAk9AcfPx94MUmrsPRp0qBTpb9SzfX8,5369
32
32
  meeting_noter/resources/icon_64.png,sha256=TqG7Awx3kK8YdiX1e_z1odZonosZyQI2trlkNZCzUoI,607
33
33
  meeting_noter/transcription/__init__.py,sha256=7GY9diP06DzFyoli41wddbrPv5bVDzH35bmnWlIJev4,29
34
- meeting_noter/transcription/engine.py,sha256=HK2J2QOBNIDm1MXW-gkagXP8C8cqUfK_WylHQD_LqOI,6320
35
- meeting_noter-0.5.1.dist-info/METADATA,sha256=7JlWzxnwRDqLfauYCMWS_TB19ZvKgWG-TI-RCbI9jhU,7032
36
- meeting_noter-0.5.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
37
- meeting_noter-0.5.1.dist-info/entry_points.txt,sha256=rKNhzjSF5-e3bLRr8LVe22FeiwcacXabCvNpoEXfu4I,56
38
- meeting_noter-0.5.1.dist-info/top_level.txt,sha256=9Tuq04_0SXM0OXOHVbOHkHkB5tG3fqkrMrfzCMpbLpY,14
39
- meeting_noter-0.5.1.dist-info/RECORD,,
34
+ meeting_noter/transcription/engine.py,sha256=G9NcSS6Q-UhW7PlQ0E85hQXn6BWao64nIvyw4NR2yxI,7208
35
+ meeting_noter-0.6.1.dist-info/METADATA,sha256=KCJbo3BWGaFyysbFWkUDi7SMsd-65P_fNLCmiNAFOVk,6741
36
+ meeting_noter-0.6.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
37
+ meeting_noter-0.6.1.dist-info/entry_points.txt,sha256=rKNhzjSF5-e3bLRr8LVe22FeiwcacXabCvNpoEXfu4I,56
38
+ meeting_noter-0.6.1.dist-info/top_level.txt,sha256=9Tuq04_0SXM0OXOHVbOHkHkB5tG3fqkrMrfzCMpbLpY,14
39
+ meeting_noter-0.6.1.dist-info/RECORD,,