Files
WhispAssist/src-tauri/src/models.rs
T

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>,
}