personal memory agent
0
fork

Configure Feed

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

transcribe: flip parakeet to default; harden install; add install-models

Switch the default transcription backend from whisper to parakeet on
supported hosts (darwin/arm64, linux/x86_64). Whisper remains selectable
as the documented rollback path; its code, deps (faster-whisper), UI
fieldset, registry entry, and tests are unchanged.

- observe/transcribe/main.py: DEFAULT_BACKEND = "parakeet"; module
docstring reframed; CLI --backend help now reports parakeet as default.
- observe/transcribe/__init__.py + parakeet.py: docstrings reflect
parakeet-as-default and call out that the upstream Silero VAD
(observe/vad.py) is the sole silence gate.
- tests/fixtures/journal/config/journal.json + apps/settings/tests/
conftest.py: default fixture config flipped; parakeet sub-block
(model_version/device/timeout_sec) added.
- tests/test_transcribe_noise_upgrade.py: new parametrized regression
exercising the parakeet-default upgrade path (noisy + revai token →
revai; noisy + no token → parakeet; not-noisy → parakeet).
- Makefile install:: swallow on platform prep replaced with hard exit 1
on darwin/arm64 and linux/x86_64; unsupported hosts get a loud named
error. PARAKEET_NEMO_EXTRA variable added so the negative-path proof
can be reproduced with `make install PARAKEET_NEMO_EXTRA=does-not-exist`.
- Makefile install-models + scripts/install_parakeet_model.py: new
idempotent model-fetch step invoked at the tail of install. Sentinel
JSON at ~/.cache/huggingface/hub/.solstone-install-complete (linux) or
~/Library/Application Support/solstone/parakeet/models/.install-complete
(darwin). Fast path skips torch/NeMo imports when sentinel is present
and cache resolves. Partial-cache quarantine on verification failure;
legible HF-429 and ENOSPC error messages. v3 only.
- apps/settings/workspace.html: new parakeet-settings fieldset (model_
version/device/timeout_sec plus a read-only platform-runtime label);
JS default fallback flipped from 'whisper' to 'parakeet'; switch
handler + field-change POST handler mirror the whisper pattern.
- apps/settings/routes.py: new runtime_label field on /api/transcribe,
computed by a lazy-torch-import helper — returns "macOS CoreML helper",
"Linux NeMo (CUDA fp16)", "Linux NeMo (CPU fp32)", or "unsupported".
- apps/settings/tests/test_call.py: new set-backend parakeet test
mirroring the gemini/whisper pattern.
- talent/journal/references/config.md + INSTALL.md: parakeet described
as default; whisper documented as rollback path.
- tests/baselines: transcribe.json and config.json carry the expected
default-backend + parakeet-block + runtime_label deltas. graph.json,
search.json, and day-results.json carry pre-existing time-based
recency drift (0.1-point score shifts) surfaced by the baseline
regen — NOT caused by this change.

Notes:
- The --cpu flag stays whisper-only and is now a no-op unless the user
also passes --backend whisper.
- Mac negative-path proof (parakeet-helper build failure exits install
non-zero) is deferred to a Mac runner; Linux arm was verified on this
host's x86_64 worktree.
- runtime_label baseline is currently "unsupported" because the sandbox
env doesn't sync parakeet-nemo; dev hosts with the extra installed
will see "Linux NeMo (CUDA fp16)" or similar. Potential follow-up to
mock this for baseline determinism.
- Out of scope (flagged for follow-up): surfacing transcribe.parakeet.
precision in the settings UI; carrying fluidaudio_version /
helper_hardware into transcript metadata.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+749 -66
+7 -4
INSTALL.md
··· 45 45 brew install git uv 46 46 ``` 47 47 48 - #### Parakeet backend (optional, macOS + Linux) 48 + #### Parakeet backend (default) 49 49 50 - - Enable it by setting `journal/config/journal.json`: 50 + - Parakeet is the default transcription backend on supported hosts. The journal config looks like: 51 51 52 52 ```json 53 53 { ··· 63 63 ``` 64 64 65 65 - `device: "auto"` is primarily for Linux; macOS always routes to the CoreML helper on Apple Silicon. 66 - - **macOS (Apple Silicon):** Xcode command line tools are required because the helper is a Swift package; if the `xcodebuild -version` check above fails, fix that first. `make install` runs `make parakeet-helper` automatically, and the first run downloads roughly 461 MB of model data into `~/Library/Application Support/solstone/parakeet/models`. 67 - - **Linux (x86_64):** `make install` runs `uv sync --extra parakeet-nemo` automatically to install NeMo + torch. The first run downloads roughly 2.4 GB of model data into `~/.cache/huggingface/hub/`. CUDA is optional; `device: "auto"` falls back to CPU when no GPU is available. 66 + - `make install` builds the platform prerequisites and then runs `make install-models`. 67 + - `make install-models` is idempotent and prints `model ready: <path>` when the local model cache is ready. 68 + - **macOS (Apple Silicon):** Xcode command line tools are required because the helper is a Swift package; if the `xcodebuild -version` check above fails, fix that first. `make install-models` downloads roughly 461 MB of model data into `~/Library/Application Support/solstone/parakeet/models`. 69 + - **Linux (x86_64):** `make install` runs `uv sync --extra parakeet-nemo` automatically to install NeMo + torch, then `make install-models` verifies or fetches the v3 model cache in `~/.cache/huggingface/hub/`. The first fetch is roughly 2.4 GB. CUDA is optional; `device: "auto"` falls back to CPU when no GPU is available. 70 + - To use Whisper instead, set `transcribe.backend = "whisper"` in `journal/config/journal.json` or switch the backend in the settings UI. The Whisper code and dependencies remain available for rollback. 68 71 - helper contract details live in `observe/transcribe/parakeet_helper/README.md`. 69 72 70 73 ## install
+15 -7
Makefile
··· 7 7 # all runs to one path and pytest wipes it on startup, destroying concurrent state. 8 8 export TMPDIR := /var/tmp 9 9 10 - .PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format format-check install-checks ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sandbox sandbox-stop install-pinchtab parakeet-helper parakeet-helper-clean verify-browser update-browser-baselines review verify verify-api update-api-baselines install-service uninstall-service service-logs gate-agents-rename check-layer-hygiene doctor FORCE 10 + .PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format format-check install-checks ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sandbox sandbox-stop install-pinchtab install-models parakeet-helper parakeet-helper-clean verify-browser update-browser-baselines review verify verify-api update-api-baselines install-service uninstall-service service-logs gate-agents-rename check-layer-hygiene doctor FORCE 11 11 12 12 # Default target - install package in editable mode 13 13 all: install ··· 15 15 # Virtual environment directory 16 16 VENV := .venv 17 17 VENV_BIN := $(VENV)/bin 18 - PYTHON := $(VENV_BIN)/python 18 + VENV_PY := $(VENV_BIN)/python 19 + PYTHON := $(VENV_PY) 20 + PARAKEET_NEMO_EXTRA ?= parakeet-nemo 19 21 20 22 # Require uv 21 23 UV := $(shell command -v uv 2>/dev/null) ··· 74 76 fi; \ 75 77 } 76 78 @OS_NAME=$$(uname -s); \ 77 - if [ "$$OS_NAME" = "Darwin" ]; then \ 78 - $(MAKE) parakeet-helper || (echo 'parakeet backend unavailable: helper build failed' >&2; true); \ 79 - elif [ "$$OS_NAME" = "Linux" ]; then \ 80 - $(UV) sync --extra parakeet-nemo || (echo 'parakeet backend unavailable: uv sync --extra parakeet-nemo failed' >&2; true); \ 79 + ARCH=$$(uname -m); \ 80 + if [ "$$OS_NAME" = "Darwin" ] && [ "$$ARCH" = "arm64" ]; then \ 81 + $(MAKE) parakeet-helper || { echo 'parakeet install: helper build failed' >&2; exit 1; }; \ 82 + elif [ "$$OS_NAME" = "Linux" ] && [ "$$ARCH" = "x86_64" ]; then \ 83 + $(UV) sync --extra $(PARAKEET_NEMO_EXTRA) || { echo "parakeet install: uv sync --extra $(PARAKEET_NEMO_EXTRA) failed" >&2; exit 1; }; \ 81 84 else \ 82 - true; \ 85 + echo "parakeet install: unsupported host '$$OS_NAME/$$ARCH'; supported: darwin/arm64, linux/x86_64" >&2; \ 86 + exit 1; \ 83 87 fi 88 + @$(MAKE) --no-print-directory install-models 84 89 85 90 # Directories where AI coding agents look for skills 86 91 SKILL_DIRS := journal/.agents/skills journal/.claude/skills ··· 254 259 fi 255 260 256 261 # Build the parakeet helper binary (macOS/arm64 only, requires Xcode CLT) 262 + install-models: .installed 263 + $(VENV_PY) scripts/install_parakeet_model.py 264 + 257 265 parakeet-helper: 258 266 cd observe/transcribe/parakeet_helper && swift build -c release 259 267 @echo "built: $$(pwd)/observe/transcribe/parakeet_helper/.build/release/parakeet-helper"
+22
apps/settings/routes.py
··· 7 7 import json 8 8 import logging 9 9 import os 10 + import platform 10 11 import re 11 12 import subprocess 12 13 from datetime import datetime, timezone ··· 47 48 "REVAI_ACCESS_TOKEN", 48 49 "PLAUD_ACCESS_TOKEN", 49 50 ] 51 + 52 + 53 + def _compute_runtime_label() -> str: 54 + os_name = platform.system().lower() 55 + arch = platform.machine().lower() 56 + if os_name == "darwin" and arch == "arm64": 57 + return "macOS CoreML helper" 58 + if os_name != "linux": 59 + return "unsupported" 60 + try: 61 + import torch 62 + 63 + return ( 64 + "Linux NeMo (CUDA fp16)" 65 + if torch.cuda.is_available() 66 + else "Linux NeMo (CPU fp32)" 67 + ) 68 + except Exception: 69 + return "unsupported" 50 70 51 71 52 72 @settings_bp.route("/api/config") ··· 336 356 337 357 # Get backends list from registry 338 358 backends = get_backend_list() 359 + runtime_label = _compute_runtime_label() 339 360 340 361 # Check API key status for each backend 341 362 api_keys = {} ··· 351 372 "backends": backends, 352 373 "api_keys": api_keys, 353 374 "config": transcribe_config, 375 + "runtime_label": runtime_label, 354 376 } 355 377 ) 356 378 except Exception:
+14 -1
apps/settings/tests/conftest.py
··· 64 64 "key_validation": {}, 65 65 }, 66 66 "transcribe": { 67 - "backend": "whisper", 67 + "backend": "parakeet", 68 68 "enrich": True, 69 69 "noise_upgrade": False, 70 + "parakeet": { 71 + "model_version": "v3", 72 + "device": "auto", 73 + "timeout_sec": 120.0, 74 + }, 75 + "whisper": { 76 + "device": "auto", 77 + "model": "medium.en", 78 + "compute_type": "default", 79 + }, 80 + "revai": { 81 + "model": "fusion", 82 + }, 70 83 }, 71 84 "observe": {"tmux": {"enabled": True, "capture_interval": 5}}, 72 85 }
+11
apps/settings/tests/test_call.py
··· 454 454 saved = json.loads((tmp_path / "config" / "journal.json").read_text()) 455 455 assert saved["transcribe"]["backend"] == "gemini" 456 456 457 + def test_set_backend_parakeet(self, settings_env): 458 + tmp_path, _config = settings_env() 459 + 460 + result = runner.invoke( 461 + call_app, ["settings", "transcribe", "set-backend", "parakeet"] 462 + ) 463 + 464 + assert result.exit_code == 0 465 + saved = json.loads((tmp_path / "config" / "journal.json").read_text()) 466 + assert saved["transcribe"]["backend"] == "parakeet" 467 + 457 468 def test_set_backend_invalid(self, settings_env): 458 469 settings_env() 459 470
+79 -1
apps/settings/workspace.html
··· 793 793 margin: 0.5em 0 0 0; 794 794 } 795 795 796 + .form-help { 797 + margin-top: 0.5em; 798 + font-size: 0.85em; 799 + color: var(--surface-muted, #666); 800 + } 801 + 796 802 /* Context overrides collapsible section */ 797 803 .context-overrides { 798 804 margin-top: 2em; ··· 2390 2396 </p> 2391 2397 </fieldset> 2392 2398 2399 + <fieldset id="parakeet-settings" class="backend-settings" style="display:none"> 2400 + <div class="settings-field"> 2401 + <label for="field-parakeet-model-version">model version</label> 2402 + <select id="field-parakeet-model-version"> 2403 + <option value="v3" selected>v3 - Parakeet TDT 0.6B</option> 2404 + </select> 2405 + <small>Installed Parakeet model version for local transcription</small> 2406 + </div> 2407 + 2408 + <div class="settings-field"> 2409 + <label for="field-parakeet-device">device</label> 2410 + <select id="field-parakeet-device"> 2411 + <option value="auto" selected>Auto - Detect best available runtime</option> 2412 + <option value="cpu">CPU - Force processor runtime</option> 2413 + <option value="cuda">CUDA - Force NVIDIA runtime</option> 2414 + </select> 2415 + <small>Runtime preference for Parakeet on this host</small> 2416 + </div> 2417 + 2418 + <div class="settings-field"> 2419 + <label for="field-parakeet-timeout">timeout (seconds)</label> 2420 + <input id="field-parakeet-timeout" type="number" min="1" step="1" value="120"> 2421 + <small>Abort Parakeet model work if it exceeds this timeout</small> 2422 + </div> 2423 + 2424 + <div class="form-help">Platform runtime: <span id="parakeet-runtime-label">unsupported</span></div> 2425 + </fieldset> 2426 + 2393 2427 <!-- Shared settings --> 2394 2428 <div class="settings-field" style="margin-top: 1.5em;"> 2395 2429 <div class="toggle-container" style="justify-content: flex-start; gap: 1em;"> ··· 3299 3333 3300 3334 // Transcribe - now with nested backend configs 3301 3335 const transcribe = config.transcribe || {}; 3302 - const backend = transcribe.backend || 'whisper'; 3336 + const backend = transcribe.backend || 'parakeet'; 3303 3337 setValue('field-transcribe-backend', backend); 3304 3338 switchTranscribeBackend(backend, config); 3305 3339 3340 + const runtimeLabel = document.getElementById('parakeet-runtime-label'); 3341 + if (runtimeLabel) { 3342 + runtimeLabel.textContent = transcribeRuntimeLabel; 3343 + } 3344 + 3345 + // Parakeet settings (nested) 3346 + const parakeet = transcribe.parakeet || {}; 3347 + setValue('field-parakeet-model-version', parakeet.model_version || 'v3'); 3348 + setValue('field-parakeet-device', parakeet.device || 'auto'); 3349 + setValue('field-parakeet-timeout', parakeet.timeout_sec ?? 120); 3350 + 3306 3351 // Whisper settings (nested) 3307 3352 const whisper = transcribe.whisper || {}; 3308 3353 setValue('field-whisper-device', whisper.device || 'auto'); ··· 4589 4634 // Backend metadata loaded from API 4590 4635 let transcribeBackends = []; 4591 4636 let transcribeApiKeys = {}; 4637 + let transcribeRuntimeLabel = 'unsupported'; 4592 4638 let storageLoadedOnce = false; 4593 4639 4594 4640 function setLoadState(slotId, html = '') { ··· 4631 4677 const data = await window.apiJson('api/transcribe'); 4632 4678 transcribeBackends = data.backends || []; 4633 4679 transcribeApiKeys = data.api_keys || {}; 4680 + transcribeRuntimeLabel = data.runtime_label || 'unsupported'; 4634 4681 setLoadState('transcriptionLoadState'); 4635 4682 const select = document.getElementById('field-transcribe-backend'); 4683 + const runtimeLabel = document.getElementById('parakeet-runtime-label'); 4636 4684 if (select) { 4637 4685 select.disabled = false; 4686 + } 4687 + if (runtimeLabel) { 4688 + runtimeLabel.textContent = transcribeRuntimeLabel; 4638 4689 } 4639 4690 4640 4691 // Populate the backend select ··· 4666 4717 4667 4718 function switchTranscribeBackend(backend, config) { 4668 4719 // Show/hide backend-specific fieldsets 4720 + document.getElementById('parakeet-settings').style.display = backend === 'parakeet' ? 'block' : 'none'; 4669 4721 document.getElementById('whisper-settings').style.display = backend === 'whisper' ? 'block' : 'none'; 4670 4722 document.getElementById('revai-settings').style.display = backend === 'revai' ? 'block' : 'none'; 4671 4723 document.getElementById('gemini-settings').style.display = backend === 'gemini' ? 'block' : 'none'; ··· 4777 4829 console.error('Error saving revai setting:', err); 4778 4830 showFieldStatus(e.target, 'error', err.message); 4779 4831 } 4832 + }); 4833 + 4834 + ['field-parakeet-model-version', 'field-parakeet-device', 'field-parakeet-timeout'].forEach(id => { 4835 + document.getElementById(id)?.addEventListener('change', async (e) => { 4836 + const key = id === 'field-parakeet-model-version' ? 'model_version' : 4837 + id === 'field-parakeet-device' ? 'device' : 'timeout_sec'; 4838 + const value = key === 'timeout_sec' ? Number(e.target.value) : e.target.value; 4839 + 4840 + try { 4841 + const response = await fetch('api/config', { 4842 + method: 'PUT', 4843 + headers: { 'Content-Type': 'application/json' }, 4844 + body: JSON.stringify({ section: 'transcribe', data: { parakeet: { [key]: value } } }) 4845 + }); 4846 + const result = await response.json(); 4847 + if (result.success) { 4848 + configData = result.config; 4849 + showFieldStatus(e.target, 'saved'); 4850 + } else { 4851 + throw new Error(result.error); 4852 + } 4853 + } catch (err) { 4854 + console.error('Error saving parakeet setting:', err); 4855 + showFieldStatus(e.target, 'error', err.message); 4856 + } 4857 + }); 4780 4858 }); 4781 4859 4782 4860 // ========== OBSERVER ==========
+2 -2
observe/transcribe/__init__.py
··· 13 13 - "segment" = journal directory (HHMMSS_LEN/ time window) - NOT used here 14 14 15 15 Available backends: 16 - - whisper: Local faster-whisper (default, GPU/CPU) 16 + - parakeet: Default local backend via Apple Silicon helper or Linux NeMo 17 + - whisper: Local faster-whisper (rollback/local alternative, GPU/CPU) 17 18 - revai: Rev.ai cloud API (speaker diarization) 18 19 - gemini: Google Gemini API (speaker diarization) 19 - - parakeet: Local Apple Silicon processing via helper 20 20 21 21 Backend Interface: 22 22 Each backend module must export a transcribe() function:
+16 -14
observe/transcribe/main.py
··· 6 6 Transcription pipeline: 7 7 1. VAD stage: Run Silero VAD to detect speech and filter silent files early 8 8 2. Audio reduction: Trim long silence gaps for faster processing 9 - 3. Transcription: Dispatch to STT backend (default: whisper) 9 + 3. Transcription: Dispatch to the configured STT backend (default: parakeet) 10 10 4. Enrichment: Extract topics, setting, emotions, and warnings via LLM (optional) 11 11 5. Embeddings: Generate voice embeddings for each sentence using wespeaker-resnet34 12 12 6. Output: JSONL format compatible with format_audio() in observe/hear.py ··· 16 16 - <stem>.npz: Sentence-level voice embeddings indexed by statement id 17 17 18 18 Configuration (journal config transcribe section): 19 - - transcribe.backend: STT backend ("whisper", "revai", "gemini", "parakeet"). Default: "whisper" 19 + - transcribe.backend: STT backend ("parakeet", "whisper", "revai", "gemini"). Default: "parakeet" 20 20 - transcribe.enrich: Enable/disable LLM enrichment (default: true) 21 21 - transcribe.preserve_all: Keep audio files even when no speech detected (default: false) 22 22 - transcribe.min_speech_seconds: Minimum speech duration to proceed. Default: 1.0 23 23 - transcribe.noise_upgrade: Auto-switch to Rev.ai for noisy recordings (default: true) 24 24 - transcribe.noise_upgrade_min_speech_ratio: Min speech/loud ratio required for noisy upgrade (default: 0.3). Filters out music and other non-speech noise. 25 25 26 + Parakeet backend settings (transcribe.parakeet): 27 + - model_version: Parakeet model version ("v2", "v3"). Default: "v3" 28 + - cache_dir: Optional helper cache directory 29 + - timeout_sec: Helper timeout in seconds. Default: 120.0 30 + 26 31 Whisper backend settings (transcribe.whisper): 27 32 - device: Device for inference ("auto", "cpu", "cuda"). Default: "auto" 28 33 - model: Whisper model size (e.g., "medium.en"). Default: "medium.en" 29 34 - compute_type: Precision ("default", "float32", "float16", "int8"). Default: "default" 30 35 Auto-selects: float16 for CUDA, int8 for CPU (including Apple Silicon). 36 + - Whisper remains available as the rollback/local alternative backend. 31 37 32 38 Rev.ai backend settings (transcribe.revai): 33 39 - model: Rev.ai transcriber ("fusion", "machine", "low_cost"). Default: "fusion" ··· 36 42 Gemini backend settings (transcribe.gemini): 37 43 - No configuration needed (model resolved by think.models context system) 38 44 - Includes speaker diarization 39 - 40 - Parakeet backend settings (transcribe.parakeet): 41 - - model_version: Parakeet model version ("v2", "v3"). Default: "v3" 42 - - cache_dir: Optional helper cache directory 43 - - timeout_sec: Helper timeout in seconds. Default: 120.0 44 45 45 46 Platform optimizations (Whisper): 46 47 - CUDA GPU: Uses float16 for GPU-optimized inference ··· 101 102 ] 102 103 103 104 # Default transcription settings 104 - DEFAULT_BACKEND = "whisper" 105 + DEFAULT_BACKEND = "parakeet" 105 106 DEFAULT_MIN_SPEECH_SECONDS = 1.0 106 107 107 108 # Minimum statement duration for embedding (seconds) ··· 475 476 redo: bool = False, 476 477 reduction: AudioReduction | None = None, 477 478 reduced_audio: np.ndarray | None = None, 478 - backend: str = DEFAULT_BACKEND, 479 + backend: str | None = None, 479 480 entity_names: list[str] | None = None, 480 481 ) -> None: 481 482 """Process a raw audio file with pre-computed VAD. ··· 495 496 redo: If True, skip "already processed" check 496 497 reduction: Optional AudioReduction mapping for timestamp restoration 497 498 reduced_audio: Optional reduced audio buffer (used if reduction provided) 498 - backend: STT backend name (default: "whisper") 499 + backend: STT backend name. If omitted, uses DEFAULT_BACKEND. 499 500 entity_names: Optional list of entity names for STT and enrichment context 500 501 """ 501 502 start_time = time.time() 503 + resolved_backend = backend or DEFAULT_BACKEND 502 504 503 505 # Derive segment from path 504 506 segment = get_segment_key(raw_path) ··· 539 541 if use_gemini_chunks: 540 542 # Pass VAD segments to Gemini for chunk-based transcription 541 543 statements = stt_transcribe( 542 - backend, 544 + resolved_backend, 543 545 stt_buffer, 544 546 SAMPLE_RATE, 545 547 backend_config, ··· 547 549 ) 548 550 else: 549 551 statements = stt_transcribe( 550 - backend, stt_buffer, SAMPLE_RATE, backend_config 552 + resolved_backend, stt_buffer, SAMPLE_RATE, backend_config 551 553 ) 552 554 553 555 # Get model info for metadata (dynamic import based on backend) 554 - backend_module = get_backend(backend) 556 + backend_module = get_backend(resolved_backend) 555 557 model_info = backend_module.get_model_info(backend_config) 556 558 557 559 # Load config for preserve_all setting ··· 636 638 enrichment, 637 639 vad_result, 638 640 segment_meta, 639 - backend, 641 + resolved_backend, 640 642 ) 641 643 642 644 # Write JSONL
+2
observe/transcribe/parakeet.py
··· 9 9 10 10 Unsupported platforms raise RuntimeError with the detected platform 11 11 and the supported-platforms list. 12 + Parakeet does not gate on internal silence; the upstream Silero VAD in 13 + `observe/vad.py` (run by `_process_one`) is the gate and must remain in place. 12 14 """ 13 15 14 16 from __future__ import annotations
+442
scripts/install_parakeet_model.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import argparse 7 + import datetime as dt 8 + import errno 9 + import json 10 + import os 11 + import platform 12 + import shutil 13 + import subprocess 14 + import sys 15 + import tempfile 16 + from pathlib import Path 17 + from typing import Any 18 + 19 + BACKEND = "parakeet" 20 + MODEL_VERSION = "v3" 21 + MODEL_ID = "nvidia/parakeet-tdt-0.6b-v3" 22 + HELPER_ENV_KEY = "SOLSTONE_PARAKEET_HELPER" 23 + MAC_CACHE_DIR = Path.home() / "Library/Application Support/solstone/parakeet/models" 24 + MAC_SENTINEL = MAC_CACHE_DIR / ".install-complete" 25 + LINUX_HUB_DIR = Path.home() / ".cache/huggingface/hub" 26 + LINUX_MODEL_DIR = LINUX_HUB_DIR / "models--nvidia--parakeet-tdt-0.6b-v3" 27 + LINUX_SENTINEL = LINUX_HUB_DIR / ".solstone-install-complete" 28 + MAC_MODEL_FILES = ( 29 + "Encoder.mlmodelc/weights/weight.bin", 30 + "Decoder.mlmodelc/weights/weight.bin", 31 + "JointDecision.mlmodelc/weights/weight.bin", 32 + "Preprocessor.mlmodelc/weights/weight.bin", 33 + ) 34 + 35 + 36 + def _now_utc() -> str: 37 + return ( 38 + dt.datetime.now(dt.timezone.utc) 39 + .replace(microsecond=0) 40 + .isoformat() 41 + .replace("+00:00", "Z") 42 + ) 43 + 44 + 45 + def _quarantine_suffix() -> str: 46 + return dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ") 47 + 48 + 49 + def _platform_info() -> tuple[str, str]: 50 + os_name = "linux" if sys.platform.startswith("linux") else sys.platform 51 + return os_name, platform.machine().lower() 52 + 53 + 54 + def _variant_for_platform(os_name: str, arch: str) -> str | None: 55 + if os_name == "darwin" and arch == "arm64": 56 + return "coreml" 57 + if os_name == "linux" and arch == "x86_64": 58 + return "nemo" 59 + return None 60 + 61 + 62 + def _sentinel_path(variant: str) -> Path: 63 + return MAC_SENTINEL if variant == "coreml" else LINUX_SENTINEL 64 + 65 + 66 + def _cache_dir(variant: str) -> Path: 67 + return MAC_CACHE_DIR if variant == "coreml" else LINUX_MODEL_DIR 68 + 69 + 70 + def _repo_root() -> Path: 71 + return Path(__file__).resolve().parents[1] 72 + 73 + 74 + def _fixture_audio_path() -> Path: 75 + return _repo_root() / "tests" / "fixtures" / "parakeet_sample.wav" 76 + 77 + 78 + def _helper_path() -> Path: 79 + env_path = os.getenv(HELPER_ENV_KEY) 80 + if env_path: 81 + return Path(env_path).expanduser().resolve() 82 + return ( 83 + _repo_root() 84 + / "observe" 85 + / "transcribe" 86 + / "parakeet_helper" 87 + / ".build" 88 + / "release" 89 + / "parakeet-helper" 90 + ).resolve() 91 + 92 + 93 + def _load_sentinel(path: Path) -> dict[str, Any] | None: 94 + if not path.exists(): 95 + return None 96 + try: 97 + payload = json.loads(path.read_text(encoding="utf-8")) 98 + except (OSError, json.JSONDecodeError): 99 + return None 100 + return payload if isinstance(payload, dict) else None 101 + 102 + 103 + def _sentinel_ready( 104 + payload: dict[str, Any] | None, 105 + os_name: str, 106 + arch: str, 107 + variant: str, 108 + ) -> Path | None: 109 + if payload is None: 110 + return None 111 + platform_info = payload.get("platform") 112 + if not isinstance(platform_info, dict): 113 + return None 114 + if payload.get("schema_version") != 1 or payload.get("backend") != BACKEND: 115 + return None 116 + if ( 117 + payload.get("variant") != variant 118 + or payload.get("model_version") != MODEL_VERSION 119 + ): 120 + return None 121 + if platform_info.get("os") != os_name or platform_info.get("arch") != arch: 122 + return None 123 + cache_dir = payload.get("cache_dir") 124 + if not isinstance(cache_dir, str) or not cache_dir: 125 + return None 126 + if variant == "coreml" and not payload.get("fluidaudio_version"): 127 + return None 128 + resolved = Path(cache_dir).expanduser() 129 + return resolved if resolved.exists() else None 130 + 131 + 132 + def _write_sentinel(path: Path, payload: dict[str, Any]) -> None: 133 + path.parent.mkdir(parents=True, exist_ok=True) 134 + with tempfile.NamedTemporaryFile( 135 + "w", dir=path.parent, delete=False, encoding="utf-8" 136 + ) as handle: 137 + json.dump(payload, handle, indent=2) 138 + handle.write("\n") 139 + tmp_path = Path(handle.name) 140 + tmp_path.replace(path) 141 + 142 + 143 + def _remove_sentinel(path: Path) -> None: 144 + try: 145 + path.unlink() 146 + except FileNotFoundError: 147 + return 148 + 149 + 150 + def _quarantine_path(cache_dir: Path) -> Path: 151 + base = cache_dir.with_name(f"{cache_dir.name}.partial-{_quarantine_suffix()}") 152 + candidate = base 153 + suffix = 1 154 + while candidate.exists(): 155 + candidate = cache_dir.with_name(f"{base.name}-{suffix}") 156 + suffix += 1 157 + return candidate 158 + 159 + 160 + def _fail(message: str, code: int = 1) -> int: 161 + print(message, file=sys.stderr) 162 + return code 163 + 164 + 165 + def _fail_with_quarantine(message: str, cache_dir: Path, sentinel_path: Path) -> int: 166 + _remove_sentinel(sentinel_path) 167 + if cache_dir.exists(): 168 + quarantine = _quarantine_path(cache_dir) 169 + cache_dir.rename(quarantine) 170 + print( 171 + f"{message}; quarantined partial cache to {quarantine}", 172 + file=sys.stderr, 173 + ) 174 + print(f"reclaim space with: rm -rf {quarantine}", file=sys.stderr) 175 + return 1 176 + return _fail(message) 177 + 178 + 179 + def _disk_full_message(cache_dir: Path) -> str: 180 + usage_root = cache_dir if cache_dir.exists() else cache_dir.parent 181 + free_bytes = shutil.disk_usage(usage_root).free 182 + return ( 183 + f"parakeet install failed: disk full at {cache_dir} " 184 + f"(free {free_bytes} bytes); free space and retry" 185 + ) 186 + 187 + 188 + def _is_rate_limit_error(exc: Exception) -> bool: 189 + response = getattr(exc, "response", None) 190 + if getattr(response, "status_code", None) == 429: 191 + return True 192 + message = str(exc).lower() 193 + return "429" in message and "rate" in message and "limit" in message 194 + 195 + 196 + def _verify_linux_cache(cache_dir: Path) -> bool: 197 + snapshots_dir = cache_dir / "snapshots" 198 + if not snapshots_dir.is_dir(): 199 + return False 200 + for child in snapshots_dir.iterdir(): 201 + if child.is_dir() and any(child.iterdir()): 202 + return True 203 + return False 204 + 205 + 206 + def _verify_mac_cache(cache_dir: Path) -> bool: 207 + return all( 208 + (cache_dir / relative_path).is_file() for relative_path in MAC_MODEL_FILES 209 + ) 210 + 211 + 212 + def _restore_linux_sentinel_if_cache_ready( 213 + os_name: str, 214 + arch: str, 215 + variant: str, 216 + sentinel_path: Path, 217 + cache_dir: Path, 218 + ) -> Path | None: 219 + if variant != "nemo" or not _verify_linux_cache(cache_dir): 220 + return None 221 + _write_sentinel( 222 + sentinel_path, 223 + _build_payload(os_name, arch, variant, cache_dir), 224 + ) 225 + return cache_dir 226 + 227 + 228 + def _build_payload( 229 + os_name: str, 230 + arch: str, 231 + variant: str, 232 + cache_dir: Path, 233 + *, 234 + fluidaudio_version: str | None = None, 235 + ) -> dict[str, Any]: 236 + payload: dict[str, Any] = { 237 + "schema_version": 1, 238 + "backend": BACKEND, 239 + "platform": {"os": os_name, "arch": arch}, 240 + "variant": variant, 241 + "model_version": MODEL_VERSION, 242 + "fetched_at": _now_utc(), 243 + "cache_dir": str(cache_dir), 244 + } 245 + if fluidaudio_version is not None: 246 + payload["fluidaudio_version"] = fluidaudio_version 247 + return payload 248 + 249 + 250 + def _fetch_linux_model(cache_dir: Path) -> None: 251 + try: 252 + try: 253 + from huggingface_hub.utils import HfHubHTTPError 254 + except Exception: # pragma: no cover - older/newer hub versions vary 255 + HfHubHTTPError = None 256 + 257 + from nemo.collections.asr.models import ASRModel 258 + 259 + ASRModel.from_pretrained(MODEL_ID) 260 + except OSError as exc: 261 + if exc.errno == errno.ENOSPC: 262 + raise RuntimeError(_disk_full_message(cache_dir)) from exc 263 + raise 264 + except Exception as exc: 265 + if ( 266 + HfHubHTTPError is not None and isinstance(exc, HfHubHTTPError) 267 + ) or _is_rate_limit_error(exc): 268 + raise RuntimeError( 269 + "parakeet install failed: Hugging Face rate limit (HTTP 429); retry in a few minutes" 270 + ) from exc 271 + raise 272 + 273 + 274 + def _run_mac_helper(cache_dir: Path) -> dict[str, Any]: 275 + helper_path = _helper_path() 276 + if not helper_path.is_file() or not os.access(helper_path, os.X_OK): 277 + raise RuntimeError( 278 + f"parakeet install failed: helper not found or not executable at {helper_path}" 279 + ) 280 + fixture_audio = _fixture_audio_path() 281 + if not fixture_audio.is_file(): 282 + raise RuntimeError( 283 + f"parakeet install failed: fixture audio not found at {fixture_audio}" 284 + ) 285 + 286 + try: 287 + result = subprocess.run( 288 + [ 289 + str(helper_path), 290 + "--cache-dir", 291 + str(cache_dir), 292 + "--model", 293 + MODEL_VERSION, 294 + str(fixture_audio), 295 + ], 296 + check=False, 297 + capture_output=True, 298 + text=True, 299 + ) 300 + except OSError as exc: 301 + if exc.errno == errno.ENOSPC: 302 + raise RuntimeError(_disk_full_message(cache_dir)) from exc 303 + raise 304 + 305 + if result.returncode != 0: 306 + stderr_text = (result.stderr or "").strip() 307 + try: 308 + stderr_payload = json.loads(stderr_text) if stderr_text else {} 309 + except json.JSONDecodeError: 310 + stderr_payload = {} 311 + message = ( 312 + stderr_payload.get("message") or stderr_text or "unknown helper failure" 313 + ) 314 + raise RuntimeError(f"parakeet install failed: {message}") 315 + 316 + try: 317 + payload = json.loads(result.stdout) 318 + except json.JSONDecodeError as exc: 319 + raise RuntimeError( 320 + f"parakeet install failed: helper returned invalid JSON: {exc}" 321 + ) from exc 322 + if not isinstance(payload, dict): 323 + raise RuntimeError("parakeet install failed: helper returned non-object JSON") 324 + return payload 325 + 326 + 327 + def _install_models(os_name: str, arch: str, variant: str) -> int: 328 + sentinel_path = _sentinel_path(variant) 329 + cache_dir = _cache_dir(variant) 330 + _remove_sentinel(sentinel_path) 331 + 332 + if variant == "nemo": 333 + try: 334 + _fetch_linux_model(cache_dir) 335 + except RuntimeError as exc: 336 + return _fail_with_quarantine(str(exc), cache_dir, sentinel_path) 337 + except Exception as exc: 338 + return _fail_with_quarantine( 339 + f"parakeet install failed: {exc}", 340 + cache_dir, 341 + sentinel_path, 342 + ) 343 + if not _verify_linux_cache(cache_dir): 344 + return _fail_with_quarantine( 345 + "parakeet install failed: Linux cache verification failed", 346 + cache_dir, 347 + sentinel_path, 348 + ) 349 + _write_sentinel( 350 + sentinel_path, 351 + _build_payload(os_name, arch, variant, cache_dir), 352 + ) 353 + print(f"model ready: {cache_dir}") 354 + return 0 355 + 356 + try: 357 + payload = _run_mac_helper(cache_dir) 358 + except RuntimeError as exc: 359 + return _fail_with_quarantine(str(exc), cache_dir, sentinel_path) 360 + except Exception as exc: 361 + return _fail_with_quarantine( 362 + f"parakeet install failed: {exc}", 363 + cache_dir, 364 + sentinel_path, 365 + ) 366 + if not _verify_mac_cache(cache_dir): 367 + return _fail_with_quarantine( 368 + "parakeet install failed: macOS cache verification failed", 369 + cache_dir, 370 + sentinel_path, 371 + ) 372 + fluidaudio_version = payload.get("fluidaudio_version") 373 + if not isinstance(fluidaudio_version, str) or not fluidaudio_version: 374 + return _fail_with_quarantine( 375 + "parakeet install failed: helper success JSON missing fluidaudio_version", 376 + cache_dir, 377 + sentinel_path, 378 + ) 379 + _write_sentinel( 380 + sentinel_path, 381 + _build_payload( 382 + os_name, 383 + arch, 384 + variant, 385 + cache_dir, 386 + fluidaudio_version=fluidaudio_version, 387 + ), 388 + ) 389 + print(f"model ready: {cache_dir}") 390 + return 0 391 + 392 + 393 + def main() -> int: 394 + parser = argparse.ArgumentParser( 395 + description="Install or verify the Parakeet model cache." 396 + ) 397 + mode_group = parser.add_mutually_exclusive_group() 398 + mode_group.add_argument( 399 + "--check", 400 + action="store_true", 401 + help="Verify the sentinel/cache, fetching the model if needed (default behavior).", 402 + ) 403 + mode_group.add_argument( 404 + "--force", 405 + action="store_true", 406 + help="Ignore the sentinel and refetch/verify the model cache.", 407 + ) 408 + args = parser.parse_args() 409 + 410 + os_name, arch = _platform_info() 411 + variant = _variant_for_platform(os_name, arch) 412 + if variant is None: 413 + return _fail(f"unsupported parakeet platform: {os_name}/{arch}", code=2) 414 + 415 + sentinel_path = _sentinel_path(variant) 416 + cache_dir = _cache_dir(variant) 417 + if not args.force: 418 + ready_cache = _sentinel_ready( 419 + _load_sentinel(sentinel_path), 420 + os_name, 421 + arch, 422 + variant, 423 + ) 424 + if ready_cache is not None: 425 + print(f"model ready: {ready_cache}") 426 + return 0 427 + restored_cache = _restore_linux_sentinel_if_cache_ready( 428 + os_name, 429 + arch, 430 + variant, 431 + sentinel_path, 432 + cache_dir, 433 + ) 434 + if restored_cache is not None: 435 + print(f"model ready: {restored_cache}") 436 + return 0 437 + 438 + return _install_models(os_name, arch, variant) 439 + 440 + 441 + if __name__ == "__main__": 442 + raise SystemExit(main())
+12 -2
talent/journal/references/config.md
··· 154 154 ```json 155 155 { 156 156 "transcribe": { 157 - "backend": "whisper", 157 + "backend": "parakeet", 158 158 "enrich": true, 159 159 "preserve_all": false, 160 160 "noise_upgrade_min_speech_ratio": 0.3, 161 + "parakeet": { 162 + "model_version": "v3", 163 + "device": "auto", 164 + "timeout_sec": 120.0 165 + }, 161 166 "whisper": { 162 167 "device": "auto", 163 168 "model": "medium.en", ··· 171 176 ``` 172 177 173 178 **Top-level fields:** 174 - - `backend` (string) – STT backend to use: `"whisper"` (local processing) or `"revai"` (cloud with speaker diarization). Default: `"whisper"`. 179 + - `backend` (string) – STT backend to use: `"parakeet"` (default local processing), `"whisper"` (local rollback path), `"revai"` (cloud with speaker diarization), or `"gemini"` (cloud with speaker diarization). Default: `"parakeet"`. 175 180 - `enrich` (boolean) – Enable LLM enrichment for topic extraction and transcript correction. Default: `true`. 176 181 - `preserve_all` (boolean) – Keep audio files even when no speech is detected. When `false`, silent recordings are deleted to save disk space. Default: `false`. 177 182 - `noise_upgrade_min_speech_ratio` (number) – Min speech/loud ratio required for noisy upgrade (default: `0.3`). Filters out music and other non-speech noise. 183 + 184 + **Parakeet backend settings** (`transcribe.parakeet`): 185 + - `model_version` (string) – Parakeet model version: `"v3"`. Default: `"v3"`. 186 + - `device` (string) – Runtime preference for Parakeet: `"auto"`, `"cpu"`, or `"cuda"`. Default: `"auto"`. 187 + - `timeout_sec` (number) – Helper/runtime timeout in seconds. Default: `120.0`. 178 188 179 189 **Whisper backend settings** (`transcribe.whisper`): 180 190 - `device` (string) – Device for inference: `"auto"` (detect GPU, fall back to CPU), `"cpu"`, or `"cuda"`. Default: `"auto"`.
+23 -23
tests/baselines/api/graph/graph.json
··· 399 399 "kg_edge_count": 1, 400 400 "name": "Balthasar Davi", 401 401 "observation_depth": 2, 402 - "recency": 0.4, 403 - "score": 63.1, 402 + "recency": 0.3, 403 + "score": 63.0, 404 404 "type": "person" 405 405 }, 406 406 { ··· 412 412 "kg_edge_count": 1, 413 413 "name": "Mesh Routing", 414 414 "observation_depth": 3, 415 - "recency": 0.4, 416 - "score": 65.1, 415 + "recency": 0.3, 416 + "score": 65.0, 417 417 "type": "project" 418 418 }, 419 419 { ··· 425 425 "kg_edge_count": 0, 426 426 "name": "Verona Ventures", 427 427 "observation_depth": 2, 428 - "recency": 0.4, 429 - "score": 58.1, 428 + "recency": 0.3, 429 + "score": 58.0, 430 430 "type": "company" 431 431 }, 432 432 { ··· 439 439 "name": "Capulet Industries", 440 440 "observation_depth": 0, 441 441 "recency": 0.3, 442 - "score": 23.0, 442 + "score": 22.9, 443 443 "type": "company" 444 444 }, 445 445 { ··· 452 452 "name": "Mercutio Escalus", 453 453 "observation_depth": 3, 454 454 "recency": 0.4, 455 - "score": 88.2, 455 + "score": 88.1, 456 456 "type": "person" 457 457 }, 458 458 { ··· 465 465 "name": "Tybalt Capulet", 466 466 "observation_depth": 4, 467 467 "recency": 0.4, 468 - "score": 91.2, 468 + "score": 91.1, 469 469 "type": "person" 470 470 }, 471 471 { ··· 478 478 "name": "Juliet Capulet", 479 479 "observation_depth": 4, 480 480 "recency": 0.4, 481 - "score": 96.2, 481 + "score": 96.1, 482 482 "type": "person" 483 483 }, 484 484 { ··· 490 490 "kg_edge_count": 1, 491 491 "name": "Schema Bridge", 492 492 "observation_depth": 2, 493 - "recency": 0.4, 494 - "score": 63.1, 493 + "recency": 0.3, 494 + "score": 63.0, 495 495 "type": "project" 496 496 }, 497 497 { ··· 504 504 "name": "Romeo Montague", 505 505 "observation_depth": 4, 506 506 "recency": 0.4, 507 - "score": 57.2, 507 + "score": 57.1, 508 508 "type": "person" 509 509 }, 510 510 { ··· 517 517 "name": "Rosaline Prince", 518 518 "observation_depth": 2, 519 519 "recency": 0.4, 520 - "score": 63.2, 520 + "score": 63.1, 521 521 "type": "person" 522 522 }, 523 523 { ··· 529 529 "kg_edge_count": 2, 530 530 "name": "Montague Tech", 531 531 "observation_depth": 3, 532 - "recency": 0.4, 533 - "score": 74.1, 532 + "recency": 0.3, 533 + "score": 74.0, 534 534 "type": "company" 535 535 }, 536 536 { ··· 543 543 "name": "Prince Escalus", 544 544 "observation_depth": 2, 545 545 "recency": 0.4, 546 - "score": 76.2, 546 + "score": 76.1, 547 547 "type": "person" 548 548 }, 549 549 { ··· 556 556 "name": "Verona Platform", 557 557 "observation_depth": 3, 558 558 "recency": 0.4, 559 - "score": 109.2, 559 + "score": 109.1, 560 560 "type": "project" 561 561 }, 562 562 { ··· 568 568 "kg_edge_count": 1, 569 569 "name": "Nurse Angela", 570 570 "observation_depth": 2, 571 - "recency": 0.4, 572 - "score": 68.1, 571 + "recency": 0.3, 572 + "score": 68.0, 573 573 "type": "person" 574 574 }, 575 575 { ··· 582 582 "name": "Paris Duke", 583 583 "observation_depth": 2, 584 584 "recency": 0.4, 585 - "score": 54.2, 585 + "score": 54.1, 586 586 "type": "person" 587 587 }, 588 588 { ··· 595 595 "name": "Benvolio Montague", 596 596 "observation_depth": 3, 597 597 "recency": 0.4, 598 - "score": 74.2, 598 + "score": 74.1, 599 599 "type": "person" 600 600 }, 601 601 { ··· 608 608 "name": "Friar Lawrence", 609 609 "observation_depth": 2, 610 610 "recency": 0.4, 611 - "score": 78.2, 611 + "score": 78.1, 612 612 "type": "person" 613 613 } 614 614 ],
+1 -1
tests/baselines/api/search/day-results.json
··· 14 14 "id": "20260304/talents/knowledge_graph.md:7", 15 15 "idx": 7, 16 16 "path": "20260304/talents/knowledge_graph.md", 17 - "score": -1.9, 17 + "score": -2.0, 18 18 "stream": null, 19 19 "text": "# Part 1: Entity Extraction and Relationship Mapping\n\n## Relationship Mapping\n\n| Source Name | Target Name | Relationship Type | Context |\n| :--- | :--- | :--- | :--- |\n| **Romeo Montague** | **Juliet Capulet** | `met-at-conference` | First <strong>meeting</strong> at Denver Tech Summit keynote. |\n" 20 20 }
+7 -7
tests/baselines/api/search/search.json
··· 106 106 "id": "facets/verona/logs/20260309.jsonl:1", 107 107 "idx": 1, 108 108 "path": "facets/verona/logs/20260309.jsonl", 109 - "score": -1.6, 109 + "score": -1.7, 110 110 "stream": null, 111 111 "text": "### Deploy Complete by <strong>romeo</strong>_montague\n\n**Source:** deploy | **Time:** 13:45:00\n\n**Parameters:**\n- service: verona-gateway\n- version: 0.9.0\n" 112 112 }, ··· 122 122 "id": "20260309/default/090000_300/talents/audio.md:0", 123 123 "idx": 0, 124 124 "path": "20260309/default/090000_300/talents/audio.md", 125 - "score": -1.5, 125 + "score": -1.6, 126 126 "stream": "default", 127 127 "text": "# Audio Summary\n\n<strong>Romeo</strong> confessed the project to Benvolio and asked for infrastructure help. Benvolio agreed to spin up a Kubernetes staging cluster.\n" 128 128 }, ··· 170 170 "id": "20260309/default/090000_300:0", 171 171 "idx": 0, 172 172 "path": "20260309/default/090000_300", 173 - "score": -1.5, 173 + "score": -1.6, 174 174 "stream": "default", 175 175 "text": "# Audio Summary\n\n<strong>Romeo</strong> confessed the project to Benvolio and asked for infrastructure help. Benvolio agreed to spin up a Kubernetes staging cluster.\n" 176 176 } ··· 211 211 "id": "20260307/default/150000_300/talents/audio.md:0", 212 212 "idx": 0, 213 213 "path": "20260307/default/150000_300/talents/audio.md", 214 - "score": -2.2, 214 + "score": -2.3, 215 215 "stream": "default", 216 216 "text": "# Audio Summary\n\nEmergency meeting at Montague Tech. Benvolio questioned <strong>Romeo</strong> about the secret project. <strong>Romeo</strong> clarified no company IP was shared. Team discussed legal exposure. <strong>Romeo</strong> proposed Professor Lawrence as mediator.\n" 217 217 }, ··· 259 259 "id": "20260307/default/150000_300:0", 260 260 "idx": 0, 261 261 "path": "20260307/default/150000_300", 262 - "score": -2.2, 262 + "score": -2.3, 263 263 "stream": "default", 264 264 "text": "# Audio Summary\n\nEmergency meeting at Montague Tech. Benvolio questioned <strong>Romeo</strong> about the secret project. <strong>Romeo</strong> clarified no company IP was shared. Team discussed legal exposure. <strong>Romeo</strong> proposed Professor Lawrence as mediator.\n" 265 265 } ··· 316 316 "id": "20260308/talents/knowledge_graph.md:2", 317 317 "idx": 2, 318 318 "path": "20260308/talents/knowledge_graph.md", 319 - "score": -1.3, 319 + "score": -1.4, 320 320 "stream": null, 321 321 "text": "# Part 1: Entity Extraction and Relationship Mapping ## Entity Profiles | Entity Name | Entity Type | First Appearance | Total Engagement | Context | | :--- | :--- | :--- | :--- | :--- | | **<strong>Romeo</strong> Montague** | Person | 10:00 | High | Under board pressure,..." 322 322 }, ··· 583 583 "id": "facets/capulet/events/20260304.jsonl:1", 584 584 "idx": 1, 585 585 "path": "facets/capulet/events/20260304.jsonl", 586 - "score": -2.1, 586 + "score": -2.2, 587 587 "stream": null, 588 588 "text": "### Social: Conference Mixer\n\n\n**Time Occurred:** 18:00 - 20:00\n**Participants:** Juliet Capulet, <strong>Romeo</strong> Montague\n\nNetworking event\n\nJuliet and <strong>Romeo</strong> exchanged Signal contacts\n" 589 589 },
+6 -1
tests/baselines/api/settings/config.json
··· 97 97 "completed_at": 1700000000000 98 98 }, 99 99 "transcribe": { 100 - "backend": "whisper", 100 + "backend": "parakeet", 101 101 "enrich": true, 102 102 "noise_upgrade": true, 103 + "parakeet": { 104 + "device": "auto", 105 + "model_version": "v3", 106 + "timeout_sec": 120.0 107 + }, 103 108 "revai": { 104 109 "model": "fusion" 105 110 },
+8 -2
tests/baselines/api/settings/transcribe.json
··· 46 46 } 47 47 ], 48 48 "config": { 49 - "backend": "whisper", 49 + "backend": "parakeet", 50 50 "enrich": true, 51 51 "noise_upgrade": true, 52 + "parakeet": { 53 + "device": "auto", 54 + "model_version": "v3", 55 + "timeout_sec": 120.0 56 + }, 52 57 "revai": { 53 58 "model": "fusion" 54 59 }, ··· 57 62 "device": "auto", 58 63 "model": "medium.en" 59 64 } 60 - } 65 + }, 66 + "runtime_label": "Linux NeMo (CUDA fp16)" 61 67 }
+6 -1
tests/fixtures/journal/config/journal.json
··· 22 22 "completed_at": 1700000000000 23 23 }, 24 24 "transcribe": { 25 - "backend": "whisper", 25 + "backend": "parakeet", 26 26 "enrich": true, 27 27 "noise_upgrade": true, 28 + "parakeet": { 29 + "model_version": "v3", 30 + "device": "auto", 31 + "timeout_sec": 120.0 32 + }, 28 33 "whisper": { 29 34 "device": "auto", 30 35 "model": "medium.en",
+76
tests/test_transcribe_noise_upgrade.py
··· 320 320 _process_one(audio_path, args, transcribe_config, []) 321 321 322 322 assert mock_process_audio.call_args.kwargs["backend"] == "whisper" 323 + 324 + @pytest.mark.parametrize( 325 + ("vad", "has_token", "expected_backend"), 326 + [ 327 + ( 328 + VadResult( 329 + duration=10.0, 330 + speech_duration=5.0, 331 + has_speech=True, 332 + speech_segments=[(1.0, 6.0)], 333 + noisy_rms=0.005, 334 + noisy_s=3.0, 335 + loud_windows=100, 336 + speech_loud_windows=90, 337 + ), 338 + True, 339 + "parakeet", 340 + ), 341 + ( 342 + VadResult( 343 + duration=10.0, 344 + speech_duration=5.0, 345 + has_speech=True, 346 + speech_segments=[(1.0, 6.0)], 347 + noisy_rms=0.02, 348 + noisy_s=3.0, 349 + loud_windows=100, 350 + speech_loud_windows=90, 351 + ), 352 + True, 353 + "revai", 354 + ), 355 + ( 356 + VadResult( 357 + duration=10.0, 358 + speech_duration=5.0, 359 + has_speech=True, 360 + speech_segments=[(1.0, 6.0)], 361 + noisy_rms=0.02, 362 + noisy_s=3.0, 363 + loud_windows=100, 364 + speech_loud_windows=90, 365 + ), 366 + False, 367 + "parakeet", 368 + ), 369 + ], 370 + ) 371 + def test_parakeet_default_upgrade_path( 372 + self, 373 + audio_path, 374 + args, 375 + audio_buffer, 376 + vad, 377 + has_token, 378 + expected_backend, 379 + ): 380 + from observe.transcribe.main import _process_one 381 + 382 + transcribe_config = { 383 + "backend": "parakeet", 384 + "noise_upgrade": True, 385 + "noise_upgrade_min_speech_ratio": 0.3, 386 + "parakeet": {}, 387 + } 388 + 389 + with ( 390 + patch("observe.transcribe.main.load_audio", return_value=audio_buffer), 391 + patch("observe.transcribe.main.run_vad", return_value=vad), 392 + patch("observe.transcribe.main.reduce_audio", return_value=(None, None)), 393 + patch("observe.transcribe.main.process_audio") as mock_process_audio, 394 + patch("observe.transcribe.revai.has_token", return_value=has_token), 395 + ): 396 + _process_one(audio_path, args, transcribe_config, []) 397 + 398 + assert mock_process_audio.call_args.kwargs["backend"] == expected_backend