personal memory agent
0
fork

Configure Feed

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

Merge branch 'hopper-5zduu6vy-media-registry'

+44 -40
+2 -1
apps/import/routes.py
··· 13 13 from werkzeug.utils import secure_filename 14 14 15 15 from convey import emit, state 16 + from media import MEDIA_EXTENSIONS 16 17 from think.detect_created import detect_created 17 18 from think.importers.utils import ( 18 19 build_import_info, ··· 120 121 "input_type": "file", 121 122 "upload_prompt": "Upload an audio file (.m4a, .mp3, .wav)", 122 123 "has_guide": False, 123 - "accept": ".m4a,.mp3,.wav,.ogg,.webm", 124 + "accept": ",".join(sorted(MEDIA_EXTENSIONS)), 124 125 }, 125 126 { 126 127 "name": "document",
+25
media.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Media format registry - single source of truth for extensions, MIME types, and kind.""" 5 + 6 + FORMATS = [ 7 + (".flac", "audio/flac", "audio"), 8 + (".opus", "audio/opus", "audio"), 9 + (".ogg", "audio/ogg", "audio"), 10 + (".m4a", "audio/mp4", "audio"), 11 + (".mp3", "audio/mpeg", "audio"), 12 + (".wav", "audio/wav", "audio"), 13 + (".webm", "video/webm", "video"), 14 + (".mp4", "video/mp4", "video"), 15 + (".mov", "video/quicktime", "video"), 16 + ] 17 + 18 + AUDIO_EXTENSIONS: frozenset[str] = frozenset( 19 + ext for ext, _, kind in FORMATS if kind == "audio" 20 + ) 21 + VIDEO_EXTENSIONS: frozenset[str] = frozenset( 22 + ext for ext, _, kind in FORMATS if kind == "video" 23 + ) 24 + MEDIA_EXTENSIONS: frozenset[str] = frozenset(ext for ext, _, _ in FORMATS) 25 + MIME_TYPES: dict[str, str] = {ext: mime for ext, mime, _ in FORMATS}
+1 -3
observe/transcribe/main.py
··· 54 54 55 55 import numpy as np 56 56 57 + from media import AUDIO_EXTENSIONS as SUPPORTED_AUDIO_FORMATS 57 58 from observe.transcribe import ( 58 59 BACKEND_REGISTRY, 59 60 get_backend, ··· 98 99 99 100 # Number of recent entity names to load for transcription context 100 101 ENTITY_NAMES_LIMIT = 40 101 - 102 - # Supported audio file formats for transcription 103 - SUPPORTED_AUDIO_FORMATS = {".flac", ".m4a", ".mp3", ".ogg", ".opus", ".wav"} 104 102 105 103 # Module-level voice encoder cache 106 104 _voice_encoder = None
+4 -2
observe/utils.py
··· 16 16 import numpy as np 17 17 import soundfile as sf 18 18 19 + from media import AUDIO_EXTENSIONS as _AUDIO_EXTENSIONS 20 + from media import VIDEO_EXTENSIONS as _VIDEO_EXTENSIONS 19 21 from think.utils import day_path 20 22 21 23 logger = logging.getLogger(__name__) ··· 23 25 # Standard sample rate for audio processing 24 26 SAMPLE_RATE = 16000 25 27 26 - VIDEO_EXTENSIONS = (".webm", ".mp4", ".mov") 27 - AUDIO_EXTENSIONS = (".flac", ".ogg", ".m4a", ".opus", ".mp3") 28 + VIDEO_EXTENSIONS = tuple(_VIDEO_EXTENSIONS) 29 + AUDIO_EXTENSIONS = tuple(_AUDIO_EXTENSIONS) 28 30 29 31 30 32 def audio_to_flac_bytes(audio: np.ndarray, sample_rate: int) -> bytes:
+1 -1
pyproject.toml
··· 104 104 include = ["apps*", "think*", "convey*", "observe*", "muse*"] 105 105 106 106 [tool.setuptools] 107 - py-modules = ["sol"] 107 + py-modules = ["media", "sol"] 108 108 109 109 [tool.setuptools.package-data] 110 110 apps = ["*/templates/*.html", "*/muse/*.md"]
+1 -1
tests/test_retention.py
··· 26 26 27 27 class TestIsRawMedia: 28 28 def test_audio_extensions(self, tmp_path): 29 - for ext in (".flac", ".opus", ".ogg", ".m4a"): 29 + for ext in (".flac", ".opus", ".ogg", ".m4a", ".mp3", ".wav"): 30 30 p = tmp_path / f"audio{ext}" 31 31 p.touch() 32 32 assert is_raw_media(p), f"{ext} should be raw media"
+2 -9
think/importers/shared.py
··· 12 12 from pathlib import Path 13 13 from typing import TYPE_CHECKING, Any, Callable 14 14 15 + from media import MIME_TYPES 15 16 from think.importers.utils import save_import_file, write_import_metadata 16 17 from think.utils import day_path, get_journal, now_ms 17 18 ··· 283 284 284 285 # MIME type mapping for import metadata 285 286 _MIME_TYPES = { 286 - ".m4a": "audio/mp4", 287 - ".mp3": "audio/mpeg", 288 - ".wav": "audio/wav", 289 - ".flac": "audio/flac", 290 - ".ogg": "audio/ogg", 291 - ".opus": "audio/opus", 292 - ".mp4": "video/mp4", 293 - ".webm": "video/webm", 294 - ".mov": "video/quicktime", 287 + **MIME_TYPES, 295 288 ".txt": "text/plain", 296 289 ".md": "text/markdown", 297 290 ".pdf": "application/pdf",
+4 -5
think/retention.py
··· 23 23 from pathlib import Path 24 24 from typing import Any 25 25 26 + from media import AUDIO_EXTENSIONS as RAW_AUDIO_EXTENSIONS 27 + from media import MEDIA_EXTENSIONS as RAW_MEDIA_EXTENSIONS 28 + from media import VIDEO_EXTENSIONS as RAW_VIDEO_EXTENSIONS 26 29 from think.utils import day_dirs, get_journal, iter_segments 27 30 28 31 logger = logging.getLogger(__name__) ··· 31 34 # Raw media file identification 32 35 # --------------------------------------------------------------------------- 33 36 34 - RAW_AUDIO_EXTENSIONS = frozenset({".flac", ".opus", ".ogg", ".m4a"}) 35 - RAW_VIDEO_EXTENSIONS = frozenset({".webm", ".mov", ".mp4"}) 36 - RAW_MEDIA_EXTENSIONS = RAW_AUDIO_EXTENSIONS | RAW_VIDEO_EXTENSIONS 37 - 38 37 39 38 def is_raw_media(path: Path) -> bool: 40 39 """Check if a file is raw media (layer 1 capture). 41 40 42 - Raw media: *.flac, *.opus, *.ogg, *.m4a (audio), 41 + Raw media: *.flac, *.opus, *.ogg, *.m4a, *.mp3, *.wav (audio), 43 42 *.webm, *.mov, *.mp4 (video), monitor_*_diff.png (screen diffs). 44 43 """ 45 44 if path.suffix.lower() in RAW_MEDIA_EXTENSIONS:
+4 -18
think/utils.py
··· 26 26 from dotenv import load_dotenv 27 27 from timefhuman import timefhuman 28 28 29 + from media import MIME_TYPES 30 + 29 31 DATE_RE = re.compile(r"\d{8}") 30 32 _journal_path_cache: str | None = None 31 33 ··· 800 802 if not rel: 801 803 raise ValueError(f"No 'raw' field found in metadata for {name}") 802 804 803 - # Determine MIME type from raw file extension 804 - if rel.endswith(".flac"): 805 - mime = "audio/flac" 806 - elif rel.endswith(".ogg"): 807 - mime = "audio/ogg" 808 - elif rel.endswith(".m4a"): 809 - mime = "audio/mp4" 810 - elif rel.endswith(".png"): 811 - mime = "image/png" 812 - elif rel.endswith(".webm"): 813 - mime = "video/webm" 814 - elif rel.endswith(".mp4"): 815 - mime = "video/mp4" 816 - elif rel.endswith(".mov"): 817 - mime = "video/quicktime" 818 - else: 819 - # Default fallback for unknown types 820 - mime = "application/octet-stream" 805 + suffix = Path(rel).suffix.lower() 806 + mime = {**MIME_TYPES, ".png": "image/png"}.get(suffix, "application/octet-stream") 821 807 822 808 return rel, mime, meta 823 809