From c2b3815f56d988f3e9d71a1e9ecb89203c9eae9a Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 11 Aug 2025 15:19:48 -0700 Subject: [PATCH] 'custom words' everywhere instead of 'correct words' --- src-tauri/src/lib.rs | 2 +- src-tauri/src/managers/transcription.rs | 77 +++++++++------ src-tauri/src/settings.rs | 4 +- src-tauri/src/shortcut.rs | 8 +- src/components/settings/CorrectWords.tsx | 91 ------------------ src/components/settings/CustomWords.tsx | 115 +++++++++++++++++++++++ src/components/settings/Settings.tsx | 4 +- src/components/settings/index.ts | 2 +- src/hooks/useSettings.ts | 10 +- src/lib/types.ts | 2 +- 10 files changed, 180 insertions(+), 135 deletions(-) delete mode 100644 src/components/settings/CorrectWords.tsx create mode 100644 src/components/settings/CustomWords.tsx diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9bc0702..a7761d4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -187,7 +187,7 @@ pub fn run() { shortcut::change_selected_language_setting, shortcut::change_overlay_position_setting, shortcut::change_debug_mode_setting, - shortcut::update_correct_words, + shortcut::update_custom_words, shortcut::suspend_binding, shortcut::resume_binding, trigger_update_check, diff --git a/src-tauri/src/managers/transcription.rs b/src-tauri/src/managers/transcription.rs index 9f70501..51ea000 100644 --- a/src-tauri/src/managers/transcription.rs +++ b/src-tauri/src/managers/transcription.rs @@ -27,22 +27,22 @@ pub struct TranscriptionManager { current_model_id: Mutex>, } -fn correct_words(text: &str, correct_words: &[String]) -> String { - if correct_words.is_empty() { +fn apply_custom_words(text: &str, custom_words: &[String]) -> String { + if custom_words.is_empty() { return text.to_string(); } // Pre-compute lowercase versions to avoid repeated allocations - let correct_words_lower: Vec = correct_words.iter() - .map(|w| w.to_lowercase()) - .collect(); + let custom_words_lower: Vec = custom_words.iter().map(|w| w.to_lowercase()).collect(); 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(); - + 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; @@ -57,31 +57,35 @@ fn correct_words(text: &str, correct_words: &[String]) -> String { let mut best_match: Option<&String> = None; let mut best_score = f64::MAX; - for (i, correct_word_lower) in correct_words_lower.iter().enumerate() { + for (i, custom_word_lower) in custom_words_lower.iter().enumerate() { // Skip if lengths are too different (optimization) - let len_diff = (cleaned_word.len() as i32 - correct_word_lower.len() as i32).abs(); + let len_diff = (cleaned_word.len() as i32 - custom_word_lower.len() as i32).abs(); if len_diff > 5 { continue; } - + // 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 }; - + let levenshtein_dist = levenshtein(&cleaned_word, custom_word_lower); + let max_len = cleaned_word.len().max(custom_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); - + let phonetic_match = soundex(&cleaned_word, custom_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 + 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_words[i]); + best_match = Some(&custom_words[i]); best_score = combined_score; } } @@ -99,15 +103,30 @@ fn correct_words(text: &str, correct_words: &[String]) -> String { } else { replacement.clone() }; - + // Preserve punctuation from original word - optimized version let prefix_end = word.chars().take_while(|c| !c.is_alphabetic()).count(); - let suffix_start = word.char_indices().rev().take_while(|(_, c)| !c.is_alphabetic()).count(); - - let original_prefix = if prefix_end > 0 { &word[..prefix_end] } else { "" }; - let original_suffix = if suffix_start > 0 { &word[word.len() - suffix_start..] } else { "" }; - - corrected_words.push(format!("{}{}{}", original_prefix, corrected, original_suffix)); + let suffix_start = word + .char_indices() + .rev() + .take_while(|(_, c)| !c.is_alphabetic()) + .count(); + + let original_prefix = if prefix_end > 0 { + &word[..prefix_end] + } else { + "" + }; + let original_suffix = if suffix_start > 0 { + &word[word.len() - suffix_start..] + } else { + "" + }; + + corrected_words.push(format!( + "{}{}{}", + original_prefix, corrected, original_suffix + )); } else { corrected_words.push(word.to_string()); } @@ -298,9 +317,9 @@ impl TranscriptionManager { 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) + // Apply word correction if custom words are configured + let corrected_result = if !settings.custom_words.is_empty() { + apply_custom_words(&result, &settings.custom_words) } else { result }; diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 39863d3..6b08cf8 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -43,7 +43,7 @@ pub struct AppSettings { #[serde(default = "default_debug_mode")] pub debug_mode: bool, #[serde(default)] - pub correct_words: Vec, + pub custom_words: Vec, } fn default_model() -> String { @@ -106,7 +106,7 @@ pub fn get_default_settings() -> AppSettings { selected_language: "auto".to_string(), overlay_position: OverlayPosition::Bottom, debug_mode: false, - correct_words: Vec::new(), + custom_words: Vec::new(), } } diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index 30eca98..d1db091 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -148,10 +148,10 @@ pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Resu }; settings.overlay_position = parsed; settings::write_settings(&app, settings); - + // Update overlay position without recreating window crate::utils::update_overlay_position(&app); - + Ok(()) } @@ -164,9 +164,9 @@ pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), St } #[tauri::command] -pub fn update_correct_words(app: AppHandle, words: Vec) -> Result<(), String> { +pub fn update_custom_words(app: AppHandle, words: Vec) -> Result<(), String> { let mut settings = settings::get_settings(&app); - settings.correct_words = words; + settings.custom_words = words; settings::write_settings(&app, settings); Ok(()) } diff --git a/src/components/settings/CorrectWords.tsx b/src/components/settings/CorrectWords.tsx deleted file mode 100644 index 451f2d7..0000000 --- a/src/components/settings/CorrectWords.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useState } from "react"; -import { useSettings } from "../../hooks/useSettings"; -import { Input } from "../ui/Input"; -import { Button } from "../ui/Button"; -import { SettingContainer } from "../ui/SettingContainer"; - -interface CorrectWordsProps { - descriptionMode?: "inline" | "tooltip"; - grouped?: boolean; -} - -export const CorrectWords: React.FC = React.memo(({ - descriptionMode = "tooltip", - grouped = false, -}) => { - const { getSetting, updateSetting, isUpdating } = useSettings(); - const [newWord, setNewWord] = useState(""); - const correctWords = getSetting("correct_words") || []; - - const handleAddWord = () => { - const trimmedWord = newWord.trim(); - const sanitizedWord = trimmedWord.replace(/[<>"'&]/g, ''); - if (sanitizedWord && !sanitizedWord.includes(' ') && sanitizedWord.length <= 50 && !correctWords.includes(sanitizedWord)) { - updateSetting("correct_words", [...correctWords, sanitizedWord]); - 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 ( - <> - -
- setNewWord(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Add a word" - variant="compact" - disabled={isUpdating("correct_words")} - /> - -
-
- {correctWords.length > 0 && ( -
- {correctWords.map((word) => ( - - ))} -
- )} - - ); -}); \ No newline at end of file diff --git a/src/components/settings/CustomWords.tsx b/src/components/settings/CustomWords.tsx new file mode 100644 index 0000000..c5d2710 --- /dev/null +++ b/src/components/settings/CustomWords.tsx @@ -0,0 +1,115 @@ +import React, { useState } from "react"; +import { useSettings } from "../../hooks/useSettings"; +import { Input } from "../ui/Input"; +import { Button } from "../ui/Button"; +import { SettingContainer } from "../ui/SettingContainer"; + +interface CustomWordsProps { + descriptionMode?: "inline" | "tooltip"; + grouped?: boolean; +} + +export const CustomWords: React.FC = React.memo( + ({ descriptionMode = "tooltip", grouped = false }) => { + const { getSetting, updateSetting, isUpdating } = useSettings(); + const [newWord, setNewWord] = useState(""); + const customWords = getSetting("custom_words") || []; + + const handleAddWord = () => { + const trimmedWord = newWord.trim(); + const sanitizedWord = trimmedWord.replace(/[<>"'&]/g, ""); + if ( + sanitizedWord && + !sanitizedWord.includes(" ") && + sanitizedWord.length <= 50 && + !customWords.includes(sanitizedWord) + ) { + updateSetting("custom_words", [...customWords, sanitizedWord]); + setNewWord(""); + } + }; + + const handleRemoveWord = (wordToRemove: string) => { + updateSetting( + "custom_words", + customWords.filter((word) => word !== wordToRemove), + ); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddWord(); + } + }; + + return ( + <> + +
+ setNewWord(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Add a word" + variant="compact" + disabled={isUpdating("custom_words")} + /> + +
+
+ {customWords.length > 0 && ( +
+ {customWords.map((word) => ( + + ))} +
+ )} + + ); + }, +); diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index fa530f6..ca83a8d 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -8,7 +8,7 @@ import { ShowOverlay } from "./ShowOverlay"; import { HandyShortcut } from "./HandyShortcut"; import { TranslateToEnglish } from "./TranslateToEnglish"; import { LanguageSelector } from "./LanguageSelector"; -import { CorrectWords } from "./CorrectWords"; +import { CustomWords } from "./CustomWords"; import { SettingsGroup } from "../ui/SettingsGroup"; import { DebugSettings } from "./debug"; import { useSettings } from "../../hooks/useSettings"; @@ -55,7 +55,7 @@ export const Settings: React.FC = () => { - + diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index cd7dd78..4838851 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -7,4 +7,4 @@ export { AudioFeedback } from "./AudioFeedback"; export { ShowOverlay } from "./ShowOverlay"; export { HandyShortcut } from "./HandyShortcut"; export { TranslateToEnglish } from "./TranslateToEnglish"; -export { CorrectWords } from "./CorrectWords"; +export { CustomWords } from "./CustomWords"; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 5ad26b4..7b64811 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -200,13 +200,15 @@ export const useSettings = (): UseSettingsReturn => { }); break; case "overlay_position": - await invoke("change_overlay_position_setting", { position: value }); + await invoke("change_overlay_position_setting", { + position: value, + }); break; case "debug_mode": await invoke("change_debug_mode_setting", { enabled: value }); break; - case "correct_words": - await invoke("update_correct_words", { words: value }); + case "custom_words": + await invoke("update_custom_words", { words: value }); break; case "bindings": // Handle bindings separately - they use their own invoke methods @@ -254,7 +256,7 @@ export const useSettings = (): UseSettingsReturn => { selected_language: "auto", overlay_position: "bottom", debug_mode: false, - correct_words: [], + custom_words: [], }; const defaultValue = defaults[key]; diff --git a/src/lib/types.ts b/src/lib/types.ts index 7979b04..cde2622 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -34,7 +34,7 @@ export const SettingsSchema = z.object({ selected_language: z.string(), overlay_position: OverlayPositionSchema, debug_mode: z.boolean(), - correct_words: z.array(z.string()).optional().default([]), + custom_words: z.array(z.string()).optional().default([]), }); export const BindingResponseSchema = z.object({