Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ serde_json = "1"
rdev = { git = "https://github.com/rustdesk-org/rdev" }
cpal = "0.16.0"
anyhow = "1.0.95"
regex = "1.10.0"
rubato = "0.16.2"
hound = "3.5.1"
log = "0.4.25"
Expand Down
1 change: 1 addition & 0 deletions src-tauri/resources/default_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
}
},
"push_to_talk": true,
"replacements": [],
"selected_language": "auto"
}
2 changes: 2 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ pub fn run() {
shortcut::delete_post_process_prompt,
shortcut::set_post_process_selected_prompt,
shortcut::update_custom_words,
shortcut::update_replacements,
shortcut::update_replacements_enabled,
shortcut::suspend_binding,
shortcut::resume_binding,
shortcut::change_mute_while_recording_setting,
Expand Down
381 changes: 380 additions & 1 deletion src-tauri/src/managers/transcription.rs

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions src-tauri/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ pub enum RecordingRetentionPeriod {
Months3,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
#[serde(rename_all = "snake_case")]
pub enum CapitalizationRule {
None,
ForceUppercase,
ForceLowercase,
}

impl Default for ModelUnloadTimeout {
fn default() -> Self {
ModelUnloadTimeout::Never
Expand All @@ -173,6 +181,12 @@ impl Default for ClipboardHandling {
}
}

impl Default for CapitalizationRule {
fn default() -> Self {
CapitalizationRule::None
}
}

impl ModelUnloadTimeout {
pub fn to_minutes(self) -> Option<u64> {
match self {
Expand Down Expand Up @@ -262,6 +276,10 @@ pub struct AppSettings {
#[serde(default)]
pub custom_words: Vec<String>,
#[serde(default)]
pub replacements: Vec<Replacement>,
#[serde(default = "default_true")]
pub replacements_enabled: bool,
#[serde(default)]
pub model_unload_timeout: ModelUnloadTimeout,
#[serde(default = "default_word_correction_threshold")]
pub word_correction_threshold: f64,
Expand Down Expand Up @@ -295,6 +313,30 @@ pub struct AppSettings {
pub app_language: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, Type)]
pub struct Replacement {
pub search: String,
pub replace: String,
#[serde(default)]
pub is_regex: bool,
#[serde(default)]
pub trim_punctuation_before: bool,
#[serde(default)]
pub trim_punctuation_after: bool,
#[serde(default)]
pub trim_spaces_before: bool,
#[serde(default)]
pub trim_spaces_after: bool,
#[serde(default)]
pub capitalization_rule: CapitalizationRule,
#[serde(default = "default_true")]
pub enabled: bool,
}

fn default_true() -> bool {
true
}

fn default_model() -> String {
"".to_string()
}
Expand Down Expand Up @@ -545,6 +587,8 @@ pub fn get_default_settings() -> AppSettings {
debug_mode: false,
log_level: default_log_level(),
custom_words: Vec::new(),
replacements: Vec::new(),
replacements_enabled: true,
model_unload_timeout: ModelUnloadTimeout::Never,
word_correction_threshold: default_word_correction_threshold(),
history_limit: default_history_limit(),
Expand Down
18 changes: 18 additions & 0 deletions src-tauri/src/shortcut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,3 +929,21 @@ pub fn unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<

Ok(())
}

#[tauri::command]
#[specta::specta]
pub fn update_replacements_enabled(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app);
settings.replacements_enabled = enabled;
settings::write_settings(&app, settings);
Ok(())
}

#[tauri::command]
#[specta::specta]
pub fn update_replacements(app: AppHandle, replacements: Vec<settings::Replacement>) -> Result<(), String> {
let mut settings = settings::get_settings(&app);
settings.replacements = replacements;
settings::write_settings(&app, settings);
Ok(())
}
26 changes: 21 additions & 5 deletions src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,22 @@ async updateCustomWords(words: string[]) : Promise<Result<null, string>> {
else return { status: "error", error: e as any };
}
},
async updateReplacements(replacements: Replacement[]) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("update_replacements", { replacements }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async updateReplacementsEnabled(enabled: boolean) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("update_replacements_enabled", { enabled }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
/**
* Temporarily unregister a binding while the user is editing it in the UI.
* This avoids firing the action while keys are being recorded.
Expand Down Expand Up @@ -596,10 +612,8 @@ async updateRecordingRetentionPeriod(period: string) : Promise<Result<null, stri
}
},
/**
* Checks if the Mac is a laptop by detecting battery presence
*
* This uses pmset to check for battery information.
* Returns true if a battery is detected (laptop), false otherwise (desktop)
* Stub implementation for non-macOS platforms
* Always returns false since laptop detection is macOS-specific
*/
async isLaptop() : Promise<Result<boolean, string>> {
try {
Expand All @@ -621,9 +635,10 @@ async isLaptop() : Promise<Result<boolean, string>> {

/** user-defined types **/

export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string }
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; replacements?: Replacement[]; replacements_enabled?: boolean; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string }
export type AudioDevice = { index: string; name: string; is_default: boolean }
export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null }
export type CapitalizationRule = "none" | "force_uppercase" | "force_lowercase"
export type ClipboardHandling = "dont_modify" | "copy_to_clipboard"
export type CustomSounds = { start: boolean; stop: boolean }
export type EngineType = "Whisper" | "Parakeet"
Expand All @@ -637,6 +652,7 @@ export type OverlayPosition = "none" | "top" | "bottom"
export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_shift_v"
export type PostProcessProvider = { id: string; label: string; base_url: string; allow_base_url_edit?: boolean; models_endpoint?: string | null }
export type RecordingRetentionPeriod = "never" | "preserve_limit" | "days_3" | "weeks_2" | "months_3"
export type Replacement = { search: string; replace: string; is_regex?: boolean; trim_punctuation_before?: boolean; trim_punctuation_after?: boolean; trim_spaces_before?: boolean; trim_spaces_after?: boolean; capitalization_rule?: CapitalizationRule; enabled?: boolean }
export type ShortcutBinding = { id: string; name: string; description: string; default_binding: string; current_binding: string }
export type SoundTheme = "marimba" | "pop" | "custom"

Expand Down
9 changes: 8 additions & 1 deletion src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Cog, FlaskConical, History, Info, Sparkles } from "lucide-react";
import { Cog, FlaskConical, History, Info, Sparkles, Replace } from "lucide-react";
import HandyTextLogo from "./icons/HandyTextLogo";
import HandyHand from "./icons/HandyHand";
import { useSettings } from "../hooks/useSettings";
Expand All @@ -11,6 +11,7 @@ import {
DebugSettings,
AboutSettings,
PostProcessingSettings,
Replacements,
} from "./settings";

export type SidebarSection = keyof typeof SECTIONS_CONFIG;
Expand Down Expand Up @@ -43,6 +44,12 @@ export const SECTIONS_CONFIG = {
component: AdvancedSettings,
enabled: () => true,
},
replacements: {
labelKey: "sidebar.replacements",
icon: Replace,
component: Replacements,
enabled: () => true,
},
postprocessing: {
labelKey: "sidebar.postProcessing",
icon: Sparkles,
Expand Down
Loading