470 lines
16 KiB
Rust
470 lines
16 KiB
Rust
//! Shared data types crossing the IPC boundary and between services.
|
|
//! Mirrors `docs/03-data-model.md` and `docs/04-api-contracts.md`.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
pub type MeetingId = String; // uuid v4
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum BackendId {
|
|
Npu,
|
|
Nvidia,
|
|
Amd,
|
|
Intel,
|
|
Cpu,
|
|
}
|
|
|
|
impl BackendId {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
BackendId::Npu => "npu",
|
|
BackendId::Nvidia => "nvidia",
|
|
BackendId::Amd => "amd",
|
|
BackendId::Intel => "intel",
|
|
BackendId::Cpu => "cpu",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BackendInfo {
|
|
pub id: BackendId,
|
|
pub name: String,
|
|
pub available: bool,
|
|
/// Lower rank = higher priority (NPU=0 … CPU=4).
|
|
pub rank: u8,
|
|
pub vram_mb: Option<u32>,
|
|
}
|
|
|
|
/// A downloadable/installed Whisper model (T3.7, FR-MODEL-1).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ModelInfo {
|
|
pub id: String, // e.g. "base.en-q5_1" — also the ggml filename stem
|
|
pub label: String,
|
|
pub size_mb: u32, // approximate download size
|
|
pub installed: bool,
|
|
pub active: bool,
|
|
/// `false` for the `.en` (English-only) ggml variants; `true` for the
|
|
/// multilingual variants (no `.en` suffix, T8.7/FR-TRX-4/M4.2) — gates
|
|
/// whether the Settings language picker is enabled for this model.
|
|
pub multilingual: bool,
|
|
}
|
|
|
|
/// One selectable transcription language (T8.7, FR-TRX-4) — ISO-639-1 code
|
|
/// (as accepted by `whisper_rs::FullParams::set_language`) plus a display
|
|
/// label for the Settings dropdown.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct LanguageOption {
|
|
pub code: String,
|
|
pub label: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum MeetingStatus {
|
|
Recording,
|
|
Transcribing,
|
|
Ready,
|
|
Recovering,
|
|
Error,
|
|
}
|
|
|
|
impl MeetingStatus {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
MeetingStatus::Recording => "recording",
|
|
MeetingStatus::Transcribing => "transcribing",
|
|
MeetingStatus::Ready => "ready",
|
|
MeetingStatus::Recovering => "recovering",
|
|
MeetingStatus::Error => "error",
|
|
}
|
|
}
|
|
|
|
/// Parses a `meetings.status` DB value. Unrecognized values map to `Error`
|
|
/// rather than panicking — a stored value should always be one written by
|
|
/// this app, but a row is not worth crashing the app over.
|
|
pub fn parse(s: &str) -> Self {
|
|
match s {
|
|
"recording" => MeetingStatus::Recording,
|
|
"transcribing" => MeetingStatus::Transcribing,
|
|
"ready" => MeetingStatus::Ready,
|
|
"recovering" => MeetingStatus::Recovering,
|
|
_ => MeetingStatus::Error,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TranscriptSegment {
|
|
pub id: u64,
|
|
pub start_ms: u64,
|
|
pub end_ms: u64,
|
|
/// Internal speaker label ("S1"…); name resolved at render time (FR-SPK-5).
|
|
pub speaker: String,
|
|
pub text: String,
|
|
pub confidence: Option<f32>,
|
|
pub interim: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SpeakerInfo {
|
|
pub label: String, // "S1"
|
|
pub display_name: Option<String>,
|
|
pub participant_id: Option<String>,
|
|
}
|
|
|
|
/// A user-typed note attached to a moment in the recording, anchored by
|
|
/// timestamp rather than segment id — a segment id can be invalidated by a
|
|
/// later batch re-transcription (T3.8), but the moment in time it pointed at
|
|
/// never changes. `text: ""` marks a cleared note (kept rather than removed
|
|
/// so `updated_at` still reflects the clear).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SegmentNote {
|
|
pub anchor_ms: u64,
|
|
pub text: String,
|
|
pub created_at: i64,
|
|
pub updated_at: i64,
|
|
}
|
|
|
|
/// On-disk shape of `manual_notes.json` (`docs/03-data-model.md`) — the raw
|
|
/// user-authored input a live recording accumulates (freeform notes typed
|
|
/// while recording, plus any per-moment annotations), kept distinct from the
|
|
/// transcript-derived `notes.md` so a re-render never has to guess which
|
|
/// parts of `notes.md` were hand-written.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ManualNotes {
|
|
pub schema: u32,
|
|
pub freeform_md: String,
|
|
pub segment_notes: Vec<SegmentNote>,
|
|
}
|
|
|
|
impl Default for ManualNotes {
|
|
fn default() -> Self {
|
|
Self {
|
|
schema: 1,
|
|
freeform_md: String::new(),
|
|
segment_notes: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A diarization result span before alignment to transcript segments.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SpeakerSpan {
|
|
pub start_ms: u64,
|
|
pub end_ms: u64,
|
|
pub speaker: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MeetingListItem {
|
|
pub id: MeetingId,
|
|
pub title: String,
|
|
pub started_at: i64,
|
|
pub duration_secs: Option<i64>,
|
|
pub status: MeetingStatus,
|
|
/// Tags (Phase 8, FR-SEARCH-2), sorted.
|
|
pub tags: Vec<String>,
|
|
}
|
|
|
|
/// One full-text search hit (Phase 8, FR-SEARCH-1) — the same shape as
|
|
/// `MeetingListItem` plus a highlighted excerpt of what matched.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SearchHit {
|
|
pub id: MeetingId,
|
|
pub title: String,
|
|
pub started_at: i64,
|
|
pub duration_secs: Option<i64>,
|
|
pub status: MeetingStatus,
|
|
pub tags: Vec<String>,
|
|
pub snippet: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ActionItem {
|
|
pub id: Option<String>,
|
|
pub text: String,
|
|
pub owner: Option<String>,
|
|
pub due_at: Option<i64>,
|
|
pub confirmed: bool,
|
|
/// Schedule a local OS reminder for `due_at` (Phase 8, T8.6, FR-CAL-5).
|
|
#[serde(default)]
|
|
pub reminder_set: bool,
|
|
}
|
|
|
|
/// Portable meeting export manifest — the `meeting.json` inside an export
|
|
/// bundle folder (FR-STORE-4). Carries everything needed to reconstruct a
|
|
/// meeting on another machine alongside the bundle's files (`audio.wav`,
|
|
/// `transcript.json`, `notes.md`, `summary.json`). Deliberately excludes the
|
|
/// meeting id (a fresh one is minted on import to avoid collisions) and the
|
|
/// calendar-event link (event ids are machine-local).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MeetingBundle {
|
|
pub schema: u32,
|
|
pub title: String,
|
|
pub started_at: i64,
|
|
pub ended_at: Option<i64>,
|
|
pub duration_secs: Option<i64>,
|
|
pub language: Option<String>,
|
|
pub backend_used: Option<String>,
|
|
pub model_used: Option<String>,
|
|
pub recorded: bool,
|
|
pub template_id: Option<String>,
|
|
pub tags: Vec<String>,
|
|
pub speakers: Vec<SpeakerInfo>,
|
|
pub action_items: Vec<ActionItem>,
|
|
/// `audio.wav` channel layout (FR-SPK/FR-CAP): `"split"` or `"summed"`.
|
|
/// `default` so bundles exported before this field deserialize as `None`
|
|
/// (treated as `"summed"`).
|
|
#[serde(default)]
|
|
pub audio_layout: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CalendarEvent {
|
|
pub id: String,
|
|
pub source: String, // pst|graph|ics
|
|
pub subject: Option<String>,
|
|
pub organizer: Option<String>,
|
|
pub starts_at: Option<i64>,
|
|
pub ends_at: Option<i64>,
|
|
pub description: Option<String>,
|
|
/// The source's own stable id (e.g. an iCalendar UID) — used to dedup
|
|
/// re-imports of the same event rather than creating duplicate rows.
|
|
pub raw_uid: Option<String>,
|
|
}
|
|
|
|
/// A parsed attendee before persistence — no DB row/id exists until storage
|
|
/// upserts it (T6.1/T6.2, FR-CAL-1).
|
|
#[derive(Debug, Clone)]
|
|
pub struct AttendeeInfo {
|
|
pub name: String,
|
|
pub email: Option<String>,
|
|
pub role: Option<String>, // organizer|required|optional
|
|
}
|
|
|
|
/// An imported event with the attendees pulled from the same source record —
|
|
/// PST/ICS both list attendees inline per-appointment, so there's no
|
|
/// separate "fetch attendees for event X" round trip against the source.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ImportedEvent {
|
|
pub event: CalendarEvent,
|
|
pub attendees: Vec<AttendeeInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Participant {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub email: Option<String>,
|
|
pub role: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Settings {
|
|
pub theme: String, // system|light|dark
|
|
pub storage_root: String,
|
|
pub llm_provider: String, // ollama|custom|off
|
|
pub llm_endpoint: String,
|
|
pub llm_model: String,
|
|
/// Advanced Ollama tuning (T5.2): `{ system?, think?, keep_alive?, options: {…} }`.
|
|
/// Sparse — only user-overridden values are stored; `Null`/absent = all defaults.
|
|
#[serde(default)]
|
|
pub llm_advanced: serde_json::Value,
|
|
pub preferred_backend: String, // auto|npu|nvidia|amd|intel|cpu
|
|
pub whisper_model: String, // ModelInfo.id, e.g. "base.en-q5_1"
|
|
/// Global default transcription language (T8.7, FR-TRX-4): `None`/`"auto"`
|
|
/// lets whisper.cpp auto-detect; an ISO-639-1 code (e.g. "es") forces
|
|
/// that language. Only takes effect with a multilingual model loaded —
|
|
/// an English-only (`.en`) model always decodes English regardless of
|
|
/// this setting (see `transcription::resolve_language`). Each meeting
|
|
/// persists whatever was actually used at `Meeting.language`, so this is
|
|
/// just the default applied at the next `start_recording`.
|
|
#[serde(default)]
|
|
pub whisper_language: Option<String>,
|
|
pub low_overhead: bool,
|
|
// Recording retention (ADR-0009). Default OFF.
|
|
pub default_record: bool,
|
|
pub consent_acknowledged: bool,
|
|
/// One-time "data leaves your device" acknowledgment for hosted
|
|
/// (non-local) AI providers — Anthropic, or a hosted OpenAI-compatible
|
|
/// gateway (ADR-0011, T10.3/M3.3). Shown once before first hosted use;
|
|
/// this flag is what makes it not nag every time. Independent of
|
|
/// `consent_acknowledged` (that one's specifically about recording law).
|
|
#[serde(default)]
|
|
pub hosted_ai_acknowledged: bool,
|
|
// Sync master switch (ADR-0010). Default OFF. Target rows live in the DB; secrets in OS keychain.
|
|
pub sync_enabled: bool,
|
|
// Storage retention policy (FR-STORE-2). None = no cap on that dimension.
|
|
pub retention_max_age_days: Option<u32>,
|
|
pub retention_max_size_gb: Option<u32>,
|
|
// Calendar / .pst (T6.2) — remembered so the user doesn't re-browse every
|
|
// launch. `pst_auto_sync` re-imports this path once at startup if set.
|
|
#[serde(default)]
|
|
pub pst_last_path: Option<String>,
|
|
#[serde(default)]
|
|
pub pst_auto_sync: bool,
|
|
/// How far back to import (days before "now"); `None` = full mailbox
|
|
/// history (the original, unbounded behavior). Applied to both a manual
|
|
/// Import click and the `pst_auto_sync` startup re-import — a long-lived
|
|
/// mailbox otherwise re-imports its entire multi-year history (every
|
|
/// recurring series expanded to its cap, every one-off holiday entry
|
|
/// Outlook ever generated) on every launch.
|
|
#[serde(default)]
|
|
pub pst_import_range_days: Option<u32>,
|
|
/// Auto-start recording when a calendar event begins while the app is open
|
|
/// (FR-CAL, opt-in). OFF by default. No background timer runs for this: the
|
|
/// UI arms a single one-shot timer to the next event while the app is open
|
|
/// and disarms it on close, so idle resource use stays at zero (NFR-RES-1).
|
|
#[serde(default)]
|
|
pub auto_record_calendar: bool,
|
|
// Microsoft Graph calendar source (M4.4, T8.9, ADR-0008, FR-CAL-6). Opt-in,
|
|
// explicit consent via OAuth PKCE — off by default. The credential ref
|
|
// points into the OS credential store; the token itself never lives here.
|
|
#[serde(default)]
|
|
pub graph_calendar_enabled: bool,
|
|
#[serde(default)]
|
|
pub graph_calendar_credential_ref: Option<String>,
|
|
// Audio capture device override (FR-CAP-1). `Device::get_id()` string;
|
|
// None = system default render device (loopback / system audio).
|
|
#[serde(default)]
|
|
pub audio_output_device: Option<String>,
|
|
// Microphone capture (FR-CAP-7): mix the user's own voice into the live
|
|
// transcript. Local-only, no egress; default ON. Turn off to transcribe just
|
|
// the system/loopback audio, as WA did before.
|
|
#[serde(default = "default_true")]
|
|
pub microphone_enabled: bool,
|
|
// Microphone device override — `Device::get_id()` string; None = system
|
|
// default capture device.
|
|
#[serde(default)]
|
|
pub audio_input_device: Option<String>,
|
|
// Local MCP server (Phase 10b, ADR-0011, FR-MCP-1). OFF by default; the
|
|
// auth token itself is NEVER stored here — only in the OS credential
|
|
// store (see `mcp::token`). `mcp_expose` is one of none|selected|all;
|
|
// `mcp_expose_recordings` gates access to meetings with retained audio
|
|
// (ADR-0009) regardless of `mcp_expose` (FR-MCP-3).
|
|
#[serde(default)]
|
|
pub mcp_enabled: bool,
|
|
#[serde(default = "default_mcp_transport")]
|
|
pub mcp_transport: String, // http|stdio
|
|
#[serde(default = "default_mcp_port")]
|
|
pub mcp_port: u16,
|
|
#[serde(default = "default_mcp_expose")]
|
|
pub mcp_expose: String, // none|selected|all
|
|
#[serde(default)]
|
|
pub mcp_expose_recordings: bool,
|
|
/// Launch WhispAssist automatically at login (opt-in, NFR-RES-4). OFF by
|
|
/// default; toggled via `set_auto_start`, which writes a per-user
|
|
/// `HKCU\...\Run` entry through `tauri-plugin-autostart` (no admin). An
|
|
/// enterprise deploy file may set this to `true` (see `deploy.rs`).
|
|
#[serde(default)]
|
|
pub auto_start: bool,
|
|
}
|
|
|
|
fn default_mcp_transport() -> String {
|
|
"http".into()
|
|
}
|
|
|
|
fn default_mcp_port() -> u16 {
|
|
4849
|
|
}
|
|
|
|
fn default_mcp_expose() -> String {
|
|
"none".into()
|
|
}
|
|
|
|
/// serde default for a `bool` field that should be `true` when absent from an
|
|
/// older `settings.json` (so upgrading users get the microphone, FR-CAP-7).
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
// ---- Sync (ADR-0010) ----
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum SyncKind {
|
|
WebDav,
|
|
OneDrive,
|
|
Dropbox,
|
|
Box,
|
|
}
|
|
|
|
/// What the UI sees about a target. NEVER contains the secret (FR-SYNC-6).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SyncTargetInfo {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub kind: SyncKind,
|
|
pub provider_hint: Option<String>, // nextcloud|owncloud|cloudreve|seafile|synology|generic
|
|
pub base_url: Option<String>,
|
|
pub remote_base_path: String,
|
|
pub username: Option<String>,
|
|
pub enabled: bool,
|
|
pub third_party: bool,
|
|
pub host: Option<String>,
|
|
// Upload selection + options, so the UI can pre-fill an edit form (the secret
|
|
// is never included — FR-SYNC-6).
|
|
pub upload_transcript: bool,
|
|
pub upload_notes: bool,
|
|
pub upload_summary: bool,
|
|
pub upload_recording: bool,
|
|
pub trigger_on_finalize: bool,
|
|
pub allow_plaintext_lan: bool,
|
|
pub encrypt_before_upload: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SyncJobInfo {
|
|
pub id: String,
|
|
pub target_id: String,
|
|
pub meeting_id: MeetingId,
|
|
pub artifact: String, // transcript|notes|summary|recording
|
|
pub status: String, // pending|uploading|done|failed|skipped
|
|
pub attempts: u32,
|
|
pub bytes_sent: u64,
|
|
pub bytes_total: Option<u64>,
|
|
pub last_error: Option<String>,
|
|
}
|
|
|
|
// ---- Feature briefs + MCP (ADR-0011) ----
|
|
|
|
/// Agent-ready spec distilled from a meeting; served by the MCP `get_feature_brief` tool.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct FeatureBrief {
|
|
pub id: String,
|
|
pub meeting_id: MeetingId,
|
|
pub title: String,
|
|
pub problem: String,
|
|
pub desired_outcome: String,
|
|
pub acceptance_criteria: Vec<String>,
|
|
pub target_repo: Option<String>,
|
|
pub context_excerpts: Vec<ContextExcerpt>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ContextExcerpt {
|
|
pub speaker: String,
|
|
pub text: String,
|
|
}
|
|
|
|
/// Lightweight listing row (no full body).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct FeatureBriefInfo {
|
|
pub id: String,
|
|
pub meeting_id: MeetingId,
|
|
pub title: String,
|
|
pub target_repo: Option<String>,
|
|
pub exposed: bool,
|
|
}
|
|
|
|
/// One row of the MCP audit log (FR-MCP-5).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct McpAccessEntry {
|
|
pub at: i64,
|
|
pub tool: String,
|
|
pub meeting_id: Option<MeetingId>,
|
|
pub client: Option<String>,
|
|
}
|