personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix macOS observer startup and refactor segment finalization

- Add missing Path import in screencapture.py that caused NameError on startup
- Extract shared finalization logic into _finalize_segment() helper method
- Move Path imports to module level in observer.py (was duplicated in 3 functions)
- Update TODO.md and JOURNAL.md to document correct file naming and macOS formats

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+97 -126
+11 -6
docs/JOURNAL.md
··· 535 535 536 536 #### Audio captures 537 537 538 - Audio files are initially written to the day root with the segment key prefix: 538 + Audio files are initially written to the day root with the segment key prefix (Linux) or directly to segment folders (macOS): 539 539 540 - - `HHMMSS_LEN_*.flac` – audio files in day root (e.g., `143022_300_audio.flac`) 540 + - **Linux**: `HHMMSS_LEN_*.flac` – audio files in day root (e.g., `143022_300_audio.flac`) 541 + - **macOS**: `HHMMSS_LEN/audio.m4a` – audio files written directly to segment folder 541 542 542 543 After transcription, audio files are moved into their segment folder: 543 544 544 - - `HHMMSS_LEN/*.flac` – audio files moved here after processing, preserving descriptive suffix (e.g., `audio.flac`, `mic.flac`) 545 + - `HHMMSS_LEN/*.flac` or `*.m4a` – audio files moved here after processing, preserving descriptive suffix (e.g., `audio.flac`, `audio.m4a`, `mic.flac`) 545 546 546 547 Note: The descriptive portion after the segment key (e.g., `_audio`, `_recording`) is preserved when files are moved into segment directories. Processing tools match files by extension only, ignoring the descriptive suffix. 547 548 548 549 #### Screen captures 549 550 550 - Screen recordings use per-monitor files with position and connector in the filename: 551 + Screen recordings use per-monitor files with position and connector/displayID in the filename: 552 + 553 + - **Linux**: `HHMMSS_LEN_<position>_<connector>_screen.webm` – screencast video files in day root (e.g., `143022_300_center_DP-3_screen.webm`) 554 + - **macOS**: `HHMMSS_LEN/<position>_<displayID>_screen.mov` – video files written directly to segment folder (e.g., `center_1_screen.mov`) 551 555 552 - - `HHMMSS_LEN_<position>_<connector>_screen.webm` – screencast video files in day root (e.g., `143022_300_center_DP-3_screen.webm`) 553 - - `HHMMSS_LEN/<position>_<connector>_screen.webm` – video files moved here after analysis (e.g., `center_DP-3_screen.webm`) 556 + After analysis, files are in their segment folder: 557 + 558 + - `HHMMSS_LEN/<position>_<connector>_screen.webm` or `*.mov` – video files (e.g., `center_DP-3_screen.webm`, `center_1_screen.mov`) 554 559 555 560 For multi-monitor setups, each monitor produces a separate file. Position labels include: `center`, `left`, `right`, `top`, `bottom`, and combinations like `left-top`. 556 561
+6 -3
observe/macos/TODO.md
··· 60 60 ## Reference 61 61 62 62 ### File Naming Convention 63 - - **Video**: `HHMMSS_LEN_position_displayID_screen.mov` (e.g., `120000_300_center_1_screen.mov`) 64 - - **Audio**: `HHMMSS_LEN_audio.m4a` (e.g., `120000_300_audio.m4a`) 65 - - **Temp files**: `.HHMMSS_displayID.mov`, `.HHMMSS.m4a` (hidden during capture) 63 + 64 + Files are stored in segment directories: `YYYYMMDD/HHMMSS_LEN/` 65 + 66 + - **Video**: `position_displayID_screen.mov` (e.g., `center_1_screen.mov`) 67 + - **Audio**: `audio.m4a` 68 + - **Draft folder**: `HHMMSS_draft/` during capture, renamed to `HHMMSS_LEN/` on completion 66 69 67 70 ### Differences from Linux Observer 68 71 - **Audio threshold**: macOS checks at boundary (post-capture), Linux checks real-time
+79 -117
observe/macos/observer.py
··· 18 18 import signal 19 19 import sys 20 20 import time 21 + from pathlib import Path 21 22 22 23 import av 23 24 import numpy as np ··· 212 213 if container is not None: 213 214 container.close() 214 215 215 - def handle_boundary(self, is_active: bool): 216 + def _finalize_segment(self) -> tuple[str, str, list[str]]: 216 217 """ 217 - Handle window boundary rollover. 218 + Finalize current segment by renaming files and folder. 218 219 219 - Closes the current draft folder, renames files to simple names, 220 - renames folder to final segment name, and emits the observing event. 220 + Renames video files to simple names, checks audio threshold and 221 + renames/deletes accordingly, then renames draft folder to final name. 221 222 222 - Args: 223 - is_active: Whether system is currently active 223 + Returns: 224 + Tuple of (date_part, segment_key, saved_files) 224 225 """ 225 - from pathlib import Path 226 - 227 226 # Get timestamp parts for this window and calculate duration 228 227 date_part, time_part = self.get_timestamp_parts(self.start_at) 229 228 duration = int(time.time() - self.start_at) ··· 232 231 233 232 saved_files: list[str] = [] 234 233 235 - if self.capture_running: 236 - logger.info("Stopping previous capture") 237 - self.screencapture.stop() 238 - self.capture_running = False 234 + # Rename video files to simple names in draft folder 235 + for display in self.current_displays: 236 + if os.path.exists(display.file_path): 237 + # Simple name: position_displayID_screen.mov 238 + simple_name = f"{display.position}_{display.display_id}_screen.mov" 239 + simple_path = Path(self.draft_dir) / simple_name 240 + try: 241 + os.rename(display.file_path, simple_path) 242 + saved_files.append(simple_name) 243 + except OSError as e: 244 + logger.error(f"Failed to rename {display.file_path}: {e}") 239 245 240 - # Rename video files to simple names in draft folder 241 - for display in self.current_displays: 242 - if os.path.exists(display.file_path): 243 - # Simple name: position_displayID_screen.mov 244 - simple_name = f"{display.position}_{display.display_id}_screen.mov" 245 - simple_path = Path(self.draft_dir) / simple_name 246 - try: 247 - os.rename(display.file_path, simple_path) 248 - saved_files.append(simple_name) 249 - except OSError as e: 250 - logger.error(f"Failed to rename {display.file_path}: {e}") 246 + # Check audio threshold and rename if passing 247 + if self.current_audio and os.path.exists(self.current_audio.file_path): 248 + if self._check_audio_threshold(self.current_audio.file_path): 249 + simple_name = "audio.m4a" 250 + simple_path = Path(self.draft_dir) / simple_name 251 + try: 252 + os.rename(self.current_audio.file_path, simple_path) 253 + saved_files.append(simple_name) 254 + logger.info(f"Audio passed threshold check, saving: {simple_name}") 255 + except OSError as e: 256 + logger.error(f"Failed to rename audio: {e}") 257 + else: 258 + # Delete the audio file 259 + try: 260 + os.remove(self.current_audio.file_path) 261 + logger.info("Audio below threshold, discarded") 262 + except OSError as e: 263 + logger.warning(f"Failed to remove audio file: {e}") 251 264 252 - # Check audio threshold and rename if passing 253 - if self.current_audio and os.path.exists(self.current_audio.file_path): 254 - if self._check_audio_threshold(self.current_audio.file_path): 255 - simple_name = "audio.m4a" 256 - simple_path = Path(self.draft_dir) / simple_name 257 - try: 258 - os.rename(self.current_audio.file_path, simple_path) 259 - saved_files.append(simple_name) 260 - logger.info( 261 - f"Audio passed threshold check, saving: {simple_name}" 262 - ) 263 - except OSError as e: 264 - logger.error(f"Failed to rename audio: {e}") 265 - else: 266 - # Delete the audio file 267 - try: 268 - os.remove(self.current_audio.file_path) 269 - logger.info("Audio below threshold, discarded") 270 - except OSError as e: 271 - logger.warning(f"Failed to remove audio file: {e}") 272 - 273 - # Clear state 274 - self.current_displays = [] 275 - self.current_audio = None 265 + # Clear capture state 266 + self.current_displays = [] 267 + self.current_audio = None 276 268 277 269 # Rename draft folder to final segment name (atomic handoff) 278 270 if self.draft_dir and saved_files: 279 271 final_segment_dir = str(day_dir / segment_key) 280 272 try: 281 273 os.rename(self.draft_dir, final_segment_dir) 282 - logger.info( 283 - f"Segment finalized: {self.draft_dir} -> {final_segment_dir}" 284 - ) 274 + logger.info(f"Segment finalized: {segment_key}") 285 275 except OSError as e: 286 276 logger.error(f"Failed to rename draft folder: {e}") 287 277 saved_files = [] # Don't emit event if rename failed 288 - elif self.draft_dir and not saved_files: 278 + elif self.draft_dir: 289 279 # No files to save, remove empty draft folder 290 280 try: 291 281 os.rmdir(self.draft_dir) ··· 295 285 296 286 self.draft_dir = None 297 287 288 + return date_part, segment_key, saved_files 289 + 290 + def handle_boundary(self, is_active: bool): 291 + """ 292 + Handle window boundary rollover. 293 + 294 + Closes the current draft folder, renames files to simple names, 295 + renames folder to final segment name, and emits the observing event. 296 + 297 + Args: 298 + is_active: Whether system is currently active 299 + """ 300 + date_part = "" 301 + segment_key = "" 302 + saved_files: list[str] = [] 303 + 304 + if self.capture_running: 305 + logger.info("Stopping previous capture") 306 + self.screencapture.stop() 307 + self.capture_running = False 308 + 309 + # Finalize segment (rename files and folder) 310 + date_part, segment_key, saved_files = self._finalize_segment() 311 + 298 312 # Reset timing for new window 299 313 self.start_at = time.time() 300 314 self.start_at_mono = time.monotonic() ··· 347 361 Returns: 348 362 True if capture started successfully, False otherwise 349 363 """ 350 - from pathlib import Path 351 364 352 365 # Create draft folder for this segment 353 366 draft_path = self._create_draft_folder() ··· 518 531 519 532 async def shutdown(self): 520 533 """Clean shutdown of observer.""" 521 - from pathlib import Path 522 534 523 535 # Stop capture if running 524 536 if self.capture_running: 525 537 logger.info("Stopping capture for shutdown") 526 538 self.screencapture.stop() 539 + self.capture_running = False 527 540 528 541 # Brief delay for files to be written 529 542 await asyncio.sleep(0.5) 530 543 531 - # Get timestamp parts for finalization 532 - date_part, time_part = self.get_timestamp_parts(self.start_at) 533 - duration = int(time.time() - self.start_at) 534 - day_dir = day_path(date_part) 535 - segment_key = f"{time_part}_{duration}" 536 - 537 - saved_files: list[str] = [] 538 - 539 - # Rename video files to simple names in draft folder 540 - for display in self.current_displays: 541 - if os.path.exists(display.file_path): 542 - simple_name = f"{display.position}_{display.display_id}_screen.mov" 543 - simple_path = Path(self.draft_dir) / simple_name 544 - try: 545 - os.rename(display.file_path, simple_path) 546 - saved_files.append(simple_name) 547 - except OSError as e: 548 - logger.error(f"Failed to rename {display.file_path}: {e}") 544 + # Finalize segment (rename files and folder) 545 + date_part, segment_key, saved_files = self._finalize_segment() 549 546 550 - # Check and rename audio if threshold met 551 - if self.current_audio and os.path.exists(self.current_audio.file_path): 552 - if self._check_audio_threshold(self.current_audio.file_path): 553 - simple_name = "audio.m4a" 554 - simple_path = Path(self.draft_dir) / simple_name 555 - try: 556 - os.rename(self.current_audio.file_path, simple_path) 557 - saved_files.append(simple_name) 558 - except OSError as e: 559 - logger.error(f"Failed to rename audio: {e}") 560 - else: 561 - try: 562 - os.remove(self.current_audio.file_path) 563 - logger.info("Final audio below threshold, discarded") 564 - except OSError: 565 - pass 566 - 567 - # Rename draft folder to final segment name 568 - if self.draft_dir and saved_files: 569 - final_segment_dir = str(day_dir / segment_key) 570 - try: 571 - os.rename(self.draft_dir, final_segment_dir) 572 - logger.info(f"Segment finalized: {segment_key}") 573 - 574 - # Emit observing event for final segment 575 - if self.callosum: 576 - self.callosum.emit( 577 - "observe", 578 - "observing", 579 - day=date_part, 580 - segment=segment_key, 581 - files=saved_files, 582 - host=HOST, 583 - platform=PLATFORM, 584 - ) 585 - except OSError as e: 586 - logger.error(f"Failed to rename draft folder: {e}") 587 - elif self.draft_dir: 588 - # No files, clean up draft folder 589 - try: 590 - os.rmdir(self.draft_dir) 591 - except OSError: 592 - pass 593 - 594 - self.draft_dir = None 595 - self.capture_running = False 547 + # Emit observing event for final segment 548 + if saved_files and self.callosum: 549 + self.callosum.emit( 550 + "observe", 551 + "observing", 552 + day=date_part, 553 + segment=segment_key, 554 + files=saved_files, 555 + host=HOST, 556 + platform=PLATFORM, 557 + ) 596 558 597 559 # Stop Callosum connection 598 560 if self.callosum:
+1
observe/macos/screencapture.py
··· 16 16 import threading 17 17 import time 18 18 from dataclasses import dataclass 19 + from pathlib import Path 19 20 from typing import Optional 20 21 21 22 from observe.utils import assign_monitor_positions