OpenOats Documentation

repository·main·Indexed 25 days ago

https://github.com/yazinsai/openoats

A local-first meeting note-taker for macOS featuring real-time transcription and intelligent suggestions. OpenOats supports offline operation via Ollama or cloud integration through OpenRouter and Voyage AI. The documentation covers installation via Homebrew, knowledge base configuration, privacy data flows, and a detailed meeting file format specification including YAML frontmatter and Obsidian Dataview-compatible action items.

Tokens
36K
Snippets
61
Records
145
Agent score
82%

What's inside OpenOats

  1. Understand the LiveSessionController and LiveSessionState architecture

    main

    The LiveSessionController is the central authority for managing active recording sessions. It replaces business logic previously held in the view layer (ContentView).

    Key Responsibilities:

    • Manages the 100ms polling loop.
    • Publishes LiveSessionState to the UI.
    • Handles utterance ingestion (both local and remote).
    • Reacts to settings changes (e.g., updating Knowledge Base paths).
    • Processes external commands for starting and stopping sessions.
    • Coordinates with AppCoordinator for state transitions (calling coordinator.handle() rather than transition() directly).

    Critical Implementation Rules:

    • startSession() must set the state synchronously via the coordinator; no await should occur before the phase change.
    • state.isRunning must mirror the transcriptionEngine.isRunning status, not the sessionPhase.

    ContentView should be a pure projection of LiveSessionController.state and should contain zero business logic.

    struct LiveSessionState: Equatable {
        var isRunning: Bool = false
        var sessionPhase: MeetingState = .idle
        var audioLevel: Float = 0
        var liveTranscript: [Utterance] = []
        var volatileYouText: String = ""
        var volatileThemText: String = ""
        var suggestions: [Suggestion] = []
        var isGeneratingSuggestions: Bool = false
        var batchStatus: BatchTranscriptionEngine.Status = .idle
        var lastEndedSession: String? = nil
        var lastSessionHasNotes: Bool = false
        var kbIndexingProgress: String = ""
        var statusMessage: String? = nil
        var errorMessage: String? = nil
        var needsDownload: Bool = false
        var transcriptionPrompt: String? = nil
        var modelDisplayName: String = ""
    }
  2. Track Question Density in TranscriptStore

    main

    The TranscriptStore is updated to support real-time suggestion triggers by tracking question density. This involves maintaining rolling timestamp arrays for:

    1. Recent utterances in the last 60 seconds.
    2. Recent question-bearing utterances in the last 60 seconds.

    A computed questionDensity property is used to prune stale timestamps and return the ratio of question-bearing utterances to total recent utterances.

  3. Extend OpenOats schema with custom metadata

    main

    You can add custom metadata to the YAML frontmatter using the x_ prefix. This namespace is reserved for tool-specific or user-specific extensions.

    Rules:

    • All extension fields MUST start with x_.
    • Extension fields are optional; parsers MUST NOT require them.
    • Parsers MUST ignore unrecognized x_ fields.
    • It is recommended to namespace extensions: x_toolname_field (e.g., x_openoats_session).
    • Extension fields must follow standard frontmatter rules (flat structure, consistent types).
    x_openoats_session: "session_2026-03-20_14-00-06"
    x_openoats_template: "customer-discovery"
    x_calendar_event_id: "abc123def456"
    x_project: "OpenOats v1.0"
    x_confidence: 0.92
  4. How meeting detection triggers work

    main

    Meeting detection in OpenOats is driven by multiple signals, primarily the microphone and the camera. Detection is not triggered by the microphone alone; it requires a combination of an active microphone and a recognized meeting application.

    • Mic + App Trigger: If the microphone is active and a known meeting application is running, a detection event is triggered with the detectionTrigger set to .micAndApp.
    • Camera Trigger: If the camera becomes active, it can trigger detection immediately (with detectionTrigger set to .camera), even if a specific meeting app isn't identified, or it can upgrade an existing .micAndApp session.
    • Hysteresis: To prevent rapid toggling when a camera is turned off, the system uses a 3-second hysteresis period. If the microphone and a meeting app remain active during this period, the session is downgraded to .micAndApp rather than ending the detection.

    Detection events are emitted via an event stream as either .detected(MeetingApp) or .ended.

    // Example of the detection trigger logic
    if cameraIsActive {
        if !isActive {
            let app = await scanForMeetingApp()
            isActive = true
            detectedApp = app
            detectionTrigger = .camera
            eventContinuation.yield(.detected(app))
        } else {
            detectionTrigger = .camera
        }
    }
  5. Verify session finalization pipeline

    main

    When a session is stopped via .userStopped, the AppCoordinator triggers a finalization pipeline. A successful finalization must ensure:

    1. The session file exists.
    2. A sidecar file is written.
    3. The session history is updated.
    4. The TranscriptLogger is closed.
    5. lastEndedSession is set.
    6. The state returns to .idle.

    If a session is discarded via .userDiscarded, the state should return to .idle immediately without setting lastEndedSession or performing finalization.

    To run integration tests for finalization:

    cd /Users/yazin/projects/openoats/OpenOats && swift test --filter AppCoordinatorIntegrationTests 2>&1 | tail -10
    func testFinalizationWritesSidecarWithCorrectMetadata() async {
        // ... setup code ...
        coordinator.handle(.userStarted(metadata), settings: settings)
        // ... wait for engine ...
        coordinator.handle(.userStopped, settings: settings)
        // ... wait for finalization ...
        XCTAssertEqual(coordinator.state, .idle)
        XCTAssertFalse(indices.isEmpty)
    }
  6. Implement the Real-Time Suggestion Engine architecture

    main

    The Suggestion Engine is designed as a 3-layer concurrent architecture to achieve sub-2-second latency, replacing the previous 5-stage serial pipeline.

    Architecture Layers:

    1. Layer 1: KB Pre-fetch: Performs periodic Knowledge Base (KB) pre-fetching based on partial speech.
    2. Layer 2: Local Heuristic Gate: Uses local heuristics on finalized utterances to decide if a suggestion is needed.
    3. Layer 3: Streaming LLM Synthesis: Uses a fast model to provide streaming synthesis of suggestions.

    Both speakers (the user and the meeting participants) are analyzed to trigger suggestions.

  7. How camera-based meeting detection works

    main

    OpenOats uses a priority-based multi-signal evaluation system to detect meetings, reducing false positives from non-meeting microphone usage (like dictation or voice messages). The system evaluates signals in the following order of priority:

    1. Camera ON (Priority 0 - Strongest): Triggers detection immediately. This is considered the most reliable signal.
    2. Mic ON + Meeting App Running (Priority 1): Triggers detection after a 5-second debounce, provided a known meeting application is active.
    3. Mic ON alone (Priority 2): Does not trigger a meeting detection.

    Overlap and End Conditions:

    • If both a camera and a meeting app are active, the system tracks the camera as the strongest trigger. If the camera turns off, the system downgrades to the micAndApp trigger, and the meeting continues. If the mic also turns off, the meeting ends.
    • The meeting only ends when all active signals (camera and mic+app) are turned off.
    • Camera-off hysteresis: To prevent interruptions from brief camera toggles (like device handoffs), the system applies a 3-second grace period after the camera turns off before evaluating end conditions.
    enum DetectionTrigger: Sendable {
        case camera
        case micAndApp
    }
  8. How the Real-Time Suggestion Engine works

    main

    The Real-Time Suggestion Engine operates through a three-layer pipeline designed to provide low-latency context-aware suggestions during live sessions:

    1. Background State Tracking & Pre-fetching: The engine continuously tracks the conversation state (topics, summaries, goals) in the background and pre-fetches relevant knowledge base (KB) context packs based on recent transcript text to minimize latency when a suggestion is triggered.
    2. Gate & Retrieval: When a new utterance is finalized, the engine retrieves context packs (either from the pre-fetch cache or via a fresh KB search). It then passes this data through a gate to evaluate if the suggestion is relevant and a throttle to prevent suggestion bursts or repetitive content.
    3. Streaming Synthesis: If the gate and throttle allow, the engine creates a RealtimeSuggestion and begins streaming a synthesized response from an LLM. This allows the user to see the suggestion being generated in real-time.
  9. Integrate Real-Time Suggestions via LiveSessionController

    main

    The LiveSessionController manages the lifecycle of real-time suggestions. To integrate suggestions correctly, follow these patterns:

    1. State Synchronization: Observe real-time suggestion IDs and streaming state from coordinator.suggestionEngine. Use the onSuggestionPanelContentUpdate callback to notify the UI of changes.
    2. Utterance Ingestion: When a new utterance is finalized, call coordinator.suggestionEngine?.onUtterance(last) to trigger new suggestions.
    3. Session Lifecycle:
      • When onRunningStateChanged(true) fires: Start pre-fetching and show the suggestion panel if settings.suggestionPanelEnabled is true.
      • When onRunningStateChanged(false) fires: Stop pre-fetching and hide the panel after approximately 2 seconds.
  10. Use LiveSessionController for session orchestration and state

    main

    The LiveSessionController manages the active recording session, including utterance ingestion, transcript logging, and batch status polling. It acts as the bridge between the UI (ContentView) and the underlying services.

    Key Responsibilities

    • Orchestration: Starting and stopping sessions by calling AppCoordinator.handle().
    • Utterance Ingestion: Managing silence timers, transcript logging, and triggering refinements/suggestions.
    • Polling: Running a background task to poll transcriptionEngine and batch status, updating the LiveSessionState.
    • Settings: Reacting to changes in KB folders, Voyage API keys, or transcription models.

    LiveSessionState Structure

    To bind the UI, observe the LiveSessionState struct, which contains:

    • isRunning: Bool: Mirrors transcriptionEngine.isRunning (the source of truth for recording status).
    • sessionPhase: MeetingState: The current phase of the meeting.
    • audioLevel: Float: Drives UI waveform/pulse animations.
    • liveTranscript: [Utterance]: The current stream of text.
    • suggestions: [Suggestion]: AI-generated suggestions.
    • batchStatus: BatchStatus: Status of background processing.
    • errorMessage: String?: Any errors encountered during the session.
    struct LiveSessionState {
        var isRunning: Bool          // mirrors transcriptionEngine.isRunning
        var sessionPhase: MeetingState
        var audioLevel: Float        // mirrors transcriptionEngine.audioLevel
        var liveTranscript: [Utterance]
        var volatileYouText: String
        var volatileThemText: String
        var suggestions: [Suggestion]
        var isGeneratingSuggestions: Bool
        var batchStatus: BatchStatus
        var lastEndedSession: String?
        var lastSessionHasNotes: Bool
        var kbIndexingProgress: String
        var statusMessage: String?
        var errorMessage: String?
        var needsDownload: Bool
        var transcriptionPrompt: String?
        var modelDisplayName: String
    }
  11. Configure the user-visible notes folder via notesFolderPath

    main

    The notesFolderPath setting (configured in SettingsView) defines where users expect to see their meeting artifacts. To maintain compatibility with existing workflows, the SessionRepository mirrors files from the internal Application Support storage to this user-configured folder at specific lifecycle events:

    • On session finalization: Copies notes.md and plain-text.txt to the folder. If saveAudioRecording is enabled, the .m4a file is also copied/moved.
    • On batch transcription completion: Re-exports notes.md with the updated transcript section to the folder.
    • On notes generation: Re-exports notes.md with LLM-generated sections included to the folder.
  12. Privacy and Data Flow in OpenOats

    main

    OpenOats is designed with privacy in mind. Key principles include:

    • Local Transcription: Audio is transcribed locally on your Mac using Apple Speech; audio is never sent to the cloud.
    • Local Mode (Ollama): When using Ollama for both LLM and embeddings, all processing stays on your machine with zero network calls.
    • Cloud Mode Data Flow: When using cloud providers, only text is sent. Specifically:
      • Voyage AI: Receives text chunks from your knowledge base for embedding, query strings for search, and candidate chunks for reranking.
      • OpenRouter: Receives conversation context (topic, summary, recent utterances) and KB evidence chunks to generate suggestions or meeting notes.
    • Storage: API keys are stored in the macOS Keychain, and transcripts are saved locally to ~/Documents/OpenOats/.