Skip to content

Latest commit

ย 

History

History
418 lines (330 loc) ยท 20 KB

File metadata and controls

418 lines (330 loc) ยท 20 KB

Architecture โ€” Sayso

ๆœฌๆ–‡ๆกฃๆ่ฟฐ Sayso ็š„ๆŠ€ๆœฏๆžถๆž„ใ€ๅ…ณ้”ฎ่ฎพ่ฎกๅ†ณ็ญ–ๅ’Œ็ป„ไปถไบคไบ’ใ€‚

็ณป็ปŸๆžถๆž„ๅ›พ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                              Sayso Desktop App                              โ”‚
โ”‚                         (Tauri v2: Rust + React)                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚   Frontend   โ”‚  โ”‚   Backend    โ”‚  โ”‚   Platform   โ”‚  โ”‚   External   โ”‚     โ”‚
โ”‚  โ”‚   (React)    โ”‚  โ”‚    (Rust)    โ”‚  โ”‚    APIs      โ”‚  โ”‚    APIs      โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚         โ”‚                 โ”‚                 โ”‚                 โ”‚             โ”‚
โ”‚    Settings UI       Core Engine      Audio/Mic          STT/LLM           โ”‚
โ”‚    Statistics     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”‚
โ”‚    Onboarding     โ”‚   5-State   โ”‚    โ”‚  cpal   โ”‚      โ”‚ OpenAI   โ”‚         โ”‚
โ”‚                   โ”‚    FSM      โ”‚    โ”‚         โ”‚      โ”‚ Groq     โ”‚         โ”‚
โ”‚                   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜      โ”‚ etc.     โ”‚         โ”‚
โ”‚                          โ”‚                โ”‚            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ”‚
โ”‚                   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”                          โ”‚
โ”‚                   โ”‚  Recording  โ”‚    โ”‚  Audio  โ”‚                          โ”‚
โ”‚                   โ”‚  Pipeline   โ”‚    โ”‚ Capture โ”‚                          โ”‚
โ”‚                   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                          โ”‚
โ”‚                          โ”‚                                                 โ”‚
โ”‚         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                โ”‚
โ”‚         โ–ผ                โ–ผ                โ–ผ                                โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                          โ”‚
โ”‚    โ”‚ Mode A  โ”‚     โ”‚  Mode B  โ”‚     โ”‚  Mode C  โ”‚                          โ”‚
โ”‚    โ”‚(Type)   โ”‚     โ”‚(Type+Sendโ”‚     โ”‚(Command) โ”‚                          โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜                          โ”‚
โ”‚         โ”‚               โ”‚                โ”‚                                  โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”                         โ”‚
โ”‚    โ”‚ enigo   โ”‚     โ”‚ enigo   โ”‚     โ”‚  Safety    โ”‚                         โ”‚
โ”‚    โ”‚ CGEvent โ”‚     โ”‚ CGEvent โ”‚     โ”‚  Filter    โ”‚                         โ”‚
โ”‚    โ”‚Fallback โ”‚     โ”‚ + Enter โ”‚     โ”‚(Rule+LLM)  โ”‚                         โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜                         โ”‚
โ”‚                                          โ”‚                                  โ”‚
โ”‚                                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”                         โ”‚
โ”‚                                    โ”‚  Intent    โ”‚                         โ”‚
โ”‚                                    โ”‚  Parser    โ”‚                         โ”‚
โ”‚                                    โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜                         โ”‚
โ”‚                                          โ”‚                                  โ”‚
โ”‚                                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”                         โ”‚
โ”‚                                    โ”‚  Shell     โ”‚                         โ”‚
โ”‚                                    โ”‚  Executor  โ”‚                         โ”‚
โ”‚                                    โ”‚ (Direct)   โ”‚                         โ”‚
โ”‚                                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๆ ธๅฟƒ็ป„ไปถ

1. ๆœ‰้™็Šถๆ€ๆœบ (FSM)

ๆ–‡ไปถ: src-tauri/src/fsm.rs

pub enum FsmState {
    Idle,
    Recording { handle: RecordingHandle },
    SttWaiting { audio_data: Vec<u8> },
    Injecting { text: String },
    Done,
    Error { message: String },
}

็Šถๆ€่ฝฌๆข่ง„ๅˆ™๏ผš

ไปŽ็Šถๆ€ ่งฆๅ‘ๆกไปถ ็›ฎๆ ‡็Šถๆ€
Idle ็ƒญ้”ฎๆŒ‰ไธ‹ Recording
Recording ็ƒญ้”ฎ้‡Šๆ”พ SttWaiting
SttWaiting STT ๅฎŒๆˆ Injecting
SttWaiting STT ๅคฑ่ดฅ Error
Injecting ๆณจๅ…ฅๅฎŒๆˆ Done
Injecting ๆณจๅ…ฅๅคฑ่ดฅ Error
Done/Error ้‡็ฝฎ Idle

ๅ…ณ้”ฎ่ฎพ่ฎก: ๆ‰€ๆœ‰็Šถๆ€่ฝฌๆข้ƒฝๆ˜ฏๅŒๆญฅ็š„๏ผŒๅชๆœ‰ๆ•ฐๆฎๅค„็†ๆ˜ฏๅผ‚ๆญฅ็š„ใ€‚่ฟ™็กฎไฟไบ† FSM ็š„็บฟ็จ‹ๅฎ‰ๅ…จๆ€งใ€‚

2. ้Ÿณ้ข‘ๅญ็ณป็ปŸ

ๆ–‡ไปถ: src-tauri/src/audio.rs

  • ๆ•่Žท: ไฝฟ็”จ cpal crate ่ฟ›่กŒ่ทจๅนณๅฐ้Ÿณ้ข‘ๆ•่Žท
  • ๆ ผๅผ: ๆ”ฏๆŒ f32 ๆ ผๅผ๏ผˆi16/u16 ้œ€่ฆ้ขๅค–ๅค„็†๏ผ‰
  • ้™ๆทท: ๅคšๅฃฐ้“้Ÿณ้ข‘่‡ชๅŠจๅนณๅ‡ไธบๅ•ๅฃฐ้“
  • ็ผ–็ : ๅฝ•ๅˆถๅฎŒๆˆๅŽ็ผ–็ ไธบ WAV ๆ ผๅผ (16 kHz, 16-bit, mono)
// ้Ÿณ้ข‘ๅ›ž่ฐƒไธญๆ‰ง่กŒ้™ๆทท
let sample_sum: f32 = data.iter().step_by(channels).sum();
let mixed_sample = sample_sum / channels as f32;

3. STT ๅฎขๆˆท็ซฏ

ๆ–‡ไปถ: src-tauri/src/stt.rs

  • ๅ่ฎฎ: OpenAI /audio/transcriptions API ๅ…ผๅฎน
  • ่ถ…ๆ—ถ: 120 ็ง’๏ผˆ2ร—ๆœ€ๅคงๅฝ•้Ÿณๆ—ถ้•ฟ๏ผ‰
  • ้‡่ฏ•: ๆ— ๏ผˆfail-fast๏ผŒ้”™่ฏฏ้€š่ฟ‡ Toast ้€š็Ÿฅ๏ผ‰

4. LLM ๅฎขๆˆท็ซฏ

ๆ–‡ไปถ: src-tauri/src/llm.rs

ๅ…ฑไบซ HTTP ๅฎขๆˆท็ซฏไพ›ไธ‰ไธช็ป„ไปถไฝฟ็”จ๏ผš

  1. TextPolisher: ๆถฆ่‰ฒ่ฏญ้Ÿณ่ฝฌๅฝ•ๆ–‡ๅญ—๏ผˆๅฏ้€‰๏ผŒMode A/B๏ผ‰
  2. SafetyFilter: ๅ‘ฝไปค่ฏญไน‰ๅฎ‰ๅ…จๆฃ€ๆŸฅ๏ผˆMode C๏ผ‰
  3. IntentParser: ่งฃๆž่‡ช็„ถ่ฏญ่จ€ไธบ็ป“ๆž„ๅŒ–ๅ‘ฝไปค๏ผˆMode C๏ผ‰

่ถ…ๆ—ถ่ฎพ็ฝฎ๏ผš

  • ๆ™ฎ้€š่ฏทๆฑ‚: 15 ็ง’
  • ๅ‘ฝไปคๆ‰ง่กŒ: 30 ็ง’

5. ๆ–‡ๅญ—ๆณจๅ…ฅๅ™จ

ๆ–‡ไปถ: src-tauri/src/injector.rs

ไธคๅฑ‚ๆณจๅ…ฅ็ญ–็•ฅ๏ผš

pub enum InjectStrategy {
    Enigo,      // ้ฆ–้€‰๏ผš่ทจๅนณๅฐ keystroke ๆจกๆ‹Ÿ
    Clipboard,  // ๅ›ž้€€๏ผšๅ‰ช่ดดๆฟ + ็ฒ˜่ดด
}

macOS ็‰นๆœ‰: CGEvent ๅ›ž้€€๏ผŒ็”จไบŽๅค„็†ๆŸไบ›ไธๆ”ฏๆŒ enigo ็š„ๅบ”็”จใ€‚

็„ฆ็‚นๆฃ€ๆŸฅ: ๆณจๅ…ฅๅ‰ๆฃ€ๆŸฅๅฝ“ๅ‰็ช—ๅฃๆ˜ฏๅฆไธŽ็ƒญ้”ฎ้‡Šๆ”พๆ—ถไธ€่‡ด๏ผŒ้˜ฒๆญขๆ–‡ๅญ—ๆณจๅ…ฅๅˆฐ้”™่ฏฏ็ช—ๅฃใ€‚

ๅ‰ช่ดดๆฟ็ญ–็•ฅ: ไธๅ›žๆขๅคๅŽŸๅ‰ช่ดดๆฟๅ†…ๅฎน๏ผˆ่ฎพ่ฎกๅ†ณ็ญ–๏ผŒ้ฟๅ…ๆ•ฐๆฎ็ซžไบ‰้ฃŽ้™ฉ๏ผ‰ใ€‚

6. ๅฎ‰ๅ…จ่ฟ‡ๆปคๅ™จ

ๆ–‡ไปถ: src-tauri/src/safety.rs

ไธคๅฑ‚ๅฎ‰ๅ…จๆžถๆž„๏ผš

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Safety Pipeline                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Layer 1: Rule-based Filter (O(1))                     โ”‚
โ”‚  - Block list: rm -rf /, mkfs.*, dd if=/dev/zero, etc. โ”‚
โ”‚  - Allow list: ls, cat, pwd, git status, etc.          โ”‚
โ”‚                                                         โ”‚
โ”‚  Layer 2: LLM Semantic Filter                          โ”‚
โ”‚  - Gray-zone commands sent to LLM for judgment         โ”‚
โ”‚  - Fail-closed: LLM unavailable โ†’ reject               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

7. ๅ‘ฝไปคๆ‰ง่กŒๅ™จ

ๆ–‡ไปถ: src-tauri/src/executor.rs

ๅฎ‰ๅ…จ่ฎพ่ฎก: ็›ดๆŽฅๆ‰ง่กŒ็จ‹ๅบ๏ผŒไธ้€š่ฟ‡ shell

// ๅฎ‰ๅ…จ๏ผš็›ดๆŽฅๆ‰ง่กŒ๏ผŒๆ—  shell ๆณจๅ…ฅ้ฃŽ้™ฉ
let output = tokio::process::Command::new(&program)
    .args(&args)
    .kill_on_drop(true)  // ่ถ…ๆ—ถๅŽ่‡ชๅŠจ็ปˆๆญข
    .output()
    .await?;

ๅ‚ๆ•ฐ่งฃๆžไฝฟ็”จ shell-words crate ๅค„็†ๅธฆๅผ•ๅท็š„ๅ‚ๆ•ฐ๏ผˆๅฆ‚่ทฏๅพ„ๅซ็ฉบๆ ผ๏ผ‰ใ€‚

PATH ๅค„็†: ่‡ชๅŠจ่กฅๅ…จๅธธ่ง่ทฏๅพ„๏ผˆ/opt/homebrew/bin, /usr/local/bin ็ญ‰๏ผ‰๏ผŒ่งฃๅ†ณไปŽ Finder ๅฏๅŠจๆ—ถ็ผบๅฐ‘ PATH ็š„้—ฎ้ข˜ใ€‚

8. ็ปŸ่ฎกๆ•ฐๆฎ

ๆ–‡ไปถ: src-tauri/src/stats.rs

ๅญ˜ๅ‚จไบŽ ~/Library/Application Support/com.sayso.app/stats.json๏ผš

{
  "total_sessions": 100,
  "total_chars": 50000,
  "total_words": 8000,
  "speaking_time_seconds": 3600,
  "time_saved_seconds": 7200,
  "commands_executed": 50,
  "last_updated": "2026-03-23T12:00:00Z"
}

ๅ†…ๅญ˜็ผ“ๅญ˜: ๅฏๅŠจๆ—ถๅŠ ่ฝฝๅˆฐๅ†…ๅญ˜๏ผŒๆ›ดๆ–ฐๆ—ถๅ…ˆๅ†™ๅ†…ๅญ˜ๅ†ๅผ‚ๆญฅๅˆท็›˜ใ€‚

ๆ•ฐๆฎๆต

Mode A: ๅฝ•้Ÿณ โ†’ ๆ‰“ๅญ—

Hotkey Press โ”€โ”€โ”
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  1. FSM: Idle โ†’ Recording                                   โ”‚
โ”‚     - Start cpal audio stream                               โ”‚
โ”‚     - Begin capturing samples                               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
Hotkey Release โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  2. FSM: Recording โ†’ SttWaiting                             โ”‚
โ”‚     - Stop audio stream                                     โ”‚
โ”‚     - Encode to WAV                                         โ”‚
โ”‚     - Send to STT API                                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
STT Response   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  3. FSM: SttWaiting โ†’ Injecting                             โ”‚
โ”‚     - Receive transcribed text                              โ”‚
โ”‚     - Optional: TextPolisher (if enabled)                   โ”‚
โ”‚     - Inject text via enigo/CGEvent                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
Injection Done โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  4. FSM: Injecting โ†’ Done โ†’ Idle                            โ”‚
โ”‚     - Update stats (chars, words, speaking time)            โ”‚
โ”‚     - Show success toast                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Mode C: ๅฝ•้Ÿณ โ†’ ๅ‘ฝไปคๆ‰ง่กŒ

STT Response   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  1. Intent Parsing                                          โ”‚
โ”‚     - Send text to LLM for intent extraction                โ”‚
โ”‚     - Receive {"command": "...", "description": "..."}      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  2. Safety Filter (Rule + LLM)                              โ”‚
โ”‚     - Layer 1: Check allow/block lists                      โ”‚
โ”‚     - Layer 2: LLM semantic judgment (if gray-zone)         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  3. Command Execution                                       โ”‚
โ”‚     - Parse command with shell-words                        โ”‚
โ”‚     - Execute with 30s timeout                              โ”‚
โ”‚     - Capture stdout/stderr                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  4. Result Notification                                     โ”‚
โ”‚     - Show toast with command output (truncated)            โ”‚
โ”‚     - Update stats                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๅ…ณ้”ฎ่ฎพ่ฎกๅ†ณ็ญ–

1. ไธบไป€ไนˆ้€‰ๆ‹ฉ Tauri v2๏ผŸ

  • ๅฎ‰ๅ…จๆ€ง: Rust ็š„ๅ†…ๅญ˜ๅฎ‰ๅ…จไฟ่ฏ
  • ๆ€ง่ƒฝ: ๅŽŸ็”ŸไบŒ่ฟ›ๅˆถ๏ผŒๆ—  Electron ็š„ๅ†…ๅญ˜ๅผ€้”€
  • ไฝ“็งฏ: ๅบ”็”จๅŒ…ไฝ“็งฏๅฐ๏ผˆ~10MB vs Electron ็š„ ~100MB๏ผ‰
  • ็ณป็ปŸ้›†ๆˆ: ่ฝปๆพ่ฎฟ้—ฎ็ณป็ปŸ API๏ผˆKeychainใ€ๅ…จๅฑ€็ƒญ้”ฎใ€้Ÿณ้ข‘๏ผ‰

2. ไธบไป€ไนˆไธไฝฟ็”จๅ‰ช่ดดๆฟๆขๅค๏ผŸ

่ฎพ่ฎกๅ†ณ็ญ–๏ผšไธๅ›žๆขๅคๅŽŸๅ‰ช่ดดๆฟๅ†…ๅฎน

ๅŽŸๅ› ๏ผš

  • ้ฟๅ…ๆ•ฐๆฎ็ซžไบ‰๏ผˆ็”จๆˆทๅฏ่ƒฝๅœจๆณจๅ…ฅๆœŸ้—ดๆ“ไฝœๅ‰ช่ดดๆฟ๏ผ‰
  • ็ฎ€ๅŒ–ๅฎž็Žฐ
  • ๅ‰ช่ดดๆฟๅ›ž้€€ๆœฌ่บซๅทฒๆ˜ฏ่พน็ผ˜ๆƒ…ๅ†ต

3. ไธบไป€ไนˆ็›ดๆŽฅๆ‰ง่กŒ่€Œ้ž shell๏ผŸ

// ๅฑ้™ฉ๏ผš้€š่ฟ‡ shell ๆ‰ง่กŒ
tokio::process::Command::new("sh")
    .arg("-c")
    .arg(user_input)  // CVE-2024-24576 ๆณจๅ…ฅ้ฃŽ้™ฉ

// ๅฎ‰ๅ…จ๏ผš็›ดๆŽฅๆ‰ง่กŒ็จ‹ๅบ
let args = shell_words::split(user_input)?;
let program = &args[0];
let program_args = &args[1..];
tokio::process::Command::new(program)
    .args(program_args)

ๆƒ่กก๏ผšไธๆ”ฏๆŒ็ฎก้“ใ€้‡ๅฎšๅ‘ใ€shell ๅ†…็ฝฎๅ‘ฝไปคใ€‚่ฆ†็›– 95% ็š„่ฏญ้Ÿณๅ‘ฝไปคๅœบๆ™ฏใ€‚

4. ไธบไป€ไนˆไฝฟ็”จ JSON ่€Œ้ž SQLite๏ผŸ

็ปŸ่ฎกๆ•ฐๆฎไฝฟ็”จ JSON ๆ–‡ไปถๅญ˜ๅ‚จ๏ผš

  • ้š็ง: ็”จๆˆทๅฏไปฅ็›ดๆŽฅๆŸฅ็œ‹/ๅˆ ้™ค่‡ชๅทฑ็š„ๆ•ฐๆฎ
  • ้€ๆ˜Ž: ๆ— ้œ€ๅทฅๅ…ทๅณๅฏ่ฏปๅ–
  • ็ฎ€ๅ•: ่ถณๅคŸๆปก่ถณ็ปŸ่ฎกๆ•ฐๆฎ้œ€ๆฑ‚
  • ๆ— ไพ่ต–: ๆ— ้œ€ SQLite ๅบ“

5. Fail-Closed ๅฎ‰ๅ…จๆจกๅž‹

ๅœจ Mode C ไธญ๏ผŒๆ‰€ๆœ‰ๅฎ‰ๅ…จๆฃ€ๆŸฅ้ƒฝๆ˜ฏ fail-closed๏ผš

ๆฃ€ๆŸฅ็‚น ๅคฑ่ดฅ่กŒไธบ
่ง„ๅˆ™่ฟ‡ๆปคๅ™จ ๆ‹’็ปๆ‰ง่กŒ
LLM ๅฎ‰ๅ…จๆฃ€ๆŸฅ ๆ‹’็ปๆ‰ง่กŒ
Intent ่งฃๆžๅคฑ่ดฅ ๆ‹’็ปๆ‰ง่กŒ
ๅ‘ฝไปคๆ‰ง่กŒ่ถ…ๆ—ถ ็ปˆๆญข่ฟ›็จ‹๏ผŒ่ฟ”ๅ›ž้”™่ฏฏ

ๅฎ‰ๅ…จ > ไพฟๅˆฉๆ€งใ€‚

้”™่ฏฏๅค„็†

ๆ‰€ๆœ‰้”™่ฏฏ้€š่ฟ‡ SaysoError ๆžšไธพ่กจ็คบ๏ผš

pub enum SaysoError {
    Audio(String),
    Stt(String),
    Llm(String),
    Injection(String),
    SafetyViolation(String),
    Execution(String),
    Config(String),
    // ...
}

้”™่ฏฏ้€š่ฟ‡ Tauri event ๅ‘้€ๅˆฐๅ‰็ซฏๆ˜พ็คบ Toast ้€š็Ÿฅใ€‚

ๆต‹่ฏ•็ญ–็•ฅ

ๅ•ๅ…ƒๆต‹่ฏ• (33 ไธช)

cd src-tauri
cargo test

่ฆ†็›–๏ผš

  • FSM ็Šถๆ€่ฝฌๆข
  • ๅฎ‰ๅ…จ่ฟ‡ๆปคๅ™จ่ง„ๅˆ™ๅŒน้…
  • LLM ๅฎขๆˆท็ซฏ่ถ…ๆ—ถๅค„็†
  • Intent ่งฃๆž JSON ้ชŒ่ฏ
  • ๅ‘ฝไปคๆ‰ง่กŒๅ‚ๆ•ฐ่งฃๆž
  • ๆ–‡ๅญ—ๆถฆ่‰ฒ fallback
  • STT ๅ“ๅบ”่งฃๆž
  • ้Ÿณ้ข‘็ผ–็  WAV ๆ ผๅผ
  • ็ปŸ่ฎกๆ•ฐๆฎ่ฎก็ฎ—

้›†ๆˆๆต‹่ฏ•

  • ็ƒญ้”ฎ โ†’ ๅฝ•้Ÿณ โ†’ STT โ†’ ๆณจๅ…ฅๅฎŒๆ•ดๆต็จ‹
  • ้œ€่ฆๅœจๆœ‰้บฆๅ…‹้ฃŽ็š„็Žฏๅขƒไธญๆ‰‹ๅŠจ้ชŒ่ฏ

E2E ๆต‹่ฏ•

v1.1 ๅŽ้€š่ฟ‡ Playwright ๆต‹่ฏ•ๅ…ณ้”ฎ็”จๆˆทๆต็จ‹ใ€‚

ๆ€ง่ƒฝ่€ƒ่™‘

ๅ†…ๅญ˜ไฝฟ็”จ

  • ้Ÿณ้ข‘ๆ•ฐๆฎ: ๆœ€ๅคง 60s ร— 16kHz ร— 2bytes โ‰ˆ 1.9MB
  • ็ปŸ่ฎกๆ•ฐๆฎ: ๅธธ้ฉปๅ†…ๅญ˜๏ผŒ<1KB
  • HTTP ๅฎขๆˆท็ซฏ: ๅ…ฑไบซ reqwest::Client๏ผŒ่ฟžๆŽฅๆฑ ๅค็”จ

ๅฏๅŠจๆ—ถ้—ด

  • Rust ไบŒ่ฟ›ๅˆถ: <100ms
  • React ๅ‰็ซฏ: ็”ฑ Tauri ๅŠ ่ฝฝ๏ผŒ<50ms
  • ๆ€ปๅฏๅŠจๆ—ถ้—ด: <1s๏ผˆๅŒ…ๆ‹ฌ้…็ฝฎๅŠ ่ฝฝใ€Keychain ่ฏปๅ–๏ผ‰

็ƒญ้”ฎๅ“ๅบ”

็ƒญ้”ฎๅค„็†ๅœจไธ“็”จ็บฟ็จ‹่ฟ›่กŒ๏ผŒๅปถ่ฟŸ <10msใ€‚

ๅฎ‰ๅ…จ่พน็•Œ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    ๅฎ‰ๅ…จ่พน็•Œ                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Untrusted                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  - User voice input                              โ”‚  โ”‚
โ”‚  โ”‚  - STT API response                              โ”‚  โ”‚
โ”‚  โ”‚  - LLM API response                              โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                              โ”‚
โ”‚                          โ–ผ                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  SafetyFilter (Rule + LLM)                       โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                          โ”‚                              โ”‚
โ”‚  Trusted                  โ–ผ                             โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  - Shell execution                               โ”‚  โ”‚
โ”‚  โ”‚  - Keystroke injection                           โ”‚  โ”‚
โ”‚  โ”‚  - File system access (stats.json)               โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๆ‰€ๆœ‰ๅค–้ƒจ่พ“ๅ…ฅ๏ผˆ่ฏญ้Ÿณ่ฝฌๅฝ•ใ€LLM ่พ“ๅ‡บ๏ผ‰้ƒฝ็ป่ฟ‡ๅฎ‰ๅ…จๆฃ€ๆŸฅๅŽๆ‰ๆ‰ง่กŒๆ•ๆ„Ÿๆ“ไฝœใ€‚