personal memory agent
0
fork

Configure Feed

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

refactor: rename 'period' to 'segment' throughout codebase

Replaces all uses of "period" (referring to HHMMSS_LEN timestamped
duration folders) with "segment" for better clarity. The term "segment"
more accurately describes these discrete time-bounded recording windows.

Changes:
- Rename core functions: period_key() → segment_key(), period_parse() → segment_parse()
- Update all CLI tools: --period → --segment flag
- Update Callosum event protocol: period field → segment field
- Update documentation: JOURNAL.md and CALLOSUM.md formal definitions
- Rename all internal variables, methods, and test fixtures
- Preserve unrelated uses: "periodic" (frequency), "trailing period" (punctuation)

All 438 tests passing. No functional changes.

+483 -483
+3 -3
CALLOSUM.md
··· 58 58 - `status`: Periodic state (every 5s, only when active) 59 59 - From `observer.py`: `screencast`, `audio`, `activity` - Live capture state 60 60 - From `sense.py`: `describe`, `transcribe` - Processing pipeline state 61 - - `observing`: `period`, `files` - Recording window boundary crossed with saved files 61 + - `observing`: `segment`, `files` - Recording window boundary crossed with saved files 62 62 - `detected`: `file`, `handler`, `ref` - File detected and handler spawned 63 63 - `described`/`transcribed`/`reduced`: `input`, `output`, `duration_ms` - Processing complete 64 - - `observed`: `period`, `duration` - All files for period fully processed 64 + - `observed`: `segment`, `duration` - All files for segment fully processed 65 65 **Purpose:** Track observation pipeline from live capture state through processing completion 66 66 **Path Format:** Relative to `JOURNAL_PATH` (e.g., `20251102/163045_screen.webm`, `20251102/seen/163045_screen.webm`) 67 - **Correlation:** `detected.ref` matches `logs.exec.ref` for the same handler process; `observed.period` groups all files from same capture window 67 + **Correlation:** `detected.ref` matches `logs.exec.ref` for the same handler process; `observed.segment` groups all files from same capture window 68 68 69 69 ### `importer` - Media import and transcription processing 70 70 **Source:** `think/importer.py`
+7 -7
JOURNAL.md
··· 1 1 # Sunstone Journal Guide 2 2 3 - This document describes the layout of a **journal** directory where all audio, screen and analysis artifacts are stored. Each dated `YYYYMMDD` folder is referred to as a **day**, and within each day captured content is organized into **periods** (timestamped duration folders). Each period folder uses the format `HHMMSS_LEN/` where `HHMMSS` is the start time and `LEN` is the duration in seconds. This folder name serves as the **period key**, uniquely identifying the period within a given day. 3 + This document describes the layout of a **journal** directory where all audio, screen and analysis artifacts are stored. Each dated `YYYYMMDD` folder is referred to as a **day**, and within each day captured content is organized into **segments** (timestamped duration folders). Each segment folder uses the format `HHMMSS_LEN/` where `HHMMSS` is the start time and `LEN` is the duration in seconds. This folder name serves as the **segment key**, uniquely identifying the segment within a given day. 4 4 5 5 ## Top level files 6 6 ··· 405 405 406 406 ## Day folder contents 407 407 408 - Within each day, captured content is organized into **periods** (timestamped duration folders). The folder name is the **period key**, which uniquely identifies the period within the day and follows this format: 408 + Within each day, captured content is organized into **segments** (timestamped duration folders). The folder name is the **segment key**, which uniquely identifies the segment within the day and follows this format: 409 409 410 - - `HHMMSS_LEN/` – Start time and duration in seconds (e.g., `143022_300/` for a 5-minute period starting at 14:30:22) 410 + - `HHMMSS_LEN/` – Start time and duration in seconds (e.g., `143022_300/` for a 5-minute segment starting at 14:30:22) 411 411 412 412 Audio capture tools write FLAC files and transcripts: 413 413 414 - - `HHMMSS_LEN_*.flac` – audio files in day root (e.g., `143022_300_audio.flac`), moved to period after transcription. 414 + - `HHMMSS_LEN_*.flac` – audio files in day root (e.g., `143022_300_audio.flac`), moved to segment after transcription. 415 415 - `HHMMSS_LEN/*.flac` – audio files moved here after processing, preserving descriptive suffix (e.g., `audio.flac`, `mic.flac`). 416 416 - `HHMMSS_LEN/audio.jsonl` – transcript JSONL produced by transcription. 417 417 418 - Note: The descriptive portion after the period (e.g., `_audio`, `_recording`) is preserved when files are moved into period directories. Processing tools match files by extension only, ignoring the descriptive suffix. 418 + Note: The descriptive portion after the segment (e.g., `_audio`, `_recording`) is preserved when files are moved into segment directories. Processing tools match files by extension only, ignoring the descriptive suffix. 419 419 420 420 ### Audio transcript output 421 421 ··· 447 447 448 448 Screen capture produces screencast videos with multi-monitor metadata: 449 449 450 - - `HHMMSS_LEN_*.webm` – screencast video files in day root (e.g., `143022_300_screen.webm`), moved to period after analysis. 450 + - `HHMMSS_LEN_*.webm` – screencast video files in day root (e.g., `143022_300_screen.webm`), moved to segment after analysis. 451 451 - `HHMMSS_LEN/*.webm` – video files moved here after analysis, preserving descriptive suffix (e.g., `screen.webm`, `monitor1.webm`). 452 452 - `HHMMSS_LEN/screen.jsonl` – vision analysis results in JSON Lines format. 453 453 - `HHMMSS_LEN/screen.md` – human-readable markdown summary of the video. 454 454 455 - Note: Like audio files, the descriptive portion is preserved when files are moved into period directories. 455 + Note: Like audio files, the descriptive portion is preserved when files are moved into segment directories. 456 456 457 457 ### Screencast video format 458 458
+25 -25
apps/calendar/routes.py
··· 152 152 153 153 if not re.fullmatch(DATE_RE.pattern, day): 154 154 return "", 404 155 - from think.utils import period_key 155 + from think.utils import segment_key 156 156 157 157 start = request.args.get("start", "") 158 158 end = request.args.get("end", "") 159 - if not period_key(start) or not period_key(end): 159 + if not segment_key(start) or not segment_key(end): 160 160 return "", 400 161 161 162 162 # Get checkbox states from query params ··· 224 224 225 225 if not re.fullmatch(DATE_RE.pattern, day): 226 226 return "", 404 227 - from think.utils import period_key 227 + from think.utils import segment_key 228 228 229 229 start = request.args.get("start", "") 230 230 end = request.args.get("end", "") 231 - if not period_key(start) or not period_key(end): 231 + if not segment_key(start) or not segment_key(end): 232 232 return "", 400 233 233 234 234 file_type = request.args.get("type", None) # 'audio', 'screen', or None for both ··· 280 280 281 281 if not re.fullmatch(DATE_RE.pattern, day): 282 282 return "", 404 283 - from think.utils import period_key 283 + from think.utils import segment_key 284 284 285 285 start = request.args.get("start", "") 286 286 end = request.args.get("end", "") 287 - if not period_key(start) or not period_key(end): 287 + if not segment_key(start) or not segment_key(end): 288 288 return "", 400 289 289 290 290 file_type = request.args.get("type", None) # 'audio', 'screen', or None for both ··· 387 387 if not re.fullmatch(DATE_RE.pattern, day): 388 388 return "", 404 389 389 390 - from think.utils import period_key 390 + from think.utils import segment_key 391 391 392 392 start = request.args.get("start", "") 393 393 end = request.args.get("end", "") 394 - if not period_key(start) or not period_key(end): 394 + if not segment_key(start) or not segment_key(end): 395 395 return "", 400 396 396 397 397 import subprocess ··· 654 654 """Render detail view for a specific screen.jsonl file.""" 655 655 if not re.fullmatch(DATE_RE.pattern, day): 656 656 return "", 404 657 - from think.utils import period_key 657 + from think.utils import segment_key 658 658 659 - if not period_key(timestamp): 659 + if not segment_key(timestamp): 660 660 return "", 404 661 661 662 662 day_dir = str(day_path(day)) 663 663 if not os.path.isdir(day_dir): 664 664 return "", 404 665 665 666 - # Check if the screen.jsonl file exists in period 667 - period_dir = os.path.join(day_dir, timestamp) 668 - jsonl_path = os.path.join(period_dir, "screen.jsonl") 666 + # Check if the screen.jsonl file exists in segment 667 + segment_dir = os.path.join(day_dir, timestamp) 668 + jsonl_path = os.path.join(segment_dir, "screen.jsonl") 669 669 if not os.path.isfile(jsonl_path): 670 670 return "", 404 671 671 ··· 694 694 if not os.path.isdir(day_dir): 695 695 return jsonify({"files": []}) 696 696 697 - from think.utils import period_key 697 + from think.utils import segment_key 698 698 699 699 files = [] 700 - # Look for periods (HHMMSS/) 700 + # Look for segments (HHMMSS/) 701 701 for item in sorted(os.listdir(day_dir)): 702 702 item_path = os.path.join(day_dir, item) 703 - if os.path.isdir(item_path) and period_key(item): 704 - # Found period, check for screen.jsonl 703 + if os.path.isdir(item_path) and segment_key(item): 704 + # Found segment, check for screen.jsonl 705 705 jsonl_path = os.path.join(item_path, "screen.jsonl") 706 706 if os.path.isfile(jsonl_path): 707 707 timestamp = item ··· 744 744 """Return all frame records and pre-cache decoded frames from video.""" 745 745 if not re.fullmatch(DATE_RE.pattern, day): 746 746 return "", 404 747 - from think.utils import period_key 747 + from think.utils import segment_key 748 748 749 - if not period_key(timestamp): 749 + if not segment_key(timestamp): 750 750 return "", 404 751 751 752 752 day_dir = str(day_path(day)) 753 - period_dir = os.path.join(day_dir, timestamp) 754 - jsonl_path = os.path.join(period_dir, "screen.jsonl") 753 + segment_dir = os.path.join(day_dir, timestamp) 754 + jsonl_path = os.path.join(segment_dir, "screen.jsonl") 755 755 756 756 if not os.path.isfile(jsonl_path): 757 757 return "", 404 ··· 774 774 # Decode and cache all frames from the video 775 775 cache_key = (day, timestamp) 776 776 if cache_key not in _frame_cache and raw_video_path: 777 - # Video path is relative to period directory (e.g., "screen.webm") 778 - video_path = os.path.join(period_dir, raw_video_path) 777 + # Video path is relative to segment directory (e.g., "screen.webm") 778 + video_path = os.path.join(segment_dir, raw_video_path) 779 779 if os.path.isfile(video_path): 780 780 # Use the new decode_frames utility 781 781 images = decode_frames(video_path, frames, annotate_boxes=True) ··· 800 800 """Serve a cached frame image as JPEG.""" 801 801 if not re.fullmatch(DATE_RE.pattern, day): 802 802 return "", 404 803 - from think.utils import period_key 803 + from think.utils import segment_key 804 804 805 - if not period_key(timestamp): 805 + if not segment_key(timestamp): 806 806 return "", 404 807 807 808 808 try:
+9 -9
muse/agents/default.txt
··· 7 7 ### Tool: `search_summaries` 8 8 **Purpose**: Searches pre-processed topic summaries representing key themes and subjects across journal entries 9 9 **Returns**: Topic summaries with metadata including day, topic name, and relevance scores 10 - **Use when**: Looking for general themes, concepts, or patterns across time periods 10 + **Use when**: Looking for general themes, concepts, or patterns across time segments 11 11 12 12 ### Tool: `search_transcripts` 13 13 **Purpose**: Searches raw audio transcripts and screen diffs for specific days to find exact matches to help zoom in on relevant time segments ··· 43 43 - `time`: HHMMSS format (start time) 44 44 - `length`: Duration in minutes 45 45 **Returns**: Markdown-formatted raw transcripts organized in 5-minute intervals 46 - **Use when**: You need to examine detailed activity during a specific time period, particularly useful for reconstructing exact sequences of events 46 + **Use when**: You need to examine detailed activity during a specific time segment, particularly useful for reconstructing exact sequences of events 47 47 48 48 ### Resource vs Tool Selection 49 49 ··· 61 61 62 62 ### Resource Usage Strategy 63 63 64 - 1. **Discovery First**: Use search tools to identify relevant topics, days, and time periods 64 + 1. **Discovery First**: Use search tools to identify relevant topics, days, and time segments 65 65 2. **Deep Dive**: Use resources to retrieve complete data for identified items 66 66 3. **Comprehensive Analysis**: Combine multiple resource calls to build complete pictures 67 67 ··· 75 75 76 76 ### Important Notes 77 77 - Transcript resources can return large amounts of data; be mindful of context windows 78 - - Start with shorter time ranges (15-30 minutes) unless you need longer periods 78 + - Start with shorter time ranges (15-30 minutes) unless you need longer segments 79 79 - Resources provide unfiltered access - process and summarize appropriately for users 80 80 81 81 ## Decision Framework ··· 92 92 **Use search_summaries when:** 93 93 - Query asks about general themes, concepts, or patterns (e.g., "my thoughts on AI", "team meetings") 94 94 - No specific date is mentioned and you need to discover when topics occurred 95 - - Looking for high-level summaries or overviews across time periods 95 + - Looking for high-level summaries or overviews across time segments 96 96 - Starting a multi-step search to identify relevant days before deep diving 97 97 98 98 **Use search_transcripts when:** 99 - - Query mentions specific dates or time periods (e.g., "yesterday", "January 2024", "last Monday") 99 + - Query mentions specific dates or time segments (e.g., "yesterday", "January 2024", "last Monday") 100 100 - Looking for exact phrases, quotes, or specific events 101 101 - Need to find precise details like error messages, names, or numbers 102 102 - Following up on topics found via search_topic to get original context 103 103 104 104 **Use search_events when:** 105 - - You've identified specific relevant time periods through searching 105 + - You've identified specific relevant time segments through searching 106 106 - Query requests information about specific activities, meetings, or notable events 107 107 - Need to understand structured event data with timestamps and context 108 108 - Looking for occurrences of particular types of activities across the journal ··· 155 155 5. **Additional Context**: Related findings that might be helpful 156 156 157 157 ### For Pattern Analysis 158 - - Group findings by theme or time period 158 + - Group findings by theme or time segment 159 159 - Highlight trends or changes over time 160 160 - Note frequency of topic mentions 161 161 - Identify connections between related topics ··· 188 188 When timeframe matters: 189 189 1. Pay attention to chronological patterns in search results 190 190 2. Note evolution of topics over time 191 - 3. Identify key dates or periods of intense activity on specific topics 191 + 3. Identify key dates or segments of intense activity on specific topics 192 192 193 193 ## Response Optimization 194 194
+22 -22
observe/describe.py
··· 510 510 contents.append(image) 511 511 return contents 512 512 513 - def _move_to_period(self, media_path: Path) -> Path: 514 - """Move media file to its period and return new path.""" 513 + def _move_to_segment(self, media_path: Path) -> Path: 514 + """Move media file to its segment and return new path.""" 515 515 from observe.utils import extract_descriptive_suffix 516 - from think.utils import period_key 516 + from think.utils import segment_key 517 517 518 - period = period_key(media_path.stem) 519 - if period is None: 518 + segment = segment_key(media_path.stem) 519 + if segment is None: 520 520 raise ValueError(f"Invalid media filename: {media_path.stem}") 521 521 suffix = extract_descriptive_suffix(media_path.stem) 522 - period_dir = media_path.parent / period 522 + segment_dir = media_path.parent / segment 523 523 try: 524 - period_dir.mkdir(exist_ok=True) 524 + segment_dir.mkdir(exist_ok=True) 525 525 # Preserve the original extension 526 526 ext = media_path.suffix 527 - new_path = period_dir / f"{suffix}{ext}" 527 + new_path = segment_dir / f"{suffix}{ext}" 528 528 media_path.rename(new_path) 529 - logger.info(f"Moved {media_path} to {period_dir}") 529 + logger.info(f"Moved {media_path} to {segment_dir}") 530 530 return new_path 531 531 except Exception as exc: 532 - logger.error(f"Failed to move {media_path} to period: {exc}") 532 + logger.error(f"Failed to move {media_path} to segment: {exc}") 533 533 return media_path 534 534 535 535 def _create_crumb( ··· 860 860 all_failed = total_frames > 0 and failed_frames == total_frames 861 861 862 862 if all_failed: 863 - # Don't move video to period - leave for retry 863 + # Don't move video to segment - leave for retry 864 864 error_detail = ( 865 865 f"Error details in {output_path}" if output_path else "No output file" 866 866 ) ··· 875 875 f"All {total_frames} frame(s) failed vision analysis after retries" 876 876 ) 877 877 else: 878 - # At least some frames succeeded - move to period and create crumb 878 + # At least some frames succeeded - move to segment and create crumb 879 879 if failed_frames > 0: 880 880 logger.warning( 881 881 f"{failed_frames}/{total_frames} frame(s) failed processing. " 882 - f"Moving video to period anyway." 882 + f"Moving video to segment anyway." 883 883 ) 884 884 if output_path: 885 - moved_path = self._move_to_period(self.video_path) 885 + moved_path = self._move_to_segment(self.video_path) 886 886 self._create_crumb(output_path, moved_path, used_prompts, used_models) 887 887 888 888 # Clear qualified_frames to free memory ··· 964 964 # Determine output path and warn if overwriting 965 965 output_path = None 966 966 if not args.frames_only: 967 - # Extract period and create output in period 968 - from think.utils import period_key 967 + # Extract segment and create output in segment 968 + from think.utils import segment_key 969 969 970 - period = period_key(video_path.stem) 971 - if period is None: 970 + segment = segment_key(video_path.stem) 971 + if segment is None: 972 972 parser.error( 973 973 f"Invalid video filename: {video_path.stem} (must start with HHMMSS)" 974 974 ) 975 - period_dir = video_path.parent / period 976 - period_dir.mkdir(exist_ok=True) 977 - output_path = period_dir / "screen.jsonl" 975 + segment_dir = video_path.parent / segment 976 + segment_dir.mkdir(exist_ok=True) 977 + output_path = segment_dir / "screen.jsonl" 978 978 if output_path.exists(): 979 979 logger.warning(f"Overwriting existing analysis file: {output_path}") 980 980 ··· 1001 1001 # Emit completion event 1002 1002 if output_path and output_path.exists(): 1003 1003 journal_path = Path(os.getenv("JOURNAL_PATH", "")) 1004 - # Moved path is in period: YYYYMMDD/HHMMSS/screen.webm 1004 + # Moved path is in segment: YYYYMMDD/HHMMSS/screen.webm 1005 1005 time_part = video_path.stem.split("_")[0] 1006 1006 moved_path = video_path.parent / time_part / "screen.webm" 1007 1007
+4 -4
observe/gnome/active.py
··· 49 49 day_dir = day_path() # Uses today by default, creates if needed, returns Path 50 50 if not day_dir.exists(): 51 51 return False 52 - from think.utils import period_key 52 + from think.utils import segment_key 53 53 54 54 cutoff = time.time() - window 55 - # Check periods (HHMMSS/) 55 + # Check segments (HHMMSS/) 56 56 for item in os.listdir(day_dir): 57 57 item_path = day_dir / item 58 - if item_path.is_dir() and period_key(item): 59 - # Found period, check for audio files 58 + if item_path.is_dir() and segment_key(item): 59 + # Found segment, check for audio files 60 60 for audio_file in item_path.glob("*audio.jsonl"): 61 61 try: 62 62 if os.path.getmtime(audio_file) >= cutoff:
+4 -4
observe/gnome/observer.py
··· 4 4 5 5 Continuously captures audio and manages screencast recording based on activity. 6 6 Creates 5-minute windows, saving audio if voice activity detected and recording 7 - screencasts during active periods. 7 + screencasts during active segments. 8 8 """ 9 9 10 10 import argparse ··· 209 209 files.append(f"{time_part}_{duration}_screen.webm") 210 210 211 211 if files: 212 - period = f"{time_part}_{duration}" 212 + segment = f"{time_part}_{duration}" 213 213 self.callosum.emit( 214 214 "observe", 215 215 "observing", 216 - period=period, 216 + segment=segment, 217 217 files=files, 218 218 ) 219 - logger.info(f"Period observing: {period} ({len(files)} files)") 219 + logger.info(f"Segment observing: {segment} ({len(files)} files)") 220 220 221 221 async def initialize_screencast(self) -> bool: 222 222 """
+4 -4
observe/hear.py
··· 344 344 345 345 The JSONL format has metadata as the first line (may be empty {}) 346 346 and transcript entries as subsequent lines. Handles both native 347 - transcripts (period/audio.jsonl) and imported transcripts (period/imported_audio.jsonl). 347 + transcripts (segment/audio.jsonl) and imported transcripts (segment/imported_audio.jsonl). 348 348 349 349 Args: 350 350 file_path: Path to the JSONL transcript file ··· 476 476 for i, part in enumerate(reversed(parts)): 477 477 if re.match(r"^\d{8}$", part): 478 478 day_str = part 479 - # Check if previous part (parent dir) is HHMMSS period 479 + # Check if previous part (parent dir) is HHMMSS segment 480 480 if i > 0: 481 - from think.utils import period_parse 481 + from think.utils import segment_parse 482 482 483 483 prev_part = list(reversed(parts))[i - 1] 484 - start_time, _ = period_parse(prev_part) 484 + start_time, _ = segment_parse(prev_part) 485 485 break 486 486 487 487 # Build header line
+1 -1
observe/macos/TODO.md
··· 132 132 - [ ] If active and screen not locked: 133 133 - Call `initialize_capture()` 134 134 - [ ] Build list of files that were captured 135 - - [ ] Emit Callosum event: `self.callosum.emit("observe", "observing", period="...", files=[...])` 135 + - [ ] Emit Callosum event: `self.callosum.emit("observe", "observing", segment="...", files=[...])` 136 136 - [ ] Log boundary handling 137 137 138 138 ### 3.4 Implement `initialize_capture()`
+10 -10
observe/reduce.py
··· 65 65 lines.append("# Frame Analyses") 66 66 lines.append("") 67 67 68 - # Extract base timestamp from period directory (HHMMSS) 68 + # Extract base timestamp from segment directory (HHMMSS) 69 69 # Expected structure: YYYYMMDD/HHMMSS/screen.jsonl 70 70 base_hour = base_minute = base_second = 0 71 71 if video_path: 72 72 try: 73 - from think.utils import period_parse 73 + from think.utils import segment_parse 74 74 75 - # Get period start time from parent directory 76 - start_time, _ = period_parse(video_path.parent.name) 75 + # Get segment start time from parent directory 76 + start_time, _ = segment_parse(video_path.parent.name) 77 77 if start_time: 78 78 base_hour = start_time.hour 79 79 base_minute = start_time.minute ··· 309 309 help="Day in YYYYMMDD format", 310 310 ) 311 311 parser.add_argument( 312 - "--period", 312 + "--segment", 313 313 type=str, 314 314 required=True, 315 - help="Period key (HHMMSS or HHMMSS_LEN)", 315 + help="Segment key (HHMMSS or HHMMSS_LEN)", 316 316 ) 317 317 args = setup_cli(parser) 318 318 ··· 327 327 logger.error(f"Day directory not found: {day_dir}") 328 328 sys.exit(1) 329 329 330 - period_dir = day_dir / args.period 331 - if not period_dir.exists(): 332 - logger.error(f"Period directory not found: {period_dir}") 330 + segment_dir = day_dir / args.segment 331 + if not segment_dir.exists(): 332 + logger.error(f"Segment directory not found: {segment_dir}") 333 333 sys.exit(1) 334 334 335 - jsonl_path = period_dir / "screen.jsonl" 335 + jsonl_path = segment_dir / "screen.jsonl" 336 336 if not jsonl_path.exists(): 337 337 logger.error(f"Analysis file not found: {jsonl_path}") 338 338 sys.exit(1)
+1 -1
observe/reduce.txt
··· 47 47 48 48 Provide a complete and detailed markdown report with: 49 49 50 - - **Clear structure**: Use headers (##, ###) to organize by time periods or major activity shifts 50 + - **Clear structure**: Use headers (##, ###) to organize by time segments or major activity shifts 51 51 - **Narrative flow**: Write in past tense as a natural chronological account 52 52 - **Time attribution**: Include approximate times for major activities or transitions 53 53 - **Relevant excerpts**: Use blockquotes or code blocks for extracted text when meaningful
+56 -56
observe/sense.py
··· 73 73 # Track last status emission time 74 74 self.last_status_emit = 0.0 75 75 76 - # Track period processing: {period_key: {pending_files}} 77 - self.period_files: Dict[str, set[Path]] = {} 78 - # Track period start times: {period_key: start_timestamp} 79 - self.period_start_time: Dict[str, float] = {} 76 + # Track segment processing: {segment_key: {pending_files}} 77 + self.segment_files: Dict[str, set[Path]] = {} 78 + # Track segment start times: {segment_key: start_timestamp} 79 + self.segment_start_time: Dict[str, float] = {} 80 80 81 81 def register(self, pattern: str, handler_name: str, command: List[str]): 82 82 """ ··· 96 96 if file_path.name.startswith("."): 97 97 return None 98 98 99 - # Ignore files in subdirectories (periods, trash/) 99 + # Ignore files in subdirectories (segments, trash/) 100 100 # Expected structure: journal_dir/YYYYMMDD/file.ext (2 parts from journal_dir) 101 101 # Reject: journal_dir/YYYYMMDD/HHMMSS/file.ext (3+ parts from journal_dir) 102 102 try: ··· 120 120 logger.debug(f"File {file_path.name} already being processed") 121 121 return 122 122 123 - # Register file for period tracking 124 - from think.utils import period_key 123 + # Register file for segment tracking 124 + from think.utils import segment_key 125 125 126 - period = period_key(file_path.name) 127 - if period: 128 - if period not in self.period_files: 129 - self.period_files[period] = set() 130 - self.period_start_time[period] = time.time() 131 - self.period_files[period].add(file_path) 126 + segment = segment_key(file_path.name) 127 + if segment: 128 + if segment not in self.segment_files: 129 + self.segment_files[segment] = set() 130 + self.segment_start_time[segment] = time.time() 131 + self.segment_files[segment].add(file_path) 132 132 133 133 # Queue describe requests to ensure only one runs at a time 134 134 if handler_name == "describe": ··· 216 216 exc_info=True, 217 217 ) 218 218 219 - # Check if period is fully observed 220 - self._check_period_observed(handler_proc.file_path) 219 + # Check if segment is fully observed 220 + self._check_segment_observed(handler_proc.file_path) 221 221 else: 222 222 logger.error( 223 223 f"{handler_proc.handler_name} failed for {handler_proc.file_path.name} " ··· 254 254 handler_name, command = handler_info 255 255 self._spawn_handler(next_file, handler_name, command) 256 256 257 - def _check_period_observed(self, file_path: Path): 258 - """Check if all files for this period have completed processing.""" 259 - from think.utils import period_key 257 + def _check_segment_observed(self, file_path: Path): 258 + """Check if all files for this segment have completed processing.""" 259 + from think.utils import segment_key 260 260 261 - period = period_key(file_path.name) 262 - if not period: 261 + segment = segment_key(file_path.name) 262 + if not segment: 263 263 return 264 264 265 265 with self.lock: 266 - if period in self.period_files: 267 - self.period_files[period].discard(file_path) 266 + if segment in self.segment_files: 267 + self.segment_files[segment].discard(file_path) 268 268 269 269 # If no more pending files, emit observed event 270 - if not self.period_files[period]: 270 + if not self.segment_files[segment]: 271 271 # Calculate processing duration 272 - duration = int(time.time() - self.period_start_time[period]) 272 + duration = int(time.time() - self.segment_start_time[segment]) 273 273 274 274 if self.callosum: 275 275 self.callosum.emit( 276 276 "observe", 277 277 "observed", 278 - period=period, 278 + segment=segment, 279 279 duration=duration, 280 280 ) 281 - logger.info(f"Period fully observed: {period} ({duration}s)") 281 + logger.info(f"Segment fully observed: {segment} ({duration}s)") 282 282 283 283 # Cleanup 284 - del self.period_files[period] 285 - del self.period_start_time[period] 284 + del self.segment_files[segment] 285 + del self.segment_start_time[segment] 286 286 287 287 def _run_reduce(self, video_path: Path): 288 288 """Run reduce on the video file after describe completes.""" 289 - from think.utils import period_key 289 + from think.utils import segment_key 290 290 291 - # Extract day and period from video path 291 + # Extract day and segment from video path 292 292 # Expected structure: journal_dir/YYYYMMDD/HHMMSS_LEN_suffix.webm 293 293 day = video_path.parent.name # YYYYMMDD 294 - period = period_key(video_path.stem) # HHMMSS or HHMMSS_LEN 294 + segment = segment_key(video_path.stem) # HHMMSS or HHMMSS_LEN 295 295 296 - if not period: 297 - logger.error(f"Cannot extract period from {video_path.stem}") 296 + if not segment: 297 + logger.error(f"Cannot extract segment from {video_path.stem}") 298 298 return 299 299 300 - cmd = ["observe-reduce", "--day", day, "--period", period] 300 + cmd = ["observe-reduce", "--day", day, "--segment", segment] 301 301 302 302 # Add verbose/debug flags if set 303 303 if self.debug: ··· 305 305 elif self.verbose: 306 306 cmd.append("-v") 307 307 308 - logger.info(f"Running reduce for {day}/{period}") 308 + logger.info(f"Running reduce for {day}/{segment}") 309 309 310 310 # Use unified runner with automatic logging and timeout 311 311 success, exit_code = run_task(cmd, timeout=300) # 5 minute timeout 312 312 313 313 if success: 314 - logger.info(f"Reduce completed successfully for {day}/{period}") 314 + logger.info(f"Reduce completed successfully for {day}/{segment}") 315 315 else: 316 - logger.warning(f"Reduce failed for {day}/{period} (exit code {exit_code})") 316 + logger.warning(f"Reduce failed for {day}/{segment} (exit code {exit_code})") 317 317 318 318 def _handle_file(self, file_path: Path): 319 319 """Route file to appropriate handler.""" ··· 521 521 """Process all matching unprocessed files from a specific day directory. 522 522 523 523 Files are considered unprocessed if the source media file has not been 524 - moved to periods (HHMMSS/). This approach handles incomplete 524 + moved to segments (HHMMSS/). This approach handles incomplete 525 525 processing gracefully by re-running even if output files exist. 526 526 527 527 Also finds JSONL files without corresponding MD files and runs reduce on them. ··· 535 535 logger.error(f"Day directory not found: {day_dir}") 536 536 return 537 537 538 - # Find all matching unprocessed files (not yet moved to periods) 538 + # Find all matching unprocessed files (not yet moved to segments) 539 539 to_process = [] 540 540 for file_path in day_dir.iterdir(): 541 541 if file_path.is_file(): ··· 544 544 handler_name, command = handler_info 545 545 to_process.append((file_path, handler_name, command)) 546 546 547 - # Find incomplete reduces (screen.jsonl files in periods without corresponding screen.md) 548 - for period in day_dir.iterdir(): 549 - from think.utils import period_key 547 + # Find incomplete reduces (screen.jsonl files in segments without corresponding screen.md) 548 + for segment in day_dir.iterdir(): 549 + from think.utils import segment_key 550 550 551 - if period.is_dir() and period_key(period.name): 552 - screen_jsonl = period / "screen.jsonl" 553 - screen_md = period / "screen.md" 551 + if segment.is_dir() and segment_key(segment.name): 552 + screen_jsonl = segment / "screen.jsonl" 553 + screen_md = segment / "screen.md" 554 554 if screen_jsonl.exists() and not screen_md.exists(): 555 555 # Register reduce as a handler task with semantic args 556 556 to_process.append( 557 557 ( 558 558 screen_jsonl, 559 559 "reduce", 560 - ["observe-reduce", "--day", day, "--period", period.name], 560 + ["observe-reduce", "--day", day, "--segment", segment.name], 561 561 ) 562 562 ) 563 563 ··· 620 620 621 621 Returns: 622 622 Dictionary with: 623 - - "processed": List of JSONL output files in periods (HHMMSS/audio.jsonl, HHMMSS/screen.jsonl) 623 + - "processed": List of JSONL output files in segments (HHMMSS/audio.jsonl, HHMMSS/screen.jsonl) 624 624 - "unprocessed": List of unprocessed source media files in day root 625 625 """ 626 - # Find processed output files in periods (HHMMSS/) 627 - from think.utils import period_key 626 + # Find processed output files in segments (HHMMSS/) 627 + from think.utils import segment_key 628 628 629 629 processed = [] 630 - for period in day_dir.iterdir(): 631 - if period.is_dir() and period_key(period.name): 630 + for segment in day_dir.iterdir(): 631 + if segment.is_dir() and segment_key(segment.name): 632 632 # Check for audio.jsonl and split audio files 633 - for audio_file in period.glob("*audio.jsonl"): 634 - processed.append(f"{period.name}/{audio_file.name}") 633 + for audio_file in segment.glob("*audio.jsonl"): 634 + processed.append(f"{segment.name}/{audio_file.name}") 635 635 # Check for screen.jsonl 636 - screen_jsonl = period / "screen.jsonl" 636 + screen_jsonl = segment / "screen.jsonl" 637 637 if screen_jsonl.exists(): 638 - processed.append(f"{period.name}/screen.jsonl") 638 + processed.append(f"{segment.name}/screen.jsonl") 639 639 640 640 processed.sort() 641 641 642 - # Find unprocessed source media (still in day root, not yet moved to periods) 642 + # Find unprocessed source media (still in day root, not yet moved to segments) 643 643 # Match by extension only - any descriptive suffix is allowed 644 644 unprocessed = [] 645 645 unprocessed.extend(sorted(p.name for p in day_dir.glob("*.flac")))
+22 -22
observe/transcribe.py
··· 161 161 162 162 self.vad_model = load_silero_vad() 163 163 164 - def _move_to_period(self, audio_path: Path) -> Path: 165 - """Move audio file to its period and return new path.""" 164 + def _move_to_segment(self, audio_path: Path) -> Path: 165 + """Move audio file to its segment and return new path.""" 166 166 from observe.utils import extract_descriptive_suffix 167 - from think.utils import period_key 167 + from think.utils import segment_key 168 168 169 - period = period_key(audio_path.stem) 170 - if period is None: 169 + segment = segment_key(audio_path.stem) 170 + if segment is None: 171 171 raise ValueError(f"Invalid audio filename: {audio_path.stem}") 172 172 suffix = extract_descriptive_suffix(audio_path.stem) 173 - period_dir = audio_path.parent / period 173 + segment_dir = audio_path.parent / segment 174 174 try: 175 - period_dir.mkdir(exist_ok=True) 176 - new_path = period_dir / f"{suffix}.flac" 175 + segment_dir.mkdir(exist_ok=True) 176 + new_path = segment_dir / f"{suffix}.flac" 177 177 audio_path.rename(new_path) 178 - logging.info("Moved %s to %s", audio_path, period_dir) 178 + logging.info("Moved %s to %s", audio_path, segment_dir) 179 179 return new_path 180 180 except Exception as exc: 181 - logging.error("Failed to move %s to period: %s", audio_path, exc) 181 + logging.error("Failed to move %s to segment: %s", audio_path, exc) 182 182 return audio_path 183 183 184 184 def _process_audio( ··· 431 431 audio_path: Path to the audio file (in day root) 432 432 stream: Optional stream identifier ('mic' or 'sys') for split processing 433 433 """ 434 - from think.utils import period_key 434 + from think.utils import segment_key 435 435 436 - period = period_key(audio_path.stem) 437 - if period is None: 436 + segment = segment_key(audio_path.stem) 437 + if segment is None: 438 438 raise ValueError(f"Invalid audio filename: {audio_path.stem}") 439 - period_dir = audio_path.parent / period 440 - period_dir.mkdir(exist_ok=True) 439 + segment_dir = audio_path.parent / segment 440 + segment_dir.mkdir(exist_ok=True) 441 441 442 - # Generate simple filename within period 442 + # Generate simple filename within segment 443 443 if stream: 444 444 json_name = f"{stream}_audio.jsonl" 445 445 else: 446 446 json_name = "audio.jsonl" 447 447 448 - return period_dir / json_name 448 + return segment_dir / json_name 449 449 450 450 def _transcribe( 451 451 self, ··· 501 501 transcript_items = result[:-1] 502 502 503 503 # Add audio file reference to metadata 504 - # Path is relative to the JSONL file (both in same period directory) 504 + # Path is relative to the JSONL file (both in same segment directory) 505 505 from observe.utils import extract_descriptive_suffix 506 506 507 507 suffix = extract_descriptive_suffix(raw_path.stem) ··· 542 542 logging.info( 543 543 f"Already processed (split), moving to timestamp dir: {raw_path}" 544 544 ) 545 - self._move_to_period(raw_path) 545 + self._move_to_segment(raw_path) 546 546 return 547 547 548 548 # Process audio in split mode ··· 573 573 return 574 574 575 575 if success: 576 - moved_path = self._move_to_period(raw_path) 576 + moved_path = self._move_to_segment(raw_path) 577 577 578 578 # Emit completion events for split mode 579 579 journal_path = Path(os.getenv("JOURNAL_PATH", "")) ··· 603 603 json_path = self._get_json_path(raw_path) 604 604 if json_path.exists(): 605 605 logging.info(f"Already processed, moving to timestamp dir: {raw_path}") 606 - self._move_to_period(raw_path) 606 + self._move_to_segment(raw_path) 607 607 return 608 608 609 609 # Process audio ··· 622 622 # Transcribe 623 623 success = self._transcribe(raw_path, segments) 624 624 if success: 625 - moved_path = self._move_to_period(raw_path) 625 + moved_path = self._move_to_segment(raw_path) 626 626 627 627 # Emit completion event for standard mode 628 628 journal_path = Path(os.getenv("JOURNAL_PATH", ""))
+3 -3
observe/utils.py
··· 12 12 import numpy as np 13 13 from skimage.metrics import structural_similarity as ssim 14 14 15 - from think.utils import period_key 15 + from think.utils import segment_key 16 16 17 17 logger = logging.getLogger(__name__) 18 18 ··· 21 21 """ 22 22 Extract descriptive suffix from media filename. 23 23 24 - Returns the portion after the period (HHMMSS or HHMMSS_LEN), preserving 25 - the descriptive information for the final filename in the period directory. 24 + Returns the portion after the segment (HHMMSS or HHMMSS_LEN), preserving 25 + the descriptive information for the final filename in the segment directory. 26 26 27 27 Parameters 28 28 ----------
+14 -14
tests/conftest.py
··· 183 183 184 184 def scan_day(day_dir): 185 185 # Stub matching real scan_day behavior: 186 - # - "raw": processed files in periods 187 - # - "processed": output JSON files in periods 188 - # - "repairable": source media files in day root without matching period 186 + # - "raw": processed files in segments 187 + # - "processed": output JSON files in segments 188 + # - "repairable": source media files in day root without matching segment 189 189 from pathlib import Path 190 190 191 191 day_path = Path(day_dir) ··· 194 194 repairable_files = [] 195 195 196 196 if day_path.is_dir(): 197 - # Find raw (processed) files in periods (HHMMSS/) 197 + # Find raw (processed) files in segments (HHMMSS/) 198 198 for item in day_path.iterdir(): 199 - from think.utils import period_key 199 + from think.utils import segment_key 200 200 201 - if item.is_dir() and period_key(item.name): 202 - # Found period 201 + if item.is_dir() and segment_key(item.name): 202 + # Found segment 203 203 for p in item.glob("*.flac"): 204 204 raw_files.append(f"{item.name}/{p.name}") 205 205 for p in item.glob("*.m4a"): ··· 209 209 for p in item.glob("*.mp4"): 210 210 raw_files.append(f"{item.name}/{p.name}") 211 211 212 - # Find processed output files in periods 212 + # Find processed output files in segments 213 213 for item in day_path.iterdir(): 214 - from think.utils import period_key 214 + from think.utils import segment_key 215 215 216 - if item.is_dir() and period_key(item.name): 216 + if item.is_dir() and segment_key(item.name): 217 217 for p in item.glob("*audio.jsonl"): 218 218 processed_files.append(f"{item.name}/{p.name}") 219 219 for p in item.glob("*screen.jsonl"): 220 220 processed_files.append(f"{item.name}/{p.name}") 221 221 222 - # Find repairable files (source media in root without matching period) 222 + # Find repairable files (source media in root without matching segment) 223 223 for audio_ext in ["*.flac", "*.m4a"]: 224 224 for p in day_path.glob(audio_ext): 225 225 if "_" in p.stem: 226 - period_name = p.stem.split("_")[0] 227 - period_dir = day_path / period_name 228 - if not period_dir.exists(): 226 + segment_name = p.stem.split("_")[0] 227 + segment_dir = day_path / segment_name 228 + if not segment_dir.exists(): 229 229 repairable_files.append(p.name) 230 230 231 231 for video_ext in ["*.webm", "*.mp4"]:
+2 -2
tests/test_cluster.py
··· 8 8 day_dir = day_path("20240101") 9 9 10 10 mod = importlib.import_module("think.cluster") 11 - # Write JSONL format: metadata first, then entry in periodies 11 + # Write JSONL format: metadata first, then entry in segment directory 12 12 (day_dir / "120000").mkdir() 13 13 (day_dir / "120000" / "audio.jsonl").write_text('{}\n{"text": "hi"}\n') 14 14 (day_dir / "120500").mkdir() ··· 24 24 day_dir = day_path("20240101") 25 25 26 26 mod = importlib.import_module("think.cluster") 27 - # Write JSONL format: metadata first, then entry with proper start time and source in periody 27 + # Write JSONL format: metadata first, then entry with proper start time and source in segment directory 28 28 (day_dir / "120000").mkdir() 29 29 (day_dir / "120000" / "audio.jsonl").write_text( 30 30 '{"raw": "raw.flac", "model": "whisper-1"}\n'
+9 -9
tests/test_get_raw_file.py
··· 8 8 monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) 9 9 day_dir = day_path("20240101") 10 10 11 - # Create periods 12 - period_123000 = day_dir / "123000" 13 - period_123000.mkdir() 14 - period_090000 = day_dir / "090000" 15 - period_090000.mkdir() 11 + # Create segments 12 + segment_123000 = day_dir / "123000" 13 + segment_123000.mkdir() 14 + segment_090000 = day_dir / "090000" 15 + segment_090000.mkdir() 16 16 17 - (period_123000 / "monitor_1_diff.png").write_bytes(b"data") 18 - (period_123000 / "monitor_1_diff.json").write_text( 17 + (segment_123000 / "monitor_1_diff.png").write_bytes(b"data") 18 + (segment_123000 / "monitor_1_diff.json").write_text( 19 19 '{"visual_description": "screen", "raw": "monitor_1_diff.png"}' 20 20 ) 21 21 22 - (period_090000 / "raw.flac").write_bytes(b"data") 22 + (segment_090000 / "raw.flac").write_bytes(b"data") 23 23 # Write JSONL format: metadata first, then entry 24 - (period_090000 / "audio.jsonl").write_text( 24 + (segment_090000 / "audio.jsonl").write_text( 25 25 '{"raw": "raw.flac"}\n{"text": "hello"}\n' 26 26 ) 27 27
+40 -40
tests/test_indexer.py
··· 74 74 os.environ["JOURNAL_PATH"] = str(journal) 75 75 day = journal / "20240103" 76 76 day.mkdir() 77 - # Write JSONL format: metadata first, then entries in periody 77 + # Write JSONL format: metadata first, then entries in segment directory 78 78 ts_dir = day / "123000" 79 79 ts_dir.mkdir() 80 80 (ts_dir / "audio.jsonl").write_text( ··· 137 137 138 138 day1 = journal / "20240105" 139 139 day1.mkdir() 140 - # Write JSONL format: metadata first, then entries in periody 140 + # Write JSONL format: metadata first, then entries in segment directory 141 141 ts_dir1 = day1 / "123000" 142 142 ts_dir1.mkdir() 143 143 (ts_dir1 / "audio.jsonl").write_text( ··· 151 151 152 152 day2 = journal / "20240106" 153 153 day2.mkdir() 154 - # Write JSONL format: metadata first, then entries in periody 154 + # Write JSONL format: metadata first, then entries in segment directory 155 155 ts_dir2 = day2 / "090000" 156 156 ts_dir2.mkdir() 157 157 (ts_dir2 / "audio.jsonl").write_text( ··· 180 180 181 181 day = journal / "20240107" 182 182 day.mkdir() 183 - # Write JSONL format: metadata first, then entries in periodies 183 + # Write JSONL format: metadata first, then entries in segment directory 184 184 ts_dir1 = day / "090000" 185 185 ts_dir1.mkdir() 186 186 (ts_dir1 / "audio.jsonl").write_text( ··· 279 279 assert total == 2 280 280 281 281 282 - def test_scan_transcripts_single_period(tmp_path): 283 - """Test scanning transcripts for a specific period within a day.""" 282 + def test_scan_transcripts_single_segment(tmp_path): 283 + """Test scanning transcripts for a specific segment within a day.""" 284 284 mod = importlib.import_module("think.indexer") 285 285 journal = tmp_path 286 286 os.environ["JOURNAL_PATH"] = str(journal) 287 287 288 - # Create a day with multiple periods 288 + # Create a day with multiple segments 289 289 day = journal / "20240110" 290 290 day.mkdir() 291 291 292 - # Period 1: 100000 293 - period1 = day / "100000" 294 - period1.mkdir() 295 - (period1 / "audio.jsonl").write_text( 292 + # Segment 1: 100000 293 + segment1 = day / "100000" 294 + segment1.mkdir() 295 + (segment1 / "audio.jsonl").write_text( 296 296 json.dumps({"topics": ["test"], "setting": "personal"}) 297 297 + "\n" 298 298 + json.dumps( 299 - {"start": "00:00:01", "source": "mic", "speaker": 1, "text": "period one"} 299 + {"start": "00:00:01", "source": "mic", "speaker": 1, "text": "segment one"} 300 300 ) 301 301 + "\n" 302 302 ) 303 303 304 - # Period 2: 110000_300 (with duration) 305 - period2 = day / "110000_300" 306 - period2.mkdir() 307 - (period2 / "audio.jsonl").write_text( 304 + # Segment 2: 110000_300 (with duration) 305 + segment2 = day / "110000_300" 306 + segment2.mkdir() 307 + (segment2 / "audio.jsonl").write_text( 308 308 json.dumps({"topics": ["test"], "setting": "personal"}) 309 309 + "\n" 310 310 + json.dumps( 311 - {"start": "00:00:01", "source": "mic", "speaker": 1, "text": "period two"} 311 + {"start": "00:00:01", "source": "mic", "speaker": 1, "text": "segment two"} 312 312 ) 313 313 + "\n" 314 314 ) 315 315 316 - # Period 3: 120000 317 - period3 = day / "120000" 318 - period3.mkdir() 319 - (period3 / "audio.jsonl").write_text( 316 + # Segment 3: 120000 317 + segment3 = day / "120000" 318 + segment3.mkdir() 319 + (segment3 / "audio.jsonl").write_text( 320 320 json.dumps({"topics": ["test"], "setting": "personal"}) 321 321 + "\n" 322 322 + json.dumps( 323 - {"start": "00:00:01", "source": "mic", "speaker": 1, "text": "period three"} 323 + {"start": "00:00:01", "source": "mic", "speaker": 1, "text": "segment three"} 324 324 ) 325 325 + "\n" 326 326 ) 327 327 328 - # Scan only period 2 with duration format 328 + # Scan only segment 2 with duration format 329 329 changed = mod.scan_transcripts( 330 - str(journal), verbose=True, day="20240110", period="110000_300" 330 + str(journal), verbose=True, day="20240110", segment="110000_300" 331 331 ) 332 332 assert changed is True 333 333 334 334 # Day should have index 335 335 assert (day / "indexer" / "transcripts.sqlite").exists() 336 336 337 - # Search should only find period 2 338 - total, results = mod.search_transcripts("period", limit=10, day="20240110") 337 + # Search should only find segment 2 338 + total, results = mod.search_transcripts("segment", limit=10, day="20240110") 339 339 assert total == 1 340 340 assert results[0]["metadata"]["time"] == "110000_300" 341 - assert "period two" in results[0]["text"] 341 + assert "segment two" in results[0]["text"] 342 342 343 - # Now scan period 1 (without duration format) 344 - # This replaces the index with only period 1 (removes period 2) 343 + # Now scan segment 1 (without duration format) 344 + # This replaces the index with only segment 1 (removes segment 2) 345 345 changed = mod.scan_transcripts( 346 - str(journal), verbose=True, day="20240110", period="100000" 346 + str(journal), verbose=True, day="20240110", segment="100000" 347 347 ) 348 348 assert changed is True 349 349 350 - # Search should now find only period 1 (period 2 was removed) 351 - total, results = mod.search_transcripts("period", limit=10, day="20240110") 350 + # Search should now find only segment 1 (segment 2 was removed) 351 + total, results = mod.search_transcripts("segment", limit=10, day="20240110") 352 352 assert total == 1 353 353 assert results[0]["metadata"]["time"] == "100000" 354 - assert "period one" in results[0]["text"] 354 + assert "segment one" in results[0]["text"] 355 355 356 - # Scan all periods in the day - this is the proper way to get all periods 356 + # Scan all segments in the day - this is the proper way to get all segments 357 357 changed = mod.scan_transcripts(str(journal), verbose=True, day="20240110") 358 358 assert changed is True 359 359 360 - # Search should now find all 3 periods 361 - total, results = mod.search_transcripts("period", limit=10, day="20240110") 360 + # Search should now find all 3 segments 361 + total, results = mod.search_transcripts("segment", limit=10, day="20240110") 362 362 assert total == 3 363 363 assert {r["metadata"]["time"] for r in results} == { 364 364 "100000", ··· 366 366 "120000", 367 367 } 368 368 369 - # Verify period-specific scan with duration still works after full scan 369 + # Verify segment-specific scan with duration still works after full scan 370 370 changed = mod.scan_transcripts( 371 - str(journal), verbose=True, day="20240110", period="110000_300" 371 + str(journal), verbose=True, day="20240110", segment="110000_300" 372 372 ) 373 373 # Should return False because file hasn't changed (mtime caching) 374 - # Actually returns True because it removes other periods 374 + # Actually returns True because it removes other segments 375 375 assert changed is True 376 - total, results = mod.search_transcripts("period", limit=10, day="20240110") 376 + total, results = mod.search_transcripts("segment", limit=10, day="20240110") 377 377 assert total == 1 378 378 assert results[0]["metadata"]["time"] == "110000_300"
+2 -2
tests/test_journal_stats.py
··· 8 8 day = journal / "20240101" 9 9 day.mkdir() 10 10 11 - # Create an audio jsonl file in periody 11 + # Create an audio jsonl file in segment directory 12 12 ts_dir = day / "123456" 13 13 ts_dir.mkdir() 14 14 (ts_dir / "audio.jsonl").write_text( ··· 174 174 day = journal / "20240101" 175 175 day.mkdir() 176 176 177 - # Create an audio jsonl file in periody 177 + # Create an audio jsonl file in segment directory 178 178 ts_dir = day / "123456" 179 179 ts_dir.mkdir() 180 180 (ts_dir / "audio.jsonl").write_text(
+13 -13
tests/test_load_transcript.py
··· 262 262 def test_load_transcript_formatted_text_basic(): 263 263 """Test formatted text output with metadata and entries.""" 264 264 with tempfile.TemporaryDirectory() as tmpdir: 265 - # Create test transcript file in YYYYMMDD/HHMMSS period 265 + # Create test transcript file in YYYYMMDD/HHMMSS segment 266 266 day_dir = Path(tmpdir) / "20250615" 267 267 day_dir.mkdir() 268 - period_dir = day_dir / "100500" 269 - period_dir.mkdir() 270 - file_path = period_dir / "audio.jsonl" 268 + segment_dir = day_dir / "100500" 269 + segment_dir.mkdir() 270 + file_path = segment_dir / "audio.jsonl" 271 271 272 272 # Write JSONL with metadata and entries 273 273 metadata = {"topics": ["meeting", "planning"], "setting": "work"} ··· 303 303 with tempfile.TemporaryDirectory() as tmpdir: 304 304 day_dir = Path(tmpdir) / "20250615" 305 305 day_dir.mkdir() 306 - period_dir = day_dir / "100500" 307 - period_dir.mkdir() 308 - file_path = period_dir / "audio.jsonl" 306 + segment_dir = day_dir / "100500" 307 + segment_dir.mkdir() 308 + file_path = segment_dir / "audio.jsonl" 309 309 310 310 # Minimal metadata (empty dict) 311 311 metadata = {} ··· 330 330 with tempfile.TemporaryDirectory() as tmpdir: 331 331 day_dir = Path(tmpdir) / "20250615" 332 332 day_dir.mkdir() 333 - period_dir = day_dir / "100500" 334 - period_dir.mkdir() 335 - file_path = period_dir / "imported_audio.jsonl" 333 + segment_dir = day_dir / "100500" 334 + segment_dir.mkdir() 335 + file_path = segment_dir / "imported_audio.jsonl" 336 336 337 337 metadata = { 338 338 "imported": {"id": "abc123", "facet": "uavionix"}, ··· 365 365 with tempfile.TemporaryDirectory() as tmpdir: 366 366 day_dir = Path(tmpdir) / "20250615" 367 367 day_dir.mkdir() 368 - period_dir = day_dir / "100500" 369 - period_dir.mkdir() 370 - file_path = period_dir / "audio.jsonl" 368 + segment_dir = day_dir / "100500" 369 + segment_dir.mkdir() 370 + file_path = segment_dir / "audio.jsonl" 371 371 372 372 metadata = {"setting": "personal"} 373 373 entries = [
+14 -14
tests/test_reduce.py
··· 10 10 from observe.reduce import assemble_markdown, reduce_analysis 11 11 12 12 13 - def test_assemble_markdown_extracts_period_from_directory(): 14 - """Test that assemble_markdown correctly extracts base time from period directory.""" 15 - # Mock frames with relative timestamps (seconds from period start) 13 + def test_assemble_markdown_extracts_segment_from_directory(): 14 + """Test that assemble_markdown correctly extracts base time from segment directory.""" 15 + # Mock frames with relative timestamps (seconds from segment start) 16 16 frames = [ 17 17 { 18 18 "timestamp": 0, ··· 38 38 frames, entity_names="", video_path=jsonl_path, include_entity_context=False 39 39 ) 40 40 41 - # Verify absolute times are calculated correctly from period (14:30:22) 42 - assert "14:30:22" in markdown # Base time from period 41 + # Verify absolute times are calculated correctly from segment (14:30:22) 42 + assert "14:30:22" in markdown # Base time from segment 43 43 assert "14:30:52" in markdown # Base + 30 seconds 44 44 assert "14:32:22" in markdown # Base + 120 seconds (2 minutes) 45 45 ··· 49 49 assert "Reading docs" in markdown 50 50 51 51 52 - def test_assemble_markdown_handles_period_with_duration_suffix(): 53 - """Test that assemble_markdown handles HHMMSS_LEN period format.""" 52 + def test_assemble_markdown_handles_segment_with_duration_suffix(): 53 + """Test that assemble_markdown handles HHMMSS_LEN segment format.""" 54 54 frames = [ 55 55 { 56 56 "timestamp": 0, ··· 64 64 }, 65 65 ] 66 66 67 - # Period with duration suffix: 143022_300 (5 minutes) 67 + # Segment with duration suffix: 143022_300 (5 minutes) 68 68 jsonl_path = Path("20240101/143022_300/screen.jsonl") 69 69 70 70 markdown = assemble_markdown( ··· 185 185 assert "All tests passed" in markdown 186 186 187 187 188 - def test_main_constructs_path_from_day_and_period(tmp_path, monkeypatch): 189 - """Test that main() constructs correct JSONL path from --day and --period args.""" 188 + def test_main_constructs_path_from_day_and_segment(tmp_path, monkeypatch): 189 + """Test that main() constructs correct JSONL path from --day and --segment args.""" 190 190 # Create mock journal structure 191 191 journal_path = tmp_path / "journal" 192 192 day_dir = journal_path / "20251109" 193 - period_dir = day_dir / "222502_303" 194 - period_dir.mkdir(parents=True) 193 + segment_dir = day_dir / "222502_303" 194 + segment_dir.mkdir(parents=True) 195 195 196 196 # Create mock screen.jsonl with minimal valid data 197 - screen_jsonl = period_dir / "screen.jsonl" 197 + screen_jsonl = segment_dir / "screen.jsonl" 198 198 frames = [ 199 199 { 200 200 "timestamp": 0, ··· 237 237 assert exit_code == 0 238 238 239 239 # Verify markdown was written 240 - output_md = period_dir / "screen.md" 240 + output_md = segment_dir / "screen.md" 241 241 assert output_md.exists() 242 242 assert "Test Summary" in output_md.read_text()
+5 -5
tests/test_sense.py
··· 136 136 txt_file = day_dir / "test.txt" 137 137 assert sensor._match_pattern(txt_file) is None 138 138 139 - # Should not match - in period 140 - period_dir = day_dir / "123456" 141 - period_dir.mkdir() 142 - period_file = period_dir / "audio.jsonl" 143 - assert sensor._match_pattern(period_file) is None 139 + # Should not match - in segment 140 + segment_dir = day_dir / "123456" 141 + segment_dir.mkdir() 142 + segment_file = segment_dir / "audio.jsonl" 143 + assert sensor._match_pattern(segment_file) is None 144 144 145 145 146 146 @patch("think.runner._get_journal_path")
+56 -56
tests/test_think_utils.py
··· 8 8 import pytest 9 9 10 10 from think.entities import load_entity_names 11 - from think.utils import period_key 11 + from think.utils import segment_key 12 12 13 13 14 14 def write_entities_jsonl(path: Path, entities: list[tuple[str, str, str]] | list[dict]): ··· 507 507 assert result == "Alice Johnson (Ali, AJ); TechCorp; PostgreSQL (Postgres, PG)" 508 508 509 509 510 - def test_period_key_hhmmss_only(): 511 - """Test period_key with HHMMSS format only.""" 512 - assert period_key("143022") == "143022" 513 - assert period_key("095604") == "095604" 514 - assert period_key("000000") == "000000" 515 - assert period_key("235959") == "235959" 510 + def test_segment_key_hhmmss_only(): 511 + """Test segment_key with HHMMSS format only.""" 512 + assert segment_key("143022") == "143022" 513 + assert segment_key("095604") == "095604" 514 + assert segment_key("000000") == "000000" 515 + assert segment_key("235959") == "235959" 516 516 517 517 518 - def test_period_key_hhmmss_with_duration(): 519 - """Test period_key with HHMMSS_LEN format.""" 520 - assert period_key("143022_300") == "143022_300" 521 - assert period_key("095604_303") == "095604_303" 522 - assert period_key("120000_3600") == "120000_3600" 523 - assert period_key("000000_1") == "000000_1" 518 + def test_segment_key_hhmmss_with_duration(): 519 + """Test segment_key with HHMMSS_LEN format.""" 520 + assert segment_key("143022_300") == "143022_300" 521 + assert segment_key("095604_303") == "095604_303" 522 + assert segment_key("120000_3600") == "120000_3600" 523 + assert segment_key("000000_1") == "000000_1" 524 524 525 525 526 - def test_period_key_hhmmss_with_suffix(): 527 - """Test period_key with HHMMSS_suffix format (old format).""" 528 - assert period_key("143022_audio") == "143022" 529 - assert period_key("143022_screen") == "143022" 530 - assert period_key("095604_recording") == "095604" 531 - assert period_key("120000_mic_sys") == "120000" 526 + def test_segment_key_hhmmss_with_suffix(): 527 + """Test segment_key with HHMMSS_suffix format (old format).""" 528 + assert segment_key("143022_audio") == "143022" 529 + assert segment_key("143022_screen") == "143022" 530 + assert segment_key("095604_recording") == "095604" 531 + assert segment_key("120000_mic_sys") == "120000" 532 532 533 533 534 - def test_period_key_hhmmss_len_with_suffix(): 535 - """Test period_key with HHMMSS_LEN_suffix format (new format).""" 536 - assert period_key("143022_300_audio") == "143022_300" 537 - assert period_key("095604_303_screen") == "095604_303" 538 - assert period_key("120000_3600_recording") == "120000_3600" 539 - assert period_key("000000_1_mic_sys") == "000000_1" 534 + def test_segment_key_hhmmss_len_with_suffix(): 535 + """Test segment_key with HHMMSS_LEN_suffix format (new format).""" 536 + assert segment_key("143022_300_audio") == "143022_300" 537 + assert segment_key("095604_303_screen") == "095604_303" 538 + assert segment_key("120000_3600_recording") == "120000_3600" 539 + assert segment_key("000000_1_mic_sys") == "000000_1" 540 540 541 541 542 - def test_period_key_with_file_extension(): 543 - """Test period_key with various file extensions.""" 544 - assert period_key("143022_300_audio.flac") == "143022_300" 545 - assert period_key("095604_303_screen.webm") == "095604_303" 546 - assert period_key("143022_audio.flac") == "143022" 547 - assert period_key("143022.flac") == "143022" 548 - assert period_key("143022_300.jsonl") == "143022_300" 542 + def test_segment_key_with_file_extension(): 543 + """Test segment_key with various file extensions.""" 544 + assert segment_key("143022_300_audio.flac") == "143022_300" 545 + assert segment_key("095604_303_screen.webm") == "095604_303" 546 + assert segment_key("143022_audio.flac") == "143022" 547 + assert segment_key("143022.flac") == "143022" 548 + assert segment_key("143022_300.jsonl") == "143022_300" 549 549 550 550 551 - def test_period_key_in_path(): 552 - """Test period_key extraction from full paths.""" 553 - assert period_key("/journal/20250109/143022_300/audio.jsonl") == "143022_300" 554 - assert period_key("/journal/20250109/143022/screen.webm") == "143022" 555 - assert period_key("/home/user/20250110/095604_303_screen.webm") == "095604_303" 556 - assert period_key("20250110/143022_300_audio.flac") == "143022_300" 551 + def test_segment_key_in_path(): 552 + """Test segment_key extraction from full paths.""" 553 + assert segment_key("/journal/20250109/143022_300/audio.jsonl") == "143022_300" 554 + assert segment_key("/journal/20250109/143022/screen.webm") == "143022" 555 + assert segment_key("/home/user/20250110/095604_303_screen.webm") == "095604_303" 556 + assert segment_key("20250110/143022_300_audio.flac") == "143022_300" 557 557 558 558 559 - def test_period_key_invalid_formats(): 560 - """Test period_key with invalid formats returns None.""" 561 - assert period_key("invalid") is None 562 - assert period_key("12345") is None # Too short 563 - assert period_key("1234567") is None # Too long 564 - assert period_key("abcdef") is None # Not digits 565 - assert period_key("14:30:22") is None # Wrong separator 566 - assert period_key("") is None 567 - assert period_key("_143022") is None 559 + def test_segment_key_invalid_formats(): 560 + """Test segment_key with invalid formats returns None.""" 561 + assert segment_key("invalid") is None 562 + assert segment_key("12345") is None # Too short 563 + assert segment_key("1234567") is None # Too long 564 + assert segment_key("abcdef") is None # Not digits 565 + assert segment_key("14:30:22") is None # Wrong separator 566 + assert segment_key("") is None 567 + assert segment_key("_143022") is None 568 568 569 569 570 - def test_period_key_edge_cases(): 571 - """Test period_key with edge cases.""" 570 + def test_segment_key_edge_cases(): 571 + """Test segment_key with edge cases.""" 572 572 # Multiple underscores in suffix 573 - assert period_key("143022_300_mic_sys_audio") == "143022_300" 574 - # Period key with non-word boundary prefix (should not match) 575 - assert period_key("prefix_143022_300_suffix") is None 576 - # Period key with space/path separator (word boundary - should match) 577 - assert period_key("prefix/143022_300/suffix") == "143022_300" 578 - assert period_key("prefix 143022_300 suffix") == "143022_300" 573 + assert segment_key("143022_300_mic_sys_audio") == "143022_300" 574 + # Segment key with non-word boundary prefix (should not match) 575 + assert segment_key("prefix_143022_300_suffix") is None 576 + # Segment key with space/path separator (word boundary - should match) 577 + assert segment_key("prefix/143022_300/suffix") == "143022_300" 578 + assert segment_key("prefix 143022_300 suffix") == "143022_300" 579 579 # Multiple potential matches (should match first) 580 - assert period_key("143022_300 and 150000_600") == "143022_300" 580 + assert segment_key("143022_300 and 150000_600") == "143022_300"
+30 -30
think/cluster.py
··· 28 28 day_path_obj = Path(day_dir) 29 29 30 30 # Check timestamp subdirectories for transcript files 31 - from think.utils import period_parse 31 + from think.utils import segment_parse 32 32 33 33 for item in day_path_obj.iterdir(): 34 - start_time, _ = period_parse(item.name) 34 + start_time, _ = segment_parse(item.name) 35 35 if not (item.is_dir() and start_time): 36 36 continue 37 37 ··· 238 238 day_path_obj = Path(day_dir) 239 239 240 240 # Check timestamp subdirectories for transcript files 241 - from think.utils import period_parse 241 + from think.utils import segment_parse 242 242 243 243 for item in day_path_obj.iterdir(): 244 - start_time, _ = period_parse(item.name) 244 + start_time, _ = segment_parse(item.name) 245 245 if item.is_dir() and start_time: 246 - # Found period - combine with date to get datetime 246 + # Found segment - combine with date to get datetime 247 247 day_date = datetime.strptime(date_str, "%Y%m%d").date() 248 248 dt = datetime.combine(day_date, start_time) 249 249 slot = dt.replace( ··· 280 280 return markdown, len(entries) 281 281 282 282 283 - def cluster_period(day: str, period: str) -> Tuple[str, int]: 284 - """Return Markdown summary for one period's JSON files and the number processed. 283 + def cluster_period(day: str, segment: str) -> Tuple[str, int]: 284 + """Return Markdown summary for one segment's JSON files and the number processed. 285 285 286 286 Args: 287 287 day: Day in YYYYMMDD format 288 - period: Period key in HHMMSS_LEN format (e.g., "163045_300") 288 + segment: Segment key in HHMMSS_LEN format (e.g., "163045_300") 289 289 290 290 Returns: 291 291 (markdown, file_count) tuple 292 292 """ 293 293 day_dir = str(day_path(day)) 294 - period_dir = Path(day_dir) / period 294 + segment_dir = Path(day_dir) / segment 295 295 296 - if not period_dir.is_dir(): 297 - return f"Period folder not found: {period_dir}", 0 296 + if not segment_dir.is_dir(): 297 + return f"Segment folder not found: {segment_dir}", 0 298 298 299 - # Load entries from this specific period directory 300 - entries = _load_entries_from_period(str(period_dir), True, "summary") 299 + # Load entries from this specific segment directory 300 + entries = _load_entries_from_segment(str(segment_dir), True, "summary") 301 301 if not entries: 302 - return f"No audio or screen files found for period {period}", 0 302 + return f"No audio or screen files found for segment {segment}", 0 303 303 304 304 groups = _group_entries(entries) 305 305 markdown = _groups_to_markdown(groups) 306 306 return markdown, len(entries) 307 307 308 308 309 - def _load_entries_from_period( 310 - period_dir: str, audio: bool, screen_mode: Optional[str] 309 + def _load_entries_from_segment( 310 + segment_dir: str, audio: bool, screen_mode: Optional[str] 311 311 ) -> List[Dict[str, str]]: 312 - """Load entries from a single period directory. 312 + """Load entries from a single segment directory. 313 313 314 314 Args: 315 - period_dir: Path to period directory (e.g., /path/to/20251109/163045_300) 315 + segment_dir: Path to segment directory (e.g., /path/to/20251109/163045_300) 316 316 audio: Whether to load audio transcripts 317 317 screen_mode: "summary" for screen.md, "raw" for screen.jsonl, None to skip 318 318 319 319 Returns: 320 320 List of entry dicts with timestamp, prefix, content, etc. 321 321 """ 322 - period_path = Path(period_dir) 322 + segment_path = Path(segment_dir) 323 323 entries: List[Dict[str, str]] = [] 324 324 325 - # Extract day and time from period directory path 326 - day_dir = period_path.parent 325 + # Extract day and time from segment directory path 326 + day_dir = segment_path.parent 327 327 date_str = _date_str(str(day_dir)) 328 - period_name = period_path.name 328 + segment_name = segment_path.name 329 329 330 - from think.utils import period_parse 330 + from think.utils import segment_parse 331 331 332 - start_time, _ = period_parse(period_name) 332 + start_time, _ = segment_parse(segment_name) 333 333 if not start_time: 334 334 return entries 335 335 336 336 # Process audio transcripts 337 337 if audio: 338 - audio_files = [f for f in period_path.glob("*audio.jsonl") if f.is_file()] 338 + audio_files = [f for f in segment_path.glob("*audio.jsonl") if f.is_file()] 339 339 for audio_file in audio_files: 340 340 from observe.hear import load_transcript 341 341 ··· 359 359 "monitor": None, 360 360 "source": None, 361 361 "id": None, 362 - "name": f"{period_name}/{audio_file.name}", 362 + "name": f"{segment_name}/{audio_file.name}", 363 363 } 364 364 ) 365 365 366 366 # Process screen summaries or transcripts 367 367 if screen_mode == "summary": 368 - screen_md = period_path / "screen.md" 368 + screen_md = segment_path / "screen.md" 369 369 if screen_md.exists(): 370 370 try: 371 371 content = screen_md.read_text() ··· 379 379 "monitor": None, 380 380 "source": None, 381 381 "id": None, 382 - "name": f"{period_name}/screen.md", 382 + "name": f"{segment_name}/screen.md", 383 383 } 384 384 ) 385 385 except Exception as e: 386 386 print(f"Warning: Could not read file screen.md: {e}", file=sys.stderr) 387 387 388 388 elif screen_mode == "raw": 389 - screen_jsonl = period_path / "screen.jsonl" 389 + screen_jsonl = segment_path / "screen.jsonl" 390 390 if screen_jsonl.exists(): 391 391 try: 392 392 frames = load_analysis_frames(screen_jsonl) ··· 403 403 "monitor": None, 404 404 "source": None, 405 405 "id": None, 406 - "name": f"{period_name}/screen.jsonl", 406 + "name": f"{segment_name}/screen.jsonl", 407 407 } 408 408 ) 409 409 except Exception as e:
+16 -16
think/dream.py
··· 29 29 30 30 31 31 def build_commands( 32 - day: str, force: bool, verbose: bool = False, period: str | None = None 32 + day: str, force: bool, verbose: bool = False, segment: str | None = None 33 33 ) -> list[list[str]]: 34 - """Build processing commands for a day or specific period. 34 + """Build processing commands for a day or specific segment. 35 35 36 36 Args: 37 37 day: YYYYMMDD format 38 - period: Optional HHMMSS_LEN format (e.g., "163045_300") 38 + segment: Optional HHMMSS_LEN format (e.g., "163045_300") 39 39 force: Overwrite existing files 40 40 verbose: Verbose logging 41 41 """ 42 42 commands: list[list[str]] = [] 43 43 44 44 # Determine target frequency and what to run 45 - if period: 46 - logging.info("Running period processing for %s/%s", day, period) 47 - target_frequency = "period" 48 - # No sense repair for periods (already processed during observation) 45 + if segment: 46 + logging.info("Running segment processing for %s/%s", day, segment) 47 + target_frequency = "segment" 48 + # No sense repair for segments (already processed during observation) 49 49 else: 50 50 logging.info("Running daily processing for %s", day) 51 51 target_frequency = "daily" ··· 69 69 continue 70 70 71 71 cmd = ["think-summarize", day, "-f", topic_data["path"], "-p"] 72 - if period: 73 - cmd.extend(["--period", period]) 72 + if segment: 73 + cmd.extend(["--segment", segment]) 74 74 if verbose: 75 75 cmd.append("--verbose") 76 76 if force: ··· 79 79 80 80 # Targeted indexing 81 81 indexer_cmd = ["think-indexer", "--rescan-all", "--day", day] 82 - if period: 83 - indexer_cmd.extend(["--period", period]) 82 + if segment: 83 + indexer_cmd.extend(["--segment", segment]) 84 84 if verbose: 85 85 indexer_cmd.append("--verbose") 86 86 commands.append(indexer_cmd) 87 87 88 88 # Daily-only: journal stats 89 - if not period: 89 + if not segment: 90 90 stats_cmd = ["think-journal-stats"] 91 91 if verbose: 92 92 stats_cmd.append("--verbose") ··· 97 97 98 98 def parse_args() -> argparse.ArgumentParser: 99 99 parser = argparse.ArgumentParser( 100 - description="Run processing tasks on a journal day or period" 100 + description="Run processing tasks on a journal day or segment" 101 101 ) 102 102 parser.add_argument( 103 103 "--day", 104 104 help="Day folder in YYYYMMDD format (defaults to yesterday)", 105 105 ) 106 106 parser.add_argument( 107 - "--period", 108 - help="Period key in HHMMSS_LEN format (processes period topics only)", 107 + "--segment", 108 + help="Segment key in HHMMSS_LEN format (processes segment topics only)", 109 109 ) 110 110 parser.add_argument("--force", action="store_true", help="Overwrite existing files") 111 111 return parser ··· 126 126 if not day_dir.is_dir(): 127 127 parser.error(f"Day folder not found: {day_dir}") 128 128 129 - commands = build_commands(day, args.force, verbose=args.verbose, period=args.period) 129 + commands = build_commands(day, args.force, verbose=args.verbose, segment=args.segment) 130 130 success_count = 0 131 131 fail_count = 0 132 132 for cmd in commands:
+6 -6
think/importer.py
··· 296 296 ts = base_dt + timedelta(minutes=idx * 5) 297 297 time_part = ts.strftime("%H%M%S") 298 298 299 - # Create period directory with 5-minute (300 second) duration suffix 300 - period_name = f"{time_part}_300" 301 - ts_dir = os.path.join(day_dir, period_name) 299 + # Create segment directory with 5-minute (300 second) duration suffix 300 + segment_name = f"{time_part}_300" 301 + ts_dir = os.path.join(day_dir, segment_name) 302 302 os.makedirs(ts_dir, exist_ok=True) 303 303 json_path = os.path.join(ts_dir, "imported_audio.jsonl") 304 304 ··· 435 435 ts = base_dt + timedelta(minutes=chunk_index * 5) 436 436 time_part = ts.strftime("%H%M%S") 437 437 438 - # Create period directory with 5-minute (300 second) duration suffix 439 - period_name = f"{time_part}_300" 440 - ts_dir = os.path.join(day_dir, period_name) 438 + # Create segment directory with 5-minute (300 second) duration suffix 439 + segment_name = f"{time_part}_300" 440 + ts_dir = os.path.join(day_dir, segment_name) 441 441 os.makedirs(ts_dir, exist_ok=True) 442 442 json_path = os.path.join(ts_dir, "imported_audio.jsonl") 443 443
+6 -6
think/indexer/cli.py
··· 60 60 help="Limit transcript query to a specific YYYYMMDD day", 61 61 ) 62 62 parser.add_argument( 63 - "--period", 64 - help="Limit transcript rescan to a specific period (HHMMSS or HHMMSS_LEN) within --day", 63 + "--segment", 64 + help="Limit transcript rescan to a specific segment (HHMMSS or HHMMSS_LEN) within --day", 65 65 ) 66 66 parser.add_argument( 67 67 "--source", ··· 101 101 ): 102 102 parser.error("--index is required unless using --rescan-all or --rescan-facets") 103 103 104 - # Validate --period requires --day 105 - if args.period and not args.day: 106 - parser.error("--period requires --day to be specified") 104 + # Validate --segment requires --day 105 + if args.segment and not args.day: 106 + parser.error("--segment requires --day to be specified") 107 107 108 108 journal = os.getenv("JOURNAL_PATH") 109 109 ··· 153 153 if args.rescan: 154 154 if args.index == "transcripts": 155 155 changed = scan_transcripts( 156 - journal, verbose=args.verbose, day=args.day, period=args.period 156 + journal, verbose=args.verbose, day=args.day, segment=args.segment 157 157 ) 158 158 if changed: 159 159 journal_log("indexer transcripts rescan ok")
+13 -13
think/indexer/transcripts.py
··· 21 21 22 22 23 23 def find_transcript_files( 24 - journal: str, day: str | None = None, period: str | None = None 24 + journal: str, day: str | None = None, segment: str | None = None 25 25 ) -> Dict[str, str]: 26 26 """Return mapping of transcript JSON file paths relative to ``journal``. 27 27 ··· 30 30 If ``day`` is provided (YYYYMMDD format), only scan that specific day. 31 31 Otherwise scan all days. 32 32 33 - If ``period`` is provided (HHMMSS or HHMMSS_LEN format), only scan that 34 - specific period within the day. Requires ``day`` to be specified. 33 + If ``segment`` is provided (HHMMSS or HHMMSS_LEN format), only scan that 34 + specific segment within the day. Requires ``day`` to be specified. 35 35 """ 36 36 files: Dict[str, str] = {} 37 37 days = {day: day_dirs()[day]} if day and day in day_dirs() else day_dirs() 38 38 for day_key, day_path in days.items(): 39 39 # Check timestamp subdirectories 40 - from think.utils import period_key 40 + from think.utils import segment_key 41 41 42 42 day_path_obj = Path(day_path) 43 43 for item in day_path_obj.iterdir(): 44 - if item.is_dir() and period_key(item.name): 44 + if item.is_dir() and segment_key(item.name): 45 45 # Found a timestamp directory (HHMMSS or HHMMSS_LEN) 46 - # If period filter specified, check if this matches 47 - if period: 48 - item_period_key = period_key(item.name) 49 - if item_period_key != period: 46 + # If segment filter specified, check if this matches 47 + if segment: 48 + item_segment_key = segment_key(item.name) 49 + if item_segment_key != segment: 50 50 continue 51 51 52 52 for result_file in item.glob("*.jsonl"): ··· 204 204 journal: str, 205 205 verbose: bool = False, 206 206 day: str | None = None, 207 - period: str | None = None, 207 + segment: str | None = None, 208 208 ) -> bool: 209 209 """Index transcript audio, screen diff JSON, and screen JSONL files on a per-day basis. 210 210 211 211 If ``day`` is provided (YYYYMMDD format), only scan that specific day. 212 212 Otherwise scan all days. 213 213 214 - If ``period`` is provided (HHMMSS or HHMMSS_LEN format), only scan that 215 - specific period within the day. Requires ``day`` to be specified. 214 + If ``segment`` is provided (HHMMSS or HHMMSS_LEN format), only scan that 215 + specific segment within the day. Requires ``day`` to be specified. 216 216 """ 217 217 logger = logging.getLogger(__name__) 218 - files = find_transcript_files(journal, day=day, period=period) 218 + files = find_transcript_files(journal, day=day, segment=segment) 219 219 if not files: 220 220 return False 221 221
+2 -2
think/planner.txt
··· 9 9 You have knowledge of these tools for planning purposes: 10 10 11 11 ### Search Tools 12 - - **search_summaries**: Searches pre-processed topic summaries across all journal entries. Best for discovering themes, concepts, and patterns across time periods. Returns topic names, days, and relevance scores. 12 + - **search_summaries**: Searches pre-processed topic summaries across all journal entries. Best for discovering themes, concepts, and patterns across time segments. Returns topic names, days, and relevance scores. 13 13 - **search_transcripts**: Searches raw audio transcripts and screen diffs for specific days. Best for finding exact phrases, quotes, or detailed events on particular days. Requires a specific day parameter. 14 14 - **search_events**: Searches structured events extracted from journal summaries. Best for finding meetings, tasks, or notable activities with timestamps and context. 15 15 ··· 34 34 Plan research using this progression: 35 35 36 36 **Discovery Phase** (Use search tools to identify relevant content): 37 - - Start broad with `search_summaries` to identify relevant topics and time periods 37 + - Start broad with `search_summaries` to identify relevant topics and time segments 38 38 - Use `search_events` to find structured activities related to the request 39 39 - Use `search_transcripts` for specific days when exact details are needed 40 40
+18 -18
think/summarize.py
··· 28 28 29 29 30 30 def _output_paths( 31 - day_dir: os.PathLike[str], basename: str, period: str | None = None 31 + day_dir: os.PathLike[str], basename: str, segment: str | None = None 32 32 ) -> tuple[Path, Path]: 33 33 """Return markdown and JSON output paths for ``basename`` in ``day_dir``. 34 34 35 35 Args: 36 36 day_dir: Day directory path (YYYYMMDD) 37 37 basename: Topic basename 38 - period: Optional period key (HHMMSS_LEN) 38 + segment: Optional segment key (HHMMSS_LEN) 39 39 40 40 Returns: 41 41 (md_path, json_path) tuple 42 42 - Daily: YYYYMMDD/topics/{basename}.md 43 - - Period: YYYYMMDD/{period}/{basename}.md 43 + - Segment: YYYYMMDD/{segment}/{basename}.md 44 44 """ 45 45 day = Path(day_dir) 46 46 47 - if period: 48 - # Period topics go directly in period directory 49 - period_dir = day / period 50 - return period_dir / f"{basename}.md", period_dir / f"{basename}.json" 47 + if segment: 48 + # Segment topics go directly in segment directory 49 + segment_dir = day / segment 50 + return segment_dir / f"{basename}.md", segment_dir / f"{basename}.json" 51 51 else: 52 52 # Daily topics go in topics/ subdirectory 53 53 topic_dir = day / "topics" ··· 240 240 help="Overwrite output file if it already exists", 241 241 ) 242 242 parser.add_argument( 243 - "--period", 244 - help="Period key in HHMMSS_LEN format (processes only this period within the day)", 243 + "--segment", 244 + help="Segment key in HHMMSS_LEN format (processes only this segment within the day)", 245 245 ) 246 246 args = setup_cli(parser) 247 247 248 248 # Choose clustering function based on mode 249 - if args.period: 250 - markdown, file_count = cluster_period(args.day, args.period) 249 + if args.segment: 250 + markdown, file_count = cluster_period(args.day, args.segment) 251 251 else: 252 252 markdown, file_count = cluster(args.day) 253 253 day_dir = str(day_path(args.day)) ··· 288 288 count_tokens(markdown, prompt, api_key, model) 289 289 return 290 290 291 - md_path, json_path = _output_paths(day_dir, topic_basename, period=args.period) 292 - # Use cache key scoped to day or period 293 - if args.period: 294 - cache_display_name = f"{day}_{args.period}" 291 + md_path, json_path = _output_paths(day_dir, topic_basename, segment=args.segment) 292 + # Use cache key scoped to day or segment 293 + if args.segment: 294 + cache_display_name = f"{day}_{args.segment}" 295 295 else: 296 296 cache_display_name = f"{day}" 297 297 ··· 393 393 print(f"Error: {e}") 394 394 return 395 395 396 - # Include period in occurrence JSON if in period mode 397 - if args.period: 396 + # Include segment in occurrence JSON if in segment mode 397 + if args.segment: 398 398 full_occurrence_obj = { 399 399 "day": day, 400 - "period": args.period, 400 + "segment": args.segment, 401 401 "occurrences": occurrences, 402 402 } 403 403 else:
+21 -21
think/supervisor.py
··· 927 927 return last_day 928 928 929 929 930 - def _handle_period_observed(message: dict) -> None: 931 - """Handle period completion events from observe tract.""" 930 + def _handle_segment_observed(message: dict) -> None: 931 + """Handle segment completion events from observe tract.""" 932 932 if message.get("tract") != "observe" or message.get("event") != "observed": 933 933 return 934 934 935 - period = message.get("period") # e.g., "163045_300" 936 - if not period: 937 - logging.warning("observed event missing period field") 935 + segment = message.get("segment") # e.g., "163045_300" 936 + if not segment: 937 + logging.warning("observed event missing segment field") 938 938 return 939 939 940 - # Extract day from current date (period observed on same day) 940 + # Extract day from current date (segment observed on same day) 941 941 day = datetime.now().strftime("%Y%m%d") 942 942 943 - logging.info(f"Period observed: {day}/{period}, spawning processing...") 943 + logging.info(f"Segment observed: {day}/{segment}, spawning processing...") 944 944 945 - # Spawn agents configured for period schedule 945 + # Spawn agents configured for segment schedule 946 946 agents = get_agents() 947 947 for persona_id, config in agents.items(): 948 - if config.get("schedule") == "period": 948 + if config.get("schedule") == "segment": 949 949 try: 950 950 cortex_request( 951 - prompt=f"Processing period {period} from {day}. Use available tools to analyze this specific recording window.", 951 + prompt=f"Processing segment {segment} from {day}. Use available tools to analyze this specific recording window.", 952 952 persona=persona_id, 953 953 ) 954 - logging.info(f"Spawned period agent: {persona_id}") 954 + logging.info(f"Spawned segment agent: {persona_id}") 955 955 except Exception as e: 956 956 logging.error(f"Failed to spawn {persona_id}: {e}") 957 957 958 - # Run dream in period mode (async, non-blocking) 958 + # Run dream in segment mode (async, non-blocking) 959 959 threading.Thread( 960 - target=_run_period_dream, 961 - args=(day, period), 960 + target=_run_segment_dream, 961 + args=(day, segment), 962 962 daemon=True, 963 963 ).start() 964 964 965 965 966 - def _run_period_dream(day: str, period: str) -> None: 967 - """Run think-dream for a specific period.""" 966 + def _run_segment_dream(day: str, segment: str) -> None: 967 + """Run think-dream for a specific segment.""" 968 968 from think.runner import run_task 969 969 970 - logging.info(f"Starting period dream: {day}/{period}") 971 - success, exit_code = run_task(["think-dream", "--day", day, "--period", period]) 970 + logging.info(f"Starting segment dream: {day}/{segment}") 971 + success, exit_code = run_task(["think-dream", "--day", day, "--segment", segment]) 972 972 973 973 if success: 974 - logging.info(f"Period dream completed: {day}/{period}") 974 + logging.info(f"Segment dream completed: {day}/{segment}") 975 975 else: 976 - logging.error(f"Period dream failed with exit code {exit_code}: {day}/{period}") 976 + logging.error(f"Segment dream failed with exit code {exit_code}: {day}/{segment}") 977 977 978 978 979 979 def _handle_callosum_message(message: dict) -> None: 980 980 """Dispatch incoming Callosum messages to appropriate handlers.""" 981 981 _handle_task_request(message) 982 982 _handle_supervisor_request(message) 983 - _handle_period_observed(message) 983 + _handle_segment_observed(message) 984 984 985 985 986 986 async def supervise(
+3 -3
think/topics/activity.json
··· 1 1 { 2 2 "title": "Activity Synthesis", 3 - "description": "Real-time synthesis of each period to extract active tasks, progress states, and searchable context. Interprets audio and screen activity to reveal intent and framing not obvious from raw transcripts.", 4 - "contains": "Free-form markdown analysis for each period capturing what's being worked on, progress states, facet associations, and context that makes periods discoverable through search.", 3 + "description": "Real-time synthesis of each segment to extract active tasks, progress states, and searchable context. Interprets audio and screen activity to reveal intent and framing not obvious from raw transcripts.", 4 + "contains": "Free-form markdown analysis for each segment capturing what's being worked on, progress states, facet associations, and context that makes segments discoverable through search.", 5 5 "occurrences": false, 6 6 "color": "#00bcd4", 7 - "frequency": "period" 7 + "frequency": "segment" 8 8 }
+9 -9
think/topics/activity.txt
··· 1 - # Period Activity Synthesis 1 + # Segment Activity Synthesis 2 2 3 3 ## Objective 4 4 5 - Analyze this period to synthesize what's actually happening and what matters for future searchability. Transform raw audio transcript and screen activity into interpreted understanding that reveals intent, progress, and context not obvious from the literal recordings. 5 + Analyze this segment to synthesize what's actually happening and what matters for future searchability. Transform raw audio transcript and screen activity into interpreted understanding that reveals intent, progress, and context not obvious from the literal recordings. 6 6 7 7 ## Core Focus 8 8 ··· 19 19 - **Deciding**: Weighing alternatives, discussing trade-offs, making choices 20 20 21 21 ### Searchable Context 22 - What framing makes this period findable later? Extract the kind of context that helps answer future queries like: 22 + What framing makes this segment findable later? Extract the kind of context that helps answer future queries like: 23 23 - "When was I working on that authentication bug?" 24 24 - "What did I commit to doing after talking to Alice?" 25 25 - "When did I decide to refactor that module?" ··· 29 29 30 30 ## Analysis Approach 31 31 32 - **Work with available data**: Periods may have audio only, screen only, or both. Use what's present: 32 + **Work with available data**: Segments may have audio only, screen only, or both. Use what's present: 33 33 - Audio reveals: conversations, commitments, thinking-aloud, discussions, tone 34 34 - Screen shows: what tools/files are active, code being written, commands run, apps visible 35 35 36 36 **Interpret, don't repeat**: Transform raw observations into understanding. Be specific and concrete about what's being worked on, not vague descriptions. 37 37 38 - **Always identify facets**: Determine which facet or facets this period's activities relate to. If work spans multiple facets, mention all relevant ones. Facets provide critical context for organizing and searching this period later. 38 + **Always identify facets**: Determine which facet or facets this segment's activities relate to. If work spans multiple facets, mention all relevant ones. Facets provide critical context for organizing and searching this segment later. 39 39 40 40 ## Output Format 41 41 42 - Write free-form markdown in whatever style best captures the period. This could be: 42 + Write free-form markdown in whatever style best captures the segment. This could be: 43 43 - Narrative paragraphs describing the flow of work 44 44 - Bulleted lists of parallel activities 45 45 - Structured sections if multiple distinct focuses exist 46 - - Brief notes if the period is straightforward 46 + - Brief notes if the segment is straightforward 47 47 48 - Adapt your format to the content. What matters is capturing the synthesis clearly and making the period discoverable. 48 + Adapt your format to the content. What matters is capturing the synthesis clearly and making the segment discoverable. 49 49 50 50 ## Special Considerations 51 51 ··· 53 53 - If multiple tasks are visible (context switches), note the transitions 54 54 - Emotional state or energy level may be relevant if observable (frustrated, energized, focused, scattered) 55 55 56 - Remember: Your synthesis should make this period discoverable and understandable in ways the raw transcript and screen descriptions cannot achieve alone. 56 + Remember: Your synthesis should make this segment discoverable and understandable in ways the raw transcript and screen descriptions cannot achieve alone.
+5 -5
think/topics/flow.txt
··· 8 8 9 9 ### 1. Workday Architecture & Time Patterns 10 10 - **Work Session Analysis**: 11 - - Map focused work blocks vs. fragmented periods 11 + - Map focused work blocks vs. fragmented segments 12 12 - Identify optimal session lengths for different work types 13 13 - Analyze startup/shutdown rituals and $pronouns_possessive effectiveness 14 14 - **Rhythm & Cadence**: 15 15 - Natural work cycles throughout the day 16 - - Productive vs. recovery periods 16 + - Productive vs. recovery segments 17 17 - Time between breaks and $pronouns_possessive impact on subsequent focus 18 18 - **Context Switching Cost**: 19 19 - Frequency and depth of task switches ··· 22 22 23 23 ### 2. Deep Work & Flow States 24 24 - **Flow Identification**: 25 - - Periods of sustained, uninterrupted focus 25 + - Segments of sustained, uninterrupted focus 26 26 - Environmental conditions that enabled flow 27 27 - Types of work that achieved flow state 28 28 - **Flow Blockers**: ··· 77 77 78 78 #### Work Design Improvements: 79 79 - **Time Blocking Recommendations**: Specific suggestions for structuring future days 80 - - **Energy-Task Matching**: Aligning high-energy periods with complex work 80 + - **Energy-Task Matching**: Aligning high-energy segments with complex work 81 81 - **Focus Protection Strategies**: Concrete ways to preserve deep work time 82 82 - **Transition Rituals**: Practices to improve task switching 83 83 ··· 93 93 - **Energy Investment ROI**: Where effort produces best/worst returns 94 94 95 95 ### 7. Comparative Performance Analysis 96 - - **Performance Variability**: What distinguished high vs. low productivity periods 96 + - **Performance Variability**: What distinguished high vs. low productivity segments 97 97 - **Success Pattern Recognition**: Common elements in productive sequences 98 98 - **Failure Pattern Analysis**: Recurring productivity obstacles 99 99 - **Personal Best Practices**: Effective strategies observed in action
+1 -1
think/topics/media.txt
··· 28 28 29 29 Produce a friendly markdown document with chronological individual sections with a short title and each containing these items: 30 30 31 - - Time Period(s) 31 + - Time Segment(s) 32 32 - Media Type (video, article, music, etc.) 33 33 - Work or Personal 34 34 - Topic/Description
+1 -1
think/topics/meetings.txt
··· 17 17 * The timestamps, since meetings often begin at the top of the hour or half-past. 18 18 * **Meeting End Cues:** A meeting may be ending if the audio transcript includes: 19 19 * Closing remarks (e.g., "Great, thanks everyone," "Let's wrap up," "Talk to you all later"). 20 - * A clear shift to a prolonged period of silence or non-conversational audio (e.g., only keyboard sounds). 20 + * A clear shift to a prolonged segment of silence or non-conversational audio (e.g., only keyboard sounds). 21 21 * **Use Screen Activity for Confirmation:** While screen activity can be distracting due to multitasking, it can be a helpful secondary indicator. Look for the launch or presence of meeting software (e.g., Google Meet, Zoom, Microsoft Teams) to help confirm the start of a meeting. There may be meetings in the audio transcript only with no clues on the screen. 22 22 * **Audio Required:** Meetings must have an audio component, if only visual/screen references exist without the audio transcript than it probably wasn't a meeting. 23 23
+1 -1
think/topics/research.txt
··· 8 8 9 9 1. **Assess Deep Work** 10 10 - Recognize segments of intense focus on a single problem or project. 11 - - Note the specific tools, code files, documents or websites visible during these periods. 11 + - Note the specific tools, code files, documents or websites visible during these segments. 12 12 - Example: A 30‑minute block editing `models.py` with no meeting audio indicates deep coding work. 13 13 14 14 2. **Detect Knowledge Gaps**
+1 -1
think/topics/schedule.txt
··· 49 49 Conclude with a brief summary highlighting: 50 50 - The next upcoming event 51 51 - Any particularly important or unusual scheduled items 52 - - General schedule density for the upcoming period 52 + - General schedule density for the upcoming segment 53 53 54 54 ## Important Notes 55 55 - Only extract what is clearly visible on screen or captured in a transcript
+1 -1
think/topics/timeline.json
··· 2 2 "title": "Day Timeline", 3 3 "description": "Constructs a detailed chronological timeline documenting every activity, task shift, and event throughout the workday. Creates a comprehensive historical record with rich descriptions of what happened when.", 4 4 "contains": "An hour-by-hour timeline with detailed sub-sections for each activity shift, capturing all work tasks, meetings, conversations, tools used, and transitions with descriptive context.", 5 - "occurrences": "Create an occurrence for each hour period, don't break down hours into any smaller segments the goal for timeline occurrences is for them to capture whatever happened within each hour of the day where there was activity.", 5 + "occurrences": "Create an occurrence for each hour segment, don't break down hours into any smaller segments the goal for timeline occurrences is for them to capture whatever happened within each hour of the day where there was activity.", 6 6 "color": "#9c27b0" 7 7 }
+1 -1
think/topics/timeline.txt
··· 33 33 34 34 ## Format 35 35 36 - Output a nicely formatted markdown document with per-hour sections and sub-sections within each hour for every task or focus shift. Only include time periods when there was activity in the transcripts. 36 + Output a nicely formatted markdown document with per-hour sections and sub-sections within each hour for every task or focus shift. Only include time segments when there was activity in the transcripts. 37 37 38 38 ## Documentation Guidelines 39 39
+3 -3
think/topics/tools.txt
··· 27 27 28 28 ### 4. Interaction Depth Analysis 29 29 - **Surface Interactions**: Quick checks, notifications, brief visits 30 - - **Deep Work Sessions**: Extended focused usage periods 30 + - **Deep Work Sessions**: Extended focused usage segments 31 31 - **Feature Utilization**: Which specific features/functions are actually used 32 32 - **Keyboard vs. Mouse**: Interaction patterns that indicate proficiency 33 33 ··· 46 46 47 47 ### Daily Tool Usage Chronicle 48 48 - **Hour-by-Hour Breakdown**: What tools dominated each hour of the day 49 - - **Tool Session Log**: Start/stop times for major tool usage periods 49 + - **Tool Session Log**: Start/stop times for major tool usage segments 50 50 - **Context Annotations**: Brief note on why each tool was used when it was used 51 51 52 52 ### Tool Catalog & Classification ··· 57 57 ### Critical Usage Findings 58 58 - **Essential Tool Dependencies**: Tools that block work when unavailable 59 59 - **High-Frequency Micro-Usage**: Tools used in very short but frequent bursts 60 - - **Deep Work Enablers**: Tools that correlate with extended focus periods 60 + - **Deep Work Enablers**: Tools that correlate with extended focus segments 61 61 - **Productivity Multipliers**: Tools that demonstrably accelerate task completion 62 62 - **Time Sinks**: Tools that consume disproportionate time relative to output 63 63
+19 -19
think/utils.py
··· 204 204 return days 205 205 206 206 207 - def period_key(name_or_path: str) -> str | None: 208 - """Extract full period key (HHMMSS or HHMMSS_LEN) from any path/filename. 207 + def segment_key(name_or_path: str) -> str | None: 208 + """Extract full segment key (HHMMSS or HHMMSS_LEN) from any path/filename. 209 209 210 210 Parameters 211 211 ---------- 212 212 name_or_path : str 213 - Period name, filename, or full path containing period. 213 + Segment name, filename, or full path containing segment. 214 214 215 215 Returns 216 216 ------- 217 217 str or None 218 - Full period key (HHMMSS or HHMMSS_LEN) if valid, None otherwise. 218 + Full segment key (HHMMSS or HHMMSS_LEN) if valid, None otherwise. 219 219 220 220 Examples 221 221 -------- 222 - >>> period_key("143022") 222 + >>> segment_key("143022") 223 223 "143022" 224 - >>> period_key("143022_300") 224 + >>> segment_key("143022_300") 225 225 "143022_300" 226 - >>> period_key("143022_300_summary.txt") 226 + >>> segment_key("143022_300_summary.txt") 227 227 "143022_300" 228 - >>> period_key("/journal/20250109/143022_300/audio.jsonl") 228 + >>> segment_key("/journal/20250109/143022_300/audio.jsonl") 229 229 "143022_300" 230 - >>> period_key("invalid") 230 + >>> segment_key("invalid") 231 231 None 232 232 """ 233 233 pattern = r"\b(\d{6})(?:_(\d+))?(?:_|\b)" ··· 239 239 return None 240 240 241 241 242 - def period_parse( 242 + def segment_parse( 243 243 name_or_path: str, 244 244 ) -> tuple[datetime.time | None, datetime.time | None]: 245 - """Parse period to extract start and end times as datetime objects. 245 + """Parse segment to extract start and end times as datetime objects. 246 246 247 247 Parameters 248 248 ---------- 249 249 name_or_path : str 250 - Period name (e.g., "143022" or "143022_300") or full path containing period. 250 + Segment name (e.g., "143022" or "143022_300") or full path containing segment. 251 251 252 252 Returns 253 253 ------- ··· 255 255 Tuple of (start_time, end_time) where: 256 256 - start_time: datetime.time for HHMMSS 257 257 - end_time: datetime.time computed from start + LEN seconds, or None if no LEN 258 - Returns (None, None) if not a valid period format. 258 + Returns (None, None) if not a valid segment format. 259 259 260 260 Examples 261 261 -------- 262 - >>> period_parse("143022") 262 + >>> segment_parse("143022") 263 263 (datetime.time(14, 30, 22), None) 264 - >>> period_parse("143022_300") # 14:30:22 + 300 seconds = 14:35:22 264 + >>> segment_parse("143022_300") # 14:30:22 + 300 seconds = 14:35:22 265 265 (datetime.time(14, 30, 22), datetime.time(14, 35, 22)) 266 - >>> period_parse("/journal/20250109/143022_300/audio.jsonl") 266 + >>> segment_parse("/journal/20250109/143022_300/audio.jsonl") 267 267 (datetime.time(14, 30, 22), datetime.time(14, 35, 22)) 268 - >>> period_parse("invalid") 268 + >>> segment_parse("invalid") 269 269 (None, None) 270 270 """ 271 271 from datetime import time, timedelta 272 272 273 - # Extract just the period name if it's a path 273 + # Extract just the segment name if it's a path 274 274 if "/" in name_or_path or "\\" in name_or_path: 275 275 path_parts = Path(name_or_path).parts 276 276 # Look for YYYYMMDD/HHMMSS* pattern ··· 283 283 else: 284 284 name = name_or_path 285 285 286 - # Validate and extract HHMMSS from period name 286 + # Validate and extract HHMMSS from segment name 287 287 # Simple format: HHMMSS (6 digits) 288 288 if name.isdigit() and len(name) == 6: 289 289 time_str = name