feat(transcription): add word correction with phonetic matching
This commit is contained in:
parent
e8976c2047
commit
003bf23402
13 changed files with 248 additions and 7 deletions
23
src-tauri/Cargo.lock
generated
23
src-tauri/Cargo.lock
generated
|
|
@ -1105,7 +1105,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.0",
|
"redox_users 0.5.0",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2001,6 +2001,7 @@ dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hound",
|
"hound",
|
||||||
"log",
|
"log",
|
||||||
|
"natural",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ort-sys",
|
"ort-sys",
|
||||||
"rdev",
|
"rdev",
|
||||||
|
|
@ -2010,6 +2011,7 @@ dependencies = [
|
||||||
"rustfft",
|
"rustfft",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"strsim",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-autostart",
|
"tauri-plugin-autostart",
|
||||||
|
|
@ -2962,6 +2964,15 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "natural"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65e6b44f8ddc659cde3555e0140d3441ad26cb03a1410774af1f9a19097c1867"
|
||||||
|
dependencies = [
|
||||||
|
"rust-stemmers",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndarray"
|
name = "ndarray"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
|
|
@ -4365,6 +4376,16 @@ dependencies = [
|
||||||
"realfft",
|
"realfft",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-stemmers"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
rustfft = "6.4.0"
|
rustfft = "6.4.0"
|
||||||
|
strsim = "0.11.0"
|
||||||
|
natural = "0.5.0"
|
||||||
|
|
||||||
[dependencies.ort-sys]
|
[dependencies.ort-sys]
|
||||||
version = "=2.0.0-rc.9"
|
version = "=2.0.0-rc.9"
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@ pub fn run() {
|
||||||
shortcut::change_selected_language_setting,
|
shortcut::change_selected_language_setting,
|
||||||
shortcut::change_overlay_position_setting,
|
shortcut::change_overlay_position_setting,
|
||||||
shortcut::change_debug_mode_setting,
|
shortcut::change_debug_mode_setting,
|
||||||
|
shortcut::update_correct_words,
|
||||||
shortcut::suspend_binding,
|
shortcut::suspend_binding,
|
||||||
shortcut::resume_binding,
|
shortcut::resume_binding,
|
||||||
trigger_update_check,
|
trigger_update_check,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::managers::model::ModelManager;
|
use crate::managers::model::ModelManager;
|
||||||
use crate::settings::get_settings;
|
use crate::settings::get_settings;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use natural::phonetics::soundex;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use strsim::levenshtein;
|
||||||
use tauri::{App, AppHandle, Emitter, Manager};
|
use tauri::{App, AppHandle, Emitter, Manager};
|
||||||
use whisper_rs::install_whisper_log_trampoline;
|
use whisper_rs::install_whisper_log_trampoline;
|
||||||
use whisper_rs::{
|
use whisper_rs::{
|
||||||
|
|
@ -25,6 +27,82 @@ pub struct TranscriptionManager {
|
||||||
current_model_id: Mutex<Option<String>>,
|
current_model_id: Mutex<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn correct_words(text: &str, correct_words: &[String]) -> String {
|
||||||
|
if correct_words.is_empty() {
|
||||||
|
return text.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let words: Vec<&str> = text.split_whitespace().collect();
|
||||||
|
let mut corrected_words = Vec::new();
|
||||||
|
|
||||||
|
for word in words {
|
||||||
|
let cleaned_word = word.trim_matches(|c: char| !c.is_alphabetic()).to_lowercase();
|
||||||
|
|
||||||
|
if cleaned_word.is_empty() {
|
||||||
|
corrected_words.push(word.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut best_match: Option<&String> = None;
|
||||||
|
let mut best_score = f64::MAX;
|
||||||
|
|
||||||
|
for correct_word in correct_words {
|
||||||
|
let correct_word_lower = correct_word.to_lowercase();
|
||||||
|
|
||||||
|
// Calculate Levenshtein distance (normalized by length)
|
||||||
|
let levenshtein_dist = levenshtein(&cleaned_word, &correct_word_lower);
|
||||||
|
let max_len = cleaned_word.len().max(correct_word_lower.len()) as f64;
|
||||||
|
let levenshtein_score = if max_len > 0.0 { levenshtein_dist as f64 / max_len } else { 1.0 };
|
||||||
|
|
||||||
|
// Calculate phonetic similarity using Soundex
|
||||||
|
let phonetic_match = soundex(&cleaned_word, &correct_word_lower);
|
||||||
|
|
||||||
|
// Combine scores: favor phonetic matches, but also consider string similarity
|
||||||
|
let combined_score = if phonetic_match {
|
||||||
|
levenshtein_score * 0.3 // Give significant boost to phonetic matches
|
||||||
|
} else {
|
||||||
|
levenshtein_score
|
||||||
|
};
|
||||||
|
|
||||||
|
// Accept if the score is good enough (threshold: 0.4 for good matches)
|
||||||
|
if combined_score < 0.4 && combined_score < best_score {
|
||||||
|
best_match = Some(correct_word);
|
||||||
|
best_score = combined_score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(replacement) = best_match {
|
||||||
|
// Preserve the original case pattern as much as possible
|
||||||
|
let corrected = if word.chars().all(|c| c.is_uppercase()) {
|
||||||
|
replacement.to_uppercase()
|
||||||
|
} else if word.chars().next().map_or(false, |c| c.is_uppercase()) {
|
||||||
|
let mut chars: Vec<char> = replacement.chars().collect();
|
||||||
|
if let Some(first_char) = chars.get_mut(0) {
|
||||||
|
*first_char = first_char.to_uppercase().next().unwrap_or(*first_char);
|
||||||
|
}
|
||||||
|
chars.into_iter().collect()
|
||||||
|
} else {
|
||||||
|
replacement.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve punctuation from original word
|
||||||
|
let original_suffix: String = word.chars().rev()
|
||||||
|
.take_while(|c| !c.is_alphabetic())
|
||||||
|
.collect::<String>()
|
||||||
|
.chars().rev().collect();
|
||||||
|
let original_prefix: String = word.chars()
|
||||||
|
.take_while(|c| !c.is_alphabetic())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
corrected_words.push(format!("{}{}{}", original_prefix, corrected, original_suffix));
|
||||||
|
} else {
|
||||||
|
corrected_words.push(word.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
corrected_words.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
impl TranscriptionManager {
|
impl TranscriptionManager {
|
||||||
pub fn new(app: &App, model_manager: Arc<ModelManager>) -> Result<Self> {
|
pub fn new(app: &App, model_manager: Arc<ModelManager>) -> Result<Self> {
|
||||||
let app_handle = app.app_handle().clone();
|
let app_handle = app.app_handle().clone();
|
||||||
|
|
@ -207,6 +285,13 @@ impl TranscriptionManager {
|
||||||
result.push_str(&segment);
|
result.push_str(&segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply word correction if correct words are configured
|
||||||
|
let corrected_result = if !settings.correct_words.is_empty() {
|
||||||
|
correct_words(&result, &settings.correct_words)
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
let et = std::time::Instant::now();
|
let et = std::time::Instant::now();
|
||||||
let translation_note = if settings.translate_to_english {
|
let translation_note = if settings.translate_to_english {
|
||||||
" (translated)"
|
" (translated)"
|
||||||
|
|
@ -215,6 +300,6 @@ impl TranscriptionManager {
|
||||||
};
|
};
|
||||||
println!("\ntook {}ms{}", (et - st).as_millis(), translation_note);
|
println!("\ntook {}ms{}", (et - st).as_millis(), translation_note);
|
||||||
|
|
||||||
Ok(result.trim().to_string())
|
Ok(corrected_result.trim().to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ pub struct AppSettings {
|
||||||
pub overlay_position: OverlayPosition,
|
pub overlay_position: OverlayPosition,
|
||||||
#[serde(default = "default_debug_mode")]
|
#[serde(default = "default_debug_mode")]
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub correct_words: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_model() -> String {
|
fn default_model() -> String {
|
||||||
|
|
@ -104,6 +106,7 @@ pub fn get_default_settings() -> AppSettings {
|
||||||
selected_language: "auto".to_string(),
|
selected_language: "auto".to_string(),
|
||||||
overlay_position: OverlayPosition::Bottom,
|
overlay_position: OverlayPosition::Bottom,
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
|
correct_words: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,14 @@ pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), St
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn update_correct_words(app: AppHandle, words: Vec<String>) -> Result<(), String> {
|
||||||
|
let mut settings = settings::get_settings(&app);
|
||||||
|
settings.correct_words = words;
|
||||||
|
settings::write_settings(&app, settings);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Determine whether a shortcut string contains at least one non-modifier key.
|
/// Determine whether a shortcut string contains at least one non-modifier key.
|
||||||
/// We allow single non-modifier keys (e.g. "f5" or "space") but disallow
|
/// We allow single non-modifier keys (e.g. "f5" or "space") but disallow
|
||||||
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").
|
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@
|
||||||
{
|
{
|
||||||
"title": "Handy",
|
"title": "Handy",
|
||||||
"width": 540,
|
"width": 540,
|
||||||
"height": 700,
|
"height": 750,
|
||||||
"minWidth": 540,
|
"minWidth": 540,
|
||||||
"minHeight": 700,
|
"minHeight": 750,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"maximizable": false
|
"maximizable": false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Toaster } from "sonner";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import AccessibilityPermissions from "./components/AccessibilityPermissions";
|
import AccessibilityPermissions from "./components/AccessibilityPermissions";
|
||||||
import Footer from "./components/footer";
|
import Footer from "./components/footer";
|
||||||
import HandyTextLogo from "./components/icons/HandyTextLogo";
|
import HandyTextLogo from "./components/icons/HandyTextLogo";
|
||||||
import Onboarding from "./components/onboarding";
|
import Onboarding from "./components/onboarding";
|
||||||
import { Settings } from "./components/settings/Settings";
|
import { Settings } from "./components/settings/Settings";
|
||||||
import { Toaster } from "sonner";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [showOnboarding, setShowOnboarding] = useState<boolean | null>(null);
|
const [showOnboarding, setShowOnboarding] = useState<boolean | null>(null);
|
||||||
|
|
@ -35,7 +35,7 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col w-full">
|
<div className="min-h-screen flex flex-col w-full">
|
||||||
<div className="flex flex-col items-center pt-6 gap-8 px-4 flex-1">
|
<div className="flex flex-col items-center pt-6 gap-8 px-4 flex-1">
|
||||||
<HandyTextLogo width={240} />
|
<HandyTextLogo width={200} />
|
||||||
<Onboarding onModelSelected={handleModelSelected} />
|
<Onboarding onModelSelected={handleModelSelected} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,7 +46,7 @@ function App() {
|
||||||
<div className="min-h-screen flex flex-col w-full">
|
<div className="min-h-screen flex flex-col w-full">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<div className="flex flex-col items-center pt-6 gap-8 px-4 flex-1">
|
<div className="flex flex-col items-center pt-6 gap-8 px-4 flex-1">
|
||||||
<HandyTextLogo width={240} />
|
<HandyTextLogo width={200} />
|
||||||
<AccessibilityPermissions />
|
<AccessibilityPermissions />
|
||||||
<Settings />
|
<Settings />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
113
src/components/settings/CorrectWords.tsx
Normal file
113
src/components/settings/CorrectWords.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { SettingContainer } from "../ui/SettingContainer";
|
||||||
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
|
||||||
|
interface CorrectWordsProps {
|
||||||
|
descriptionMode?: "inline" | "tooltip";
|
||||||
|
grouped?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CorrectWords: React.FC<CorrectWordsProps> = ({
|
||||||
|
descriptionMode = "tooltip",
|
||||||
|
grouped = false,
|
||||||
|
}) => {
|
||||||
|
const { getSetting, updateSetting, isUpdating } = useSettings();
|
||||||
|
const [newWord, setNewWord] = useState("");
|
||||||
|
|
||||||
|
const correctWords = getSetting("correct_words") || [];
|
||||||
|
|
||||||
|
const handleAddWord = () => {
|
||||||
|
const trimmedWord = newWord.trim();
|
||||||
|
// Don't allow spaces in words
|
||||||
|
if (trimmedWord && !trimmedWord.includes(' ') && !correctWords.includes(trimmedWord)) {
|
||||||
|
updateSetting("correct_words", [...correctWords, trimmedWord]);
|
||||||
|
setNewWord("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveWord = (wordToRemove: string) => {
|
||||||
|
updateSetting(
|
||||||
|
"correct_words",
|
||||||
|
correctWords.filter((word) => word !== wordToRemove)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddWord();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`px-4 p-2 ${grouped ? "" : "rounded-lg border border-mid-gray/20"}`}>
|
||||||
|
{/* Title, info button, input and add button all in one line */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-sm font-medium">Custom Words</h3>
|
||||||
|
{descriptionMode === "tooltip" && (
|
||||||
|
<div title="Add words that are often misheard or misspelled during transcription. The system will automatically correct similar-sounding words to match your list.">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 text-mid-gray cursor-help hover:text-logo-primary transition-colors duration-200 select-none"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newWord}
|
||||||
|
onChange={(e) => setNewWord(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="Add a word"
|
||||||
|
className="flex-1 px-2 py-1 text-sm border border-mid-gray/30 rounded focus:outline-none focus:ring-1 focus:ring-logo-primary focus:border-transparent bg-background"
|
||||||
|
disabled={isUpdating("correct_words")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleAddWord}
|
||||||
|
disabled={!newWord.trim() || newWord.includes(' ') || isUpdating("correct_words")}
|
||||||
|
className="px-3 py-1 text-xs font-medium text-white bg-logo-primary rounded hover:bg-logo-primary/90 focus:outline-none focus:ring-1 focus:ring-logo-primary disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Words list - flex wrap layout */}
|
||||||
|
{correctWords.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
|
{correctWords.map((word) => (
|
||||||
|
<button
|
||||||
|
key={word}
|
||||||
|
onClick={() => handleRemoveWord(word)}
|
||||||
|
disabled={isUpdating("correct_words")}
|
||||||
|
className="inline-flex items-center gap-1 px-2 py-1 bg-mid-gray/10 rounded text-xs hover:bg-red-100 hover:text-red-700 focus:outline-none disabled:opacity-50 cursor-pointer transition-colors"
|
||||||
|
aria-label={`Remove ${word}`}
|
||||||
|
>
|
||||||
|
<span>{word}</span>
|
||||||
|
<svg
|
||||||
|
className="w-3 h-3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={3}
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -8,6 +8,7 @@ import { ShowOverlay } from "./ShowOverlay";
|
||||||
import { HandyShortcut } from "./HandyShortcut";
|
import { HandyShortcut } from "./HandyShortcut";
|
||||||
import { TranslateToEnglish } from "./TranslateToEnglish";
|
import { TranslateToEnglish } from "./TranslateToEnglish";
|
||||||
import { LanguageSelector } from "./LanguageSelector";
|
import { LanguageSelector } from "./LanguageSelector";
|
||||||
|
import { CorrectWords } from "./CorrectWords";
|
||||||
import { SettingsGroup } from "../ui/SettingsGroup";
|
import { SettingsGroup } from "../ui/SettingsGroup";
|
||||||
import { DebugSettings } from "./debug";
|
import { DebugSettings } from "./debug";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
|
@ -54,6 +55,7 @@ export const Settings: React.FC = () => {
|
||||||
<OutputDeviceSelector descriptionMode="tooltip" grouped={true} />
|
<OutputDeviceSelector descriptionMode="tooltip" grouped={true} />
|
||||||
<ShowOverlay descriptionMode="tooltip" grouped={true} />
|
<ShowOverlay descriptionMode="tooltip" grouped={true} />
|
||||||
<TranslateToEnglish descriptionMode="tooltip" grouped={true} />
|
<TranslateToEnglish descriptionMode="tooltip" grouped={true} />
|
||||||
|
<CorrectWords descriptionMode="tooltip" grouped={true} />
|
||||||
<AlwaysOnMicrophone descriptionMode="tooltip" grouped={true} />
|
<AlwaysOnMicrophone descriptionMode="tooltip" grouped={true} />
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ export { AudioFeedback } from "./AudioFeedback";
|
||||||
export { ShowOverlay } from "./ShowOverlay";
|
export { ShowOverlay } from "./ShowOverlay";
|
||||||
export { HandyShortcut } from "./HandyShortcut";
|
export { HandyShortcut } from "./HandyShortcut";
|
||||||
export { TranslateToEnglish } from "./TranslateToEnglish";
|
export { TranslateToEnglish } from "./TranslateToEnglish";
|
||||||
|
export { CorrectWords } from "./CorrectWords";
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,9 @@ export const useSettings = (): UseSettingsReturn => {
|
||||||
case "debug_mode":
|
case "debug_mode":
|
||||||
await invoke("change_debug_mode_setting", { enabled: value });
|
await invoke("change_debug_mode_setting", { enabled: value });
|
||||||
break;
|
break;
|
||||||
|
case "correct_words":
|
||||||
|
await invoke("update_correct_words", { words: value });
|
||||||
|
break;
|
||||||
case "bindings":
|
case "bindings":
|
||||||
// Handle bindings separately - they use their own invoke methods
|
// Handle bindings separately - they use their own invoke methods
|
||||||
break;
|
break;
|
||||||
|
|
@ -251,6 +254,7 @@ export const useSettings = (): UseSettingsReturn => {
|
||||||
selected_language: "auto",
|
selected_language: "auto",
|
||||||
overlay_position: "bottom",
|
overlay_position: "bottom",
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
|
correct_words: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValue = defaults[key];
|
const defaultValue = defaults[key];
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export const SettingsSchema = z.object({
|
||||||
selected_language: z.string(),
|
selected_language: z.string(),
|
||||||
overlay_position: OverlayPositionSchema,
|
overlay_position: OverlayPositionSchema,
|
||||||
debug_mode: z.boolean(),
|
debug_mode: z.boolean(),
|
||||||
|
correct_words: z.array(z.string()).optional().default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BindingResponseSchema = z.object({
|
export const BindingResponseSchema = z.object({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue