From 9a3d5ffcc6dd37fa948adc14d22ae015fffb4846 Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 30 Mar 2026 02:51:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20Cantonese=20(=E7=B2=B5=E8=AA=9E)?= =?UTF-8?q?=20language=20support=20via=20Whisper=20zh-yue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'zh-yue' to _VALID_LANGUAGES and available_languages dict - Add Cantonese option to UI language dropdown (displays as '๐Ÿ‡ญ๐Ÿ‡ฐ ็ฒต่ชž (Cantonese)') - Pass language='zh-yue' to faster-whisper for Cantonese transcription - Add Cantonese initial_prompt for better traditional Chinese character output - Add Cantonese filler word removal (ๅ—ฏ, ๅ•Š, ๅ‘€, etc.) - Add zh-yue to all _LANG_FLAGS maps with ๐Ÿ‡ญ๐Ÿ‡ฐ flag - Whisper large-v3 or medium model recommended for best Cantonese results --- voxflow/app.py | 6 +++--- voxflow/config.py | 5 +++-- voxflow/post_processor.py | 16 ++++++++++++++++ voxflow/transcriber.py | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/voxflow/app.py b/voxflow/app.py index 033e059..e51825a 100644 --- a/voxflow/app.py +++ b/voxflow/app.py @@ -479,7 +479,7 @@ def _build_quick_controls(self): text_color=C["txt3"]).pack(anchor="w") self.lang_var = ctk.StringVar(value=self.config.language) ctk.CTkOptionMenu( - lf, values=["auto", "pl", "en", "de", "fr", "es", "it", "uk"], + lf, values=["auto", "pl", "en", "de", "fr", "es", "it", "uk", "zh-yue"], variable=self.lang_var, font=ctk.CTkFont(size=11), fg_color=C["bg_input"], @@ -987,7 +987,7 @@ def _on_done(self, result: dict): _LANG_FLAGS = { "pl": "๐Ÿ‡ต๐Ÿ‡ฑ", "en": "๐Ÿ‡ฌ๐Ÿ‡ง", "de": "๐Ÿ‡ฉ๐Ÿ‡ช", "fr": "๐Ÿ‡ซ๐Ÿ‡ท", "es": "๐Ÿ‡ช๐Ÿ‡ธ", "it": "๐Ÿ‡ฎ๐Ÿ‡น", - "uk": "๐Ÿ‡บ๐Ÿ‡ฆ", + "uk": "๐Ÿ‡บ๐Ÿ‡ฆ", "zh-yue": "๐Ÿ‡ญ๐Ÿ‡ฐ", } flag = _LANG_FLAGS.get(lang, "๐ŸŒ") dur = result.get("duration", 0) @@ -1051,7 +1051,7 @@ def _refresh_history(self): _LANG_FLAGS = { "pl": "๐Ÿ‡ต๐Ÿ‡ฑ", "en": "๐Ÿ‡ฌ๐Ÿ‡ง", "de": "๐Ÿ‡ฉ๐Ÿ‡ช", "fr": "๐Ÿ‡ซ๐Ÿ‡ท", "es": "๐Ÿ‡ช๐Ÿ‡ธ", "it": "๐Ÿ‡ฎ๐Ÿ‡น", - "uk": "๐Ÿ‡บ๐Ÿ‡ฆ", + "uk": "๐Ÿ‡บ๐Ÿ‡ฆ", "zh-yue": "๐Ÿ‡ญ๐Ÿ‡ฐ", } flag = _LANG_FLAGS.get(e["language"], "๐ŸŒ") preview = e["text"][:44] + ("โ€ฆ" if len(e["text"]) > 44 else "") diff --git a/voxflow/config.py b/voxflow/config.py index 587c34b..83bcab4 100644 --- a/voxflow/config.py +++ b/voxflow/config.py @@ -18,7 +18,7 @@ def get_config_dir() -> Path: # Valid value ranges for security validation _VALID_MODELS = {"tiny", "base", "small", "medium", "large-v3"} -_VALID_LANGUAGES = {"auto", "pl", "en", "de", "fr", "es", "it", "uk"} +_VALID_LANGUAGES = {"auto", "pl", "en", "de", "fr", "es", "it", "uk", "zh-yue"} # Hotkey validation: allow any non-empty string (hotkey picker can produce # arbitrary key names). We only enforce it's a safe non-empty string. _VALID_TYPING_METHODS = {"clipboard", "keyboard"} @@ -100,7 +100,7 @@ class VoxFlowConfig: """Application configuration.""" # Model settings model_size: str = "small" - language: str = "auto" # "auto", "pl", "en", "de", "fr", "es", "it", "uk" + language: str = "auto" # "auto", "pl", "en", "de", "fr", "es", "it", "uk", "zh-yue" device: str = "cpu" # "cpu" or "cuda" compute_type: str = "int8" # "int8" for CPU, "float16" for GPU @@ -181,6 +181,7 @@ def available_languages(self) -> dict: "es": "๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol", "it": "๐Ÿ‡ฎ๐Ÿ‡น Italiano", "uk": "๐Ÿ‡บ๐Ÿ‡ฆ ะฃะบั€ะฐั—ะฝััŒะบะฐ", + "zh-yue": "๐Ÿ‡ญ๐Ÿ‡ฐ ็ฒต่ชž (Cantonese)", } @property diff --git a/voxflow/post_processor.py b/voxflow/post_processor.py index 86f81d3..c96a985 100644 --- a/voxflow/post_processor.py +++ b/voxflow/post_processor.py @@ -51,6 +51,11 @@ "รคh", "รคhm", "hm", "รถh", "รถhm", "hmm", "ehm", } +# Common Cantonese filler words/sounds that Whisper sometimes outputs +CANTONESE_FILLERS = { + "ๅ—ฏ", "ๅ•Š", "ๅ‘€", "ๅ–", "ๅ–Ž", "ๅ˜…", "ๅ–‡", "ๅ’ง", "ๅ’ฏ", +} + # Common German Whisper corrections GERMAN_CORRECTIONS = { # Common split compounds @@ -107,6 +112,8 @@ def post_process( result = _remove_fillers(result, POLISH_FILLERS) if language == "de": result = _remove_fillers(result, GERMAN_FILLERS) + if language == "zh-yue": + result = _remove_fillers(result, CANTONESE_FILLERS) # 4. Apply language-specific corrections if apply_corrections: @@ -262,6 +269,13 @@ def _final_cleanup(text: str) -> str: "Polski tekst zawiera poprawne znaki diakrytyczne: ฤ…, ฤ‡, ฤ™, ล‚, ล„, รณ, ล›, ลบ, ลผ." ) +CANTONESE_INITIAL_PROMPT = ( + "Transcription of a recording in Cantonese (็ฒต่ชž). " + "The text is in traditional Cantonese Chinese. " + "Use traditional Chinese characters (็น้ซ”ไธญๆ–‡). " + "Common Cantonese words: ไฟ‚, ๅ””, ๅนพ, ๅ’, ๅ–Ž, ๅ‘€, ๅ–, ๅ–Ž, ๅ˜…, ๅ–‡, ๅ’ง, ๅ’ฏ, ๅ™ถ." +) + def get_initial_prompt(language: str) -> str: """Get the initial prompt for Whisper based on language. @@ -275,5 +289,7 @@ def get_initial_prompt(language: str) -> str: return ENGLISH_INITIAL_PROMPT elif language == "de": return GERMAN_INITIAL_PROMPT + elif language == "zh-yue": + return CANTONESE_INITIAL_PROMPT else: return AUTO_INITIAL_PROMPT diff --git a/voxflow/transcriber.py b/voxflow/transcriber.py index 12658e8..c4b3596 100644 --- a/voxflow/transcriber.py +++ b/voxflow/transcriber.py @@ -190,7 +190,7 @@ def transcribe( _LANG_FLAGS = { "pl": "๐Ÿ‡ต๐Ÿ‡ฑ", "en": "๐Ÿ‡ฌ๐Ÿ‡ง", "de": "๐Ÿ‡ฉ๐Ÿ‡ช", "fr": "๐Ÿ‡ซ๐Ÿ‡ท", "es": "๐Ÿ‡ช๐Ÿ‡ธ", "it": "๐Ÿ‡ฎ๐Ÿ‡น", - "uk": "๐Ÿ‡บ๐Ÿ‡ฆ", + "uk": "๐Ÿ‡บ๐Ÿ‡ฆ", "zh-yue": "๐Ÿ‡ญ๐Ÿ‡ฐ", } flag = _LANG_FLAGS.get(info.language, "๐ŸŒ") prob = info.language_probability * 100