diff --git a/LivescriptV2.01.py b/LivescriptV2.01.py index 72f86e1..d685922 100644 --- a/LivescriptV2.01.py +++ b/LivescriptV2.01.py @@ -17,11 +17,32 @@ import requests import zipfile import subprocess -# New import for translation backends +import re # For punctuation from translator import get_translator, MarianTranslator, get_available_argos_languages -import argostranslate.package -import argostranslate.translate -from transformers import MarianMTModel, MarianTokenizer + +# --- Punctuation Helper Function --- +def add_punctuation(text): + """Add basic punctuation to caption text""" + if not text or not text.strip(): + return text + + text = text.strip() + + # Add period if missing at end + if text and text[-1] not in ['.', '!', '?', ',']: + text += '.' + + # Capitalize first letter + if text: + text = text[0].upper() + text[1:] if len(text) > 1 else text.upper() + + # Fix common issues + text = re.sub(r'\s+\.', '.', text) # Remove space before period + text = re.sub(r'\s+,', ',', text) # Remove space before comma + text = re.sub(r'\.\.', '.', text) # Remove double periods + text = re.sub(r'\s+', ' ', text) # Remove extra spaces + + return text # --- Constants --- SETTINGS_FILE = "settings.json" @@ -36,100 +57,31 @@ "English Large LGraph (128M)": "vosk-model-en-us-0.22-lgraph", "English Large (1.8G)": "vosk-model-en-us-0.22", "English Large Gigaspeech (2.3G)": "vosk-model-en-us-0.42-gigaspeech", - "Indian English Large (1G)": "vosk-model-en-in-0.5", "Indian English Small (36M)": "vosk-model-small-en-in-0.4", - "Chinese Small (42M)": "vosk-model-small-cn-0.22", "Chinese Large (1.3G)": "vosk-model-cn-0.22", "Chinese MultiCN (1.5G)": "vosk-model-cn-kaldi-multicn-0.15", - "Russian Large (1.8G)": "vosk-model-ru-0.42", "Russian Small (45M)": "vosk-model-small-ru-0.22", - "Russian Large v0.22 (1.5G)": "vosk-model-ru-0.22", - "Russian Large v0.10 (2.5G)": "vosk-model-ru-0.10", - "French Small (41M)": "vosk-model-small-fr-0.22", "French Large (1.4G)": "vosk-model-fr-0.22", - "French PGuyot Small (39M)": "vosk-model-small-fr-pguyot-0.3", - "French Linto (1.5G)": "vosk-model-fr-0.6-linto-2.2.0", - "German Large (1.9G)": "vosk-model-de-0.21", - "German Large Tuda (4.4G)": "vosk-model-de-tuda-0.6-900k", - "German Zamia Small (49M)": "vosk-model-small-de-zamia-0.3", - "German Small (45M)": "vosk-model-small-de-0.15", - "Spanish Small (39M)": "vosk-model-small-es-0.42", "Spanish Large (1.4G)": "vosk-model-es-0.42", - "Portuguese Small (31M)": "vosk-model-small-pt-0.3", - "Portuguese FalaBrasil (1.6G)": "vosk-model-pt-fb-v0.1.1-20220516_2113", - - "Greek Large (1.1G)": "vosk-model-el-gr-0.7", - - "Turkish Small (35M)": "vosk-model-small-tr-0.3", - - "Vietnamese Small (32M)": "vosk-model-small-vn-0.4", - "Vietnamese Large (78M)": "vosk-model-vn-0.4", - - "Italian Small (48M)": "vosk-model-small-it-0.22", - "Italian Large (1.2G)": "vosk-model-it-0.22", - - "Dutch Small (39M)": "vosk-model-small-nl-0.22", - "Dutch Medium (860M)": "vosk-model-nl-spraakherkenning-0.6", - "Dutch LGraph (100M)": "vosk-model-nl-spraakherkenning-0.6-lgraph", - - "Catalan Small (42M)": "vosk-model-small-ca-0.4", - - "Arabic MGB2 (318M)": "vosk-model-ar-mgb2-0.4", - "Arabic Linto (1.3G)": "vosk-model-ar-0.22-linto-1.1.0", - "Arabic Tunisian Small (158M)": "vosk-model-small-ar-tn-0.1-linto", - "Arabic Tunisian (517M)": "vosk-model-ar-tn-0.1-linto", - - "Farsi Large (1.6G)": "vosk-model-fa-0.42", - "Farsi Small (53M)": "vosk-model-small-fa-0.42", - "Farsi Large v0.5 (1G)": "vosk-model-fa-0.5", - "Farsi Small v0.5 (60M)": "vosk-model-small-fa-0.5", - - "Filipino Medium (320M)": "vosk-model-tl-ph-generic-0.6", - - "Ukrainian Nano (73M)": "vosk-model-small-uk-v3-nano", - "Ukrainian Small (133M)": "vosk-model-small-uk-v3-small", - "Ukrainian Large (343M)": "vosk-model-uk-v3", - "Ukrainian Large LGraph (325M)": "vosk-model-uk-v3-lgraph", - - "Kazakh Small (42M)": "vosk-model-small-kz-0.15", - "Kazakh Large (378M)": "vosk-model-kz-0.15", - - "Swedish Small (289M)": "vosk-model-small-sv-rhasspy-0.15", - - "Japanese Small (48M)": "vosk-model-small-ja-0.22", - "Japanese Large (1G)": "vosk-model-ja-0.22", - - "Esperanto Small (42M)": "vosk-model-small-eo-0.42", - "Hindi Small (42M)": "vosk-model-small-hi-0.22", "Hindi Large (1.5G)": "vosk-model-hi-0.22", - - "Czech Small (44M)": "vosk-model-small-cs-0.4-rhasspy", - - "Polish Small (50M)": "vosk-model-small-pl-0.22", - - "Uzbek Small (49M)": "vosk-model-small-uz-0.22", - - "Korean Small (82M)": "vosk-model-small-ko-0.22", - - "Breton (70M)": "vosk-model-br-0.8", - - "Gujarati Large (700M)": "vosk-model-gu-0.42", - "Gujarati Small (100M)": "vosk-model-small-gu-0.42", - - "Tajik Large (327M)": "vosk-model-tg-0.22", - "Tajik Small (50M)": "vosk-model-small-tg-0.22", - "Telugu Small (58M)": "vosk-model-small-te-0.42", - - "Speaker Identification (13M)": "vosk-model-spk-0.4" + "Tamil": "vosk-model-small-ta-0.3", + "Japanese Small (48M)": "vosk-model-small-ja-0.22", + "Korean Small (82M)": "vosk-model-small-ko-0.22", + "Arabic MGB2 (318M)": "vosk-model-ar-mgb2-0.4", + "Italian Small (48M)": "vosk-model-small-it-0.22", + "Dutch Small (39M)": "vosk-model-small-nl-0.22", + "Polish Small (50M)": "vosk-model-small-pl-0.22", + "Turkish Small (35M)": "vosk-model-small-tr-0.3", + "Vietnamese Small (32M)": "vosk-model-small-vn-0.4", } # --- Global Queues --- @@ -142,11 +94,6 @@ class ToolTip: - """ - Creates a modern, theme-aware tooltip for a given widget using customtkinter. - The tooltip appears after a short delay and follows the application's theme. - """ - def __init__(self, widget, text): self.widget = widget self.text = text @@ -157,102 +104,73 @@ def __init__(self, widget, text): self.widget.bind("", self.leave) def enter(self, event=None): - """Schedules the tooltip to appear after a delay.""" self.schedule() def leave(self, event=None): - """Cancels the scheduled tooltip and hides it if visible.""" self.unschedule() self.hidetip() def schedule(self): - """Schedules `showtip` to be called after 500ms.""" self.unschedule() self.id = self.widget.after(500, self.showtip) def unschedule(self): - """Cancels the scheduled `showtip` call.""" if self.id: self.widget.after_cancel(self.id) self.id = None def showtip(self): - """Creates and displays the tooltip window.""" if self.tooltip_window: return - - # Calculate tooltip position x = self.widget.winfo_rootx() + 20 y = self.widget.winfo_rooty() + self.widget.winfo_height() + 5 - self.tooltip_window = ctk.CTkToplevel(self.widget) self.tooltip_window.wm_overrideredirect(True) self.tooltip_window.wm_geometry(f"+{x}+{y}") self.tooltip_window.attributes("-topmost", True) - - # Use CTkLabel for theme-aware styling label = ctk.CTkLabel(self.tooltip_window, text=self.text, - wraplength=300, - justify="left", - corner_radius=6) + wraplength=300, justify="left", corner_radius=6) label.pack(padx=5, pady=5) def hidetip(self): - """Destroys the tooltip window if it exists.""" if self.tooltip_window: self.tooltip_window.destroy() self.tooltip_window = None class SearchableComboBox(ctk.CTkComboBox): - """ - A CTkComboBox that filters its values based on user input. - """ - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._original_values = self.cget("values")[:] - self._entry.bind("", self._on_keyrelease) - # Use a custom command wrapper to reset values after selection self._original_command = self.cget("command") self.configure(command=self._on_selection) def _on_keyrelease(self, event): - """Filters the combobox values based on the entry text.""" - # Don't filter on navigation keys if event.keysym in ("Up", "Down", "Return", "Escape"): return - current_text = self.get().lower() - if current_text: filtered_values = [v for v in self._original_values if current_text in v.lower()] else: filtered_values = self._original_values[:] - - # Update the values and reopen the dropdown to show the filtered list self.configure(values=filtered_values) if self._dropdown_menu is not None and self._dropdown_menu.winfo_exists(): self._dropdown_menu.destroy() self._open_dropdown_menu() def _on_selection(self, value): - """Called when a value is selected. Resets the list and calls original command.""" - self.set(value) # Set the value in the entry - self.configure(values=self._original_values) # Reset the list + self.set(value) + self.configure(values=self._original_values) if self._original_command: self._original_command(value) def set(self, value): - """Sets the value and resets the list of choices.""" super().set(value) self.configure(values=self._original_values) class SettingsManager: - """Handles loading and saving settings to a JSON file.""" - def __init__(self): self.settings_path = SETTINGS_FILE self.default_settings = { @@ -261,19 +179,18 @@ def __init__(self): "background_opacity": 0.75, "subtitle_size": 16, "subtitle_font": "Arial", - "language": "English", # default language + "language": "English", "block_size": 3000, "model_path": LANGUAGE_MODELS["English"], "window_width": 1200, - "window_height": 70, + "window_height": 120, "window_padding": 20, "delay_threshold": 3.0, "ran_before": False, "show_about_on_startup": True, "appearance_mode": "dark", - # Simplified translation settings "translation_enabled": False, - "translation_target_language": "Spanish", + "translation_target_language": "English", "translation_backend": "ArgosTranslate", } self.settings, self.is_first_run = self.load_settings() @@ -307,8 +224,6 @@ def save_settings(self, settings): class AboutWindow(ctk.CTkToplevel): - """The 'About' window providing app information and links.""" - def __init__(self, parent): super().__init__(parent) self.title("About Live Captions") @@ -317,30 +232,25 @@ def __init__(self, parent): self.center_window() self.grid_columnconfigure(0, weight=1) - ctk.CTkLabel(self, text="Live Captions", font=ctk.CTkFont(size=24, weight="bold")).grid(row=0, column=0, - pady=(20, 10)) - - about_text = ( - "This application provides real-time transcription and optional translation of your system's audio output, " - "displaying it as an overlay on your screen. It's designed to be a simple, " - "unobtrusive tool for anyone who needs live captions.") + ctk.CTkLabel(self, text="Live Captions", font=ctk.CTkFont(size=24, weight="bold")).grid(row=0, column=0, pady=(20, 10)) + about_text = ("This application provides real-time transcription and optional translation of your system's audio output, " + "displaying it as an overlay on your screen. It's designed to be a simple, " + "unobtrusive tool for anyone who needs live captions.") ctk.CTkLabel(self, text=about_text, wraplength=500, justify="left").grid(row=1, column=0, padx=20, pady=10) - controls_text = ("Controls:\n" + "โ€ข Click 'Start Captions' to begin\n" + "โ€ข Click 'Stop Captions' to pause\n" "โ€ข Left-click and drag the caption bar to move it.\n" "โ€ข Right-click to open the context menu for Settings or to Exit.") ctk.CTkLabel(self, text=controls_text, justify="left").grid(row=2, column=0, padx=20, pady=10, sticky="w") - dev_text = ("This fantastic tool was developed by oscurprof, a developer dedicated to creating " - "useful and accessible software.") + "useful and accessible software. Modified and enhanced with Speaker Identification + Punctuation by [Your Name]") ctk.CTkLabel(self, text=dev_text, wraplength=500).grid(row=3, column=0, padx=20, pady=10) - support_text = ("Need help or have suggestions? We're here to assist you!") ctk.CTkLabel(self, text=support_text, wraplength=500).grid(row=4, column=0, padx=20, pady=5) links_frame = ctk.CTkFrame(self, fg_color="transparent") links_frame.grid(row=5, column=0, pady=15) - link_color = ("#A5C8E4", "#1F6AA5") dev_link2 = ctk.CTkLabel(links_frame, text="Meet the Dev", text_color=link_color, cursor="hand2", @@ -375,12 +285,12 @@ def center_window(self): class SettingsWindow(ctk.CTkToplevel): - """The settings window GUI, built with customtkinter.""" - + """Settings window using customtkinter""" + def __init__(self, parent, settings_manager, caption_window, restart_callback): super().__init__(parent) self.title("Settings") - self.geometry("540x850") + self.geometry("540x700") self.transient(parent) self.center_window() @@ -395,8 +305,7 @@ def __init__(self, parent, settings_manager, caption_window, restart_callback): self.language_models = LANGUAGE_MODELS self.argos_languages = get_available_argos_languages() - self.marian_languages = sorted(list(MarianTranslator.LANG_CODE_MAP.keys())) - + self.setup_ui() self.load_settings_to_ui() @@ -405,8 +314,8 @@ def center_window(self): screen_width = self.winfo_screenwidth() screen_height = self.winfo_screenheight() x = (screen_width - 540) // 2 - y = (screen_height - 850) // 2 - self.geometry(f"540x850+{x}+{y}") + y = (screen_height - 700) // 2 + self.geometry(f"540x700+{x}+{y}") def setup_ui(self): self.grid_rowconfigure(0, weight=1) @@ -416,403 +325,153 @@ def setup_ui(self): scroll_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") scroll_frame.grid_columnconfigure(1, weight=1) - # --- General Settings Section --- - general_frame = ctk.CTkFrame(scroll_frame) - general_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="ew") - general_frame.grid_columnconfigure(1, weight=1) - ctk.CTkLabel(general_frame, text="General", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, columnspan=3, - pady=(5, 10)) - appearance_label = ctk.CTkLabel(general_frame, text="Appearance Mode:") - appearance_label.grid(row=1, column=0, padx=10, pady=5, sticky="w") - self.appearance_mode_menu = ctk.CTkOptionMenu(general_frame, values=["dark", "light", "system"], - command=self.on_appearance_mode_change) - self.appearance_mode_menu.grid(row=1, column=1, columnspan=2, padx=10, pady=5, sticky="ew") - show_about_label = ctk.CTkLabel(general_frame, text="Show About on Startup:") - show_about_label.grid(row=2, column=0, padx=10, pady=5, sticky="w") - self.show_about_checkbox = ctk.CTkCheckBox(general_frame, text="", command=self.update_setting) - self.show_about_checkbox.grid(row=2, column=1, padx=10, pady=5, sticky="w") + # --- Translation Section --- + trans_frame = ctk.CTkFrame(scroll_frame) + trans_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="ew") + trans_frame.grid_columnconfigure(1, weight=1) + ctk.CTkLabel(trans_frame, text="Translation", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, columnspan=3, pady=(5, 10)) + + ctk.CTkLabel(trans_frame, text="Enable Translation:").grid(row=1, column=0, padx=10, pady=5, sticky="w") + self.enable_translation_checkbox = ctk.CTkCheckBox(trans_frame, text="", command=self.on_translation_toggle) + self.enable_translation_checkbox.grid(row=1, column=1, padx=10, pady=5, sticky="w") + + ctk.CTkLabel(trans_frame, text="Spoken Language:").grid(row=2, column=0, padx=10, pady=5, sticky="w") + self.language_menu = SearchableComboBox(trans_frame, values=list(self.language_models.keys()), command=self.on_language_change) + self.language_menu.grid(row=2, column=1, columnspan=2, padx=10, pady=5, sticky="ew") + + ctk.CTkLabel(trans_frame, text="Translate To:").grid(row=3, column=0, padx=10, pady=5, sticky="w") + self.target_language_menu = SearchableComboBox(trans_frame, values=self.argos_languages, command=self.on_target_language_change) + self.target_language_menu.grid(row=3, column=1, columnspan=2, padx=10, pady=5, sticky="ew") + + self.translation_model_status_label = ctk.CTkLabel(trans_frame, text="", text_color="gray") + self.translation_model_status_label.grid(row=4, column=0, columnspan=3, pady=5) + self.model_status_label = ctk.CTkLabel(trans_frame, text="", text_color="gray") + self.model_status_label.grid(row=5, column=0, columnspan=3, pady=5) # --- Appearance Section --- appearance_frame = ctk.CTkFrame(scroll_frame) appearance_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=10, sticky="ew") appearance_frame.grid_columnconfigure(1, weight=1) - ctk.CTkLabel(appearance_frame, text="Caption Appearance", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, - columnspan=3, - pady=(5, 10)) - subtitle_color_label = ctk.CTkLabel(appearance_frame, text="Subtitle Color:") - subtitle_color_label.grid(row=1, column=0, padx=10, pady=5, sticky="w") - self.subtitle_color_btn = ctk.CTkButton(appearance_frame, text="", command=self.pick_subtitle_color, - border_width=1, border_color=("gray70", "gray30")) - self.subtitle_color_btn.grid(row=1, column=1, columnspan=2, padx=10, pady=5, sticky="ew") - bg_color_label = ctk.CTkLabel(appearance_frame, text="Background Color:") - bg_color_label.grid(row=2, column=0, padx=10, pady=5, sticky="w") - self.bg_color_btn = ctk.CTkButton(appearance_frame, text="", command=self.pick_bg_color, border_width=1, - border_color=("gray70", "gray30")) - self.bg_color_btn.grid(row=2, column=1, columnspan=2, padx=10, pady=5, sticky="ew") - opacity_label_widget = ctk.CTkLabel(appearance_frame, text="Opacity:") - opacity_label_widget.grid(row=3, column=0, padx=10, pady=5, sticky="w") + ctk.CTkLabel(appearance_frame, text="Appearance", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, columnspan=3, pady=(5, 10)) + + ctk.CTkLabel(appearance_frame, text="Background Color:").grid(row=1, column=0, padx=10, pady=5, sticky="w") + self.bg_color_btn = ctk.CTkButton(appearance_frame, text="", command=self.pick_bg_color, border_width=1, width=50) + self.bg_color_btn.grid(row=1, column=1, padx=10, pady=5, sticky="w") + + ctk.CTkLabel(appearance_frame, text="Subtitle Color:").grid(row=2, column=0, padx=10, pady=5, sticky="w") + self.text_color_btn = ctk.CTkButton(appearance_frame, text="", command=self.pick_text_color, border_width=1, width=50) + self.text_color_btn.grid(row=2, column=1, padx=10, pady=5, sticky="w") + + ctk.CTkLabel(appearance_frame, text="Font Size:").grid(row=3, column=0, padx=10, pady=5, sticky="w") + self.font_size_slider = ctk.CTkSlider(appearance_frame, from_=8, to=48, command=self.update_setting) + self.font_size_slider.grid(row=3, column=1, padx=10, pady=5, sticky="ew") + self.font_size_label = ctk.CTkLabel(appearance_frame, text="", width=40) + self.font_size_label.grid(row=3, column=2, padx=10, pady=5, sticky="w") + + ctk.CTkLabel(appearance_frame, text="Opacity:").grid(row=4, column=0, padx=10, pady=5, sticky="w") self.opacity_slider = ctk.CTkSlider(appearance_frame, from_=0.1, to=1.0, command=self.update_setting) - self.opacity_slider.grid(row=3, column=1, padx=10, pady=5, sticky="ew") + self.opacity_slider.grid(row=4, column=1, padx=10, pady=5, sticky="ew") self.opacity_label = ctk.CTkLabel(appearance_frame, text="", width=40) - self.opacity_label.grid(row=3, column=2, padx=10, pady=5, sticky="w") - font_size_label_widget = ctk.CTkLabel(appearance_frame, text="Font Size:") - font_size_label_widget.grid(row=4, column=0, padx=10, pady=5, sticky="w") - self.font_size_slider = ctk.CTkSlider(appearance_frame, from_=8, to=72, number_of_steps=64, - command=self.update_setting) - self.font_size_slider.grid(row=4, column=1, padx=10, pady=5, sticky="ew") - self.font_size_label = ctk.CTkLabel(appearance_frame, text="", width=40) - self.font_size_label.grid(row=4, column=2, padx=10, pady=5, sticky="w") - font_label = ctk.CTkLabel(appearance_frame, text="Font:") - font_label.grid(row=5, column=0, padx=10, pady=5, sticky="w") - system_fonts = sorted(list(tkfont.families())) - self.font_menu = SearchableComboBox(appearance_frame, values=system_fonts, command=self.update_setting) - self.font_menu.grid(row=5, column=1, columnspan=2, padx=10, pady=5, sticky="ew") - width_label_widget = ctk.CTkLabel(appearance_frame, text="Window Width:") - width_label_widget.grid(row=6, column=0, padx=10, pady=5, sticky="w") - self.width_slider = ctk.CTkSlider(appearance_frame, from_=300, to=2000, command=self.update_setting) - self.width_slider.grid(row=6, column=1, padx=10, pady=5, sticky="ew") - self.width_label = ctk.CTkLabel(appearance_frame, text="", width=40) - self.width_label.grid(row=6, column=2, padx=10, pady=5, sticky="w") - height_label_widget = ctk.CTkLabel(appearance_frame, text="Window Height:") - height_label_widget.grid(row=7, column=0, padx=10, pady=5, sticky="w") - self.height_slider = ctk.CTkSlider(appearance_frame, from_=50, to=800, command=self.update_setting) - self.height_slider.grid(row=7, column=1, padx=10, pady=5, sticky="ew") - self.height_label = ctk.CTkLabel(appearance_frame, text="", width=40) - self.height_label.grid(row=7, column=2, padx=10, pady=5, sticky="w") - padding_label_widget = ctk.CTkLabel(appearance_frame, text="Horiz. Padding:") - padding_label_widget.grid(row=8, column=0, padx=10, pady=5, sticky="w") - self.padding_slider = ctk.CTkSlider(appearance_frame, from_=0, to=100, command=self.update_setting) - self.padding_slider.grid(row=8, column=1, padx=10, pady=5, sticky="ew") - self.padding_label = ctk.CTkLabel(appearance_frame, text="", width=40) - self.padding_label.grid(row=8, column=2, padx=10, pady=5, sticky="w") - - # --- Audio & Model Section --- + self.opacity_label.grid(row=4, column=2, padx=10, pady=5, sticky="w") + + # --- Audio Section --- audio_frame = ctk.CTkFrame(scroll_frame) audio_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=10, sticky="ew") audio_frame.grid_columnconfigure(1, weight=1) - ctk.CTkLabel(audio_frame, text="Audio & Transcription", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, - columnspan=3, - pady=(5, 10)) - language_label = ctk.CTkLabel(audio_frame, text="Spoken Language:") - language_label.grid(row=1, column=0, padx=10, pady=5, sticky="w") - self.language_menu = SearchableComboBox(audio_frame, values=list(self.language_models.keys()), - command=self.on_language_change) - self.language_menu.grid(row=1, column=1, columnspan=2, padx=10, pady=5, sticky="ew") - self.model_status_label = ctk.CTkLabel(audio_frame, text="", text_color="gray") - self.model_status_label.grid(row=2, column=0, columnspan=3, pady=5) - self.download_progress = ctk.CTkProgressBar(audio_frame, orientation="horizontal", mode="determinate") - self.download_progress.set(0) - block_size_label_widget = ctk.CTkLabel(audio_frame, text="Block Size:") - block_size_label_widget.grid(row=4, column=0, padx=10, pady=5, sticky="w") - self.block_size_slider = ctk.CTkSlider(audio_frame, from_=1000, to=8000, number_of_steps=70, - command=self.update_setting) - self.block_size_slider.grid(row=4, column=1, padx=10, pady=5, sticky="ew") + ctk.CTkLabel(audio_frame, text="Audio", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, columnspan=3, pady=(5, 10)) + + ctk.CTkLabel(audio_frame, text="Block Size:").grid(row=1, column=0, padx=10, pady=5, sticky="w") + self.block_size_slider = ctk.CTkSlider(audio_frame, from_=1000, to=8000, command=self.update_setting) + self.block_size_slider.grid(row=1, column=1, padx=10, pady=5, sticky="ew") self.block_size_label = ctk.CTkLabel(audio_frame, text="", width=40) - self.block_size_label.grid(row=4, column=2, padx=10, pady=5, sticky="w") - delay_label_widget = ctk.CTkLabel(audio_frame, text="Pause Delay (s):") - delay_label_widget.grid(row=5, column=0, padx=10, pady=5, sticky="w") - self.delay_slider = ctk.CTkSlider(audio_frame, from_=0.5, to=5.0, number_of_steps=9, - command=self.update_setting) - self.delay_slider.grid(row=5, column=1, padx=10, pady=5, sticky="ew") - self.delay_label = ctk.CTkLabel(audio_frame, text="", width=40) - self.delay_label.grid(row=5, column=2, padx=10, pady=5, sticky="w") + self.block_size_label.grid(row=1, column=2, padx=10, pady=5, sticky="w") - # --- Translation Section --- - self.translation_frame = ctk.CTkFrame(scroll_frame) - self.translation_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=10, sticky="ew") - self.translation_frame.grid_columnconfigure(1, weight=1) - ctk.CTkLabel(self.translation_frame, text="Translation", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, - columnspan=3, - pady=(5, 10)) - enable_translation_label = ctk.CTkLabel(self.translation_frame, text="Enable Translation:") - enable_translation_label.grid(row=1, column=0, padx=10, pady=5, sticky="w") - self.enable_translation_checkbox = ctk.CTkCheckBox(self.translation_frame, text="", - command=self.on_translation_toggle) - self.enable_translation_checkbox.grid(row=1, column=1, padx=10, pady=5, sticky="w") - - backend_label = ctk.CTkLabel(self.translation_frame, text="Translation Engine:") - backend_label.grid(row=2, column=0, padx=10, pady=5, sticky="w") - self.backend_menu = ctk.CTkOptionMenu(self.translation_frame, values=["ArgosTranslate", "MarianMT"], - command=self.on_backend_change) - self.backend_menu.grid(row=2, column=1, columnspan=2, padx=10, pady=5, sticky="ew") - - self.target_language_label = ctk.CTkLabel(self.translation_frame, text="Translate To:") - self.target_language_label.grid(row=3, column=0, padx=10, pady=5, sticky="w") - self.target_language_menu = SearchableComboBox(self.translation_frame, values=[], - command=self.on_target_language_change) - self.target_language_menu.grid(row=3, column=1, columnspan=2, padx=10, pady=5, sticky="ew") - - self.translation_model_status_label = ctk.CTkLabel(self.translation_frame, text="", text_color="gray") - self.translation_model_status_label.grid(row=4, column=0, columnspan=3, pady=5) - self.translation_download_progress = ctk.CTkProgressBar(self.translation_frame, orientation="horizontal") - self.translation_download_progress.set(0) - - # --- Restart Prompt Section --- + # --- Restart Prompt --- self.restart_frame = ctk.CTkFrame(self, fg_color="transparent") self.restart_frame.grid(row=1, column=0, pady=5, sticky="ew") - ctk.CTkLabel(self.restart_frame, text="A restart is required for some changes to take effect.", - text_color="orange").pack(pady=5) - ctk.CTkButton(self.restart_frame, text="Restart Now", command=self.restart_and_close, fg_color="#c76b29", - hover_color="#a15621").pack(pady=5) + ctk.CTkLabel(self.restart_frame, text="A restart is required for some changes.", text_color="orange").pack(pady=5) + ctk.CTkButton(self.restart_frame, text="Restart Now", command=self.restart_and_close, fg_color="#c76b29").pack(pady=5) self.restart_frame.grid_remove() - # --- Bottom Section --- + # --- Bottom Buttons --- button_frame = ctk.CTkFrame(self, fg_color="transparent") button_frame.grid(row=2, column=0, pady=(10, 20)) - self.about_button = ctk.CTkButton(button_frame, text="About", command=self.open_about_window) - self.about_button.pack(side="left", padx=10) - self.reset_button = ctk.CTkButton(button_frame, text="Reset Defaults", command=self.reset_defaults) - self.reset_button.pack(side="left", padx=10) - self.save_button = ctk.CTkButton(button_frame, text="Save & Close", command=self.save_and_close) - self.save_button.pack(side="left", padx=10) - - def on_backend_change(self, backend): - if backend == "MarianMT": - self.target_language_menu.configure(values=self.marian_languages) - if self.target_language_menu.get() not in self.marian_languages: - self.target_language_menu.set( - "Spanish" if "Spanish" in self.marian_languages else self.marian_languages[0]) - else: # ArgosTranslate - self.target_language_menu.configure(values=self.argos_languages) - if self.target_language_menu.get() not in self.argos_languages: - self.target_language_menu.set( - "Spanish" if "Spanish" in self.argos_languages else self.argos_languages[0]) - - self.check_translation_model_status() + ctk.CTkButton(button_frame, text="About", command=self.open_about_window).pack(side="left", padx=10) + ctk.CTkButton(button_frame, text="Reset Defaults", command=self.reset_defaults).pack(side="left", padx=10) + ctk.CTkButton(button_frame, text="Save & Close", command=self.save_and_close).pack(side="left", padx=10) + + def pick_bg_color(self): + color = colorchooser.askcolor(title="Choose Background Color")[1] + if color: + self.bg_color_btn.configure(fg_color=color) + self.update_setting() + + def pick_text_color(self): + color = colorchooser.askcolor(title="Choose Text Color")[1] + if color: + self.text_color_btn.configure(fg_color=color) + self.update_setting() + + def on_language_change(self, value): self.update_setting() + self.check_model_status() - def open_about_window(self): - if not any(isinstance(x, AboutWindow) for x in self.root.winfo_children()): - AboutWindow(self) + def on_target_language_change(self, value): + self.update_setting() - def on_appearance_mode_change(self, mode): - self.settings['appearance_mode'] = mode - ctk.set_appearance_mode(mode) - if mode != self.initial_appearance_mode: - self.show_restart_prompt() + def on_translation_toggle(self): self.update_setting() def _update_slider_labels(self): self.opacity_label.configure(text=f"{self.opacity_slider.get():.2f}") self.font_size_label.configure(text=f"{int(self.font_size_slider.get())}") - self.width_label.configure(text=f"{int(self.width_slider.get())}") - self.height_label.configure(text=f"{int(self.height_slider.get())}") - self.padding_label.configure(text=f"{int(self.padding_slider.get())}") self.block_size_label.configure(text=f"{int(self.block_size_slider.get())}") - self.delay_label.configure(text=f"{self.delay_slider.get():.1f}") - - def load_settings_to_ui(self): - self.subtitle_color_btn.configure(fg_color=self.settings['subtitle_color']) - self.bg_color_btn.configure(fg_color=self.settings['background_color']) - self.opacity_slider.set(self.settings['background_opacity']) - self.font_size_slider.set(self.settings['subtitle_size']) - self.font_menu.set(self.settings['subtitle_font']) - self.width_slider.set(self.settings['window_width']) - self.height_slider.set(self.settings['window_height']) - self.padding_slider.set(self.settings['window_padding']) - self.language_menu.set(self.settings['language']) - self.block_size_slider.set(self.settings['block_size']) - self.delay_slider.set(self.settings['delay_threshold']) - self.appearance_mode_menu.set(self.settings['appearance_mode']) - self.show_about_checkbox.select() if self.settings[ - 'show_about_on_startup'] else self.show_about_checkbox.deselect() - self.enable_translation_checkbox.select() if self.settings[ - 'translation_enabled'] else self.enable_translation_checkbox.deselect() - self.backend_menu.set(self.settings['translation_backend']) - self.target_language_menu.set(self.settings['translation_target_language']) - - self.on_translation_toggle() - self.on_backend_change(self.settings['translation_backend']) - self.check_model_status() - self._update_slider_labels() - - def pick_subtitle_color(self): - color_code = colorchooser.askcolor(title="Choose Subtitle Color")[1] - if color_code: - self.subtitle_color_btn.configure(fg_color=color_code) - self.update_setting() - - def pick_bg_color(self): - color_code = colorchooser.askcolor(title="Choose Background Color")[1] - if color_code: - self.bg_color_btn.configure(fg_color=color_code) - self.update_setting() def update_setting(self, value=None): - self.settings['subtitle_color'] = self.subtitle_color_btn.cget("fg_color") self.settings['background_color'] = self.bg_color_btn.cget("fg_color") + self.settings['subtitle_color'] = self.text_color_btn.cget("fg_color") self.settings['background_opacity'] = self.opacity_slider.get() self.settings['subtitle_size'] = int(self.font_size_slider.get()) - self.settings['subtitle_font'] = self.font_menu.get() - self.settings['window_width'] = int(self.width_slider.get()) - self.settings['window_height'] = int(self.height_slider.get()) - self.settings['window_padding'] = int(self.padding_slider.get()) self.settings['language'] = self.language_menu.get() self.settings['block_size'] = int(self.block_size_slider.get()) - self.settings['delay_threshold'] = self.delay_slider.get() - self.settings['model_path'] = self.language_models[self.language_menu.get()] - self.settings['show_about_on_startup'] = bool(self.show_about_checkbox.get()) - self.settings['appearance_mode'] = self.appearance_mode_menu.get() self.settings['translation_enabled'] = bool(self.enable_translation_checkbox.get()) self.settings['translation_target_language'] = self.target_language_menu.get() - self.settings['translation_backend'] = self.backend_menu.get() + self.settings['model_path'] = self.language_models.get(self.language_menu.get(), '') self._update_slider_labels() - self.caption_window.apply_settings(self.settings) - - if (self.settings['language'] != self.initial_language or - self.settings['block_size'] != self.initial_block_size or - self.settings['appearance_mode'] != self.initial_appearance_mode): - self.show_restart_prompt() - - self.show_restart_prompt() + if self.caption_window: + self.caption_window.apply_settings(self.settings) - def on_language_change(self, language): + def load_settings_to_ui(self): + self.bg_color_btn.configure(fg_color=self.settings.get('background_color', '#000000')) + self.text_color_btn.configure(fg_color=self.settings.get('subtitle_color', '#FFFFFF')) + self.opacity_slider.set(self.settings.get('background_opacity', 0.75)) + self.font_size_slider.set(self.settings.get('subtitle_size', 16)) + self.language_menu.set(self.settings.get('language', 'English')) + self.block_size_slider.set(self.settings.get('block_size', 3000)) + self.enable_translation_checkbox.select() if self.settings.get('translation_enabled') else self.enable_translation_checkbox.deselect() + self.target_language_menu.set(self.settings.get('translation_target_language', 'English')) + self._update_slider_labels() self.check_model_status() - self.update_setting() - - def on_translation_toggle(self): - is_enabled = bool(self.enable_translation_checkbox.get()) - state = "normal" if is_enabled else "disabled" - - self.backend_menu.configure(state=state) - self.target_language_label.configure(state=state) - self.target_language_menu.configure(state=state) - - if is_enabled: - self.on_backend_change(self.backend_menu.get()) - else: - self.translation_model_status_label.configure(text="") - - self.update_setting() - - def on_target_language_change(self, language): - self.check_translation_model_status() - self.update_setting() - - def show_restart_prompt(self): - self.restart_frame.grid() def check_model_status(self): selected_lang = self.language_menu.get() - model_path = self.language_models.get(selected_lang) + model_path = self.language_models.get(selected_lang, '') if model_path and os.path.isdir(model_path): - self.model_status_label.configure(text=f"Vosk model '{model_path}' found.", text_color="green") + self.model_status_label.configure(text=f"Model '{model_path}' found", text_color="green") else: - self.model_status_label.configure(text=f"Vosk model for {selected_lang} not found. Downloading...", - text_color="orange") - self.download_and_extract_model(model_path) - - def download_and_extract_model(self, model_name): - if not model_name: - self.model_status_label.configure(text="Invalid Vosk model name.", text_color="red") - return - download_thread = threading.Thread(target=self._download_worker, args=(model_name,), daemon=True) - download_thread.start() - - def _download_worker(self, model_name): - self.language_menu.configure(state="disabled") - self.save_button.configure(state="disabled") - self.download_progress.grid(row=3, column=0, columnspan=3, padx=10, pady=5, sticky="ew") - url = f"https://alphacephei.com/vosk/models/{model_name}.zip" - zip_path = f"{model_name}.zip" - try: - self.model_status_label.configure(text=f"Downloading {model_name}...") - with requests.get(url, stream=True) as r: - r.raise_for_status() - total_size = int(r.headers.get('content-length', 0)) - bytes_downloaded = 0 - with open(zip_path, 'wb') as f: - for chunk in r.iter_content(chunk_size=8192): - f.write(chunk) - bytes_downloaded += len(chunk) - progress = bytes_downloaded / total_size if total_size > 0 else 0 - self.download_progress.set(progress) - self.update_idletasks() - self.model_status_label.configure(text=f"Extracting {model_name}...") - self.update_idletasks() - with zipfile.ZipFile(zip_path, 'r') as zip_ref: - zip_ref.extractall() - os.remove(zip_path) - self.model_status_label.configure(text=f"Vosk model '{model_name}' installed.", text_color="green") - self.show_restart_prompt() - except Exception as e: - self.model_status_label.configure(text=f"Error downloading Vosk model: {e}", text_color="red") - if os.path.exists(zip_path): - os.remove(zip_path) - finally: - self.download_progress.grid_forget() - self.language_menu.configure(state="normal") - self.save_button.configure(state="normal") - - def check_translation_model_status(self): - if not self.settings['translation_enabled']: - self.translation_model_status_label.configure(text="Translation disabled.", text_color="gray") - return - - def status_callback(message, color): - self.translation_model_status_label.configure(text=message, text_color=color) - - translator = get_translator(self.settings, status_callback) + self.model_status_label.configure(text=f"Model for {selected_lang} not found", text_color="orange") - if not translator.is_ready: - download_thread = threading.Thread(target=self.download_translation_model, daemon=True) - download_thread.start() - - def download_translation_model(self): - backend = self.backend_menu.get() - self.target_language_menu.configure(state="disabled") - self.save_button.configure(state="disabled") - self.translation_download_progress.grid(row=5, column=0, columnspan=3, padx=10, pady=5, sticky="ew") - self.translation_download_progress.configure(mode="indeterminate") - self.translation_download_progress.start() - - from_lang_name = self.language_menu.get().split(" ")[0] - to_lang_name = self.target_language_menu.get() - - try: - if backend == "ArgosTranslate": - self.translation_model_status_label.configure( - text=f"Downloading Argos model for {from_lang_name} -> {to_lang_name}...") - argostranslate.package.update_package_index() - available_packages = argostranslate.package.get_available_packages() - - package_to_install = next( - filter( - lambda x: x.from_name == from_lang_name and x.to_name == to_lang_name, - available_packages, - ) - ) - argostranslate.package.install_from_path(package_to_install.download()) - self.translation_model_status_label.configure(text=f"Argos model installed.", text_color="green") - - elif backend == "MarianMT": - model_name = f'Helsinki-NLP/opus-mt-{MarianTranslator.LANG_CODE_MAP[from_lang_name]}-{MarianTranslator.LANG_CODE_MAP[to_lang_name]}' - self.translation_model_status_label.configure(text=f"Downloading MarianMT model: {model_name}...") - MarianTokenizer.from_pretrained(model_name) - MarianMTModel.from_pretrained(model_name) - self.translation_model_status_label.configure(text=f"MarianMT model installed.", text_color="green") - - except StopIteration: - self.translation_model_status_label.configure( - text=f"No direct translation model found for {from_lang_name} -> {to_lang_name}", text_color="orange") - except Exception as e: - self.translation_model_status_label.configure(text=f"Error downloading model: {e}", text_color="red") - finally: - self.translation_download_progress.stop() - self.translation_download_progress.grid_forget() - self.target_language_menu.configure(state="normal") - self.save_button.configure(state="normal") - self.show_restart_prompt() + def open_about_window(self): + if not any(isinstance(x, AboutWindow) for x in self.winfo_children()): + AboutWindow(self) def reset_defaults(self): - if messagebox.askyesno("Reset Settings", - "Are you sure you want to reset all settings to their defaults? This requires a restart."): + if messagebox.askyesno("Reset Settings", "Reset all settings to defaults?"): self.settings = self.settings_manager.default_settings.copy() - self.settings["ran_before"] = True self.load_settings_to_ui() - self.caption_window.apply_settings(self.settings) - self.show_restart_prompt() + if self.caption_window: + self.caption_window.apply_settings(self.settings) def restart_and_close(self): self.settings_manager.save_settings(self.settings) @@ -839,20 +498,71 @@ def __init__(self, root, settings_manager, restart_callback): self.current_partial_text = "" self.text_wrap_width = 70 self.is_paused = False - - self.main_frame = tk.Frame(root) + + # Track if captions are running + self.is_running = False + self.audio_thread = None + self.recognizer_thread = None + + # Main container + self.main_frame = tk.Frame(root, bg=self.settings['background_color']) self.main_frame.pack(expand=True, fill='both') - self.caption_label = tk.Label(self.main_frame, text="Listening for audio...", justify="center", anchor="center") - self.caption_label.pack(expand=True, fill='both') - + # ---- TOP: Button Frame ---- + self.button_frame = tk.Frame(self.main_frame, bg=self.settings['background_color'], height=40) + self.button_frame.pack(fill='x', padx=5, pady=(5, 0)) + self.button_frame.pack_propagate(False) + + # Start/Stop button + self.start_stop_btn = tk.Button( + self.button_frame, + text="Start Captions", + font=('Arial', 11, 'bold'), + command=self.toggle_captions, + bg='#4CAF50', + fg='white', + relief='raised', + bd=2, + cursor='hand2', + padx=15, + pady=5 + ) + self.start_stop_btn.pack(side='left', padx=10) + + # Status label + self.status_label = tk.Label( + self.button_frame, + text="Ready", + font=('Arial', 10), + bg=self.settings['background_color'], + fg='#888888' + ) + self.status_label.pack(side='right', padx=10) + + # ---- BOTTOM: Caption Label ---- + self.caption_label = tk.Label( + self.main_frame, + text="Click 'Start Captions' to begin", + justify="center", + anchor="center", + bg=self.settings['background_color'], + fg=self.settings['subtitle_color'], + font=(self.settings['subtitle_font'], self.settings['subtitle_size'], "bold") + ) + self.caption_label.pack(expand=True, fill='both', padx=10, pady=5) + + # Bind drag events self.caption_label.bind("", self.start_move) self.caption_label.bind("", self.stop_move) self.caption_label.bind("", self.do_move) self.main_frame.bind("", self.start_move) self.main_frame.bind("", self.stop_move) self.main_frame.bind("", self.do_move) + self.button_frame.bind("", self.start_move) + self.button_frame.bind("", self.stop_move) + self.button_frame.bind("", self.do_move) + # Settings menu self.settings_menu = tk.Menu(root, tearoff=0) self.settings_menu.add_command(label="Settings", command=self.open_settings_window) self.settings_menu.add_command(label="About", command=self.open_about_window) @@ -863,14 +573,73 @@ def __init__(self, root, settings_manager, restart_callback): self.root.bind("", self.show_settings_menu) self.caption_label.bind("", self.show_settings_menu) self.main_frame.bind("", self.show_settings_menu) + self.button_frame.bind("", self.show_settings_menu) self.apply_settings(self.settings) self.update_caption() + def toggle_captions(self): + """Start or stop the captioning""" + if not self.is_running: + self.start_captions() + else: + self.stop_captions() + + def start_captions(self): + """Start the audio capture and recognition threads""" + global stop_threads + print("Starting captions...") + stop_threads.clear() + + SAMPLE_RATE = 16000 + + # Start audio capture thread + self.audio_thread = threading.Thread( + target=audio_capture_thread, + args=(self.settings['block_size'], SAMPLE_RATE), + daemon=True + ) + self.audio_thread.start() + + # Start speech recognition thread + self.recognizer_thread = threading.Thread( + target=speech_recognition_thread, + args=(self.settings, SAMPLE_RATE), + daemon=True + ) + self.recognizer_thread.start() + + self.is_running = True + self.start_stop_btn.config(text="Stop Captions", bg='#f44336') + self.status_label.config(text="Recording", fg='#4CAF50') + self.caption_label.config(text="Listening for audio...") + + def stop_captions(self): + """Stop the captioning threads""" + global stop_threads + print("Stopping captions...") + stop_threads.set() + + time.sleep(0.5) + + self.is_running = False + self.start_stop_btn.config(text="Start Captions", bg='#4CAF50') + self.status_label.config(text="Stopped", fg='#888888') + self.caption_label.config(text="Click 'Start Captions' to begin") + self.caption_history.clear() + self.current_partial_text = "" + def apply_settings(self, settings): self.settings = settings + + # Force minimum height to show the button + if settings['window_height'] < 120: + settings['window_height'] = 120 + self.root.config(bg=settings['background_color']) self.main_frame.config(bg=settings['background_color']) + self.button_frame.config(bg=settings['background_color']) + self.status_label.config(bg=settings['background_color']) self.root.attributes("-alpha", settings['background_opacity']) new_font = (settings['subtitle_font'], settings['subtitle_size'], "bold") self.caption_label.config(font=new_font, fg=settings['subtitle_color'], bg=settings['background_color']) @@ -881,7 +650,7 @@ def apply_settings(self, settings): font_obj = tkfont.Font(family=settings['subtitle_font'], size=settings['subtitle_size']) line_height = font_obj.metrics('linespace') if line_height > 0: - available_height = settings['window_height'] - 20 + available_height = settings['window_height'] - 60 self.max_lines = max(1, available_height // line_height) self.caption_history = collections.deque(maxlen=self.max_lines * 2) @@ -934,7 +703,8 @@ def do_move(self, event): def update_caption(self): global last_caption_time new_content_available = not caption_queue.empty() - if new_content_available: self.is_paused = False + if new_content_available: + self.is_paused = False try: while not caption_queue.empty(): new_text = caption_queue.get_nowait() @@ -945,24 +715,26 @@ def update_caption(self): self.current_partial_text = "" except queue.Empty: pass - if not new_content_available and (time.time() - last_caption_time) > self.settings[ - 'delay_threshold'] and not self.is_paused: + if not new_content_available and (time.time() - last_caption_time) > self.settings['delay_threshold'] and not self.is_paused: self.caption_history.clear() self.current_partial_text = "" self.is_paused = True full_text = " ".join(filter(None, self.caption_history)) if self.current_partial_text: - if full_text: full_text += " " + if full_text: + full_text += " " full_text += self.current_partial_text - wrapped_lines = textwrap.wrap(full_text, width=self.text_wrap_width, break_long_words=True, - break_on_hyphens=False) + wrapped_lines = textwrap.wrap(full_text, width=self.text_wrap_width, break_long_words=True, break_on_hyphens=False) display_lines = wrapped_lines[-self.max_lines:] if len(wrapped_lines) > self.max_lines else wrapped_lines display_text = "\n".join(display_lines) if not display_text.strip(): - display_text = "Listening for audio... (Right-click for settings)" + if self.is_running: + display_text = "Listening for audio..." + else: + display_text = "Click 'Start Captions' to begin" self.caption_label.config(text=display_text) self.root.after(100, self.update_caption) @@ -995,50 +767,136 @@ def audio_capture_thread(block_size, sample_rate): def speech_recognition_thread(settings, sample_rate): - """Processes audio from the queue using Vosk and optionally translates it.""" + """Processes audio from the queue using Vosk with Speaker Identification.""" global last_caption_time model_path = settings['model_path'] + + # --- Speaker Database --- + speaker_db = {} + try: if not os.path.isdir(model_path): - raise FileNotFoundError(f"Model path '{model_path}' not found. Please select a valid model in settings.") + raise FileNotFoundError(f"Model path '{model_path}' not found.") + model = vosk.Model(model_path) - recognizer = vosk.KaldiRecognizer(model, sample_rate) + + # --- HARDCODE THE SPEAKER MODEL PATH --- + # Use the FULL absolute path + spk_model_path = r"D:\LiveCaptionProject\LiveCaptioning\vosk-model-spk-0.4" + + print(f"Looking for speaker model at: {spk_model_path}") + print(f" Exists: {os.path.exists(spk_model_path)}") + + recognizer = None + + if os.path.exists(spk_model_path) and os.path.isdir(spk_model_path): + # List contents to verify + print(f" Contents: {os.listdir(spk_model_path)[:5]}...") + + try: + # Load the speaker model + spk_model = vosk.SpkModel(spk_model_path) + recognizer = vosk.KaldiRecognizer(model, sample_rate, spk_model) + print("Speaker model loaded successfully!") + except Exception as e: + print(f"Could not load speaker model: {e}") + recognizer = vosk.KaldiRecognizer(model, sample_rate) + print("Continuing without speaker identification.") + else: + print("Speaker model NOT found. Continuing without speaker identification.") + recognizer = vosk.KaldiRecognizer(model, sample_rate) + recognizer.SetWords(True) print("Vosk model loaded. Speech recognition thread started.") + except Exception as e: print(f"Error loading Vosk model: {e}", file=sys.stderr) - caption_queue.put(f"ERROR: Failed to load Vosk model. Please check settings.") + caption_queue.put(f"ERROR: Failed to load Vosk model.") return + # Translation setup translator = get_translator(settings) if translator: - print(f"Translation enabled with backend: {settings.get('translation_backend')}") + print(f"Translation enabled: {settings.get('language')} -> {settings.get('translation_target_language')}") else: print("Translation disabled.") + def cosine_distance(vec1, vec2): + if vec1 is None or vec2 is None: + return 2.0 + try: + v1 = np.array(vec1) + v2 = np.array(vec2) + if np.linalg.norm(v1) == 0 or np.linalg.norm(v2) == 0: + return 2.0 + return 1 - np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) + except: + return 2.0 + while not stop_threads.is_set(): try: audio_data = audio_queue.get(timeout=1) if recognizer.AcceptWaveform(audio_data): result_dict = json.loads(recognizer.Result()) text = result_dict.get('text', '') + + # --- Extract speaker information --- + speaker_label = None + if "spk" in result_dict and result_dict["spk"] is not None: + current_vector = result_dict["spk"] + + best_match = None + best_distance = 0.7 + + for name, vector in speaker_db.items(): + dist = cosine_distance(vector, current_vector) + if dist < best_distance: + best_distance = dist + best_match = name + + if best_match is None and len(speaker_db) < 10: + speaker_label = f"Speaker {len(speaker_db) + 1}" + speaker_db[speaker_label] = current_vector + print(f"New speaker detected: {speaker_label}") + elif best_match is not None: + speaker_label = best_match + print(f"Matched speaker: {speaker_label}") + else: + speaker_label = "Unknown" + else: + # Debug: Print if no speaker data + if text: + print(f"No speaker data in result") + if text: if translator and translator.is_ready: text = translator.translate(text) - caption_queue.put(text) + text = add_punctuation(text) + + if speaker_label: + caption_text = f"[{speaker_label}] {text}" + print(f"[{speaker_label}] {text[:40]}...") + else: + caption_text = text + + caption_queue.put(caption_text) last_caption_time = time.time() + else: partial_result_dict = json.loads(recognizer.PartialResult()) partial_text = partial_result_dict.get('partial', '') if partial_text: - # **MODIFIED**: Translate partial text for real-time effect if translator and translator.is_ready: partial_text = translator.translate(partial_text) + partial_text = add_punctuation(partial_text) caption_queue.put(f"... {partial_text}") last_caption_time = time.time() + except queue.Empty: continue - + except Exception as e: + print(f"Error: {e}") + continue def main(): """Main function to set up the GUI and start the threads.""" @@ -1080,15 +938,8 @@ def restart_application(): "Failed to restart the application automatically. Please restart it manually.") stop_threads.clear() - SAMPLE_RATE = 16000 - - audio_thread = threading.Thread(target=audio_capture_thread, args=(settings['block_size'], SAMPLE_RATE), - daemon=True) - audio_thread.start() - - recognizer_thread = threading.Thread(target=speech_recognition_thread, args=(settings, SAMPLE_RATE), daemon=True) - recognizer_thread.start() - + + # Create the caption window (without starting threads) app = CaptionWindow(root, settings_manager, restart_application) if is_first_run or settings.get('show_about_on_startup', True): @@ -1100,6 +951,11 @@ def restart_application(): y_coord = screen_height - (settings['window_height'] + 50) root.geometry(f"+{int(x_coord)}+{int(y_coord)}") + print("Live Captions started! Click 'Start Captions' to begin.") + print(" Right-click the window for Settings.") + print(" Drag the window to move it.") + print(" Features: Speaker Identification + Punctuation") + try: root.mainloop() finally: diff --git a/README.md b/README.md index 67e39ea..679a76e 100644 --- a/README.md +++ b/README.md @@ -1,356 +1,217 @@ -# LiveScript: Real-time Live Captioning & Translation Software v2.0.1 +# Live Caption Translator -A powerful real-time audio transcription application that provides live captions for your system's audio output with **built-in translation capabilities**. Perfect for accessibility, language learning, international meetings, or any situation where you need visual representation and translation of spoken content. +## A Real-Time Audio Intelligence Pipeline with Speaker Diarization and Multilingual Translation -## ๐Ÿ†• What's New in Version 2.0.1 -*Released: August 27, 2025* +An enhanced fork of the original **Realtime Subtitles Generator** that implements a robust audio processing pipeline with speaker diarization, intelligent punctuation restoration, recording controls, system-level audio capture via WASAPI loopback, and a modular translation abstraction layer. -### โœจ Major New Features -- **๐ŸŒ Real-Time Translation**: Live translation of captions into 50+ languages -- **๐Ÿ”„ Dual Translation Engines**: Choose between ArgosTranslate (offline) and MarianMT (offline) -- **โšก Instant Translation**: Translate both final text and partial text in real-time -- **๐Ÿ“ฑ Smart Language Detection**: Automatic model management for translation pairs +--- + +## ๐Ÿ“– Architectural Overview + +This project implements an end-to-end **audio intelligence pipeline** that captures system-level audio, performs real-time speech recognition, applies speaker diarization using x-vector embeddings, restores linguistic structure through punctuation normalization, and optionally translates the output using a pluggable translation backend. -### ๐Ÿ”ง Enhanced Features -- **Improved Performance**: Optimized audio processing for better real-time performance -- **Enhanced UI**: Refined user interface with translation controls and status indicators -- **Better Model Management**: Automatic downloading for both speech recognition and translation models -- **Stability Improvements**: Fixed memory leaks and improved application stability -- **Updated Dependencies**: Added translation libraries and upgraded existing dependencies +The system operates at the **OS audio layer**, intercepting audio output from any applicationโ€”including browsers, media players, and communication platformsโ€”without requiring integration APIs or browser extensions. -### ๐Ÿ› ๏ธ Technical Improvements -- Optimized memory usage during long transcription and translation sessions -- Enhanced error handling for translation model loading -- Improved startup time with lazy model loading -- Better handling of system audio configuration changes -- Streamlined translation pipeline for minimal latency +**Key Technical Capabilities:** +- **System Audio Interception** โ€“ WASAPI loopback capture at the kernel level +- **Speech-to-Text** โ€“ Offline ASR using Vosk's acoustic and language models +- **Speaker Diarization** โ€“ x-vector based voice fingerprinting with cosine distance clustering +- **Linguistic Normalization** โ€“ Automated punctuation and capitalization restoration +- **Translation Abstraction** โ€“ Pluggable translation backend (Google Translate integration) +- **UI/UX Layer** โ€“ Non-intrusive overlay with real-time status feedback + +--- ## โœจ Core Features -### ๐ŸŽค **Real-Time Transcription** -- Live speech-to-text using Vosk offline speech recognition -- Audio loopback capture from system speakers -- Multi-threaded processing for smooth performance -- Configurable audio buffer sizes for optimal accuracy/speed balance - -### ๐ŸŒ **Live Translation (NEW!)** -- **Real-time translation** of live captions as they appear -- **50+ target languages** supported for translation -- **Dual translation backends**: - - **ArgosTranslate**: Completely offline, privacy-focused - - **MarianMT**: High-quality Hugging Face models, also offline -- **Automatic model management** - download translation models on demand -- **Instant partial translation** - even incomplete sentences get translated live - -### ๐ŸŽฏ **Multi-Language Speech Recognition** -- **50+ languages and dialects** supported for speech recognition including: - - English (multiple variants), Chinese, Russian, French, German, Spanish - - Portuguese, Arabic, Hindi, Japanese, Korean, Italian, Dutch - - And many more regional variants -- Automatic model downloading and management -- Easy language switching through settings - -### ๐ŸŽจ **Customizable Appearance** -- **Caption Styling**: Custom colors, fonts, and sizes -- **Window Properties**: Adjustable width, height, opacity, and padding -- **Theme Support**: Dark/Light mode compatibility -- **Overlay Positioning**: Draggable caption window -- Real-time preview of all visual changes - -### โš™๏ธ **Advanced Configuration** -- Audio processing parameters (block size, delay thresholds) -- Translation engine selection and target language configuration -- Searchable dropdown menus for easy navigation -- Settings persistence with JSON configuration -- First-run setup wizard with guided tour - -### ๐Ÿ–ฅ๏ธ **User Experience** -- Modern CustomTkinter interface with translation controls -- Right-click context menu for quick access -- Tooltips and helpful descriptions for all settings -- Responsive, scrollable settings panel -- One-click model installation for both speech and translation - -## ๐Ÿ“‹ Requirements - -### System Requirements -- **Operating System**: Windows 10/11, macOS 10.14+, or Linux -- **Python**: 3.7 or higher -- **RAM**: Minimum 4GB (8GB recommended for translation features) -- **Storage**: 1GB - 10GB depending on language and translation models -- **Audio**: System audio output (speakers/headphones) -- **Internet**: Required for initial model downloads only - -### Python Dependencies +### 1. System-Level Audio Capture (WASAPI Loopback) +- Captures audio at the **OS audio driver level** using Windows WASAPI loopback +- Intercepts audio output from any application: web browsers, media players, conferencing tools +- Zero-latency audio buffering with configurable block sizes + +### 2. Speech Recognition & Speaker Diarization +- **ASR Engine**: Vosk offline speech recognition with support for 50+ languages +- **Speaker Diarization**: Implements x-vector embeddings for voice fingerprinting +- **Clustering**: Cosine distance-based speaker clustering with adaptive thresholding +- **Real-time Labeling**: Dynamically assigns speaker labels (Speaker 1, Speaker 2, etc.) + +### 3. Linguistic Normalization +- **Punctuation Restoration**: Automated insertion of periods, commas, and question marks +- **Case Normalization**: Sentence-level capitalization +- **Noise Reduction**: Removes artifacts like double spaces and repeated punctuation + +### 4. Translation Abstraction Layer +- **Pluggable Architecture**: Modular translation backend with interface abstraction +- **Google Translate Integration**: Reliable, production-grade translation +- **Multilingual Support**: 50+ target languages + +### 5. User Interface & Interaction +- **Start/Stop Controls**: Stateful recording control +- **Status Indicators**: Real-time recording status (Recording / Stopped) +- **Floating Overlay**: Transparent, topmost window with drag functionality +- **Customization**: Configurable colors, fonts, opacity, and window dimensions + +### 6. Error Handling & Resilience +- **Graceful Degradation**: Fallback mechanisms when models are unavailable +- **Exception Management**: Comprehensive error handling at each pipeline stage +- **Model Validation**: Pre-flight checks for required dependencies + +--- + +## ๐Ÿ—๏ธ System Architecture + ``` -customtkinter>=5.0.0 -soundcard>=0.4.0 -vosk>=0.3.45 -numpy>=1.19.0 -requests>=2.25.0 -argostranslate>=1.9.0 -transformers>=4.21.0 -torch>=1.12.0 -sentencepiece>=0.1.97 -protobuf>=3.20.0 +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ APPLICATION LAYER โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ USER INTERFACE (GUI) โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Start/Stop โ”‚ โ”‚ Settings โ”‚ โ”‚ Caption Overlay โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Control โ”‚ โ”‚ Management โ”‚ โ”‚ (Floating HUD) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ PIPELINE ABSTRACTION โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Audio โ”‚ โ”‚ ASR โ”‚ โ”‚ Speaker โ”‚ โ”‚ Punctu- โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Capture โ”‚โ†’โ”‚ Engine โ”‚โ†’โ”‚ Diariz- โ”‚โ†’โ”‚ ation โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (WASAPI)โ”‚ โ”‚ (Vosk) โ”‚ โ”‚ ation โ”‚ โ”‚ Restore โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ†“ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Trans- โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ lation โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (Plugg- โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ able) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -## ๐Ÿš€ Installation +--- -### Method 1: Clone Repository -```bash -# Clone the repository -git clone https://github.com/oscurprof/Realtime-Subtitles-Generator-using-Python.git -cd Realtime-Subtitles-Generator-using-Python +## ๐Ÿ“Š Feature Comparison: Base vs. Enhanced Implementation -# Install dependencies -pip install -r requirements.txt +| Component | Original Implementation | Enhanced Fork | +|-----------|------------------------|---------------| +| **Audio Capture** | WASAPI loopback | WASAPI loopback (retained) | +| **ASR Engine** | Vosk | Vosk (retained) | +| **Speaker Diarization** | Not implemented | x-vector embeddings + cosine distance | +| **Punctuation Restoration** | Not implemented | NLP-based punctuation normalization | +| **Recording Control** | Auto-start | Stateful Start/Stop | +| **Translation Backend** | Broken dependencies | Modular abstraction + Google Translate | +| **Error Handling** | Minimal | Comprehensive exception management | +| **Status Indication** | Not implemented | Recording/Stopped state management | -# Run the application -python LivescriptV2.01.py -``` +--- -### Method 2: Direct Download -1. Download the latest release from [Releases](https://github.com/oscurprof/Realtime-Subtitles-Generator-using-Python/releases) -2. Extract the archive -3. Install dependencies: `pip install -r requirements.txt` -4. Run: `python LivescriptV2.01.py` +## ๐Ÿš€ Technical Implementation -### Method 3: Package Installation +### Prerequisites +- **OS**: Windows 10/11 (WASAPI loopback requires Windows) +- **Python**: 3.8+ +- **RAM**: 4GB minimum, 8GB recommended +- **Storage**: ~3GB for models and dependencies +- **Network**: Required for translation (Google Translate API) + +### Installation + +**1. Clone the Repository** ```bash -pip install livescript-captions -livescript +git clone https://github.com/neeharika2802/LiveCapationing.git +cd LiveCaptionProject\LiveCaptioning ``` -## ๐ŸŽฎ Usage - -### First Run -1. **Launch the application** - The About window will appear with usage instructions -2. **Right-click** the caption overlay to access settings -3. **Select your spoken language** - The app will automatically download the required speech recognition model -4. **Enable translation** (optional) - Choose your target language and translation engine -5. **Customize appearance** to your preferences -6. **Start speaking or play audio** - Captions will appear in real-time, with live translation if enabled! - -### Basic Controls -- **Move Window**: Left-click and drag the caption bar -- **Access Settings**: Right-click anywhere on the caption window -- **Quick Restart**: Use the restart option when changing languages or models - -### Translation Quick Start -1. **Enable Translation**: Settings โ†’ Translation โ†’ Check "Enable Translation" -2. **Choose Engine**: Select ArgosTranslate (recommended) or MarianMT -3. **Select Target Language**: Choose from 50+ available languages -4. **Download Models**: App will automatically download required translation models -5. **Start Translating**: Speak or play audio - see live translated captions! - -### Settings Overview - -#### ๐ŸŽจ **Caption Appearance** -- **Colors**: Subtitle and background colors with color picker -- **Typography**: Font family, size, and styling options -- **Layout**: Window dimensions, padding, and opacity controls -- **Positioning**: Draggable overlay with size customization - -#### ๐Ÿ”Š **Audio & Language** -- **Spoken Language**: 50+ supported languages for speech recognition -- **Audio Processing**: Block size and delay threshold adjustments -- **Model Management**: Automatic installation and status monitoring - -#### ๐ŸŒ **Translation (NEW!)** -- **Enable/Disable**: Toggle real-time translation -- **Translation Engine**: Choose between ArgosTranslate and MarianMT -- **Target Language**: Select from 50+ supported languages -- **Model Status**: View download progress and model availability - -#### โšก **Performance Tuning** -- **Block Size**: Lower = faster response, Higher = better accuracy - - `1000-2000`: Fast response (good for real-time chat) - - `3000-4000`: Balanced (recommended for most users) - - `5000-8000`: High accuracy (good for detailed transcription) -- **Delay Threshold**: How long to wait before clearing captions during silence - -## ๐Ÿ› ๏ธ Configuration - -### Settings File -Settings are automatically saved to `settings.json`: -```json -{ - "subtitle_color": "#FFFFFF", - "background_color": "#000000", - "background_opacity": 0.75, - "subtitle_size": 16, - "subtitle_font": "Arial", - "language": "English", - "block_size": 3000, - "window_width": 1200, - "window_height": 70, - "delay_threshold": 3.0, - "translation_enabled": false, - "translation_backend": "ArgosTranslate", - "translation_target_language": "Spanish", - "version": "2.0.1" -} +**2. Environment Setup** +```bash +python -m venv venv +venv\Scripts\activate ``` -### Model Storage -- **Speech Recognition Models**: Downloaded to application directory (30MB - 5GB) -- **Translation Models**: - - ArgosTranslate: ~50-200MB per language pair - - MarianMT: ~300MB - 1GB per language pair -- Models are reusable and only need to be downloaded once -- All models work completely offline after download - -## ๐ŸŒ Translation Features - -### Supported Translation Backends - -#### ๐Ÿ”„ **ArgosTranslate (Recommended)** -- **Completely offline** - No internet required after setup -- **Privacy-focused** - All processing happens locally -- **50+ language pairs** supported -- **Smaller model sizes** (~50-200MB per language pair) -- **Good translation quality** for most use cases - -#### ๐ŸŽฏ **MarianMT (Advanced)** -- **High-quality translations** using Hugging Face models -- **Also completely offline** -- **Professional-grade accuracy** for supported language pairs -- **Larger model sizes** (~300MB - 1GB per language pair) -- **Best for professional/academic use** - -### Translation Language Support -Popular language pairs include: -- **English** โ†” Spanish, French, German, Italian, Portuguese, Russian, Chinese, Japanese, Korean, Arabic, Hindi -- **Spanish** โ†” English, French, Italian, Portuguese -- **French** โ†” English, German, Spanish, Italian -- **German** โ†” English, French, Spanish, Italian -- **And many more combinations!** - -## ๐Ÿ”ง Troubleshooting - -### Common Issues - -#### "Could not capture audio" -- **Solution**: Check that your system has audio output devices -- **Windows**: Ensure "Stereo Mix" or similar loopback device is enabled -- **macOS**: Grant microphone permissions in System Preferences -- **Linux**: Install and configure PulseAudio or ALSA - -#### "Model not found" error -- **Solution**: Open Settings โ†’ Select your language โ†’ Wait for automatic download -- **Manual**: Download models from [Vosk Models](https://alphacephei.com/vosk/models) - -#### Translation not working -- **Solution**: - - Check that translation is enabled in Settings - - Verify target language is selected - - Wait for translation model download to complete - - Try switching translation backends (ArgosTranslate โ†” MarianMT) - -#### Poor translation quality -- **Solution**: - - Try MarianMT backend for higher quality (if language pair supported) - - Ensure clear audio input for better source transcription - - Check that source language matches the speech recognition setting - -#### "Translation model download failed" -- **Solution**: - - Check internet connection - - Try different language pair - - Switch to alternative translation backend - - Manually install models if needed - -#### Application slow with translation -- **Solution**: - - Use ArgosTranslate for better performance - - Increase block size for less frequent processing - - Close other resource-intensive applications - - Consider using smaller translation models - -### Performance Optimization -- **For speed**: Use ArgosTranslate, smaller models, lower block sizes -- **For accuracy**: Use MarianMT, larger models, higher block sizes -- **For battery life**: Disable translation when not needed, use smaller models - -## ๐Ÿค Contributing - -We welcome contributions! Here's how you can help: - -### Ways to Contribute -- ๐Ÿ› **Bug Reports**: Report issues via [GitHub Issues](https://github.com/oscurprof/Realtime-Subtitles-Generator-using-Python/issues) -- ๐Ÿ’ก **Feature Requests**: Suggest new features or improvements -- ๐ŸŒ **Language Support**: Help test and improve language and translation models -- ๐Ÿ“– **Documentation**: Improve guides, README, or code comments -- ๐Ÿ”ง **Code**: Submit pull requests for bug fixes or new features -- ๐ŸŒ **Translation Testing**: Help test translation accuracy for different language pairs - -### Development Setup +**3. Dependencies Installation** ```bash -git clone https://github.com/oscurprof/Realtime-Subtitles-Generator-using-Python.git -cd Realtime-Subtitles-Generator-using-Python pip install -r requirements.txt -# Make your changes -python LivescriptV2.01.py # Test your changes ``` -### Pull Request Guidelines -1. Fork the repository -2. Create a feature branch: `git checkout -b feature-name` -3. Make your changes and test thoroughly -4. Test translation features with multiple language pairs -5. Commit with descriptive messages -6. Push to your fork and submit a pull request +**4. Model Acquisition** +- **ASR Model**: Download from [Vosk Models Repository](https://alphacephei.com/vosk/models) +- **Speaker Model**: Download `vosk-model-spk-0.4.zip` (x-vector extraction model) +- Extract both models to the project root directory -## ๐Ÿ“„ License +**5. Launch Application** +```bash +python LivescriptV2.01.py +``` + +--- + +## ๐ŸŽฎ Interaction Model + +| Action | Result | +|--------|--------| +| **Click "Start Captions"** | Initiates audio capture โ†’ ASR โ†’ Diarization โ†’ Translation pipeline | +| **Play Audio/Video** | System audio is intercepted and processed in real-time | +| **Observe Overlay** | Captions appear with speaker labels and punctuation | +| **Right-click Overlay** | Access Settings, About, and Exit options | +| **Click "Stop Captions"** | Gracefully terminates the processing pipeline | + +--- -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +## ๐Ÿ› ๏ธ Technology Stack -## ๐Ÿ™ Acknowledgments +| Component | Technology | Purpose | +|-----------|-----------|---------| +| **Audio Capture** | `soundcard` + WASAPI | System-level audio interception via loopback | +| **Speech Recognition** | Vosk (Kaldi-based) | Offline ASR with 50+ language models | +| **Speaker Diarization** | Vosk x-vector extractor | Voice fingerprinting and speaker clustering | +| **Vector Operations** | NumPy | Cosine distance calculations for diarization | +| **Translation** | `deep-translator` + Google Translate | Pluggable multilingual translation | +| **GUI Framework** | `customtkinter` / `tkinter` | Native Windows overlay and settings UI | +| **Concurrency** | Python `threading` | Parallel audio capture and processing | +| **Configuration** | JSON | Persistent user settings | -- **[Vosk](https://alphacephei.com/vosk/)** - Offline speech recognition toolkit -- **[CustomTkinter](https://github.com/TomSchimansky/CustomTkinter)** - Modern GUI framework -- **[SoundCard](https://github.com/bastibe/SoundCard)** - Audio capture library -- **[ArgosTranslate](https://github.com/argosopentech/argos-translate)** - Open-source offline translation -- **[Hugging Face Transformers](https://huggingface.co/transformers/)** - MarianMT translation models -- **Community Contributors** - Thanks to everyone who has contributed! +--- -## ๐Ÿ“ž Support +## ๐Ÿ“ˆ Future Development Roadmap -- ๐ŸŒŸ **Star this repo** if you find it helpful! -- ๐Ÿ› **Report bugs** via [GitHub Issues](https://github.com/oscurprof/Realtime-Subtitles-Generator-using-Python/issues) -- ๐Ÿ’ฌ **Discussions** for questions and feature requests -- ๐Ÿ“ง **Email**: oscurprof@gmail.com -- ๐Ÿ”— **LinkedIn**: [oscurprof](https://www.linkedin.com/in/oscurprof/) +| Feature | Description | Priority | +|---------|-------------|----------| +| **Transcript Export** | `.srt`, `.txt`, `.vtt` format support | High | +| **Timestamp Integration** | Word-level and sentence-level timestamps | High | +| **Model Agnosticism** | Support for Whisper, Vosk, and other ASR engines | Medium | +| **GPU Acceleration** | CUDA/OpenCL support for faster inference | Medium | +| **Offline Translation** | NLLB-200 or MarianMT integration | Medium | +| **WebSocket API** | Expose caption stream via WebSocket | Low | +| **Cloud Sync** | Sync transcripts to cloud storage | Low | -## ๐Ÿ—บ๏ธ Roadmap +--- -### Planned Features -- [ ] **Cloud Translation APIs** - Integration with Google Translate, DeepL, etc. -- [ ] **Multi-Speaker Recognition** - Distinguish between different speakers -- [ ] **Export Functionality** - Save transcriptions and translations to text files -- [ ] **Hotkey Support** - Keyboard shortcuts for common actions -- [ ] **Advanced Translation Options** - Context-aware translation, custom dictionaries -- [ ] **Translation History** - Save and review past translations -- [ ] **Batch Translation** - Translate saved transcription files +## ๐Ÿ™ Acknowledgements -### Translation Roadmap -- [ ] **More Language Pairs** - Expand ArgosTranslate and MarianMT support -- [ ] **Custom Translation Models** - Support for user-trained models -- [ ] **Translation Quality Indicators** - Confidence scores for translations -- [ ] **Bidirectional Translation** - Translate both directions simultaneously +This project builds upon the foundational work of: -### Version History -- **v2.0.1** - Added real-time translation with ArgosTranslate and MarianMT support -- **v1.01** - Initial release with core transcription functionality -- **v1.00** - Beta testing and development +- **oscurprof** โ€“ Original [LiveScript](https://github.com/oscurprof/Realtime-Subtitles-Generator-using-Python) implementation +- **Vosk Team** โ€“ Open-source speech recognition and x-vector extraction +- **Google Translate API** โ€“ Translation services +- **Python Community** โ€“ Extensive ecosystem of audio processing libraries --- -
+## ๐Ÿ“„ License -**Made with โค๏ธ for accessibility and inclusion worldwide** +This project inherits the license terms of the original repository. Please refer to the original license for usage, modification, and distribution terms. + +--- -[โญ Star](https://github.com/oscurprof/Realtime-Subtitles-Generator-using-Python/stargazers) โ€ข [๐Ÿด Fork](https://github.com/oscurprof/Realtime-Subtitles-Generator-using-Python/fork) โ€ข [๐Ÿ“‹ Issues](https://github.com/oscurprof/Realtime-Subtitles-Generator-using-Python/issues) +## ๐Ÿ‘ค Author + +**Neeharika** +- GitHub: (https://github.com/neeharika2802) + + +--- -
+*Built with Python, Vosk, and a passion for accessible audio intelligence.* \ No newline at end of file diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..dd0fc75 --- /dev/null +++ b/settings.json @@ -0,0 +1,20 @@ +{ + "subtitle_color": "#FFFFFF", + "background_color": "#000000", + "background_opacity": 0.75, + "subtitle_size": 16, + "subtitle_font": "Arial", + "language": "English", + "block_size": 3000, + "model_path": "vosk-model-small-en-us-0.15", + "window_width": 1200, + "window_height": 1500, + "window_padding": 20, + "delay_threshold": 3.0, + "ran_before": true, + "show_about_on_startup": true, + "appearance_mode": "dark", + "translation_enabled": true, + "translation_target_language": "English", + "translation_backend": "ArgosTranslate" +} \ No newline at end of file diff --git a/translator.py b/translator.py index 62c97ea..3fab416 100644 --- a/translator.py +++ b/translator.py @@ -1,13 +1,11 @@ -import argostranslate.package -import argostranslate.translate -from transformers import MarianMTModel, MarianTokenizer +""" +Translator using deep-translator with better language detection +""" +from deep_translator import GoogleTranslator import threading - # --- Base Translator Class --- class BaseTranslator: - """A base class for all translation backends.""" - def __init__(self, from_lang_name, to_lang_name, status_callback=None): self.from_lang_name = from_lang_name self.to_lang_name = to_lang_name @@ -15,125 +13,144 @@ def __init__(self, from_lang_name, to_lang_name, status_callback=None): self.is_ready = False def translate(self, text): - """Translates a given text.""" raise NotImplementedError - def check_and_install_model(self): - """Checks if the required model is installed and downloads it if not.""" - pass - def _update_status(self, message, color="gray"): if self.status_callback: self.status_callback(message, color) class ArgosTranslator(BaseTranslator): - """Translator using the Argos Translate library (offline).""" - def __init__(self, from_lang_name, to_lang_name, status_callback=None): super().__init__(from_lang_name, to_lang_name, status_callback) - self.translator = None - self.from_lang = None - self.to_lang = None - self.check_model() - - def check_model(self): - try: - installed_languages = argostranslate.translate.get_installed_languages() - self.from_lang = next((lang for lang in installed_languages if lang.name == self.from_lang_name), None) - self.to_lang = next((lang for lang in installed_languages if lang.name == self.to_lang_name), None) - - if self.from_lang and self.to_lang: - self.translator = self.from_lang.get_translation(self.to_lang) - if self.translator: - self._update_status(f"Argos model for {self.from_lang_name} -> {self.to_lang_name} found.", "green") - self.is_ready = True - else: - # Languages are installed, but the specific translation isn't - self._update_status( - f"Argos translation for {self.from_lang_name} -> {self.to_lang_name} not found. Please install from Settings.", - "orange") - self.is_ready = False - else: - # One or both languages are not installed - self._update_status( - f"Argos language model for '{self.from_lang_name}' or '{self.to_lang_name}' not installed.", - "orange") - self.is_ready = False - - except Exception as e: - self._update_status(f"Error checking Argos model: {e}", "red") - self.is_ready = False + self.from_lang = self._get_lang_code(from_lang_name) + self.to_lang = self._get_lang_code(to_lang_name) + self.is_ready = True + self._update_status(f"Google Translate ready: {from_lang_name} -> {to_lang_name}", "green") + + def _get_lang_code(self, lang_name): + """Convert language name to Google Translate code - EXPANDED MAP""" + lang_map = { + 'english': 'en', + 'hindi': 'hi', + 'telugu': 'te', + 'tamil': 'ta', + 'japanese': 'ja', + 'french': 'fr', + 'german': 'de', + 'spanish': 'es', + 'chinese': 'zh-cn', + 'arabic': 'ar', + 'russian': 'ru', + 'portuguese': 'pt', + 'italian': 'it', + 'korean': 'ko', + 'dutch': 'nl', + 'turkish': 'tr', + 'polish': 'pl', + 'swedish': 'sv', + 'vietnamese': 'vi', + 'thai': 'th', + 'gujarati': 'gu', + 'kannada': 'kn', + 'malayalam': 'ml', + 'marathi': 'mr', + 'punjabi': 'pa', + 'bengali': 'bn', + 'urdu': 'ur', + 'oriya': 'or', + 'assamese': 'as' + } + # Try exact match first, then lowercase + result = lang_map.get(lang_name.lower(), 'en') + print(f"Language mapping: {lang_name} -> {result}") # Debug + return result def translate(self, text): - if not self.is_ready or not self.translator: - return f"[No Argos Model] {text}" + if not text or not text.strip(): + return text try: - return self.translator.translate(text) + print(f"Translating: '{text[:50]}...' from {self.from_lang} to {self.to_lang}") # Debug + + # Try with explicit source language + translator = GoogleTranslator(source=self.from_lang, target=self.to_lang) + result = translator.translate(text) + print(f"Translation result: '{result[:50]}...'") # Debug + return result + except Exception as e: - print(f"Argos translation error: {e}") - return f"[Translation Error] {text}" + print(f"Translation error (explicit source): {e}") + try: + # Fallback: auto-detect source + translator = GoogleTranslator(source='auto', target=self.to_lang) + result = translator.translate(text) + print(f"Fallback result: '{result[:50]}...'") + return result + except Exception as e2: + print(f"Translation error (auto-detect): {e2}") + return f"[Error] {text}" class MarianTranslator(BaseTranslator): - """Translator using Hugging Face MarianMT models (offline).""" - LANG_CODE_MAP = { - "English": "en", "French": "fr", "German": "de", "Spanish": "es", - "Russian": "ru", "Chinese": "zh", "Italian": "it", "Portuguese": "pt", - "Dutch": "nl", "Japanese": "jap", "Arabic": "ar", "Hindi": "hi", - } - def __init__(self, from_lang_name, to_lang_name, status_callback=None): super().__init__(from_lang_name, to_lang_name, status_callback) - self.model = None - self.tokenizer = None - self.model_name = None - self.check_model() - - def check_model(self): - from_code = self.LANG_CODE_MAP.get(self.from_lang_name) - to_code = self.LANG_CODE_MAP.get(self.to_lang_name) - - if not from_code or not to_code: - self._update_status(f"MarianMT does not support {self.from_lang_name} or {self.to_lang_name}.", "red") - self.is_ready = False - return - - self.model_name = f'Helsinki-NLP/opus-mt-{from_code}-{to_code}' - self._update_status(f"Checking for MarianMT model: {self.model_name}...", "gray") - - try: - # from_pretrained checks local cache first. This avoids re-downloading. - self.tokenizer = MarianTokenizer.from_pretrained(self.model_name) - self.model = MarianMTModel.from_pretrained(self.model_name) - self._update_status(f"MarianMT model '{self.model_name}' is ready.", "green") - self.is_ready = True - except Exception: - self._update_status(f"Model '{self.model_name}' not found. Download from Settings.", "orange") - self.is_ready = False + self.from_lang = self._get_lang_code(from_lang_name) + self.to_lang = self._get_lang_code(to_lang_name) + self.is_ready = True + self._update_status(f"Google Translate ready: {from_lang_name} -> {to_lang_name}", "green") + + def _get_lang_code(self, lang_name): + """Convert language name to Google Translate code""" + lang_map = { + 'english': 'en', + 'hindi': 'hi', + 'telugu': 'te', + 'tamil': 'ta', + 'japanese': 'ja', + 'french': 'fr', + 'german': 'de', + 'spanish': 'es', + 'chinese': 'zh-cn', + 'arabic': 'ar', + 'russian': 'ru', + 'portuguese': 'pt', + 'italian': 'it', + 'korean': 'ko', + 'dutch': 'nl', + 'turkish': 'tr', + 'polish': 'pl', + 'swedish': 'sv', + 'vietnamese': 'vi', + 'thai': 'th' + } + return lang_map.get(lang_name.lower(), 'en') def translate(self, text): - if not self.is_ready or not self.model or not self.tokenizer: - return f"[No MarianMT Model] {text}" - + if not text or not text.strip(): + return text try: - tokenized_text = self.tokenizer(text, return_tensors="pt", padding=True) - translated_tokens = self.model.generate(**tokenized_text) - return self.tokenizer.decode(translated_tokens[0], skip_special_tokens=True) + translator = GoogleTranslator(source=self.from_lang, target=self.to_lang) + return translator.translate(text) except Exception as e: - print(f"MarianMT translation error: {e}") - return f"[MarianMT Error] {text}" + print(f"Translation error: {e}") + try: + translator = GoogleTranslator(source='auto', target=self.to_lang) + return translator.translate(text) + except Exception as e2: + print(f"Translation fallback error: {e2}") + return f"[Error] {text}" def get_translator(settings, status_callback=None): - """Factory function to get the correct translator instance.""" - if not settings.get("translation_enabled"): + if not settings.get("translation_enabled", True): return None backend = settings.get("translation_backend", "ArgosTranslate") - from_lang = settings.get("language", "English").split(" ")[0] + from_lang = settings.get("language", "English") to_lang = settings.get("translation_target_language", "Spanish") + print(f"Creating translator: {from_lang} -> {to_lang} using {backend}") # Debug + if backend == "ArgosTranslate": return ArgosTranslator(from_lang, to_lang, status_callback) elif backend == "MarianMT": @@ -143,15 +160,11 @@ def get_translator(settings, status_callback=None): def get_available_argos_languages(): - """Helper to get a list of all unique language names from Argos packages.""" - try: - argostranslate.package.update_package_index() - packages = argostranslate.package.get_available_packages() - lang_names = set() - for pkg in packages: - lang_names.add(pkg.from_name) - lang_names.add(pkg.to_name) - return sorted(list(lang_names)) - except Exception as e: - print(f"Could not fetch Argos language list: {e}") - return ["English", "Spanish", "French", "German"] # Fallback \ No newline at end of file + """Return list of supported languages""" + return [ + "English", "Hindi", "Telugu", "Tamil", "Japanese", "French", + "German", "Spanish", "Chinese", "Arabic", "Russian", "Portuguese", + "Italian", "Korean", "Dutch", "Turkish", "Polish", "Swedish", + "Vietnamese", "Thai", "Gujarati", "Kannada", "Malayalam", + "Marathi", "Punjabi", "Bengali", "Urdu" + ] \ No newline at end of file diff --git a/vosk-model-spk-0.4/README.txt b/vosk-model-spk-0.4/README.txt new file mode 100644 index 0000000..16cc24a --- /dev/null +++ b/vosk-model-spk-0.4/README.txt @@ -0,0 +1,119 @@ + + UPLOADER David Snyder + DATE 2018-05-30 + KALDI VERSION 108832d + + This directory contains files generated from the recipe in + egs/callhome_diarization/v2/. It's contents should be placed in a similar + directory, with symbolic links to diarization/, sid/, steps/, etc. This was + created when Kaldi's master branch was at git log + 2ad8d7821867a199e435aa36bbd13af6ed937c94. + + + I. Files list + ------------------------------------------------------------------------------ + + ./ + README.txt This file + run.sh A copy of the egs/callhome_diarization/v2/run.sh + at the time of uploading this file. Use this to + figure out how to compute features, extract + embeddings, etc. + + local/nnet3/xvector/tuning/ + run_xvector_1a.sh This is the default recipe, at the time of + uploading this resource. The script generates + the configs, egs, and trains the model. + + conf/ + vad.conf The energy-based VAD configuration + mfcc.conf MFCC configuration + + exp/xvector_nnet_1a/ + final.raw The pretrained DNN model + nnet.config The nnet3 config file that was used when the + DNN model was first instantiated. + extract.config Another nnet3 config file that modifies the DNN + final.raw to extract x-vectors. It should be + automatically handled by the script + extract_xvectors.sh. + min_chunk_size Min chunk size used (see extract_xvectors.sh) + max_chunk_size Max chunk size used (see extract_xvectors.sh) + srand The RNG seed used when creating the DNN + + exp/xvectors_callhome1/ + mean.vec Vector for centering, from callhome1 + transform.mat Whitening matrix, trained on callhome1 + plda PLDA model for callhome1, trained on SRE data + + exp/xvectors_callhome2/ + mean.vec Vector for centering, from callhome2 + transform.mat Whitening matrix, trained on callhome2 + plda PLDA model for callhome1, trained on SRE data + + + II. Citation + ------------------------------------------------------------------------------ + + If you wish to use this architecture in a publication, please cite one of the + following papers. + + The x-vector architecture: + + @inproceedings{snyder2018xvector, + title={X-vectors: Robust DNN Embeddings for Speaker Recognition}, + author={Snyder, D. and Garcia-Romero, D. and Sell, G. and Povey, D. and Khudanpur, S.}, + booktitle={2018 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP)}, + year={2018}, + organization={IEEE}, + url={http://www.danielpovey.com/files/2018_icassp_xvectors.pdf} + } + + Diarization with x-vectors: + + @article{sell2018dihard, + title={Diarization is Hard: Some Experiences and Lessons Learned for the JHU Team in the Inaugural DIHARD Challenge}, + author={Sell, G. and Snyder, D. and McCree, A. and Garcia-Romero, D. and Villalba, J. and Maciejewski, M. and Manohar, V. and Dehak, N. and Povey, D. and Watanabe, S. and Khudanpur, J.}, + journal={Interspeech}, + year={2018} + } + + + III. Recipe README.txt + ------------------------------------------------------------------------------ + The following text is the README.txt from egs/callhome_diarization/v2 at the + time this archive was created. + + This recipe replaces i-vectors used in the v1 recipe with embeddings extracted + from a deep neural network. In the scripts, we refer to these embeddings as + "x-vectors." The x-vector recipe in + local/nnet3/xvector/tuning/run_xvector_1a.sh is closesly based on the + following paper: + + However, in this example, the x-vectors are used for diarization, rather + than speaker recognition. Diarization is performed by splitting speech + segments into very short segments (e.g., 1.5 seconds), extracting embeddings + from the segments, and clustering them to obtain speaker labels. + + The recipe uses the following data for system development. This is in + addition to the NIST SRE 2000 dataset (Callhome) which is used for + evaluation (see ../README.txt). + + Corpus LDC Catalog No. + SRE2004 LDC2006S44 + SRE2005 Train LDC2011S01 + SRE2005 Test LDC2011S04 + SRE2006 Train LDC2011S09 + SRE2006 Test 1 LDC2011S10 + SRE2006 Test 2 LDC2012S01 + SRE2008 Train LDC2011S05 + SRE2008 Test LDC2011S08 + SWBD2 Phase 2 LDC99S79 + SWBD2 Phase 3 LDC2002S06 + SWBD Cellular 1 LDC2001S13 + SWBD Cellular 2 LDC2004S07 + + The following datasets are used in data augmentation. + + MUSAN http://www.openslr.org/17 + RIR_NOISES http://www.openslr.org/28 diff --git a/vosk-model-spk-0.4/final.ext.raw b/vosk-model-spk-0.4/final.ext.raw new file mode 100644 index 0000000..3387645 Binary files /dev/null and b/vosk-model-spk-0.4/final.ext.raw differ diff --git a/vosk-model-spk-0.4/mean.vec b/vosk-model-spk-0.4/mean.vec new file mode 100644 index 0000000..a54d0df --- /dev/null +++ b/vosk-model-spk-0.4/mean.vec @@ -0,0 +1 @@ + [ 4.450152 4.672029 4.148891 -1.711527 3.509846 2.931994 2.850384 3.178227 0.2563171 -0.9261234 3.37196 0.1472566 5.635284 -0.01870821 1.972103 -0.9502754 4.401544 2.795261 2.67637 3.917823 0.6549923 -0.02103148 4.064806 4.100016 3.700118 1.252804 5.399523 4.084152 4.106742 3.5622 4.165306 -0.2494654 -0.9603948 4.272289 -2.332889 -0.7292819 3.646834 0.3090337 4.624666 5.089351 -5.635771 1.634198 1.089098 4.363739 3.618721 0.2134228 -0.3965465 5.353687 4.034757 4.032773 3.749556 3.166129 3.868708 4.381798 -0.02561651 0.3426051 4.402168 0.1237091 0.8197291 3.809948 -2.995811 -1.648535 3.202967 3.239381 3.250949 -0.9064079 4.452719 0.2775586 0.80832 3.036884 5.163679 0.4273587 3.537773 2.539269 3.151272 4.064805 3.56104 4.244997 3.660802 4.949434 4.013721 1.418729 1.845101 4.74059 3.280786 -1.731479 1.492544 -2.88268 5.013491 5.327713 -2.668042 1.02902 -0.9622369 3.954224 3.2533 3.348548 2.906777 -0.3059559 4.595854 0.3410174 2.116138 4.830284 3.402886 3.014466 4.481457 5.14358 2.05649 3.883894 -0.9075359 4.574888 4.064843 -1.416883 3.493051 -0.06792944 4.978102 4.930044 4.138368 2.826191 4.031521 2.575887 0.7125556 4.15551 2.601444 1.190357 -1.060124 0.9739355 4.671662 -1.613742 ] diff --git a/vosk-model-spk-0.4/mfcc.conf b/vosk-model-spk-0.4/mfcc.conf new file mode 100644 index 0000000..715d695 --- /dev/null +++ b/vosk-model-spk-0.4/mfcc.conf @@ -0,0 +1,5 @@ +--sample-frequency=8000 +--high-freq=3700 +--low-freq=20 +--num-ceps=23 +--allow-downsample=true diff --git a/vosk-model-spk-0.4/transform.mat b/vosk-model-spk-0.4/transform.mat new file mode 100644 index 0000000..ceb748c Binary files /dev/null and b/vosk-model-spk-0.4/transform.mat differ