ryu_hardware/ingest.rs
1//! The meeting *ingest* seam: the minimal contract the hardware ambient-audio
2//! path needs from the meeting-notes capability, inverted so this kernel crate has
3//! ZERO compile-time dependency on `ryu_meetings`.
4//!
5//! ## Why this exists
6//!
7//! An ambient-capable device (necklace / desk) opens a long-running "ambient"
8//! meeting and streams ~1 s WAV chunks into it: [`session::HardwareSession`] buffers
9//! decoded Opus uplink and, once a second of PCM has accumulated, feeds one WAV
10//! segment to the meeting transcript. That transcription + persistence is a
11//! *meetings* concern that had been welded into this crate as a direct
12//! `ryu_meetings::MeetingEngine` field (a compile-time `ryu_hardware -> ryu_meetings`
13//! edge welding the kernel hardware crate to a swappable app). Meetings is now a
14//! swappable, out-of-process app; a kernel crate cannot hard-link it.
15//!
16//! [`MeetingIngest`] is the inversion. It exposes ONLY what the ambient path needs
17//! (open/resume the ambient meeting, append a captured audio segment), in terms of
18//! plain owned types — never a `ryu_meetings` type. Core provides the impl:
19//!
20//! - in-process (`meetings_ingest::in_proc`) — wraps the in-process engine;
21//! - out-of-process (`meetings_client::MeetingsClient`) — proxies to the
22//! `ryu-meetings` sidecar over loopback (`POST /api/meetings/:id/chunk`).
23//!
24//! ## Hot-path note
25//!
26//! [`MeetingIngest::append_segment`] is called at *segment rate*, not frame rate:
27//! [`session::HardwareSession::on_audio`] only accumulates each ~20 ms Opus frame,
28//! and the append fires once per ~1 s of buffered PCM (`AMBIENT_FLUSH_SAMPLES`). So
29//! the sidecar-backed impl's HTTP hop is ~1 POST/s/device carrying a ~32 KB WAV
30//! (transcription happens on the sidecar side) — acceptable at segment rate.
31//!
32//! ## What is deliberately absent: `finalize`
33//!
34//! There is no `finalize` on this seam. The device link never ends the ambient
35//! meeting — it is a continuous 24/7 transcript that a device *resumes* on each
36//! reconnect. Finalizing a meeting (stop capture → generate notes → save) is a
37//! user-driven action through the meetings API/UI, never the hardware path, so the
38//! seam only needs open/resume + append.
39
40use async_trait::async_trait;
41
42/// The meeting-notes capability, seen through the narrow hole the hardware ambient
43/// path needs. Implemented by Core (in-process or sidecar-backed).
44#[async_trait]
45pub trait MeetingIngest: Send + Sync {
46 /// Whether a meeting with this id still exists — the ambient *resume* check
47 /// (a device's saved `ambient_meeting_id` may have been deleted).
48 async fn meeting_exists(&self, meeting_id: &str) -> bool;
49
50 /// Open a new long-running ambient meeting for a device, returning its id. The
51 /// impl bakes in the ambient provenance (app label + auto source); the caller
52 /// supplies only the title.
53 async fn start_meeting(&self, title: String) -> Result<String, String>;
54
55 /// Append one captured WAV audio segment to a meeting's live transcript,
56 /// returning the created segment id on success. A silence/empty chunk is
57 /// surfaced as an `Err` whose message contains `"silence"` or `"empty"` (the
58 /// caller maps that to an `ambient_skip`, not a failure).
59 async fn append_segment(
60 &self,
61 meeting_id: &str,
62 wav: Vec<u8>,
63 filename: String,
64 ) -> Result<String, String>;
65}