Merge pull request #79 from cjpais/tweak-correct-thresh
new default correction threshold + debug menu
This commit is contained in:
commit
b074aad19a
15 changed files with 161 additions and 54 deletions
|
|
@ -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::change_word_correction_threshold_setting,
|
||||||
shortcut::update_custom_words,
|
shortcut::update_custom_words,
|
||||||
shortcut::suspend_binding,
|
shortcut::suspend_binding,
|
||||||
shortcut::resume_binding,
|
shortcut::resume_binding,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ pub struct TranscriptionManager {
|
||||||
current_model_id: Mutex<Option<String>>,
|
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() {
|
if custom_words.is_empty() {
|
||||||
return text.to_string();
|
return text.to_string();
|
||||||
}
|
}
|
||||||
|
|
@ -83,8 +83,8 @@ fn apply_custom_words(text: &str, custom_words: &[String]) -> String {
|
||||||
levenshtein_score
|
levenshtein_score
|
||||||
};
|
};
|
||||||
|
|
||||||
// Accept if the score is good enough (threshold: 0.4 for good matches)
|
// Accept if the score is good enough (configurable threshold)
|
||||||
if combined_score < 0.4 && combined_score < best_score {
|
if combined_score < threshold && combined_score < best_score {
|
||||||
best_match = Some(&custom_words[i]);
|
best_match = Some(&custom_words[i]);
|
||||||
best_score = combined_score;
|
best_score = combined_score;
|
||||||
}
|
}
|
||||||
|
|
@ -319,7 +319,11 @@ impl TranscriptionManager {
|
||||||
|
|
||||||
// Apply word correction if custom words are configured
|
// Apply word correction if custom words are configured
|
||||||
let corrected_result = if !settings.custom_words.is_empty() {
|
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 {
|
} else {
|
||||||
result
|
result
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ pub struct AppSettings {
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub custom_words: Vec<String>,
|
pub custom_words: Vec<String>,
|
||||||
|
#[serde(default = "default_word_correction_threshold")]
|
||||||
|
pub word_correction_threshold: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_model() -> String {
|
fn default_model() -> String {
|
||||||
|
|
@ -70,13 +72,17 @@ fn default_debug_mode() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_word_correction_threshold() -> f64 {
|
||||||
|
0.2
|
||||||
|
}
|
||||||
|
|
||||||
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
|
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
|
||||||
|
|
||||||
pub fn get_default_settings() -> AppSettings {
|
pub fn get_default_settings() -> AppSettings {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let default_shortcut = "ctrl+space";
|
let default_shortcut = "ctrl+space";
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let default_shortcut = "alt+space";
|
let default_shortcut = "option+space";
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let default_shortcut = "ctrl+space";
|
let default_shortcut = "ctrl+space";
|
||||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
#[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,
|
overlay_position: OverlayPosition::Bottom,
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
custom_words: Vec::new(),
|
custom_words: Vec::new(),
|
||||||
|
word_correction_threshold: default_word_correction_threshold(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,17 @@ pub fn update_custom_words(app: AppHandle, words: Vec<String>) -> Result<(), Str
|
||||||
Ok(())
|
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.
|
/// 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").
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { TextDisplay, SettingsGroup } from "../../ui";
|
import { TextDisplay } from "../ui";
|
||||||
|
|
||||||
interface DebugPathsProps {
|
interface AppDataDirectoryProps {
|
||||||
descriptionMode?: "tooltip" | "inline";
|
descriptionMode?: "tooltip" | "inline";
|
||||||
grouped?: boolean;
|
grouped?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DebugPaths: React.FC<DebugPathsProps> = ({
|
export const AppDataDirectory: React.FC<AppDataDirectoryProps> = ({
|
||||||
descriptionMode = "inline",
|
descriptionMode = "inline",
|
||||||
grouped = false,
|
grouped = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -16,18 +16,20 @@ export const DebugPaths: React.FC<DebugPathsProps> = ({
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPaths = async () => {
|
const loadAppDirectory = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await invoke<string>("get_app_dir_path");
|
const result = await invoke<string>("get_app_dir_path");
|
||||||
setAppDirPath(result);
|
setAppDirPath(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to load paths");
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to load app directory",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadPaths();
|
loadAppDirectory();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCopy = (value: string) => {
|
const handleCopy = (value: string) => {
|
||||||
|
|
@ -37,11 +39,9 @@ export const DebugPaths: React.FC<DebugPathsProps> = ({
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="animate-pulse">
|
||||||
<div className="animate-pulse">
|
<div className="h-4 bg-gray-200 rounded w-1/3 mb-2"></div>
|
||||||
<div className="h-4 bg-gray-200 rounded w-1/3 mb-2"></div>
|
<div className="h-8 bg-gray-100 rounded"></div>
|
||||||
<div className="h-8 bg-gray-100 rounded"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +49,9 @@ export const DebugPaths: React.FC<DebugPathsProps> = ({
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +62,7 @@ export const DebugPaths: React.FC<DebugPathsProps> = ({
|
||||||
description="Main directory where application data, settings, and models are stored"
|
description="Main directory where application data, settings, and models are stored"
|
||||||
value={appDirPath}
|
value={appDirPath}
|
||||||
descriptionMode={descriptionMode}
|
descriptionMode={descriptionMode}
|
||||||
grouped={true}
|
grouped={grouped}
|
||||||
copyable={true}
|
copyable={true}
|
||||||
monospace={true}
|
monospace={true}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
|
|
@ -10,7 +10,8 @@ import { TranslateToEnglish } from "./TranslateToEnglish";
|
||||||
import { LanguageSelector } from "./LanguageSelector";
|
import { LanguageSelector } from "./LanguageSelector";
|
||||||
import { CustomWords } from "./CustomWords";
|
import { CustomWords } from "./CustomWords";
|
||||||
import { SettingsGroup } from "../ui/SettingsGroup";
|
import { SettingsGroup } from "../ui/SettingsGroup";
|
||||||
import { DebugSettings } from "./debug";
|
import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold";
|
||||||
|
import { AppDataDirectory } from "./AppDataDirectory";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
|
||||||
export const Settings: React.FC = () => {
|
export const Settings: React.FC = () => {
|
||||||
|
|
@ -61,7 +62,8 @@ export const Settings: React.FC = () => {
|
||||||
|
|
||||||
{settings?.debug_mode && (
|
{settings?.debug_mode && (
|
||||||
<SettingsGroup title="Debug">
|
<SettingsGroup title="Debug">
|
||||||
<DebugSettings descriptionMode="tooltip" grouped={true} />
|
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
|
||||||
|
<AppDataDirectory descriptionMode="tooltip" grouped={true} />
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
31
src/components/settings/debug/WordCorrectionThreshold.tsx
Normal file
31
src/components/settings/debug/WordCorrectionThreshold.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export { DebugSettings } from "./DebugSettings";
|
export { WordCorrectionThreshold } from "./WordCorrectionThreshold";
|
||||||
export { DebugPaths } from "./DebugPaths";
|
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,4 @@ export { ShowOverlay } from "./ShowOverlay";
|
||||||
export { HandyShortcut } from "./HandyShortcut";
|
export { HandyShortcut } from "./HandyShortcut";
|
||||||
export { TranslateToEnglish } from "./TranslateToEnglish";
|
export { TranslateToEnglish } from "./TranslateToEnglish";
|
||||||
export { CustomWords } from "./CustomWords";
|
export { CustomWords } from "./CustomWords";
|
||||||
|
export { AppDataDirectory } from "./AppDataDirectory";
|
||||||
|
|
|
||||||
73
src/components/ui/Slider.tsx
Normal file
73
src/components/ui/Slider.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export { Dropdown } from "./Dropdown";
|
export { Dropdown } from "./Dropdown";
|
||||||
|
export { Slider } from "./Slider";
|
||||||
export { ToggleSwitch } from "./ToggleSwitch";
|
export { ToggleSwitch } from "./ToggleSwitch";
|
||||||
export { SettingContainer } from "./SettingContainer";
|
export { SettingContainer } from "./SettingContainer";
|
||||||
export { SettingsGroup } from "./SettingsGroup";
|
export { SettingsGroup } from "./SettingsGroup";
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,11 @@ export const useSettings = (): UseSettingsReturn => {
|
||||||
case "custom_words":
|
case "custom_words":
|
||||||
await invoke("update_custom_words", { words: value });
|
await invoke("update_custom_words", { words: value });
|
||||||
break;
|
break;
|
||||||
|
case "word_correction_threshold":
|
||||||
|
await invoke("change_word_correction_threshold_setting", {
|
||||||
|
threshold: 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;
|
||||||
|
|
@ -220,7 +225,7 @@ export const useSettings = (): UseSettingsReturn => {
|
||||||
console.warn(`No handler for setting: ${String(key)}`);
|
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) {
|
} catch (error) {
|
||||||
console.error(`Failed to update setting ${String(key)}:`, error);
|
console.error(`Failed to update setting ${String(key)}:`, error);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export const SettingsSchema = z.object({
|
||||||
overlay_position: OverlayPositionSchema,
|
overlay_position: OverlayPositionSchema,
|
||||||
debug_mode: z.boolean(),
|
debug_mode: z.boolean(),
|
||||||
custom_words: z.array(z.string()).optional().default([]),
|
custom_words: z.array(z.string()).optional().default([]),
|
||||||
|
word_correction_threshold: z.number().optional().default(0.2),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BindingResponseSchema = z.object({
|
export const BindingResponseSchema = z.object({
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ const RecordingOverlay: React.FC = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
smoothedLevelsRef.current = smoothed;
|
smoothedLevelsRef.current = smoothed;
|
||||||
console.log(smoothed.length);
|
|
||||||
setLevels(smoothed.slice(0, 9));
|
setLevels(smoothed.slice(0, 9));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue