Add support for SenseVoice (#745)

* Add support for sense voice.

* format
This commit is contained in:
CJ Pais 2026-02-09 12:15:42 +08:00 committed by GitHub
parent 82d5c759ad
commit 4c9c200709
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 103 additions and 9 deletions

6
src-tauri/Cargo.lock generated
View file

@ -7086,10 +7086,11 @@ dependencies = [
[[package]] [[package]]
name = "transcribe-rs" name = "transcribe-rs"
version = "0.2.2" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a15e55d3bd4f74a474b7ff2c87ab416130223db490102c5ad32c6e2227686b0" checksum = "2039bf80d6497655ecdef8ebfa10583862fad3cc017077e105bc1c18b7502ff8"
dependencies = [ dependencies = [
"base64 0.22.1",
"derive_builder", "derive_builder",
"env_logger", "env_logger",
"hound", "hound",
@ -7098,6 +7099,7 @@ dependencies = [
"once_cell", "once_cell",
"ort", "ort",
"regex", "regex",
"rustfft",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.17", "thiserror 2.0.17",

View file

@ -68,7 +68,7 @@ chrono = "0.4"
rusqlite = { version = "0.37", features = ["bundled"] } rusqlite = { version = "0.37", features = ["bundled"] }
tar = "0.4.44" tar = "0.4.44"
flate2 = "1.0" flate2 = "1.0"
transcribe-rs = { version = "0.2.2", features = ["whisper", "parakeet", "moonshine"] } transcribe-rs = { version = "0.2.3", features = ["whisper", "parakeet", "moonshine", "sense_voice"] }
handy-keys = "0.1.4" handy-keys = "0.1.4"
ferrous-opencc = "0.2.3" ferrous-opencc = "0.2.3"
specta = "=2.0.0-rc.22" specta = "=2.0.0-rc.22"

View file

@ -21,6 +21,7 @@ pub enum EngineType {
Whisper, Whisper,
Parakeet, Parakeet,
Moonshine, Moonshine,
SenseVoice,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[derive(Debug, Clone, Serialize, Deserialize, Type)]
@ -289,6 +290,37 @@ impl ModelManager {
}, },
); );
// SenseVoice supported languages
let sense_voice_languages: Vec<String> =
vec!["zh", "zh-Hans", "zh-Hant", "en", "yue", "ja", "ko"]
.into_iter()
.map(String::from)
.collect();
available_models.insert(
"sense-voice-int8".to_string(),
ModelInfo {
id: "sense-voice-int8".to_string(),
name: "SenseVoice".to_string(),
description: "Very fast. Chinese, English, Japanese, Korean, Cantonese."
.to_string(),
filename: "sense-voice-int8".to_string(),
url: Some("https://blob.handy.computer/sense-voice-int8.tar.gz".to_string()),
size_mb: 160,
is_downloaded: false,
is_downloading: false,
partial_size: 0,
is_directory: true,
engine_type: EngineType::SenseVoice,
accuracy_score: 0.65,
speed_score: 0.95,
supports_translation: false,
is_recommended: false,
supported_languages: sense_voice_languages,
is_custom: false,
},
);
// Auto-discover custom Whisper models (.bin files) in the models directory // Auto-discover custom Whisper models (.bin files) in the models directory
if let Err(e) = Self::discover_custom_whisper_models(&models_dir, &mut available_models) { if let Err(e) = Self::discover_custom_whisper_models(&models_dir, &mut available_models) {
warn!("Failed to discover custom models: {}", e); warn!("Failed to discover custom models: {}", e);

View file

@ -15,6 +15,10 @@ use transcribe_rs::{
parakeet::{ parakeet::{
ParakeetEngine, ParakeetInferenceParams, ParakeetModelParams, TimestampGranularity, ParakeetEngine, ParakeetInferenceParams, ParakeetModelParams, TimestampGranularity,
}, },
sense_voice::{
Language as SenseVoiceLanguage, SenseVoiceEngine, SenseVoiceInferenceParams,
SenseVoiceModelParams,
},
whisper::{WhisperEngine, WhisperInferenceParams}, whisper::{WhisperEngine, WhisperInferenceParams},
}, },
TranscriptionEngine, TranscriptionEngine,
@ -32,6 +36,7 @@ enum LoadedEngine {
Whisper(WhisperEngine), Whisper(WhisperEngine),
Parakeet(ParakeetEngine), Parakeet(ParakeetEngine),
Moonshine(MoonshineEngine), Moonshine(MoonshineEngine),
SenseVoice(SenseVoiceEngine),
} }
#[derive(Clone)] #[derive(Clone)]
@ -145,6 +150,7 @@ impl TranscriptionManager {
LoadedEngine::Whisper(ref mut e) => e.unload_model(), LoadedEngine::Whisper(ref mut e) => e.unload_model(),
LoadedEngine::Parakeet(ref mut e) => e.unload_model(), LoadedEngine::Parakeet(ref mut e) => e.unload_model(),
LoadedEngine::Moonshine(ref mut e) => e.unload_model(), LoadedEngine::Moonshine(ref mut e) => e.unload_model(),
LoadedEngine::SenseVoice(ref mut e) => e.unload_model(),
} }
} }
*engine = None; // Drop the engine to free memory *engine = None; // Drop the engine to free memory
@ -284,6 +290,26 @@ impl TranscriptionManager {
})?; })?;
LoadedEngine::Moonshine(engine) LoadedEngine::Moonshine(engine)
} }
EngineType::SenseVoice => {
let mut engine = SenseVoiceEngine::new();
engine
.load_model_with_params(&model_path, SenseVoiceModelParams::int8())
.map_err(|e| {
let error_msg =
format!("Failed to load SenseVoice model {}: {}", model_id, e);
let _ = self.app_handle.emit(
"model-state-changed",
ModelStateEvent {
event_type: "loading_failed".to_string(),
model_id: Some(model_id.to_string()),
model_name: Some(model_info.name.clone()),
error: Some(error_msg.clone()),
},
);
anyhow::anyhow!(error_msg)
})?;
LoadedEngine::SenseVoice(engine)
}
}; };
// Update the current engine and model ID // Update the current engine and model ID
@ -426,6 +452,23 @@ impl TranscriptionManager {
LoadedEngine::Moonshine(moonshine_engine) => moonshine_engine LoadedEngine::Moonshine(moonshine_engine) => moonshine_engine
.transcribe_samples(audio, None) .transcribe_samples(audio, None)
.map_err(|e| anyhow::anyhow!("Moonshine transcription failed: {}", e))?, .map_err(|e| anyhow::anyhow!("Moonshine transcription failed: {}", e))?,
LoadedEngine::SenseVoice(sense_voice_engine) => {
let language = match settings.selected_language.as_str() {
"zh" | "zh-Hans" | "zh-Hant" => SenseVoiceLanguage::Chinese,
"en" => SenseVoiceLanguage::English,
"ja" => SenseVoiceLanguage::Japanese,
"ko" => SenseVoiceLanguage::Korean,
"yue" => SenseVoiceLanguage::Cantonese,
_ => SenseVoiceLanguage::Auto,
};
let params = SenseVoiceInferenceParams {
language,
use_itn: true,
};
sense_voice_engine
.transcribe_samples(audio, Some(params))
.map_err(|e| anyhow::anyhow!("SenseVoice transcription failed: {}", e))?
}
} }
}; };

View file

@ -700,7 +700,7 @@ export type AudioDevice = { index: string; name: string; is_default: boolean }
export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null } export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null }
export type ClipboardHandling = "dont_modify" | "copy_to_clipboard" export type ClipboardHandling = "dont_modify" | "copy_to_clipboard"
export type CustomSounds = { start: boolean; stop: boolean } export type CustomSounds = { start: boolean; stop: boolean }
export type EngineType = "Whisper" | "Parakeet" | "Moonshine" export type EngineType = "Whisper" | "Parakeet" | "Moonshine" | "SenseVoice"
export type HistoryEntry = { id: number; file_name: string; timestamp: number; saved: boolean; title: string; transcription_text: string; post_processed_text: string | null; post_process_prompt: string | null } export type HistoryEntry = { id: number; file_name: string; timestamp: number; saved: boolean; title: string; transcription_text: string; post_processed_text: string | null; post_process_prompt: string | null }
/** /**
* Result of changing keyboard implementation * Result of changing keyboard implementation

View file

@ -8,11 +8,13 @@ import { LANGUAGES } from "../../lib/constants/languages";
interface LanguageSelectorProps { interface LanguageSelectorProps {
descriptionMode?: "inline" | "tooltip"; descriptionMode?: "inline" | "tooltip";
grouped?: boolean; grouped?: boolean;
supportedLanguages?: string[];
} }
export const LanguageSelector: React.FC<LanguageSelectorProps> = ({ export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
descriptionMode = "tooltip", descriptionMode = "tooltip",
grouped = false, grouped = false,
supportedLanguages,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { getSetting, updateSetting, resetSetting, isUpdating } = useSettings(); const { getSetting, updateSetting, resetSetting, isUpdating } = useSettings();
@ -46,12 +48,21 @@ export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
} }
}, [isOpen]); }, [isOpen]);
const availableLanguages = useMemo(() => {
if (!supportedLanguages || supportedLanguages.length === 0)
return LANGUAGES;
return LANGUAGES.filter(
(lang) =>
lang.value === "auto" || supportedLanguages.includes(lang.value),
);
}, [supportedLanguages]);
const filteredLanguages = useMemo( const filteredLanguages = useMemo(
() => () =>
LANGUAGES.filter((language) => availableLanguages.filter((language) =>
language.label.toLowerCase().includes(searchQuery.toLowerCase()), language.label.toLowerCase().includes(searchQuery.toLowerCase()),
), ),
[searchQuery], [searchQuery, availableLanguages],
); );
const selectedLanguageName = const selectedLanguageName =

View file

@ -12,8 +12,9 @@ export const ModelSettingsCard: React.FC = () => {
const currentModelInfo = models.find((m: ModelInfo) => m.id === currentModel); const currentModelInfo = models.find((m: ModelInfo) => m.id === currentModel);
// Only Whisper models support manual language selection const supportsLanguageSelection =
const supportsLanguageSelection = currentModelInfo?.engine_type === "Whisper"; currentModelInfo?.engine_type === "Whisper" ||
currentModelInfo?.engine_type === "SenseVoice";
const supportsTranslation = currentModelInfo?.supports_translation ?? false; const supportsTranslation = currentModelInfo?.supports_translation ?? false;
const hasAnySettings = supportsLanguageSelection || supportsTranslation; const hasAnySettings = supportsLanguageSelection || supportsTranslation;
@ -29,7 +30,11 @@ export const ModelSettingsCard: React.FC = () => {
})} })}
> >
{supportsLanguageSelection && ( {supportsLanguageSelection && (
<LanguageSelector descriptionMode="tooltip" grouped={true} /> <LanguageSelector
descriptionMode="tooltip"
grouped={true}
supportedLanguages={currentModelInfo.supported_languages}
/>
)} )}
{supportsTranslation && ( {supportsTranslation && (
<TranslateToEnglish descriptionMode="tooltip" grouped={true} /> <TranslateToEnglish descriptionMode="tooltip" grouped={true} />

View file

@ -8,6 +8,7 @@ export const LANGUAGES: Language[] = [
{ value: "en", label: "English" }, { value: "en", label: "English" },
{ value: "zh-Hans", label: "Simplified Chinese" }, { value: "zh-Hans", label: "Simplified Chinese" },
{ value: "zh-Hant", label: "Traditional Chinese" }, { value: "zh-Hant", label: "Traditional Chinese" },
{ value: "yue", label: "Cantonese" },
{ value: "de", label: "German" }, { value: "de", label: "German" },
{ value: "es", label: "Spanish" }, { value: "es", label: "Spanish" },
{ value: "ru", label: "Russian" }, { value: "ru", label: "Russian" },