Files
voculinux/src/vocalinux/main.py
T
Jatin K MalikandGitHub fdf1de5180 fix(transcription): add space after punctuation when transcribing again (#151)
When transcribing text continuously (stop and start again), the new
text was being injected immediately after the last character without a
space. This caused issues like "Hello world.This is a test" instead of
"Hello world. This is a test".

Fix:
- Check if the last injected text ends with punctuation or other characters
  that typically need a space after them (., ,, !, ?, ;, :, quotes, etc.)
- If yes, add a space before injecting the new text
- Log when space is added for debugging

Characters requiring space:
- Punctuation: . , ! ? ; : ) ] } - _
- Quotes: " '

Closes #151
2026-02-05 18:47:38 -08:00

276 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""
Main entry point for Vocalinux application.
"""
import argparse
import logging
import sys
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
# Note: GTK-dependent modules (tray_indicator) are imported lazily after
# dependency checking to provide better error messages for pip/pipx users
def parse_arguments():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(description="Vocalinux")
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
# default model, language and engine are loaded from default config
# due to priority of args over config
parser.add_argument(
"--model",
type=str,
choices=["small", "medium", "large"],
help="Speech recognition model size (small, medium, large)",
)
parser.add_argument(
"--language",
type=str,
choices=[
"auto",
"en-us",
"en-in",
"hi",
"es",
"fr",
"de",
"it",
"pt",
"ru",
"zh",
"ja",
"ko",
"ar",
],
help=(
"Speech recognition language (auto for auto-detect, en-us, "
"hi, es, fr, de, it, pt, ru, zh, etc.)"
),
)
parser.add_argument(
"--engine",
type=str,
choices=["vosk", "whisper"],
help="Speech recognition engine to use",
)
parser.add_argument("--wayland", action="store_true", help="Force Wayland compatibility mode")
return parser.parse_args()
def check_dependencies():
"""Check for required dependencies and provide helpful error messages."""
missing_system_deps = []
missing_python_deps = []
# Check for GTK3
try:
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa: F401
except (ImportError, ValueError):
missing_system_deps.append(
"GTK3 (install with: sudo apt install python3-gi gir1.2-gtk-3.0)"
)
# Check for AppIndicator3 / Ayatana AppIndicator
try:
import gi
gi.require_version("AppIndicator3", "0.1")
from gi.repository import AppIndicator3 # noqa: F401
except (ImportError, ValueError):
try:
import gi
gi.require_version("AyatanaAppIndicator3", "0.1")
from gi.repository import AyatanaAppIndicator3 # noqa: F401
except (ImportError, ValueError):
missing_system_deps.append(
"AppIndicator3 (install with: sudo apt install gir1.2-appindicator3-0.1) "
"Note: On Debian 11+ use gir1.2-ayatanaappindicator3-0.1 instead"
)
# pynput is used for keyboard detection but we check at module startup
# requests is used by various components
# These are intentional checks to provide user-friendly error messages
try:
import pynput # noqa: F401
except ImportError:
missing_python_deps.append("pynput (install with: pip install pynput)")
try:
import requests # noqa: F401
except ImportError:
missing_python_deps.append("requests (install with: pip install requests)")
if missing_system_deps or missing_python_deps:
logger.error("Missing required dependencies:")
for dep in missing_system_deps + missing_python_deps:
logger.error(f" - {dep}")
if missing_system_deps:
logger.error("")
logger.error("If you installed via pip/pipx, you also need system GTK packages:")
logger.error(" sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-appindicator3-0.1")
logger.error("")
logger.error("For the best experience, install using the recommended method:")
logger.error(
" curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh | bash"
)
return False
return True
def main():
"""Main entry point for the application."""
args = parse_arguments()
# Configure debug logging if requested
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
logger.debug("Debug logging enabled")
# Check dependencies first (before importing GTK-dependent modules)
if not check_dependencies():
logger.error("Cannot start Vocalinux due to missing dependencies")
sys.exit(1)
# Now it's safe to import GTK-dependent modules
from .speech_recognition import recognition_manager
from .text_injection import text_injector
from .ui import tray_indicator
from .ui.action_handler import ActionHandler
from .ui.config_manager import ConfigManager
from .ui.logging_manager import initialize_logging
# Initialize logging manager early
initialize_logging()
logger.info("Logging system initialized")
config_manager = ConfigManager()
saved_settings = config_manager.get_settings().get("speech_recognition", {})
audio_settings = config_manager.get_settings().get("audio", {})
# CLI arguments take precedence over saved config
# We need to check if the user explicitly provided arguments
# by examining sys.argv since argparse defaults don't tell us this
cli_engine_set = any(arg.startswith("--engine") for arg in sys.argv[1:])
cli_model_set = any(arg.startswith("--model") for arg in sys.argv[1:])
cli_language_set = any(arg.startswith("--language") for arg in sys.argv[1:])
# Use CLI args if explicitly set, otherwise fall back to saved config, then defaults
if cli_engine_set:
engine = args.engine
logger.info(f"Using engine={engine} (from command line)")
else:
engine = saved_settings.get("engine", args.engine)
logger.info(f"Using engine={engine} (from saved config)")
if cli_language_set:
language = args.language
logger.info(f"Using language={language} (from command line)")
else:
language = saved_settings.get("language", args.language)
logger.info(f"Using language={language} (from saved config)")
if cli_model_set:
model_size = args.model
logger.info(f"Using model={model_size} (from command line)")
else:
model_size = saved_settings.get("model_size", args.model)
logger.info(f"Using model={model_size} (from saved config)")
vad_sensitivity = saved_settings.get("vad_sensitivity", 3)
silence_timeout = saved_settings.get("silence_timeout", 2.0)
audio_device_index = audio_settings.get("device_index", None)
logger.info(f"Final settings: engine={engine}, language={language}, model={model_size}")
if audio_device_index is not None:
logger.info(f"Using audio device index={audio_device_index} (from saved config)")
# Initialize main components
logger.info("Initializing Vocalinux...")
try:
# Initialize speech recognition engine with saved/configured settings
speech_engine = recognition_manager.SpeechRecognitionManager(
engine=engine,
model_size=model_size,
language=language,
vad_sensitivity=vad_sensitivity,
silence_timeout=silence_timeout,
audio_device_index=audio_device_index,
)
# Initialize text injection system
text_system = text_injector.TextInjector(wayland_mode=args.wayland)
# Initialize action handler
action_handler = ActionHandler(text_system)
# Create a wrapper function to track injected text for action handler
def text_callback_wrapper(text: str):
"""Wrapper to track injected text and handle it."""
# Check if we need to add a space before the new text
text_to_inject = text
# Punctuation and characters that typically need a space after them
chars_needing_space = {
".",
",",
"!",
"?",
";",
":",
")",
"]",
"}",
"-",
"_",
# Also quotes
'"',
"'",
}
# If last injected text exists and ends with a character that needs space,
# add a space before the new text
if action_handler.last_injected_text and action_handler.last_injected_text.strip():
last_char = action_handler.last_injected_text[-1]
if last_char in chars_needing_space:
text_to_inject = " " + text_to_inject
logger.debug(f"Added space before new text (last char: '{last_char}')")
success = text_system.inject_text(text_to_inject)
if success:
action_handler.set_last_injected_text(text)
# Connect speech recognition to text injection and action handling
speech_engine.register_text_callback(text_callback_wrapper)
speech_engine.register_action_callback(action_handler.handle_action)
# Initialize and start the system tray indicator
indicator = tray_indicator.TrayIndicator(
speech_engine=speech_engine,
text_injector=text_system,
)
# Start the GTK main loop
indicator.run()
except Exception as e:
logger.error(f"Failed to initialize Vocalinux: {e}")
logger.error("Please check the logs above for more details")
sys.exit(1)
if __name__ == "__main__":
main()