Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
use chrono ::Utc ;
use rusqlite ::{ params , Connection } ;
use uuid ::Uuid ;
use super ::errors ::DatabaseError ;
use super ::models ::* ;
// ── Projects ──────────────────────────────────────────────────────
pub fn create_project ( conn : & Connection , name : & str ) -> Result < Project , DatabaseError > {
let id = Uuid ::new_v4 ( ) . to_string ( ) ;
let now = Utc ::now ( ) . to_rfc3339 ( ) ;
conn . execute (
" INSERT INTO projects (id, name, created_at, updated_at, status) VALUES (?1, ?2, ?3, ?4, 'active') " ,
params! [ id , name , now , now ] ,
) ? ;
get_project ( conn , & id ) ? . ok_or_else ( | | DatabaseError ::NotFound ( " project " . into ( ) ) )
}
pub fn get_project ( conn : & Connection , id : & str ) -> Result < Option < Project > , DatabaseError > {
let mut stmt = conn . prepare (
" SELECT id, name, created_at, updated_at, settings, status FROM projects WHERE id = ?1 " ,
) ? ;
let mut rows = stmt . query_map ( params! [ id ] , | row | {
Ok ( Project {
id : row . get ( 0 ) ? ,
name : row . get ( 1 ) ? ,
created_at : row . get ( 2 ) ? ,
updated_at : row . get ( 3 ) ? ,
settings : row . get ( 4 ) ? ,
status : row . get ( 5 ) ? ,
} )
} ) ? ;
match rows . next ( ) {
Some ( row ) = > Ok ( Some ( row ? ) ) ,
None = > Ok ( None ) ,
}
}
pub fn list_projects ( conn : & Connection ) -> Result < Vec < Project > , DatabaseError > {
let mut stmt = conn . prepare (
" SELECT id, name, created_at, updated_at, settings, status FROM projects WHERE status = 'active' ORDER BY updated_at DESC " ,
) ? ;
let rows = stmt . query_map ( [ ] , | row | {
Ok ( Project {
id : row . get ( 0 ) ? ,
name : row . get ( 1 ) ? ,
created_at : row . get ( 2 ) ? ,
updated_at : row . get ( 3 ) ? ,
settings : row . get ( 4 ) ? ,
status : row . get ( 5 ) ? ,
} )
} ) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ > > ( ) ? )
}
pub fn update_project (
conn : & Connection ,
id : & str ,
name : Option < & str > ,
settings : Option < & str > ,
) -> Result < ( ) , DatabaseError > {
let now = Utc ::now ( ) . to_rfc3339 ( ) ;
if let Some ( name ) = name {
conn . execute (
" UPDATE projects SET name = ?1, updated_at = ?2 WHERE id = ?3 " ,
params! [ name , now , id ] ,
) ? ;
}
if let Some ( settings ) = settings {
conn . execute (
" UPDATE projects SET settings = ?1, updated_at = ?2 WHERE id = ?3 " ,
params! [ settings , now , id ] ,
) ? ;
}
Ok ( ( ) )
}
pub fn delete_project ( conn : & Connection , id : & str ) -> Result < ( ) , DatabaseError > {
let now = Utc ::now ( ) . to_rfc3339 ( ) ;
conn . execute (
" UPDATE projects SET status = 'deleted', updated_at = ?1 WHERE id = ?2 " ,
params! [ now , id ] ,
) ? ;
Ok ( ( ) )
}
2026-03-20 22:06:29 -07:00
// ── Media Files ──────────────────────────────────────────────────
pub fn create_media_file (
conn : & Connection ,
project_id : & str ,
file_path : & str ,
) -> Result < MediaFile , DatabaseError > {
let id = Uuid ::new_v4 ( ) . to_string ( ) ;
let now = Utc ::now ( ) . to_rfc3339 ( ) ;
conn . execute (
" INSERT INTO media_files (id, project_id, file_path, created_at) VALUES (?1, ?2, ?3, ?4) " ,
params! [ id , project_id , file_path , now ] ,
) ? ;
Ok ( MediaFile {
id ,
project_id : project_id . to_string ( ) ,
file_path : file_path . to_string ( ) ,
file_hash : None ,
duration_ms : None ,
sample_rate : None ,
channels : None ,
format : None ,
file_size : None ,
created_at : now ,
} )
}
pub fn get_media_files_for_project (
conn : & Connection ,
project_id : & str ,
) -> Result < Vec < MediaFile > , DatabaseError > {
let mut stmt = conn . prepare (
" SELECT id, project_id, file_path, file_hash, duration_ms, sample_rate, channels, format, file_size, created_at FROM media_files WHERE project_id = ?1 ORDER BY created_at " ,
) ? ;
let rows = stmt . query_map ( params! [ project_id ] , | row | {
Ok ( MediaFile {
id : row . get ( 0 ) ? ,
project_id : row . get ( 1 ) ? ,
file_path : row . get ( 2 ) ? ,
file_hash : row . get ( 3 ) ? ,
duration_ms : row . get ( 4 ) ? ,
sample_rate : row . get ( 5 ) ? ,
channels : row . get ( 6 ) ? ,
format : row . get ( 7 ) ? ,
file_size : row . get ( 8 ) ? ,
created_at : row . get ( 9 ) ? ,
} )
} ) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ > > ( ) ? )
}
Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
// ── Speakers ──────────────────────────────────────────────────────
pub fn create_speaker (
conn : & Connection ,
project_id : & str ,
label : & str ,
color : Option < & str > ,
) -> Result < Speaker , DatabaseError > {
let id = Uuid ::new_v4 ( ) . to_string ( ) ;
conn . execute (
" INSERT INTO speakers (id, project_id, label, color) VALUES (?1, ?2, ?3, ?4) " ,
params! [ id , project_id , label , color ] ,
) ? ;
Ok ( Speaker {
id ,
project_id : project_id . to_string ( ) ,
label : label . to_string ( ) ,
display_name : None ,
color : color . map ( String ::from ) ,
metadata : None ,
} )
}
pub fn get_speakers_for_project (
conn : & Connection ,
project_id : & str ,
) -> Result < Vec < Speaker > , DatabaseError > {
let mut stmt = conn . prepare (
" SELECT id, project_id, label, display_name, color, metadata FROM speakers WHERE project_id = ?1 " ,
) ? ;
let rows = stmt . query_map ( params! [ project_id ] , | row | {
Ok ( Speaker {
id : row . get ( 0 ) ? ,
project_id : row . get ( 1 ) ? ,
label : row . get ( 2 ) ? ,
display_name : row . get ( 3 ) ? ,
color : row . get ( 4 ) ? ,
metadata : row . get ( 5 ) ? ,
} )
} ) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ > > ( ) ? )
}
pub fn rename_speaker (
conn : & Connection ,
speaker_id : & str ,
display_name : & str ,
) -> Result < ( ) , DatabaseError > {
conn . execute (
" UPDATE speakers SET display_name = ?1 WHERE id = ?2 " ,
params! [ display_name , speaker_id ] ,
) ? ;
Ok ( ( ) )
}
// ── Segments ──────────────────────────────────────────────────────
pub fn get_segments_for_media (
conn : & Connection ,
media_file_id : & str ,
) -> Result < Vec < Segment > , DatabaseError > {
let mut stmt = conn . prepare (
" SELECT id, project_id, media_file_id, speaker_id, start_ms, end_ms, text, original_text, confidence, is_edited, edited_at, segment_index FROM segments WHERE media_file_id = ?1 ORDER BY segment_index " ,
) ? ;
let rows = stmt . query_map ( params! [ media_file_id ] , | row | {
Ok ( Segment {
id : row . get ( 0 ) ? ,
project_id : row . get ( 1 ) ? ,
media_file_id : row . get ( 2 ) ? ,
speaker_id : row . get ( 3 ) ? ,
start_ms : row . get ( 4 ) ? ,
end_ms : row . get ( 5 ) ? ,
text : row . get ( 6 ) ? ,
original_text : row . get ( 7 ) ? ,
confidence : row . get ( 8 ) ? ,
is_edited : row . get ( 9 ) ? ,
edited_at : row . get ( 10 ) ? ,
segment_index : row . get ( 11 ) ? ,
} )
} ) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ > > ( ) ? )
}
pub fn update_segment_text (
conn : & Connection ,
segment_id : & str ,
new_text : & str ,
) -> Result < ( ) , DatabaseError > {
let now = Utc ::now ( ) . to_rfc3339 ( ) ;
// Preserve original text on first edit
conn . execute (
" UPDATE segments SET original_text = COALESCE(original_text, text), text = ?1, is_edited = 1, edited_at = ?2 WHERE id = ?3 " ,
params! [ new_text , now , segment_id ] ,
) ? ;
Ok ( ( ) )
}
pub fn reassign_speaker (
conn : & Connection ,
segment_id : & str ,
new_speaker_id : & str ,
) -> Result < ( ) , DatabaseError > {
conn . execute (
" UPDATE segments SET speaker_id = ?1 WHERE id = ?2 " ,
params! [ new_speaker_id , segment_id ] ,
) ? ;
Ok ( ( ) )
}
2026-03-20 22:06:29 -07:00
// ── Segments (create) ────────────────────────────────────────────
pub fn create_segment (
conn : & Connection ,
project_id : & str ,
media_file_id : & str ,
speaker_id : Option < & str > ,
start_ms : i64 ,
end_ms : i64 ,
text : & str ,
segment_index : i32 ,
) -> Result < Segment , DatabaseError > {
let id = Uuid ::new_v4 ( ) . to_string ( ) ;
conn . execute (
" INSERT INTO segments (id, project_id, media_file_id, speaker_id, start_ms, end_ms, text, is_edited, segment_index) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 0, ?8) " ,
params! [ id , project_id , media_file_id , speaker_id , start_ms , end_ms , text , segment_index ] ,
) ? ;
Ok ( Segment {
id ,
project_id : project_id . to_string ( ) ,
media_file_id : media_file_id . to_string ( ) ,
speaker_id : speaker_id . map ( String ::from ) ,
start_ms ,
end_ms ,
text : text . to_string ( ) ,
original_text : None ,
confidence : None ,
is_edited : false ,
edited_at : None ,
segment_index ,
} )
}
Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
// ── Words ─────────────────────────────────────────────────────────
pub fn get_words_for_segment (
conn : & Connection ,
segment_id : & str ,
) -> Result < Vec < Word > , DatabaseError > {
let mut stmt = conn . prepare (
" SELECT id, segment_id, word, start_ms, end_ms, confidence, word_index FROM words WHERE segment_id = ?1 ORDER BY word_index " ,
) ? ;
let rows = stmt . query_map ( params! [ segment_id ] , | row | {
Ok ( Word {
id : row . get ( 0 ) ? ,
segment_id : row . get ( 1 ) ? ,
word : row . get ( 2 ) ? ,
start_ms : row . get ( 3 ) ? ,
end_ms : row . get ( 4 ) ? ,
confidence : row . get ( 5 ) ? ,
word_index : row . get ( 6 ) ? ,
} )
} ) ? ;
Ok ( rows . collect ::< Result < Vec < _ > , _ > > ( ) ? )
}
2026-03-20 22:06:29 -07:00
pub fn create_word (
conn : & Connection ,
segment_id : & str ,
word : & str ,
start_ms : i64 ,
end_ms : i64 ,
confidence : Option < f64 > ,
word_index : i32 ,
) -> Result < Word , DatabaseError > {
let id = Uuid ::new_v4 ( ) . to_string ( ) ;
conn . execute (
" INSERT INTO words (id, segment_id, word, start_ms, end_ms, confidence, word_index) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) " ,
params! [ id , segment_id , word , start_ms , end_ms , confidence , word_index ] ,
) ? ;
Ok ( Word {
id ,
segment_id : segment_id . to_string ( ) ,
word : word . to_string ( ) ,
start_ms ,
end_ms ,
confidence ,
word_index ,
} )
}
Phase 1 foundation: Tauri shell, Python sidecar, SQLite database
Tauri v2 + Svelte + TypeScript frontend:
- App shell with workspace layout (waveform, transcript, speakers, AI chat)
- Placeholder components for all major UI areas
- Typed stores (project, transcript, playback, AI)
- TypeScript interfaces matching the database schema
- Tauri bridge service with typed invoke wrappers
- svelte-check passes with 0 errors
Rust backend:
- Tauri v2 app entry point with command registration
- SQLite database layer (rusqlite with bundled SQLite)
- Full schema: projects, media_files, speakers, segments, words,
ai_outputs, annotations (with indexes)
- Model structs with serde serialization
- CRUD queries for projects, speakers, segments, words
- Segment text editing preserves original text
- Schema versioning for future migrations
- 6 tests passing
- Command stubs for project, transcribe, export, AI, settings, system
- App state management
Python sidecar:
- JSON-line IPC protocol (stdin/stdout)
- Message types: IPCMessage, progress, error, ready
- Handler registry with routing and error handling
- Ping/pong handler for connectivity testing
- Service stubs: transcribe, diarize, pipeline, AI, export
- Provider stubs: local (llama-server), OpenAI, Anthropic, LiteLLM
- Hardware detection stubs
- 14 tests passing, ruff clean
Also adds:
- Testing strategy document (docs/TESTING.md)
- Validation script (scripts/validate.sh)
- Updated .gitignore for Svelte, Rust, Python artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:16:06 -08:00
#[ cfg(test) ]
mod tests {
use super ::* ;
use crate ::db ::schema ;
fn setup_db ( ) -> Connection {
let conn = Connection ::open_in_memory ( ) . unwrap ( ) ;
conn . pragma_update ( None , " foreign_keys " , " ON " ) . unwrap ( ) ;
schema ::create_tables ( & conn ) . unwrap ( ) ;
conn
}
#[ test ]
fn test_project_crud ( ) {
let conn = setup_db ( ) ;
// Create
let project = create_project ( & conn , " Test Project " ) . unwrap ( ) ;
assert_eq! ( project . name , " Test Project " ) ;
assert_eq! ( project . status , " active " ) ;
// Read
let fetched = get_project ( & conn , & project . id ) . unwrap ( ) . unwrap ( ) ;
assert_eq! ( fetched . name , " Test Project " ) ;
// List
let projects = list_projects ( & conn ) . unwrap ( ) ;
assert_eq! ( projects . len ( ) , 1 ) ;
// Update
update_project ( & conn , & project . id , Some ( " Renamed " ) , None ) . unwrap ( ) ;
let updated = get_project ( & conn , & project . id ) . unwrap ( ) . unwrap ( ) ;
assert_eq! ( updated . name , " Renamed " ) ;
// Delete (soft)
delete_project ( & conn , & project . id ) . unwrap ( ) ;
let active = list_projects ( & conn ) . unwrap ( ) ;
assert_eq! ( active . len ( ) , 0 ) ;
}
#[ test ]
fn test_speaker_operations ( ) {
let conn = setup_db ( ) ;
let project = create_project ( & conn , " Test " ) . unwrap ( ) ;
let speaker = create_speaker ( & conn , & project . id , " Speaker 1 " , Some ( " #ff0000 " ) ) . unwrap ( ) ;
assert_eq! ( speaker . label , " Speaker 1 " ) ;
rename_speaker ( & conn , & speaker . id , " Alice " ) . unwrap ( ) ;
let speakers = get_speakers_for_project ( & conn , & project . id ) . unwrap ( ) ;
assert_eq! ( speakers [ 0 ] . display_name . as_deref ( ) , Some ( " Alice " ) ) ;
}
#[ test ]
fn test_segment_text_editing ( ) {
let conn = setup_db ( ) ;
let project = create_project ( & conn , " Test " ) . unwrap ( ) ;
// Insert a media file first
let media_id = Uuid ::new_v4 ( ) . to_string ( ) ;
let now = Utc ::now ( ) . to_rfc3339 ( ) ;
conn . execute (
" INSERT INTO media_files (id, project_id, file_path, created_at) VALUES (?1, ?2, ?3, ?4) " ,
params! [ media_id , project . id , " test.wav " , now ] ,
)
. unwrap ( ) ;
// Insert a segment
let seg_id = Uuid ::new_v4 ( ) . to_string ( ) ;
conn . execute (
" INSERT INTO segments (id, project_id, media_file_id, start_ms, end_ms, text, segment_index) VALUES (?1, ?2, ?3, 0, 1000, 'hello world', 0) " ,
params! [ seg_id , project . id , media_id ] ,
)
. unwrap ( ) ;
// Edit the text
update_segment_text ( & conn , & seg_id , " hello everyone " ) . unwrap ( ) ;
let segments = get_segments_for_media ( & conn , & media_id ) . unwrap ( ) ;
assert_eq! ( segments [ 0 ] . text , " hello everyone " ) ;
assert_eq! ( segments [ 0 ] . original_text . as_deref ( ) , Some ( " hello world " ) ) ;
assert! ( segments [ 0 ] . is_edited ) ;
}
}