Merge pull request #79 from cjpais/tweak-correct-thresh

new default correction threshold + debug menu
This commit is contained in:
CJ Pais 2025-08-15 12:09:30 -07:00 committed by GitHub
commit b074aad19a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 161 additions and 54 deletions

View file

@ -187,6 +187,7 @@ pub fn run() {
shortcut::change_selected_language_setting,
shortcut::change_overlay_position_setting,
shortcut::change_debug_mode_setting,
shortcut::change_word_correction_threshold_setting,
shortcut::update_custom_words,
shortcut::suspend_binding,
shortcut::resume_binding,

View file

@ -27,7 +27,7 @@ pub struct TranscriptionManager {
current_model_id: Mutex<Option<String>>,
}
fn apply_custom_words(text: &str, custom_words: &[String]) -> String {
fn apply_custom_words(text: &str, custom_words: &[String], threshold: f64) -> String {
if custom_words.is_empty() {
return text.to_string();
}
@ -83,8 +83,8 @@ fn apply_custom_words(text: &str, custom_words: &[String]) -> String {
levenshtein_score
};
// Accept if the score is good enough (threshold: 0.4 for good matches)
if combined_score < 0.4 && combined_score < best_score {
// Accept if the score is good enough (configurable threshold)
if combined_score < threshold && combined_score < best_score {
best_match = Some(&custom_words[i]);
best_score = combined_score;
}
@ -319,7 +319,11 @@ impl TranscriptionManager {
// Apply word correction if custom words are configured
let corrected_result = if !settings.custom_words.is_empty() {
apply_custom_words(&result, &settings.custom_words)
apply_custom_words(
&result,
&settings.custom_words,
settings.word_correction_threshold,
)
} else {
result
};

View file

@ -44,6 +44,8 @@ pub struct AppSettings {
pub debug_mode: bool,
#[serde(default)]
pub custom_words: Vec<String>,
#[serde(default = "default_word_correction_threshold")]
pub word_correction_threshold: f64,
}
fn default_model() -> String {
@ -70,13 +72,17 @@ fn default_debug_mode() -> bool {
false
}
fn default_word_correction_threshold() -> f64 {
0.2
}
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
pub fn get_default_settings() -> AppSettings {
#[cfg(target_os = "windows")]
let default_shortcut = "ctrl+space";
#[cfg(target_os = "macos")]
let default_shortcut = "alt+space";
let default_shortcut = "option+space";
#[cfg(target_os = "linux")]
let default_shortcut = "ctrl+space";
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
@ -107,6 +113,7 @@ pub fn get_default_settings() -> AppSettings {
overlay_position: OverlayPosition::Bottom,
debug_mode: false,
custom_words: Vec::new(),
word_correction_threshold: default_word_correction_threshold(),
}
}

View file

@ -171,6 +171,17 @@ pub fn update_custom_words(app: AppHandle, words: Vec<String>) -> Result<(), Str
Ok(())
}
#[tauri::command]
pub fn change_word_correction_threshold_setting(
app: AppHandle,
threshold: f64,
) -> Result<(), String> {
let mut settings = settings::get_settings(&app);
settings.word_correction_threshold = threshold;
settings::write_settings(&app, settings);
Ok(())
}
/// 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
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").

View file

@ -1,13 +1,13 @@
import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { TextDisplay, SettingsGroup } from "../../ui";
import { TextDisplay } from "../ui";
interface DebugPathsProps {
interface AppDataDirectoryProps {
descriptionMode?: "tooltip" | "inline";
grouped?: boolean;
}
export const DebugPaths: React.FC<DebugPathsProps> = ({
export const AppDataDirectory: React.FC<AppDataDirectoryProps> = ({
descriptionMode = "inline",
grouped = false,
}) => {
@ -16,18 +16,20 @@ export const DebugPaths: React.FC<DebugPathsProps> = ({
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadPaths = async () => {
const loadAppDirectory = async () => {
try {
const result = await invoke<string>("get_app_dir_path");
setAppDirPath(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load paths");
setError(
err instanceof Error ? err.message : "Failed to load app directory",
);
} finally {
setLoading(false);
}
};
loadPaths();
loadAppDirectory();
}, []);
const handleCopy = (value: string) => {
@ -37,11 +39,9 @@ export const DebugPaths: React.FC<DebugPathsProps> = ({
if (loading) {
return (
<div className="space-y-4">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/3 mb-2"></div>
<div className="h-8 bg-gray-100 rounded"></div>
</div>
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/3 mb-2"></div>
<div className="h-8 bg-gray-100 rounded"></div>
</div>
);
}
@ -49,7 +49,9 @@ export const DebugPaths: React.FC<DebugPathsProps> = ({
if (error) {
return (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 text-sm">Error loading paths: {error}</p>
<p className="text-red-600 text-sm">
Error loading app directory: {error}
</p>
</div>
);
}
@ -60,7 +62,7 @@ export const DebugPaths: React.FC<DebugPathsProps> = ({
description="Main directory where application data, settings, and models are stored"
value={appDirPath}
descriptionMode={descriptionMode}
grouped={true}
grouped={grouped}
copyable={true}
monospace={true}
onCopy={handleCopy}

View file

@ -10,7 +10,8 @@ import { TranslateToEnglish } from "./TranslateToEnglish";
import { LanguageSelector } from "./LanguageSelector";
import { CustomWords } from "./CustomWords";
import { SettingsGroup } from "../ui/SettingsGroup";
import { DebugSettings } from "./debug";
import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold";
import { AppDataDirectory } from "./AppDataDirectory";
import { useSettings } from "../../hooks/useSettings";
export const Settings: React.FC = () => {
@ -61,7 +62,8 @@ export const Settings: React.FC = () => {
{settings?.debug_mode && (
<SettingsGroup title="Debug">
<DebugSettings descriptionMode="tooltip" grouped={true} />
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
<AppDataDirectory descriptionMode="tooltip" grouped={true} />
</SettingsGroup>
)}
</div>

View file

@ -1,30 +0,0 @@
import React from "react";
import { DebugPaths } from "./DebugPaths";
interface DebugSettingsProps {
descriptionMode?: "tooltip" | "inline";
grouped?: boolean;
}
export const DebugSettings: React.FC<DebugSettingsProps> = ({
descriptionMode = "inline",
grouped = false,
}) => {
return (
<div className="space-y-6">
{descriptionMode === "inline" && !grouped && (
<div className="mb-6">
<div className="flex items-center space-x-2 mb-2">
<h2 className="text-xl font-semibold text-gray-900">Debug</h2>
</div>
<p className="text-sm text-gray-600">
Debug information and developer tools. These settings are only
visible when debug mode is enabled.
</p>
</div>
)}
<DebugPaths descriptionMode={descriptionMode} grouped={true} />
</div>
);
};

View file

@ -0,0 +1,31 @@
import React from "react";
import { Slider } from "../../ui/Slider";
import { useSettings } from "../../../hooks/useSettings";
interface WordCorrectionThresholdProps {
descriptionMode?: "tooltip" | "inline";
grouped?: boolean;
}
export const WordCorrectionThreshold: React.FC<
WordCorrectionThresholdProps
> = ({ descriptionMode = "tooltip", grouped = false }) => {
const { settings, updateSetting } = useSettings();
const handleThresholdChange = (value: number) => {
updateSetting("word_correction_threshold", value);
};
return (
<Slider
value={settings?.word_correction_threshold ?? 0.2}
onChange={handleThresholdChange}
min={0.0}
max={1.0}
label="Word Correction Threshold"
description="Controls how aggressively custom words are applied. Lower values mean fewer corrections will be made, higher values mean more corrections. Range: 0 (least aggressive) to 1 (most aggressive)."
descriptionMode={descriptionMode}
grouped={grouped}
/>
);
};

View file

@ -1,2 +1 @@
export { DebugSettings } from "./DebugSettings";
export { DebugPaths } from "./DebugPaths";
export { WordCorrectionThreshold } from "./WordCorrectionThreshold";

View file

@ -8,3 +8,4 @@ export { ShowOverlay } from "./ShowOverlay";
export { HandyShortcut } from "./HandyShortcut";
export { TranslateToEnglish } from "./TranslateToEnglish";
export { CustomWords } from "./CustomWords";
export { AppDataDirectory } from "./AppDataDirectory";

View file

@ -0,0 +1,73 @@
import React from "react";
import { SettingContainer } from "./SettingContainer";
interface SliderProps {
value: number;
onChange: (value: number) => void;
min: number;
max: number;
step?: number;
disabled?: boolean;
label: string;
description: string;
descriptionMode?: "inline" | "tooltip";
grouped?: boolean;
showValue?: boolean;
formatValue?: (value: number) => string;
}
export const Slider: React.FC<SliderProps> = ({
value,
onChange,
min,
max,
step = 0.01,
disabled = false,
label,
description,
descriptionMode = "tooltip",
grouped = false,
showValue = true,
formatValue = (v) => v.toFixed(2),
}) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(parseFloat(e.target.value));
};
return (
<SettingContainer
title={label}
description={description}
descriptionMode={descriptionMode}
grouped={grouped}
layout="horizontal"
>
<div className="w-full">
<div className="flex items-center space-x-1 h-6">
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={handleChange}
disabled={disabled}
className="flex-grow h-2 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-logo-primary disabled:opacity-50 disabled:cursor-not-allowed"
style={{
background: `linear-gradient(to right, #FAA2CA 0%, #FAA2CA ${
((value - min) / (max - min)) * 100
}%, rgba(128, 128, 128, 0.2) ${
((value - min) / (max - min)) * 100
}%, rgba(128, 128, 128, 0.2) 100%)`,
}}
/>
{showValue && (
<span className="text-sm font-medium text-text/90 min-w-10 text-right">
{formatValue(value)}
</span>
)}
</div>
</div>
</SettingContainer>
);
};

View file

@ -1,4 +1,5 @@
export { Dropdown } from "./Dropdown";
export { Slider } from "./Slider";
export { ToggleSwitch } from "./ToggleSwitch";
export { SettingContainer } from "./SettingContainer";
export { SettingsGroup } from "./SettingsGroup";

View file

@ -210,6 +210,11 @@ export const useSettings = (): UseSettingsReturn => {
case "custom_words":
await invoke("update_custom_words", { words: value });
break;
case "word_correction_threshold":
await invoke("change_word_correction_threshold_setting", {
threshold: value,
});
break;
case "bindings":
// Handle bindings separately - they use their own invoke methods
break;
@ -220,7 +225,7 @@ export const useSettings = (): UseSettingsReturn => {
console.warn(`No handler for setting: ${String(key)}`);
}
console.log(`Setting ${String(key)} updated to:`, value);
// console.log(`Setting ${String(key)} updated to:`, value);
} catch (error) {
console.error(`Failed to update setting ${String(key)}:`, error);

View file

@ -35,6 +35,7 @@ export const SettingsSchema = z.object({
overlay_position: OverlayPositionSchema,
debug_mode: z.boolean(),
custom_words: z.array(z.string()).optional().default([]),
word_correction_threshold: z.number().optional().default(0.2),
});
export const BindingResponseSchema = z.object({

View file

@ -41,7 +41,6 @@ const RecordingOverlay: React.FC = () => {
});
smoothedLevelsRef.current = smoothed;
console.log(smoothed.length);
setLevels(smoothed.slice(0, 9));
});