rust-toolchain.toml pins to stable 1.94.1 so contributors and CI runners share the exact rustc / rustfmt / clippy versions. Without the pin, every machine surfaces a different lint set depending on its local install — six pre-existing lints showed up on 1.94.1 that 1.93-era HANDOVER reported clean. Clippy fixes (all pre-existing, not introduced by feature work): - crates/storage/src/database.rs: std::iter::repeat().take() -> repeat_n() - crates/llm/src/lib.rs (docs): "+ frontends" was parsed as a markdown bullet continuation by rustdoc, breaking doc-lazy-continuation. Reworded to "and". - crates/llm/src/lib.rs (loop): while-let-on-iterator -> for-loop. - src-tauri/src/commands/security.rs: .iter().any(|a| *a == x) -> .contains(&x). - src-tauri/src/lib.rs: io::Error::new(Other, e) -> io::Error::other(e). - src-tauri/src/tauri_app_data_migration.rs: drop function-tail `return`s inside cfg blocks; each platform's block now ends with a tail expression. cargo fmt sweep across the workspace. Mechanical layout-only changes; no semantics affected. Workspace gates after this commit: - cargo fmt --check: clean - cargo clippy --workspace --all-targets -- -D warnings: clean - cargo test --workspace: 405/0 (will become 409/0 with Phase A.1+A.2)
280 lines
9.4 KiB
Rust
280 lines
9.4 KiB
Rust
// Tauri commands wrapping the lumotia_storage transcript CRUD.
|
|
// These are the bridge that lets the Svelte frontend treat SQLite as the
|
|
// canonical store rather than localStorage.
|
|
//
|
|
// Day 4 of the upgrade plan. The frontend HistoryPage rename flow has had
|
|
// a `// TODO: persist to SQLite when update_transcript exists` for some
|
|
// time; that command now exists.
|
|
//
|
|
// Task 16 — the legacy global-dictionary commands
|
|
// (`list_dictionary_command`, `add_dictionary_entry_command`,
|
|
// `delete_dictionary_entry_command`) were dropped; profile-scoped
|
|
// `profile_terms` commands in `commands::profiles` are now canonical.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use lumotia_storage::{
|
|
delete_transcript as db_delete_transcript, get_transcript as db_get_transcript,
|
|
insert_transcript as db_insert_transcript, list_transcripts_paged,
|
|
list_trashed_transcripts as db_list_trashed_transcripts,
|
|
restore_transcript as db_restore_transcript, search_transcripts as db_search_transcripts,
|
|
update_transcript as db_update_transcript, update_transcript_meta as db_update_transcript_meta,
|
|
InsertTranscriptParams, TranscriptRow,
|
|
};
|
|
|
|
use crate::AppState;
|
|
|
|
/// Frontend-facing shape of a stored transcript. Mirrors `TranscriptRow`
|
|
/// from the storage crate but drops fields the UI does not need yet
|
|
/// (sample_rate, audio_channels, format flags) and renames to camelCase.
|
|
///
|
|
/// Task 2.5 — `starred`, `manualTags`, `template`, `language`, `segmentsJson`
|
|
/// were added to back the viewer metadata that previously lived only in the
|
|
/// removed `lumotia_history` localStorage cache.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TranscriptDto {
|
|
pub id: String,
|
|
pub text: String,
|
|
pub source: String,
|
|
pub profile_id: String,
|
|
pub title: Option<String>,
|
|
pub audio_path: Option<String>,
|
|
pub duration: f64,
|
|
pub engine: Option<String>,
|
|
pub model_id: Option<String>,
|
|
pub created_at: String,
|
|
pub starred: bool,
|
|
pub manual_tags: String,
|
|
pub template: String,
|
|
pub language: String,
|
|
pub segments_json: String,
|
|
/// Phase 9 LLM-generated content tags ("topic:...", "intent:...").
|
|
pub llm_tags: String,
|
|
}
|
|
|
|
impl From<TranscriptRow> for TranscriptDto {
|
|
fn from(r: TranscriptRow) -> Self {
|
|
Self {
|
|
id: r.id,
|
|
text: r.text,
|
|
source: r.source,
|
|
profile_id: r.profile_id,
|
|
title: r.title,
|
|
audio_path: r.audio_path,
|
|
duration: r.duration,
|
|
engine: r.engine,
|
|
model_id: r.model_id,
|
|
created_at: r.created_at,
|
|
starred: r.starred,
|
|
manual_tags: r.manual_tags,
|
|
template: r.template,
|
|
language: r.language,
|
|
segments_json: r.segments_json,
|
|
llm_tags: r.llm_tags,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CreateTranscriptRequest {
|
|
pub id: String,
|
|
pub text: String,
|
|
pub source: String,
|
|
pub profile_id: Option<String>,
|
|
pub title: Option<String>,
|
|
pub audio_path: Option<String>,
|
|
pub duration: f64,
|
|
pub engine: Option<String>,
|
|
pub model_id: Option<String>,
|
|
pub inference_ms: Option<i64>,
|
|
pub sample_rate: Option<i64>,
|
|
pub audio_channels: Option<i64>,
|
|
pub format_mode: Option<String>,
|
|
pub remove_fillers: bool,
|
|
pub british_english: bool,
|
|
pub anti_hallucination: bool,
|
|
}
|
|
|
|
/// Insert a transcript into the canonical SQLite store. Called from the
|
|
/// frontend after a recording or file transcription completes. The FTS5
|
|
/// index is updated automatically by trigger.
|
|
#[tauri::command]
|
|
pub async fn add_transcript(
|
|
state: tauri::State<'_, AppState>,
|
|
transcript: CreateTranscriptRequest,
|
|
) -> Result<(), String> {
|
|
let params = InsertTranscriptParams {
|
|
id: &transcript.id,
|
|
text: &transcript.text,
|
|
source: &transcript.source,
|
|
profile_id: transcript
|
|
.profile_id
|
|
.as_deref()
|
|
.unwrap_or(lumotia_storage::DEFAULT_PROFILE_ID),
|
|
title: transcript.title.as_deref(),
|
|
audio_path: transcript.audio_path.as_deref(),
|
|
duration: transcript.duration,
|
|
engine: transcript.engine.as_deref(),
|
|
model_id: transcript.model_id.as_deref(),
|
|
inference_ms: transcript.inference_ms,
|
|
sample_rate: transcript.sample_rate,
|
|
audio_channels: transcript.audio_channels,
|
|
format_mode: transcript.format_mode.as_deref(),
|
|
remove_fillers: transcript.remove_fillers,
|
|
british_english: transcript.british_english,
|
|
anti_hallucination: transcript.anti_hallucination,
|
|
};
|
|
db_insert_transcript(&state.db, ¶ms)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Paginated history list. Defaults to 50 rows from the most recent.
|
|
#[tauri::command]
|
|
pub async fn list_transcripts(
|
|
state: tauri::State<'_, AppState>,
|
|
limit: Option<i64>,
|
|
offset: Option<i64>,
|
|
) -> Result<Vec<TranscriptDto>, String> {
|
|
let limit = limit.unwrap_or(50).clamp(1, 500);
|
|
let offset = offset.unwrap_or(0).max(0);
|
|
list_transcripts_paged(&state.db, limit, offset)
|
|
.await
|
|
.map(|rows| rows.into_iter().map(TranscriptDto::from).collect())
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn get_transcript(
|
|
state: tauri::State<'_, AppState>,
|
|
id: String,
|
|
) -> Result<Option<TranscriptDto>, String> {
|
|
db_get_transcript(&state.db, &id)
|
|
.await
|
|
.map(|opt| opt.map(TranscriptDto::from))
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Update mutable fields of an existing transcript. `text` and/or `title`.
|
|
/// Returns the row count affected (0 if id not found). Fixes the long-
|
|
/// standing "rename in History never persists" bug per
|
|
/// architecture-review.md §13.
|
|
#[tauri::command]
|
|
pub async fn update_transcript(
|
|
state: tauri::State<'_, AppState>,
|
|
id: String,
|
|
text: Option<String>,
|
|
title: Option<String>,
|
|
) -> Result<u64, String> {
|
|
db_update_transcript(&state.db, &id, text.as_deref(), title.as_deref())
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn delete_transcript(
|
|
state: tauri::State<'_, AppState>,
|
|
id: String,
|
|
) -> Result<(), String> {
|
|
db_delete_transcript(&state.db, &id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// List soft-deleted transcripts for the Trash view. Mirrors
|
|
/// `list_transcripts` shape (defaults, clamps) but reads the
|
|
/// `deleted_at IS NOT NULL` partition so the trash is the exact
|
|
/// complement of the regular history list.
|
|
#[tauri::command]
|
|
pub async fn list_trashed_transcripts(
|
|
state: tauri::State<'_, AppState>,
|
|
limit: Option<i64>,
|
|
offset: Option<i64>,
|
|
) -> Result<Vec<TranscriptDto>, String> {
|
|
let limit = limit.unwrap_or(50).clamp(1, 500);
|
|
let offset = offset.unwrap_or(0).max(0);
|
|
db_list_trashed_transcripts(&state.db, limit, offset)
|
|
.await
|
|
.map(|rows| rows.into_iter().map(TranscriptDto::from).collect())
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Restore a soft-deleted transcript by clearing its `deleted_at`
|
|
/// timestamp. Idempotent: restoring a live row is a no-op. Note that
|
|
/// the audio file at `audio_path` may already have been removed by
|
|
/// `delete_transcript`'s best-effort filesystem cleanup; restoring
|
|
/// recovers the text + metadata but the audio may be gone.
|
|
#[tauri::command]
|
|
pub async fn restore_transcript(
|
|
state: tauri::State<'_, AppState>,
|
|
id: String,
|
|
) -> Result<(), String> {
|
|
db_restore_transcript(&state.db, &id)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// FTS5 search. Query syntax: bare words AND together; quote phrases;
|
|
/// supports `OR`, `NOT`, prefix `*`. Returns up to 50 best-rank matches.
|
|
#[tauri::command]
|
|
pub async fn search_transcripts(
|
|
state: tauri::State<'_, AppState>,
|
|
query: String,
|
|
) -> Result<Vec<TranscriptDto>, String> {
|
|
let q = query.trim();
|
|
if q.is_empty() {
|
|
return Ok(Vec::new());
|
|
}
|
|
db_search_transcripts(&state.db, q, 50)
|
|
.await
|
|
.map(|rows| rows.into_iter().map(TranscriptDto::from).collect())
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Task 2.5 — patch-style update for the transcripts_meta columns. Each
|
|
/// field is optional: `Some` overwrites, omitted / `None` preserves via
|
|
/// COALESCE server-side. Separate from `update_transcript` (text / title)
|
|
/// so the existing command surface stays untouched.
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct UpdateTranscriptMetaRequest {
|
|
#[serde(default)]
|
|
pub starred: Option<bool>,
|
|
#[serde(default)]
|
|
pub manual_tags: Option<String>,
|
|
#[serde(default)]
|
|
pub template: Option<String>,
|
|
#[serde(default)]
|
|
pub language: Option<String>,
|
|
#[serde(default)]
|
|
pub segments_json: Option<String>,
|
|
/// Phase 9 LLM content tags. Same comma-joined string convention as
|
|
/// `manual_tags`. Pass `None` to leave unchanged; pass `Some("")` to
|
|
/// explicitly clear.
|
|
#[serde(default)]
|
|
pub llm_tags: Option<String>,
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn update_transcript_meta_cmd(
|
|
state: tauri::State<'_, AppState>,
|
|
id: String,
|
|
patch: UpdateTranscriptMetaRequest,
|
|
) -> Result<TranscriptDto, String> {
|
|
let row = db_update_transcript_meta(
|
|
&state.db,
|
|
&id,
|
|
patch.starred,
|
|
patch.manual_tags.as_deref(),
|
|
patch.template.as_deref(),
|
|
patch.language.as_deref(),
|
|
patch.segments_json.as_deref(),
|
|
patch.llm_tags.as_deref(),
|
|
)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(TranscriptDto::from(row))
|
|
}
|