Utility-Skripte¶
Automatisch generierte Übersicht der Hilfsskripte.
Setup-Skripte¶
setup/macos_setup.sh¶
#!/bin/bash
#
# setup/macos_setup.sh
# Run this setup script from the project's root directory.
#
SCRIPT_NAME=$(basename "$0")
# Check if the script is run from the project root.
# This check is more robust than changing directory.
if [ ! -f "requirements.txt" ]; then
echo "ERROR: Please run this script from the project's root directory."
echo ""
echo "cd .. ; ./setup/$SCRIPT_NAME"
exit 1
fi
should_remove_zips_after_unpack=true
# --- Make script location-independent ---
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(dirname "$SCRIPT_DIR")
cd "$PROJECT_ROOT"
echo "--> Running setup from project root: $(pwd)"
set -e
echo "--- Starting STT Setup for macOS ---"
# setup/macos_setup.sh
# --- 1. System Dependencies ---
if ! command -v brew &> /dev/null; then
echo "ERROR: Homebrew not found. Please install it from https://brew.sh"
exit 1
fi
echo "--> Checking for a compatible Java version (>=17)..."
JAVA_OK=0
if command -v java &> /dev/null; then
VERSION=$(java -version 2>&1 | awk -F'[."]' '/version/ {print $2}')
if [ "$VERSION" -ge 17 ]; then
echo " -> Found compatible Java version $VERSION. OK."
JAVA_OK=1
else
echo " -> Found Java version $VERSION, but we need >=17."
fi
else
echo " -> No Java executable found."
fi
if [ "$JAVA_OK" -eq 0 ]; then
echo " -> Installing a modern JDK via Homebrew..."
brew install openjdk
fi
echo "--> Installing other core dependencies..."
brew install fswatch wget unzip portaudio
# --- 2. Python Virtual Environment ---
# We check if the venv directory exists before creating it.
if [ ! -d ".venv" ]; then
echo "--> Creating Python virtual environment in './.venv'..."
python3 -m venv .venv
else
echo "--> Virtual environment already exists. Skipping creation."
fi
# --- 3. Python Requirements ---
echo "--> Preparing requirements for macOS..."
# The macOS equivalent, 'fswatch', is already installed via Homebrew.
sed -i.bak '/inotify-tools/d' requirements.txt
echo "--> Installing Python requirements into the virtual environment..."
if ! ./.venv/bin/pip install -r requirements.txt; then
echo "ERROR: Failed to install requirements. Trying to fix other common version issues..."
# Example: Fix vosk version, then retry
sed -i.bak 's/vosk==0.3.45/vosk/' requirements.txt
# We run the command again after the potential fixes
./.venv/bin/pip install -r requirements.txt
fi
# --- 4. Project Structure and Configuration ---
echo "--> Setting up project directories and initial files..."
python3 "scripts/py/func/create_required_folders.py" "$(pwd)"
# ==============================================================================
# --- 5. Download and Extract Required Components ---
# This block intelligently handles downloads and extractions.
# ==============================================================================
echo "--> Checking for required components (LanguageTool, Vosk-Models)..."
# --- Configuration ---
PREFIX="Z_"
# Format: "BaseName FinalDirName DestinationPath"
ARCHIVE_CONFIG=(
"LanguageTool-6.6 LanguageTool-6.6 ."
"vosk-model-en-us-0.22 vosk-model-en-us-0.22 ./models"
"vosk-model-small-en-us-0.15 vosk-model-small-en-us-0.15 ./models"
"vosk-model-de-0.21 vosk-model-de-0.21 ./models"
"lid.176 lid.176.bin ./models"
)
DOWNLOAD_REQUIRED=false
# --- Phase 1: Check and attempt to restore from local ZIP cache ---
echo " -> Phase 1: Checking and trying to restore from local cache..."
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
target_path="$dest_path/$final_name"
zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"
# If the component already exists, we're good for this one.
if [ -e "$target_path" ]; then
continue
fi
# The component is missing. Let's see if we can unzip it from a local cache.
echo " -> Missing: '$target_path'. Searching for '$zip_file'..."
if [ -f "$zip_file" ]; then
echo " -> Found ZIP cache. Extracting '$zip_file'..."
unzip -q "$zip_file" -d "$dest_path"
else
# The ZIP is not there. We MUST run the downloader.
echo " -> ZIP cache not found. A download is required."
DOWNLOAD_REQUIRED=true
fi
done
# --- Phase 2: Download if necessary ---
if [ "$DOWNLOAD_REQUIRED" = true ]; then
echo " -> Phase 2: Running Python downloader for missing components..."
# Create the models directory before attempting to download files into it.
mkdir -p ./models
./.venv/bin/python tools/download_all_packages.py
echo " -> Downloader finished. Retrying extraction..."
# After downloading, we must re-check and extract anything that's still missing.
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
target_path="$dest_path/$final_name"
zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"
if [ -e "$target_path" ]; then
continue
fi
if [ -f "$zip_file" ]; then
echo " -> Extracting newly downloaded '$zip_file'..."
unzip -q "$zip_file" -d "$dest_path"
else
echo " -> FATAL: Downloader ran but '$zip_file' is still missing. Aborting."
exit 1
fi
done
fi
echo "--> All components are present and correctly placed."
# ==============================================================================
# --- End of Download/Extract block ---
# ==============================================================================
source "$(dirname "${BASH_SOURCE[0]}")/../scripts/sh/get_lang.sh"
# --- 6. Completion ---
echo ""
echo "--- Setup for macOS is complete! ---"
echo ""
echo "IMPORTANT NEXT STEPS:"
echo ""
echo "1. Configure Java PATH:"
echo " To make Java available, you may need to add it to your shell's PATH."
echo " Run this command in your terminal:"
echo ' export PATH="$(brew --prefix openjdk@21)/bin:$PATH"'
echo " (Consider adding this line to your ~/.zshrc or ~/.bash_profile file to make it permanent)."
echo ""
echo "2. Activate Environment and Run:"
echo " To start the application, use the following commands:"
echo " source .venv/bin/activate"
echo " ./scripts/restart_venv_and_run-server.sh"
echo ""
echo "3. Potential macOS Permissions:"
echo " The 'xdotool' utility may require you to grant Accessibility permissions"
echo " to your Terminal app in 'System Settings -> Privacy & Security -> Accessibility'."
echo ""
setup/manjaro_arch_setup.sh¶
#!/bin/bash
#
# setup/manjaro_arch_setup.sh
# Run this setup script from the project's root directory.
#
SCRIPT_NAME=$(basename "$0")
# Check if the script is run from the project root.
# This check is more robust than changing directory.
if [ ! -f "requirements.txt" ]; then
echo "ERROR: Please run this script from the project's root directory."
echo ""
echo "cd .. ; ./setup/$SCRIPT_NAME"
exit 1
fi
should_remove_zips_after_unpack=true
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(dirname "$SCRIPT_DIR")
cd "$PROJECT_ROOT"
echo "--> Running setup from project root: $(pwd)"
# --- End of location-independent block ---
set -e
echo "--- Starting STT Setup for Manjaro/Arch Linux ---"
# setup/manjaro_arch_setup.sh
# --- 1. System Dependencies ---
echo "--> Checking for a compatible Java version (>=17)..."
JAVA_OK=0
if command -v java &> /dev/null; then
VERSION=$(java -version 2>&1 | awk -F[\".] '/version/ {print ($2 == "1") ? $3 : $2}')
if [ "$VERSION" -ge 17 ]; then
echo " -> Found compatible Java version $VERSION. OK."
JAVA_OK=1
else
echo " -> Found Java version $VERSION, but we need >=17."
fi
else
echo " -> No Java executable found."
fi
if [ "$JAVA_OK" -eq 0 ]; then
echo " -> Installing a modern JDK to satisfy the requirement..."
sudo pacman -S --noconfirm --needed jdk-openjdk
fi
echo "--> Installing other core dependencies..."
sudo pacman -S --noconfirm --needed \
inotify-tools wget unzip portaudio xdotool
# --- 2. Python Virtual Environment ---
# We check if the venv directory exists before creating it.
if [ ! -d ".venv" ]; then
echo "--> Creating Python virtual environment in './.venv'..."
python3 -m venv .venv
else
echo "--> Virtual environment already exists. Skipping creation."
fi
# --- 3. Python Requirements ---
# We call pip from the venv directly. This is more robust than sourcing 'activate'.
echo "--> Installing Python requirements into the virtual environment..."
./.venv/bin/pip install -r requirements.txt
# --- 4. Project Structure and Configuration ---
echo "--> Setting up project directories and initial files..."
# THIS IS THE KEY CHANGE. We call the Python script and pass the current
# working directory (which is the project root) as an argument.
# This one command replaces all old 'mkdir' and 'touch' commands for the project structure.
python3 "scripts/py/func/create_required_folders.py" "$(pwd)"
# ==============================================================================
# --- 5. Download and Extract Required Components ---
# This block intelligently handles downloads and extractions.
# ==============================================================================
echo "--> Checking for required components (LanguageTool, Vosk-Models)..."
# --- Configuration ---
PREFIX="Z_"
# Format: "BaseName FinalDirName DestinationPath"
ARCHIVE_CONFIG=(
"LanguageTool-6.6 LanguageTool-6.6 ."
"vosk-model-en-us-0.22 vosk-model-en-us-0.22 ./models"
"vosk-model-small-en-us-0.15 vosk-model-small-en-us-0.15 ./models"
"vosk-model-de-0.21 vosk-model-de-0.21 ./models"
"lid.176 lid.176.bin ./models"
)
DOWNLOAD_REQUIRED=false
# --- Phase 1: Check and attempt to restore from local ZIP cache ---
echo " -> Phase 1: Checking and trying to restore from local cache..."
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
target_path="$dest_path/$final_name"
zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"
# If the component already exists, we're good for this one.
if [ -e "$target_path" ]; then
continue
fi
# The component is missing. Let's see if we can unzip it from a local cache.
echo " -> Missing: '$target_path'. Searching for '$zip_file'..."
if [ -f "$zip_file" ]; then
echo " -> Found ZIP cache. Extracting '$zip_file'..."
unzip -q "$zip_file" -d "$dest_path"
else
# The ZIP is not there. We MUST run the downloader.
echo " -> ZIP cache not found. A download is required."
DOWNLOAD_REQUIRED=true
fi
done
# --- Phase 2: Download if necessary ---
if [ "$DOWNLOAD_REQUIRED" = true ]; then
echo " -> Phase 2: Running Python downloader for missing components..."
# Create the models directory before attempting to download files into it.
mkdir -p ./models
./.venv/bin/python tools/download_all_packages.py
echo " -> Downloader finished. Retrying extraction..."
# After downloading, we must re-check and extract anything that's still missing.
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
target_path="$dest_path/$final_name"
zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"
if [ -e "$target_path" ]; then
continue
fi
if [ -f "$zip_file" ]; then
echo " -> Extracting newly downloaded '$zip_file'..."
unzip -q "$zip_file" -d "$dest_path"
else
echo " -> FATAL: Downloader ran but '$zip_file' is still missing. Aborting."
exit 1
fi
done
fi
echo "--> All components are present and correctly placed."
# ==============================================================================
# --- End of Download/Extract block ---
# ==============================================================================
# Function to extract and clean up
expand_and_cleanup() {
local zip_file=$1
local expected_dir=$2
local dest_path=$3
# Check if final directory already exists
if [ -d "$dest_path/$expected_dir" ]; then
echo " -> Directory '$expected_dir' already exists. Skipping."
return
fi
# Check if the downloaded zip exists
if [ ! -f "$zip_file" ]; then
echo " -> FATAL: Expected archive not found: '$zip_file'"
exit 1
fi
echo " -> Extracting $zip_file to $dest_path..."
unzip -q "$zip_file" -d "$dest_path"
# Clean up the zip file
if [ "$should_remove_zips_after_unpack" = true ] ; then
rm "$zip_file"
fi
echo " -> Cleaned up ZIP file: $zip_file"
}
# Execute extraction for each archive
for config_line in "${BASE_CONFIG[@]}"; do
# Read the space-separated values into variables
read -r base_name final_name dest_path <<< "$config_line"
# CONSTRUCT THE FILENAMES, including the prefix for the zip file
zip_file="${PREFIX}${base_name}.zip"
expected_dir="${base_name}" # The final directory name has no prefix
if [ ! -e "$dest_path/$final_name" ]; then
echo " -> MISSING: '$dest_path/$final_name'. Download is required."
download_needed=true
break # Ein fehlendes Teil reicht, Prüfung kann stoppen
fi
expand_and_cleanup "$zip_file" "$expected_dir" "$dest_path"
done
echo " -> Extraction and cleanup successful."
source "$(dirname "${BASH_SOURCE[0]}")/../scripts/sh/get_lang.sh"
# --- 5. Project Configuration ---
# Ensures Python can treat 'config' directories as packages.
echo "--> Creating Python package markers (__init__.py)..."
touch config/__init__.py
touch config/languagetool_server/__init__.py
# --- User-Specific Configuration ---
# This part is about user config, so it's fine for it to stay here.
CONFIG_FILE="$HOME/.config/sl5-stt/config.toml"
mkdir -p "$(dirname "$CONFIG_FILE")"
# Only write the file if it doesn't exist to avoid overwriting user settings
if [ ! -f "$CONFIG_FILE" ]; then
echo "[paths]" > "$CONFIG_FILE"
echo "project_root = \"$(pwd)\"" >> "$CONFIG_FILE"
fi
# --- 6. Completion ---
echo ""
echo "--- Setup for Manjaro/Arch is complete! ---"
echo ""
echo "To activate the environment and run the server, use the following commands:"
echo " source .venv/bin/activate"
echo " ./scripts/restart_venv_and_run-server.sh"
echo ""
setup/suse_setup.sh¶
#!/bin/bash
# setup/suse_setup.sh
# Run this setup script from the project's root directory.
SCRIPT_NAME=$(basename "$0")
# Check if the script is run from the project root.
# This check is more robust than changing directory.
if [ ! -f "requirements.txt" ]; then
echo "ERROR: Please run this script from the project's root directory."
echo ""
echo "cd .. ; ./setup/$SCRIPT_NAME"
exit 1
fi
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(dirname "$SCRIPT_DIR")
#cd "$PROJECT_ROOT"
should_remove_zips_after_unpack=true
echo "--- Starting STT Setup for openSUSE ---"
# --- End of location-independent block ---
set -e
# --- 1. System Dependencies ---
# (This section remains unchanged)
echo "--> Checking for a compatible Java version (>=17)..."
JAVA_OK=0
if command -v java &> /dev/null; then
VERSION=$(java -version 2>&1 | awk -F[\".] '/version/ {print ($2 == "1") ? $3 : $2}')
if [ "$VERSION" -ge 17 ]; then
echo " -> Found compatible Java version $VERSION. OK."
JAVA_OK=1
else
echo " -> Found Java version $VERSION, but we need >=17."
fi
else
echo " -> No Java executable found."
fi
if [ "$JAVA_OK" -eq 0 ]; then
echo " -> Installing a modern JDK (>=17)..."
# SUSE CHANGE: Use zypper instead of apt
sudo zypper refresh && sudo zypper -n install java-21-openjdk
fi
echo "--> Installing other core dependencies..."
# SUSE CHANGE: Use zypper and adjust package names
sudo zypper -n install \
inotify-tools wget unzip portaudio-devel python3-pip
# --- 2. Python Virtual Environment ---
# (This section remains unchanged)
if [ ! -d ".venv" ]; then
echo "--> Creating Python virtual environment in './.venv'..."
python3 -m venv .venv
else
echo "--> Virtual environment already exists. Skipping creation."
fi
# --- 3. Python Requirements ---
# (This section remains unchanged)
echo "--> Installing Python requirements into the virtual environment..."
./.venv/bin/pip install -r requirements.txt
# --- 4. Project Structure and Configuration ---
echo "--> Setting up project directories and initial files..."
# THIS IS THE KEY CHANGE. We call the Python script and pass the current
# working directory (which is the project root) as an argument.
# This one command replaces all old 'mkdir' and 'touch' commands for the project structure.
python3 "scripts/py/func/create_required_folders.py" "$(pwd)"
# ==============================================================================
# --- 5. Download and Extract Required Components ---
# This block intelligently handles downloads and extractions.
# ==============================================================================
echo "--> Checking for required components (LanguageTool, Vosk-Models)..."
# --- Configuration ---
PREFIX="Z_"
# Format: "BaseName FinalDirName DestinationPath"
ARCHIVE_CONFIG=(
"LanguageTool-6.6 LanguageTool-6.6 ."
"vosk-model-en-us-0.22 vosk-model-en-us-0.22 ./models"
"vosk-model-small-en-us-0.15 vosk-model-small-en-us-0.15 ./models"
"vosk-model-de-0.21 vosk-model-de-0.21 ./models"
"lid.176 lid.176.bin ./models"
)
DOWNLOAD_REQUIRED=false
# --- Phase 1: Check and attempt to restore from local ZIP cache ---
echo " -> Phase 1: Checking and trying to restore from local cache..."
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
target_path="$dest_path/$final_name"
zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"
# If the component already exists, we're good for this one.
if [ -e "$target_path" ]; then
continue
fi
# The component is missing. Let's see if we can unzip it from a local cache.
echo " -> Missing: '$target_path'. Searching for '$zip_file'..."
if [ -f "$zip_file" ]; then
echo " -> Found ZIP cache. Extracting '$zip_file'..."
unzip -q "$zip_file" -d "$dest_path"
else
# The ZIP is not there. We MUST run the downloader.
echo " -> ZIP cache not found. A download is required."
DOWNLOAD_REQUIRED=true
fi
done
# --- Phase 2: Download if necessary ---
if [ "$DOWNLOAD_REQUIRED" = true ]; then
echo " -> Phase 2: Running Python downloader for missing components..."
# Create the models directory before attempting to download files into it.
mkdir -p ./models
./.venv/bin/python tools/download_all_packages.py
echo " -> Downloader finished. Retrying extraction..."
# After downloading, we must re-check and extract anything that's still missing.
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
target_path="$dest_path/$final_name"
zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"
if [ -e "$target_path" ]; then
continue
fi
if [ -f "$zip_file" ]; then
echo " -> Extracting newly downloaded '$zip_file'..."
unzip -q "$zip_file" -d "$dest_path"
else
echo " -> FATAL: Downloader ran but '$zip_file' is still missing. Aborting."
exit 1
fi
done
fi
echo "--> All components are present and correctly placed."
# ==============================================================================
# --- End of Download/Extract block ---
# ==============================================================================
source "$(dirname "${BASH_SOURCE[0]}")/../scripts/sh/get_lang.sh"
# --- 5. Project Configuration ---
# Ensures Python can treat 'config' directories as packages.
echo "--> Creating Python package markers (__init__.py)..."
touch config/__init__.py
touch config/languagetool_server/__init__.py
# --- User-Specific Configuration ---
# This part is about user config, so it's fine for it to stay here.
CONFIG_FILE="$HOME/.config/sl5-stt/config.toml"
echo "--> Ensuring user config file exists at $CONFIG_FILE..."
mkdir -p "$(dirname "$CONFIG_FILE")"
# Only write the file if it doesn't exist to avoid overwriting user settings
if [ ! -f "$CONFIG_FILE" ]; then
echo "[paths]" > "$CONFIG_FILE"
echo "project_root = \"$(pwd)\"" >> "$CONFIG_FILE"
fi
# --- 6. Completion ---
echo ""
echo "--- Setup for openSUSE is complete! ---"
echo ""
echo "To activate the environment and run the server, use the following commands:"
echo " source .venv/bin/activate"
echo " ./scripts/restart_venv_and_run-server.sh"
echo ""
setup/ubuntu_setup.sh¶
#!/bin/bash
#
# setup/ubuntu_setup.sh
# Run this setup script from the project's root directory.
#
SCRIPT_NAME=$(basename "$0")
# Check if the script is run from the project root.
# This check is more robust than changing directory.
if [ ! -f "requirements.txt" ]; then
echo "ERROR: Please run this script from the project's root directory."
echo ""
echo "cd .. ; ./setup/$SCRIPT_NAME"
exit 1
fi
should_remove_zips_after_unpack=true
# --- Make script location-independent ---
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(dirname "$SCRIPT_DIR")
cd "$PROJECT_ROOT"
echo "--> Running setup from project root: $(pwd)"
# --- End of location-independent block ---
set -e
echo "--- Starting STT Setup for Debian/Ubuntu ---"
# setup/ubuntu_setup.sh
# --- 1. System Dependencies ---
# (This section remains unchanged)
echo "--> Checking for a compatible Java version (>=17)..."
JAVA_OK=0
if command -v java &> /dev/null; then
VERSION=$(java -version 2>&1 | awk -F[\".] '/version/ {print ($2 == "1") ? $3 : $2}')
if [ "$VERSION" -ge 17 ]; then
echo " -> Found compatible Java version $VERSION. OK."
JAVA_OK=1
else
echo " -> Found Java version $VERSION, but we need >=17."
fi
else
echo " -> No Java executable found."
fi
if [ "$JAVA_OK" -eq 0 ]; then
echo " -> Installing a modern JDK (>=17)..."
sudo apt-get update && sudo apt-get install -y openjdk-21-jdk
fi
echo "--> Installing other core dependencies..."
sudo apt-get install -y \
inotify-tools wget unzip portaudio19-dev python3-pip
# --- 2. Python Virtual Environment ---
# (This section remains unchanged)
if [ ! -d ".venv" ]; then
echo "--> Creating Python virtual environment in './.venv'..."
python3 -m venv .venv
else
echo "--> Virtual environment already exists. Skipping creation."
fi
# --- 3. Python Requirements ---
# (This section remains unchanged)
echo "--> Installing Python requirements into the virtual environment..."
./.venv/bin/pip install -r requirements.txt
# --- 4. Project Structure and Configuration ---
echo "--> Setting up project directories and initial files..."
# THIS IS THE KEY CHANGE. We call the Python script and pass the current
# working directory (which is the project root) as an argument.
# This one command replaces all old 'mkdir' and 'touch' commands for the project structure.
python3 "scripts/py/func/create_required_folders.py" "$(pwd)"
# ==============================================================================
# --- 5. Download and Extract Required Components ---
# This block intelligently handles downloads and extractions.
# ==============================================================================
echo "--> Checking for required components (LanguageTool, Vosk-Models)..."
# --- Configuration ---
PREFIX="Z_"
# Format: "BaseName FinalDirName DestinationPath"
ARCHIVE_CONFIG=(
"LanguageTool-6.6 LanguageTool-6.6 ."
"vosk-model-en-us-0.22 vosk-model-en-us-0.22 ./models"
"vosk-model-small-en-us-0.15 vosk-model-small-en-us-0.15 ./models"
"vosk-model-de-0.21 vosk-model-de-0.21 ./models"
"lid.176 lid.176.bin ./models"
)
DOWNLOAD_REQUIRED=false
# --- Phase 1: Check and attempt to restore from local ZIP cache ---
echo " -> Phase 1: Checking and trying to restore from local cache..."
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
target_path="$dest_path/$final_name"
zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"
# If the component already exists, we're good for this one.
if [ -e "$target_path" ]; then
continue
fi
# The component is missing. Let's see if we can unzip it from a local cache.
echo " -> Missing: '$target_path'. Searching for '$zip_file'..."
if [ -f "$zip_file" ]; then
echo " -> Found ZIP cache. Extracting '$zip_file'..."
unzip -q "$zip_file" -d "$dest_path"
else
# The ZIP is not there. We MUST run the downloader.
echo " -> ZIP cache not found. A download is required."
DOWNLOAD_REQUIRED=true
fi
done
# --- Phase 2: Download if necessary ---
if [ "$DOWNLOAD_REQUIRED" = true ]; then
echo " -> Phase 2: Running Python downloader for missing components..."
# Create the models directory before attempting to download files into it.
mkdir -p ./models
./.venv/bin/python tools/download_all_packages.py
echo " -> Downloader finished. Retrying extraction..."
# After downloading, we must re-check and extract anything that's still missing.
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
target_path="$dest_path/$final_name"
zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"
if [ -e "$target_path" ]; then
continue
fi
if [ -f "$zip_file" ]; then
echo " -> Extracting newly downloaded '$zip_file'..."
unzip -q "$zip_file" -d "$dest_path"
else
echo " -> FATAL: Downloader ran but '$zip_file' is still missing. Aborting."
exit 1
fi
done
fi
echo "--> All components are present and correctly placed."
# ==============================================================================
# --- End of Download/Extract block ---
# ==============================================================================
source "$(dirname "${BASH_SOURCE[0]}")/../scripts/sh/get_lang.sh"
# --- 5. Project Configuration ---
# Ensures Python can treat 'config' directories as packages.
echo "--> Creating Python package markers (__init__.py)..."
touch config/__init__.py
touch config/languagetool_server/__init__.py
# --- User-Specific Configuration ---
# This part is about user config, so it's fine for it to stay here.
CONFIG_FILE="$HOME/.config/sl5-stt/config.toml"
echo "--> Ensuring user config file exists at $CONFIG_FILE..."
mkdir -p "$(dirname "$CONFIG_FILE")"
# Only write the file if it doesn't exist to avoid overwriting user settings
if [ ! -f "$CONFIG_FILE" ]; then
echo "[paths]" > "$CONFIG_FILE"
echo "project_root = \"$(pwd)\"" >> "$CONFIG_FILE"
fi
# --- 6. Completion ---
echo ""
echo "--- Setup for Ubuntu is complete! ---"
echo ""
echo "To activate the environment and run the server, use the following commands:"
echo " source .venv/bin/activate"
echo " ./scripts/restart_venv_and_run-server.sh"
echo ""
setup/windows11_setup.bat¶
@echo off
ECHO start Windows 11 Setup-Script...
REM PowerShell, use Execution Policy this time
powershell.exe -ExecutionPolicy Bypass -File "%~dp0\windows11_setup.ps1"
call .\.venv\Scripts\python.exe scripts/py/func/setup_initial_model.py
ECHO
ECHO Script is ended.
ECHO You can close the window
pause
setup/windows11_setup.ps1¶
# setup/windows11_setup.ps1
# --- Make script location-independent ---
$ProjectRoot = Split-Path -Path $PSScriptRoot -Parent
Set-Location -Path $ProjectRoot
Write-Host "--> Running setup from project root: $(Get-Location)"
$ErrorActionPreference = "Stop"
# Configuration: Set to $false to keep ZIP files after extraction or use $true
$should_remove_zips_after_unpack = $true
# --- 1. Admin Rights Check ---
Write-Host "[*] Checking for Administrator privileges"
# Only check for admin rights if NOT running in a CI environment (like GitHub Actions)
if ($env:CI -ne 'true') {
# Check if the current user is an Administrator
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Host "[ERROR] Administrator privileges are required. Re-launching..."
# Re-run the current script with elevated privileges for GitHub Actions
Start-Process -FilePath $PSCommandPath -Verb RunAs
# Exit the current (non-admin) script
exit
}
}
Write-Host "[SUCCESS] Running with Administrator privileges."
# Only check for java if NOT running in a CI environment (like GitHub Actions)
if ($env:CI -ne 'true')
{
# --- 2. Java Installation Check ---
Write-Host "--> Checking Java installation..."
$JavaVersion = $null
try
{
$JavaOutput = & java -version 2>&1
if ($JavaOutput -match 'version "(\d+)\.')
{
$JavaVersion = [int]$matches[1]
}
elseif ($JavaOutput -match 'version "1\.(\d+)\.')
{
$JavaVersion = [int]$matches[1]
}
}
catch
{
Write-Host " -> Java not found in PATH."
}
if ($JavaVersion -and $JavaVersion -ge 17)
{
Write-Host " -> Java $JavaVersion detected. OK." -ForegroundColor Green
}
else
{
Write-Host " -> Java 17+ not found. Installing OpenJDK 17..." -ForegroundColor Yellow
try
{
winget install --id Microsoft.OpenJDK.17 --silent --accept-source-agreements --accept-package-agreements
if ($LASTEXITCODE -eq 0)
{
Write-Host " -> OpenJDK 17 installed successfully." -ForegroundColor Green
# Refresh PATH for current session
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User")
}
else
{
Write-Host "ERROR: Failed to install OpenJDK 17. Please install manually." -ForegroundColor Red
# exit 1 # the script works anywas usuallly. dont exit here!
}
}
catch
{
Write-Host "ERROR: Failed to install Java. Please install Java 17+ manually." -ForegroundColor Red
exit 1
}
}
}
# --- 3. Python Installation Check ---
Write-Host "--> Checking Python installation..."
$PythonVersion = $null
try {
$PythonOutput = & python --version 2>&1
if ($PythonOutput -match 'Python (\d+)\.(\d+)') {
$PythonMajor = [int]$matches[1]
$PythonMinor = [int]$matches[2]
if ($PythonMajor -eq 3 -and $PythonMinor -ge 8) {
$PythonVersion = "$PythonMajor.$PythonMinor"
}
}
} catch {
Write-Host " -> Python not found in PATH."
}
if ($PythonVersion) {
Write-Host " -> Python $PythonVersion detected. OK." -ForegroundColor Green
} else {
Write-Host " -> Python 3.8+ not found. Installing Python 3.11..." -ForegroundColor Yellow
try {
winget install --id Python.Python.3.11 --silent --accept-source-agreements --accept-package-agreements
if ($LASTEXITCODE -eq 0) {
Write-Host " -> Python 3.11 installed successfully." -ForegroundColor Green
# Refresh PATH for current session
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User")
} else {
Write-Host "ERROR: Failed to install Python. Please install manually." -ForegroundColor Red
exit 1
}
} catch {
Write-Host "ERROR: Failed to install Python. Please install Python 3.8+ manually." -ForegroundColor Red
exit 1
}
}
# --- 3. Python Virtual Environment ---
Write-Host "--> Creating Python virtual environment in '.\.venv'..."
if (-not (Test-Path -Path ".\.venv")) {
python -m venv .venv
} else {
Write-Host " -> Virtual environment already exists. Skipping creation."
}
# --- PATCH: Replace fasttext with fasttext-wheel in requirements.txt ---
Write-Host "--> Patching requirements.txt for Windows fasttext-wheel compatibility..."
(Get-Content requirements.txt) -replace '^fasttext.*$', 'fasttext-wheel' | Set-Content requirements.txt
# --- 5. Python Requirements ---
Write-Host "--> Installing Python requirements into the virtual environment..."
.\.venv\Scripts\pip.exe install -r requirements.txt
# --- 5. External Tools & Models (from Releases) ---
$LtDir = "LanguageTool-6.6"
# --- 6. External Tools & Models (using the robust Python downloader) ---
Write-Host "--> Downloading external tools and models via Python downloader..."
# Create the models directory before attempting to download files into it.
New-Item -ItemType Directory -Path ".\models" -Force | Out-Null
# Execute the downloader and check its exit code
# It downloads all required ZIPs to the project root.
.\.venv\Scripts\python.exe tools/download_all_packages.py
# $LASTEXITCODE contains the exit code of the last program that was run.
# 0 means success. Anything else is an error.
if ($LASTEXITCODE -ne 0) {
Write-Host "FATAL: The Python download script failed. Halting setup." -ForegroundColor Red
# Wir benutzen 'exit 1', um das ganze Skript sofort zu beenden.
exit 1
}
Write-Host " -> Python downloader completed successfully." -ForegroundColor Green
# --- Now, extract the downloaded archives ---
Write-Host "--> Extracting downloaded archives..."
$Prefix = "Z_"
$BaseConfig = @(
@{ BaseName = "LanguageTool-6.6"; Dest = "." },
@{ BaseName = "vosk-model-en-us-0.22"; Dest = ".\models" },
@{ BaseName = "vosk-model-small-en-us-0.15"; Dest = ".\models" },
@{ BaseName = "vosk-model-de-0.21"; Dest = ".\models" },
@{ BaseName = "lid.176"; Dest = ".\models" }
)
$ArchiveConfig = $BaseConfig | ForEach-Object {
[PSCustomObject]@{
Zip = "$($Prefix)$($_.BaseName).zip"
Dir = $_.BaseName
Dest = $_.Dest
}
}
# Function to extract and clean up
function Expand-And-Cleanup {
param (
[string]$ZipFile,
[string]$DestinationPath,
[string]$ExpectedDirName,
[bool]$CleanupAfterExtraction
)
# Check if final directory already exists
$FinalFullPath = Join-Path -Path $DestinationPath -ChildPath $ExpectedDirName
if (Test-Path $FinalFullPath) {
Write-Host " -> Directory '$ExpectedDirName' already exists. Skipping extraction."
return
}
# Look for the downloaded ZIP (Python script creates final ZIPs without Z_ prefix)
$FinalZipPath = Join-Path -Path $ProjectRoot -ChildPath $ZipFile
if (-not (Test-Path $FinalZipPath)) {
Write-Host "FATAL: Expected archive not found: '$FinalZipPath'" -ForegroundColor Red
Write-Host " The Python download script should have created this file." -ForegroundColor Red
exit 1
}
Write-Host " -> Extracting $ZipFile to $DestinationPath..."
Expand-Archive -Path $FinalZipPath -DestinationPath $DestinationPath -Force
if ($CleanupAfterExtraction) {
Remove-Item $FinalZipPath -Force
Write-Host " -> Cleaned up ZIP file: $ZipFile"
} else {
Write-Host " -> Keeping ZIP file: $ZipFile"
}
}
# Execute extraction for each archive
foreach ($Config in $ArchiveConfig) {
Expand-And-Cleanup -ZipFile $Config.Zip -DestinationPath $Config.Dest -ExpectedDirName $Config.Dir -CleanupAfterExtraction $should_remove_zips_after_unpack
}
Write-Host " -> Extraction and cleanup successful." -ForegroundColor Green
# --- Run initial model setup ---
Write-Host "INFO: Setting up initial models based on system language..."
# Get the 2-letter language code (e.g., "de", "fr") from Windows
$LangCode = (Get-Culture).Name.Substring(0, 2)
# Define the path to the Python script
$SetupModelScriptPath = ".\scripts\py\func\setup_initial_model.py"
# Check if the Python script exists
if (Test-Path $SetupModelScriptPath) {
Write-Host " -> Running setup_initial_model.py with language code: $LangCode"
# Execute the Python script with the detected language code
.\.venv\Scripts\python.exe $SetupModelScriptPath $LangCode
if ($LASTEXITCODE -eq 0) {
Write-Host "INFO: Initial model setup completed successfully." -ForegroundColor Green
} else {
Write-Host "WARNING: Initial model setup failed with exit code: $LASTEXITCODE" -ForegroundColor Yellow
}
} else {
Write-Host "WARNING: Initial model setup script not found at: $SetupModelScriptPath" -ForegroundColor Yellow
}
# --- Create required directories ---
$tmpPath = "C:\tmp"
$sl5DictationPath = "C:\tmp\sl5_aura"
@($tmpPath, $sl5DictationPath) | ForEach-Object {
if (!(Test-Path $_)) {
New-Item -ItemType Directory -Path $_ | Out-Null
Write-Host "Created directory: $_"
} else {
Write-Host "Directory already exists: $_"
}
}
# --- Create central config file ---
Write-Host "--> Creating central config file..."
$ConfigDir = Join-Path -Path $env:USERPROFILE -ChildPath ".config\sl5-stt"
if (-not (Test-Path -Path $ConfigDir)) {
Write-Host " -> Creating config directory at $ConfigDir"
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
}
# Korrigiert, um Backslashes für Windows zu verwenden und dann für TOML zu normalisieren
$ProjectRootPath = (Get-Location).Path
$ConfigContent = @"
[paths]
project_root = "$($ProjectRootPath.Replace('\', '/'))"
"@
$ConfigFile = Join-Path -Path $ConfigDir -ChildPath "config.toml"
Set-Content -Path $ConfigFile -Value $ConfigContent
# --- 9. Project Configuration ---
Write-Host "--> Creating Python package markers (__init__.py)..."
# Create log directory if it doesn't exist
if (-not (Test-Path "log")) {
New-Item -Name "log" -ItemType Directory | Out-Null
}
# Create __init__.py files
@("log/__init__.py", "config/__init__.py", "config/languagetool_server/__init__.py") | ForEach-Object {
$Directory = Split-Path -Path $_ -Parent
if ($Directory -and (-not (Test-Path $Directory))) {
New-Item -ItemType Directory -Path $Directory -Force | Out-Null
}
New-Item -Path $_ -ItemType File -Force | Out-Null
}
# --- 10. Completion ---
Write-Host ""
Write-Host "------------------------------------------------------------------" -ForegroundColor Green
Write-Host "Setup for Windows completed successfully." -ForegroundColor Green
Write-Host "------------------------------------------------------------------" -ForegroundColor Green
Config-Skripte¶
config/__init__.py¶
config/dynamic_settings.py¶
# config/dynamic_settings.py
import collections.abc # Corrected import to collections.abc
import importlib
import sys
import os
from datetime import datetime
from pathlib import Path
from threading import RLock
from config import settings
from config.settings_local import DEV_MODE
# Get a logger instance instead of direct print statements for better control
import logging
class CustomFormatter(logging.Formatter):
def formatTime(self, record, datefmt=None):
dt_object = datetime.fromtimestamp(record.created)
# Das Standardformat des logging-Moduls für asctime ist '%Y-%m-%d %H:%M:%S,f'
# Hier formatieren wir nur den H:M:S Teil und fügen die Millisekunden an
time_str_without_msecs = dt_object.strftime("%H:%M:%S")
milliseconds = int(record.msecs)
# Die 03d sorgt dafür, dass die Millisekunden immer dreistellig sind (z.B. 001, 010, 123)
formatted_time = f"{time_str_without_msecs},{milliseconds:03d}"
return formatted_time
PROJECT_ROOT = Path(__file__).resolve().parent.parent
LOG_FILE = PROJECT_ROOT / "log/dynamic_settings.log"
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Clear any pre-existing handlers to prevent duplicates.
if len(logger.handlers) > 0:
logger.handlers.clear()
# Create a shared formatter with the custom formatTime function.
log_formatter = CustomFormatter('%(asctime)s - %(levelname)-8s - %(message)s')
# Create, configure, and add the File Handler.
file_handler = logging.FileHandler(f'{PROJECT_ROOT}/log/dynamic_settings.log', mode='w')
file_handler.setFormatter(log_formatter)
logger.addHandler(file_handler)
logger.info(f"👀 dynamic_settings.py: DEV_MODE={DEV_MODE}, settings.DEV_MODE = {settings.DEV_MODE}")
class DynamicSettings:
_instance = None
_lock = RLock()
_last_modified_time = 0
_settings_module = None
_settings_local_module = None
_last_base_modified_time = None
_last_local_modified_time = None
_settings_file_path = None
_settings_local_file_path = None
def __new__(cls):
with cls._lock:
if cls._instance is None:
cls._instance = super(DynamicSettings, cls).__new__(cls)
logger.info(f"👀 dynamic_settings.py: DEV_MODE={DEV_MODE}, settings.DEV_MODE = {settings.DEV_MODE}")
if settings.DEV_MODE:
print("👀 DEBUG: DynamicSettings.__new__ called, initializing instance.")
cls._instance._init_settings()
return cls._instance
def _init_settings(self):
self._settings_file_path = os.path.join(
os.path.dirname(__file__), "settings.py"
)
self._settings_local_file_path = os.path.join(
os.path.dirname(__file__), "settings_local.py"
)
self._last_base_modified_time = os.path.getmtime(self._settings_file_path)
self._last_local_modified_time = os.path.getmtime(self._settings_local_file_path)
logger.info(f"👀 dynamic_settings.py: settings.DEV_MODE = {settings.DEV_MODE}")
if settings.DEV_MODE:
print(f"👀 DEBUG: DynamicSettings._init_settings called. Base settings file: {self._settings_file_path}")
print(f"👀 DEBUG: DynamicSettings._init_settings called. Local settings file: {self._settings_local_file_path}")
logger.info(f"👀 DEBUG: DynamicSettings._init_settings called. Base settings file: {self._settings_file_path}")
self.reload_settings(force=False)
def reload_settings(self, force=False):
# config/dynamic_settings.py:44
logger.info(f"👀 dynamic_settings.py:reload_settings():44 DEV_MODE={DEV_MODE}, settings.DEV_MODE = {settings.DEV_MODE}")
print(f"👀 dynamic_settings.py:reload_settings():44 DEV_MODE={DEV_MODE}, settings.DEV_MODE = {settings.DEV_MODE}")
if settings.DEV_MODE:
print("👀 DEBUG: reload_settings called.")
with self._lock:
if settings.DEV_MODE:
print("👀 DEBUG: Lock acquired for settings reload.")
current_base_modified_time = os.path.getmtime(self._settings_file_path) if os.path.exists(
self._settings_file_path) else 0
current_local_modified_time = 0
if self._settings_local_file_path and os.path.exists(self._settings_local_file_path):
current_local_modified_time = os.path.getmtime(self._settings_local_file_path)
any_file_modified = (
current_base_modified_time > self._last_base_modified_time or
current_local_modified_time > self._last_local_modified_time
)
if force or self._settings_module is None or any_file_modified:
if settings.DEV_MODE:
logger.info(
f"👀 Triggering full settings reload. Reasons: force={force}, _settings_module is None={self._settings_module is None}, any_file_modified={any_file_modified}")
logger.info(
f"👀【┘】⌚ ⏳ self._settings_file_path: {self._settings_file_path} | self._settings_local_file_path={self._settings_local_file_path} | any_file_modified={any_file_modified}")
logger.info(
f"👀【┘】⌚ ⏳ current_base_modified_time: {current_base_modified_time} | current_local_modified_time: {current_local_modified_time}")
logger.info(
f"👀【┘】⌚ ⏳ _last_base_modified_time: {self._last_base_modified_time} | _last_local_modified_time: {self._last_local_modified_time}")
self._last_base_modified_time = current_base_modified_time
self._last_local_modified_time = current_local_modified_time
# --- Reloading base settings (config.settings) ---
if 'config.settings' in sys.modules:
if settings.DEV_MODE:
print("👀 DEBUG: Calling importlib.reload(sys.modules['config.settings'])")
self._settings_module = importlib.reload(sys.modules['config.settings'])
else:
if settings.DEV_MODE:
print("👀 DEBUG: Calling importlib.import_module('config.settings')")
self._settings_module = importlib.import_module('config.settings')
if settings.DEV_MODE:
print("👀 DEBUG: Base settings loaded.")
# --- Reloading local settings (config.settings_local) ---
try:
if os.path.exists(self._settings_local_file_path):
if 'config.settings_local' in sys.modules:
if settings.DEV_MODE:
print("👀 DEBUG: Calling importlib.reload(sys.modules['config.settings_local'])")
self._settings_local_module = importlib.reload(sys.modules['config.settings_local'])
else:
if settings.DEV_MODE:
print("👀 DEBUG: Calling importlib.import_module('config.settings_local')")
self._settings_local_module = importlib.import_module('config.settings_local')
if settings.DEV_MODE:
print("👀 DEBUG: Local settings loaded.")
else:
print("👀 INFO: config.settings_local.py does not exist. Skipping local settings load.")
self._settings_local_module = None
except ModuleNotFoundError:
print("👀 WARNING: config.settings_local module not found. This might indicate a path issue or missing file.")
self._settings_local_module = None
except Exception as e:
print(f"👀 CRITICAL ERROR: Exception during config.settings_local import/reload: {e}")
import traceback
traceback.print_exc()
raise
if settings.DEV_MODE:
print("👀 DEBUG: --- Merging settings ---")
# Clear existing attributes to ensure a clean merge
for attr in list(self.__dict__.keys()):
# IMPORTANT: Do not delete 'settings' itself or internal attributes like '_instance', '_lock', etc.
# Ensure that we only delete dynamically added configuration attributes.
# A robust way might be to keep track of which attributes were added,
# but for now, checking for standard internal attributes should be sufficient.
if not attr.startswith('_') and attr not in ['settings', '_settings_module', '_settings_local_module']:
delattr(self, attr)
# Apply base settings
if self._settings_module:
for attr in dir(self._settings_module):
if not attr.startswith('__'):
value = getattr(self._settings_module, attr)
setattr(self, attr, value)
if settings.DEV_MODE:
print("👀 DEBUG: Base settings attributes applied to DynamicSettings instance.")
# Apply/Merge local settings
if self._settings_local_module:
for attr in dir(self._settings_local_module):
if not attr.startswith('__'):
local_value = getattr(self._settings_local_module, attr)
# --- START MODIFICATION ---
# Special handling for PRELOAD_MODELS: always override
if attr == "PRELOAD_MODELS":
setattr(self, attr, local_value)
if settings.DEV_MODE:
print(f"👀 DEBUG: Overrode PRELOAD_MODELS with local value: {local_value}")
# --- END MODIFICATION ---
elif hasattr(self, attr) and isinstance(getattr(self, attr), collections.abc.MutableMapping) and isinstance(local_value, collections.abc.MutableMapping):
merged_dict = getattr(self, attr)
merged_dict.update(local_value)
setattr(self, attr, merged_dict)
if settings.DEV_MODE:
print(f"👀 DEBUG: Merged dictionary setting '{attr}': {getattr(self, attr)}")
elif hasattr(self, attr) and isinstance(getattr(self, attr), collections.abc.MutableSequence) and not isinstance(getattr(self, attr), (str, bytes)) and isinstance(local_value, collections.abc.MutableSequence) and not isinstance(local_value, (str, bytes)):
merged_list = getattr(self, attr)
# Only append items if they are not already in the list
for item in local_value:
if item not in merged_list:
merged_list.append(item)
setattr(self, attr, merged_list)
if settings.DEV_MODE:
print(f"👀 DEBUG: Merged list setting '{attr}': {getattr(self, attr)}")
else:
# Default: override with local value
setattr(self, attr, local_value)
if settings.DEV_MODE:
print(f"👀 DEBUG: Overrode setting '{attr}' with local value: {local_value}")
print("👀 DEBUG: Local settings attributes applied/merged to DynamicSettings instance.")
settings = DynamicSettings() # noqa: F811
config/settings.py¶
# file: config/settings.py
# Central configuration for the application
# please see also: settings_local.py_Example.txt
import os
SERVICE_START_OPTION = 0
# Option 1: Start the service only on when there is an internet connection.
# Get username
current_user = os.environ.get('USERNAME', 'default')
# Set to True to disable certain production checks for local development,
# e.g., the wrapper script enforcement.
DEV_MODE = False
soundMute = 1 # 1 is really recommandet. to know when your recording is ended.
soundUnMute = 1
soundProgramLoaded = 1
ENABLE_AUTO_LANGUAGE_DETECTION = False
# --- Notification Settings ---
# Default for new users is the most verbose level.
NOTIFICATION_LEVEL = 0 # 0=Silent, 1=Essential, 2=Verbose
# --- Language Model Preloading ---
# A list of Vosk model folder names to preload at startup if memory allows.
PRELOAD_MODELS = ["vosk-model-de-0.21", "vosk-model-en-us-0.22"] # e.g. ["vosk-model-de-0.21", "vosk-model-en-us-0.22"]
# PRELOAD_MODELS = ["vosk-model-de-0.21"]
if current_user == 'SL5.de':
PRELOAD_MODELS = ["vosk-model-de-0.21"]
# --- LanguageTool Server ---
# Set to True to use an existing LT server. AT YOUR OWN RISK!
# The application will not start its own server and will not stop the external one.
USE_EXTERNAL_LANGUAGETOOL = False # Default: False
# URL for the external server if the option above is True.
EXTERNAL_LANGUAGETOOL_URL = "http://localhost:8081"
# Settings for our internal server (if used)
LANGUAGETOOL_PORT = 8082
# --- Text Correction Settings ---
# This dictionary controls which categories of LanguageTool rules are enabled.
# The application will use these settings to enable/disable rule categories
# when checking text. Set a category to False to ignore its suggestions.
#
# You can override these in your config/settings_local.py file.
CORRECTIONS_ENABLED = {
# Core Corrections
"spelling": True, # Basic spell checking (e.g., "Rechtschreibung")
"punctuation": True, # Missing/incorrect commas, periods, etc.
"grammar": True, # Grammatical errors (e.g., subject-verb agreement)
"casing": True, # Incorrect capitalization (e.g., "berlin" -> "Berlin")
"style": True, # Stylistic suggestions (e.g., wordiness, passive voice)
"colloquialisms": True, # Flags informal or colloquial language
# Specialized Dictionaries/Rules
# These are disabled by default as they may not be relevant for all users.
# Set to True in settings_local.py to enable them.
"medical": False, # Rules related to medical terminology
"law_rules": False, # Rules related to legal terminology
"git": False, # git Basic commands
# Add other custom categories here as needed.
# "academic_writing": False,
}
PLUGINS_ENABLED = {}
# needs restart. implemented in the python part:
ADD_TO_SENCTENCE = "."
# set ADD_TO_SENCTENCE = "" when you dont want it.
# Recording & Transcription
SUSPICIOUS_TIME_WINDOW = 90
SUSPICIOUS_THRESHOLD = 3
# INITIAL_WAIT_TIMEOUT = initial_silence_timeout
# SPEECH_PAUSE_TIMEOUT = 2.0 # Standardwert
PRE_RECORDING_TIMEOUT = 12
SPEECH_PAUSE_TIMEOUT = 0.6
SAMPLE_RATE = 16000
# System
CRITICAL_THRESHOLD_MB = 1024 * 2
# LanguageTool Server Configuration
LANGUAGETOOL_BASE_URL = f"http://localhost:{LANGUAGETOOL_PORT}"
LANGUAGETOOL_CHECK_URL = f"{LANGUAGETOOL_BASE_URL}/v2/check"
LANGUAGETOOL_RELATIVE_PATH = "LanguageTool-6.6/languagetool-server.jar"
NOTIFY_SEND_PATH = "/usr/bin/notify-send"
XDOTOOL_PATH = "/usr/bin/xdotool"
TRIGGER_FILE_PATH = "/tmp/sl5_record.trigger"
# Auto-detected Java path
JAVA_EXECUTABLE_PATH = r"/usr/bin/java"
# needs NO restart. implemented in the sh part. TODO implemt for windows:
# use . for all Windows. Other examples:
# AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "."
AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "(ExampleAplicationThatNotExist|Pi, your personal AI)"
# TODO implement for windows
config/settings_local.py¶
# config/settings_local.py
# My personal settings for SL5 Aura
# This file is ignored by Git.
SERVICE_START_OPTION = 1
# Option 1: Start the service only on when there is an internet connection.
NOTIFICATION_LEVEL = 0 # 0=Silent, 1=Essential, 2=Verbose
soundMute = 1 # 1 is really recomanded. to know when your recording is endet.
soundUnMute = 1
soundProgramLoaded = 1
# Set to True to disable certain production checks for local development,
# e.g., the wrapper script enforcement.
DEV_MODE = True
# DEV_MODE = False
DEV_MODE_memory = False
# may yo want to overwrite the PRELOAD_MODELS settings from settings.py here
# PRELOAD_MODELS = ["vosk-model-de-0.21"]
# PRELOAD_MODELS = ["vosk-model-de-0.21", "vosk-model-en-us-0.22"] # e.g. ["vosk-model-de-0.21", "vosk-model-en-us-0.22"]
# PRELOAD_MODELS = ["vosk-model-de-0.21", "vosk-model-en-us-0.22"]
#PRELOAD_MODELS = ["vosk-model-en-us-0.22"]
PRELOAD_MODELS = ["vosk-model-de-0.21", "vosk-model-en-us-0.22"]
CRITICAL_THRESHOLD_MB = 1024 * 2
# CRITICAL_THRESHOLD_MB = 28000 # (also 28 GB)
# --- Custom Client-Side Plugins ---
# Enable or disable specific client-side behaviors (plugins).
# The logic is handled by client scripts (e.g., type_watcher.sh, AutoKey).
# These settings tell the backend service what to expect or how to format output.
PLUGINS_ENABLED = {
"git": True,
"wannweil": True,
"game-dealers_choice": False,
"0ad": False,
"ethiktagung": True,
"volkshochschule_tue": False,
"CCC_tue": True,
"vsp_rt": True,
"ki-maker-space": True,
"numbers_to_digits": True,
"digits_to_numbers": False,
"web-radio-funk": True,
}
# needs restart. implemented in the python part:
ADD_TO_SENCTENCE = "."
# set ADD_TO_SENCTENCE = "" when you dont want it.
# needs NO restart:
PRE_RECORDING_TIMEOUT = 8
SPEECH_PAUSE_TIMEOUT = 2
# needs NO restart. implemented in the sh part. TODO implemt for windows:
# use . for all windos. Other examples:
# AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "."
AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "(ExampleAplicationThatNotExist|Pi, your personal AI)"
# TODO implement for windows
config/settings_local.py_Example.txt¶
# config/settings_local.py
# My personal settings Example
# This file is ignored by Git.
SERVICE_START_OPTION = 0
NOTIFICATION_LEVEL = 1
soundMute = 1 # 1 is really recommandet. to know when your recording is endet.
soundUnMute = 1
soundProgramLoaded = 1
# Set to True to disable certain production checks for local development,
# e.g., the wrapper script enforcement.
DEV_MODE = False
# DEV_MODE = False
DEV_MODE_memory = False
PRELOAD_MODELS = ["vosk-model-de-0.21"]
# --- Custom Correction Settings ---
# Import the default dictionary from the main settings file.
try:
from .settings import CORRECTIONS_ENABLED
except ImportError:
CORRECTIONS_ENABLED = {} # Fallback in case the import fails
# Update the dictionary with my personal preferences.
# I want specialized legal and medical checks, but I don't want style advice.
CORRECTIONS_ENABLED.update({
"git": True,
})
CORRECTIONS_ENABLED.update({
})
PRE_RECORDING_TIMEOUT = 12
SPEECH_PAUSE_TIMEOUT = 1
# examples:
# AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "."
# needs NO restart. implemented in the sh part. TODO implemt for windows:
AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "(ExampleAplicationThatNotExist|Pi, your personal AI)"
ADD_TO_SENCTENCE = "."
# set ADD_TO_SENCTENCE = "" when you dont want it.
CRITICAL_THRESHOLD_MB = 1024 * 2
# CRITICAL_THRESHOLD_MB = 28000 # (also 28 GB)
# --- Custom Client-Side Plugins ---
# Enable or disable specific client-side behaviors (plugins).
# The logic is handled by client scripts (e.g., type_watcher.sh, AutoKey).
# These settings tell the backend service what to expect or how to format output.
#### "github_corrections": True, # Example from before ???
PLUGINS_ENABLED = {
"game-dealers_choice": False, # For Andy's poker game
"wannweil": False,
"git": False,
"0ad": False,
"ethiktagung": True
"ethiktagung": True,
"volkshochschule_tue": True,
"CCC_tue": True,
"vsp_rt": True,
"numbers_to_digits": True,
# "digits_to_numbers": False, deprecated
"ki-maker-space": False,
"web-radio-funk": False,
}
Githooks-Skripte¶
githooks/pre-push¶
#!/bin/bash
set -e
SRC1="$HOME/.config/autokey/data/stt/autokey-scripts/"
SRC2="scripts/autokey-scripts/"
# Sync newer files from SRC1 to SRC2
rsync -au --delete "$SRC1" "$SRC2"
# Sync newer files from SRC2 to SRC1
rsync -au --delete "$SRC2" "$SRC1"
# Temporary directory for requirements generation
TMPDIR=".pipreqs_temp"
FILES=("dictation_service.py" "get_suggestions.py")
# Clean up any old temp dirs
rm -rf "$TMPDIR"
mkdir "$TMPDIR"
# Copy only the relevant files
for f in "${FILES[@]}"; do
cp "$f" "$TMPDIR/"
done
# Generate requirements.txt from only those files
pipreqs "$TMPDIR" --force
# Replace the actual requirements.txt
mv "$TMPDIR/requirements.txt" requirements.txt
# Convert all package names to lowercase (keep version numbers)
awk -F'==' '{print tolower($1) "==" $2}' requirements.txt > requirements.txt.tmp && mv requirements.txt.tmp requirements.txt
# Clean up temp directory
rm -rf "$TMPDIR"
Update-Skripte¶
update/update_for_windows_users.ps1¶
# file: update/update_for_windows_users.ps1
# Description: Downloads the latest version and updates the application
# while preserving user settings. For non-developer use.
$ErrorActionPreference = 'Stop'
$repoUrl = "https://github.com/sl5net/SL5-aura-service/archive/refs/heads/master.zip"
$installDir = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$tempDir = Join-Path $env:TEMP "sl5_update_temp"
Write-Host "--- SL5 Aura Updater ---" -ForegroundColor Cyan
Write-Host "This will download the latest version and replace all application files."
Write-Host "Your personal settings in 'config\settings_local.py' will be saved."
if (-not ($env:CI -eq 'true'))
{
Write-Host "Please close the main application if it is running."
Read-Host -Prompt "Press Enter to continue or CTRL+C to cancel"
}
try {
# 1. Clean up previous temporary files if they exist
if (Test-Path $tempDir) {
Write-Host "INFO: Removing old temporary update folder..."
Remove-Item -Path $tempDir -Recurse -Force
}
New-Item -Path $tempDir -ItemType Directory | Out-Null
# 2. Backup local settings if they exist
$localSettingsPath = Join-Path $installDir "config\settings_local.py"
$backupPath = Join-Path $tempDir "settings_local.py.bak"
if (Test-Path $localSettingsPath) {
Write-Host "INFO: Backing up your local settings..." -ForegroundColor Green
Copy-Item -Path $localSettingsPath -Destination $backupPath
}
# 3. Download the latest version
$zipPath = Join-Path $tempDir "latest.zip"
Write-Host "INFO: Downloading latest version from GitHub..."
Invoke-WebRequest -Uri $repoUrl -OutFile $zipPath
# 4. Extract the archive
Write-Host "INFO: Extracting update..."
Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force
$extractedFolder = Get-ChildItem -Path $tempDir -Directory | Where-Object { $_.Name -like '*-master' } | Select-Object -First 1
if (-not $extractedFolder) { throw "Could not find extracted '*-master' folder." }
# 5. Restore local settings into the new version
if (Test-Path $backupPath) {
Write-Host "INFO: Restoring your local settings into the new version..." -ForegroundColor Green
Copy-Item -Path $backupPath -Destination (Join-Path $extractedFolder.FullName "config\")
}
# 6. Create a final batch script to perform the file replacement
$batchScript = @'
@echo off
echo Finalizing update, please wait...
timeout /t 3 /nobreak > nul
robocopy "{0}" "{1}" /E /MOVE /NFL /NDL /NJH /NJS > nul
echo.
echo Update complete! You can now restart the application.
timeout /t 5 > nul
del "%~f0"
'@ -f $extractedFolder.FullName, $installDir
$batchPath = Join-Path $installDir "_finalize_update.bat"
Set-Content -Path $batchPath -Value $batchScript
# 7. Launch the batch script and exit this PowerShell script
Write-Host "INFO: Handing over to final updater script. This window will close." -ForegroundColor Yellow
Start-Process cmd.exe -ArgumentList "/C `"$batchPath`""
} catch {
Write-Host "FATAL: An error occurred during the update." -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Read-Host -Prompt "Press Enter to exit."
}
Scripts-Skripte¶
scripts/__init__.py¶
scripts/activate-venv_and_run-server.sh¶
#!/bin/bash
# scripts/activate-venv_and_run-server.sh
# Exit immediately if a command fails
SCRIPT_firstName="dictation_service"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_ROOT="$SCRIPT_DIR/.."
os_type=$(uname -s)
if [[ "$os_type" == "MINGW"* || "$os_type" == "CYGWIN"* || "$os_type" == "MSYS"* ]]; then
# This is a Windows-based shell environment
detected_os="windows"
else
# This is any other OS (Linux, macOS, FreeBSD, etc.)
detected_os="other"
fi
if [ "$detected_os" = "windows" ]; then
echo "please start type_watcher.ahk"
echo "please start trigger-hotkeys.ahk"
else
$PROJECT_ROOT/type_watcher.sh &
fi
set -e
HEARTBEAT_FILE="/tmp/$SCRIPT_firstName.heartbeat"
SCRIPT_TO_START="$SCRIPT_DIR/../$SCRIPT_firstName.py"
MAX_STALE_SECONDS=5
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
export DISPLAY=:0
export XAUTHORITY=${HOME}/.Xauthority
export DICTATION_SERVICE_STARTED_CORRECTLY="true"
if [ -f "$HEARTBEAT_FILE" ]
then
last_update=$(cat "$HEARTBEAT_FILE")
current_time=$(date +%s)
age=$((current_time - last_update))
if [ "$age" -lt "$MAX_STALE_SECONDS" ]
then
echo "Service appears to be running and healthy."
exit 0
else
echo "Service heartbeat is stale. Attempting to restart."
fi
else
echo "Service is not running."
fi
python3 -m venv .env
echo "Activating virtual environment at '$PROJECT_ROOT/venv'..."
python3 -m venv .venv
source "$PROJECT_ROOT/.venv/bin/activate"
echo "Starting Python server from '$PROJECT_ROOT'..."
# We run the python script using its absolute path to be safe
echo "Starting service..."
python3 "$SCRIPT_TO_START" &
scripts/notification_watcher.ahk¶
#Requires AutoHotkey v2.0
#SingleInstance Force
A_IconTip := "SL5 Aura Notifier"
; Create a borderless, always-on-top GUI window for our notification
noteGui := Gui("+AlwaysOnTop -Caption +ToolWindow")
noteGui.SetFont("s10", "Segoe UI")
titleCtrl := noteGui.Add("Text", "w300")
noteGui.SetFont("s9", "Segoe UI")
bodyCtrl := noteGui.Add("Text", "w300 y+5")
SetTimer(WatchForNotification, 250)
WatchForNotification() {
static notifyFile := "C:\tmp\notification.txt"
if FileExist(notifyFile) {
content := Trim(FileRead(notifyFile))
FileDelete(notifyFile)
if (content = "")
return
parts := StrSplit(content, "|")
titleCtrl.Text := parts[1]
bodyCtrl.Text := parts.Has(2) ? parts[2] : ""
; Position and show the GUI
noteGui.Show("NA")
; Hide it after 5 seconds
SetTimer(HideGui, -5000)
}
}
HideGui() {
noteGui.Hide()
}
scripts/restart_venv_and_run-server.ahk¶
#Requires AutoHotkey v2.0
; restart_venv_and_run-server.ahk
FileReadLine(File, LineNumber)
{
FileRead FileContents, %File%
FileContents := ""
Loop Parse, FileContents, "`n", "`r"
{
If (A_Index = LineNumber)
return Trim(A_LoopField)
}
return ""
}
; Setzen Sie den Pfad zum Skript-Verzeichnis
; ScriptDir := FileGetDir(A_ScriptFullPath)
ScriptDir := A_ScriptDir
; Setzen Sie den Pfad zur Konfigurationsdatei
ConfigFile := ScriptDir . "\..\config\server.conf"
; Setzen Sie den Pfad zum Server-Skript
ServerScript := ScriptDir . "\activate-venv_and_run-server.sh"
; Lese die Konfigurationsdatei und lade die Variable PORT
FileRead ConfigFile, "r"
PORT := Trim(FileReadLine(ConfigFile, 1), " `t")
; Starte den Server neu
MsgBox "Restarting TTS Server on Port " PORT "..."
Run "pkill -f dictation_service.py"
Run "pkill -f type_watcher.sh"
; DO NOT kill LanguageTool server here. Run "pkill -f languagetool-server.jar"
Sleep 1000
; Überprüfe, ob ein Prozess auf dem Port läuft
PID := ""
PID := Run("lsof -t -i :" PORT, "", "", Output)
if (PID != "")
{
Run "kill " PID
Sleep 1000
}
; Starte das Server-Skript neu
Run ServerScript
scripts/restart_venv_and_run-server.sh¶
#!/bin/bash
#
# restart_venv_and_run-server.sh
#
# Final version: Correctly terminates ALL associated processes (main service and watcher)
# and reliably waits for them to disappear before starting a new instance.
# --- Configuration ---
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
SERVER_SCRIPT="$SCRIPT_DIR/activate-venv_and_run-server.sh"
SERVICE_NAME_MAIN="dictation_service.py"
os_type=$(uname -s)
if [[ "$os_type" == "MINGW"* || "$os_type" == "CYGWIN"* || "$os_type" == "MSYS"* ]]; then
# This is a Windows-based shell environment
detected_os="windows"
else
# This is any other OS (Linux, macOS, FreeBSD, etc.)
detected_os="other"
fi
if [ "$detected_os" = "windows" ]; then
echo "please start type_watcher.ahk"
echo "please start trigger-hotkeys.ahk"
else
SERVICE_NAME_WATCHER="type_watcher.sh"
fi
echo "Requesting restart for all services..."
# --- Step 1: Check if any of the target processes are running ---
# The -f flag for pgrep searches the full command line.
if ! pgrep -f "$SERVICE_NAME_MAIN" > /dev/null && ! pgrep -f "$SERVICE_NAME_WATCHER" > /dev/null; then
echo "Info: No running server or watcher processes found. Starting fresh."
else
# --- Step 2: Kill ALL old processes (Main Service AND Watcher) ---
echo "Stopping old processes..."
# Use -f to match the full command line, just like in pgrep.
pkill -f "$SERVICE_NAME_MAIN"
pkill -f "$SERVICE_NAME_WATCHER"
echo pkill -f "$SERVICE_NAME_MAIN"
echo pkill -f "$SERVICE_NAME_WATCHER"
# realpath /tmp/../tmp/../tmp
# /tmp
# PROJECT_ROOT=realpath $SCRIPT_DIR/..
PROJECT_ROOT=$(realpath "$SCRIPT_DIR/..")
echo SCRIPT_DIR=$SCRIPT_DIR
echo PROJECT_ROOT=$PROJECT_ROOT
echo "Activating virtual environment at '$PROJECT_ROOT/venv'..."
cd $PROJECT_ROOT
python3 -m venv .venv
source .venv/bin/activate
end_dictation_servicePY="$PROJECT_ROOT/scripts/py/end_dictation_service.py"
echo end_dictation_servicePY=$end_dictation_servicePY
python3 "$end_dictation_servicePY" &
# --- Step 3: Reliably wait for BOTH processes to terminate ---
echo -n "Waiting for all processes to shut down "
TIMEOUT_SECONDS=10
for (( i=0; i<TIMEOUT_SECONDS; i++ )); do
# The loop continues as long as EITHER the main service OR the watcher is found.
if ! pgrep -f "$SERVICE_NAME_MAIN" > /dev/null && ! pgrep -f "$SERVICE_NAME_WATCHER" > /dev/null; then
echo -e "\nInfo: All services have been terminated."
break # Exit the loop, we are done waiting.
fi
echo -n "."
sleep 1
done
fi
sleep 1
# --- Step 4: Start the new server instance ---
echo "Starting new server and watcher..."
if [ -x "$SERVER_SCRIPT" ]; then
"$SERVER_SCRIPT"
else
echo "Error: Server script is not executable: $SERVER_SCRIPT"
echo "Please run: chmod +x $SERVER_SCRIPT"
exit 1
fi
echo "Server started or closed now."
scripts/type_watcher_Stash_Wird_bald_geloescht.sh¶
#!/bin/bash
# type_watcher.sh (Version 4 - Final)
# --- Set FULL Environment explicitly for background tools ---
export DISPLAY=:0
export XAUTHORITY=${HOME}/.Xauthority
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus"
# ... (der Rest des Skripts bleibt exakt gleich) ...
# --- Dependency Check ---
if ! command -v inotifywait &> /dev/null || ! command -v xdotool &> /dev/null; then
exit 1
fi
DIR_TO_WATCH="/tmp"
# DIR_TO_WATCH="${HOME}/.sl5_stt_tmp"
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
PROJECT_ROOT="$SCRIPT_DIR/.."
FILE_PATTERN_BASE="tts_output_"
# LOCKFILE="/tmp/type_watcher.lock"
LOCKFILE="${DIR_TO_WATCH}/type_watcher.lock" # Lockfile also moves
LOG_FILE="$PROJECT_ROOT/type_watcher.log"
if [ -e "$LOCKFILE" ] && ps -p "$(cat "$LOCKFILE")" > /dev/null; then
exit 0
fi
echo $$ > "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT
inotifywait -m -q -e create --format '%f' "$DIR_TO_WATCH" | while read -r FILE; do
if [[ "$FILE" == ${FILE_PATTERN_BASE}* ]]; then
FULL_PATH="${DIR_TO_WATCH}/${FILE}"
sleep 0.05
if [ -s "$FULL_PATH" ]; then
TEXT=$(cat "$FULL_PATH")
rm "$FULL_PATH"
[ -n "$TEXT" ] && xdotool type --clearmodifiers "$TEXT"
fi
fi
done
Root-Verzeichnis Skripte¶
Folgende Skripte sind hier dokumentiert:
start_dictation_v2.0.bat
update.bat
install_hooks.sh
type_watcher.ahk
type_watcher.sh
start_dictation_v2.0.bat¶
:: start this script like: & .\start_dictation_v2.0.bat
:: Version: v2.1
@echo off
setlocal
title SL5 Aura - One-Click Starter
:: --- Step 1: Set correct working directory ---
cd /d "%~dp0"
# --- 2. Admin Rights Check ---
echo [*] Checking for Administrator privileges
REM Only check for admin rights if NOT running in a CI environment
if /I NOT "%CI%"=="true" (
net session >nul 2>&1
if %errorLevel% neq 0 (
echo [ERROR] Re-launching with Admin rights...
powershell -Command "Start-Process '%~f0' -Verb RunAs"
exit /b
)
)
echo [SUCCESS] Running with Administrator privileges.
:: --- Step 3: VEREINFACHT - Check if venv exists, otherwise run full setup ---
if not exist ".\.venv\Scripts\python.exe" (
echo [WARNING] Virtual environment is missing or incomplete.
echo [ACTION] Running full setup. This may take a moment...
powershell.exe -ExecutionPolicy Bypass -File ".\setup\windows11_setup.ps1"
if not exist ".\.venv\Scripts\python.exe" (
echo [FATAL] Automated setup failed. Please check setup script.
pause
exit /b
)
echo [SUCCESS] Virtual environment has been set up successfully.
)
echo.
:: --- Step 4: Start background components ---
start "SL5 Type Watcher" type_watcher.ahk
start "SL5 Notification Watcher" scripts\notification_watcher.ahk
echo [INFO] Background watchers have been started.
echo.
:: --- Step 5: Activate venv and start the main service with auto-repair ---
echo [INFO] Activating virtual environment...
call .\.venv\Scripts\activate.bat
set REPAIR_ATTEMPTED=
:START_SERVICE_LOOP
echo [INFO] Starting the Python STT backend service...
python -u dictation_service.py
echo [INFO] Waiting 5 seconds for the service to initialize...
timeout /t 5 >nul
echo [INFO] Verifying service status via log file...
findstr /C:"Setup validation successful" "log\dictation_service.log" >nul 2>&1
IF %ERRORLEVEL% EQU 0 goto :CONTINUE_SCRIPT
:: --- ERROR HANDLING & REPAIR ---
echo [WARNING] Service verification failed. Log does not contain success signal.
if defined REPAIR_ATTEMPTED (
echo [FATAL] The automatic repair attempt has failed. Please check setup manually.
pause
exit /b 1
)
echo [ACTION] Attempting automatic repair by reinstalling dependencies...
set REPAIR_ATTEMPTED=true
call .\.venv\Scripts\python.exe -m pip install -r requirements.txt
echo [INFO] Repair finished. Retrying service start...
echo.
goto :START_SERVICE_LOOP
:CONTINUE_SCRIPT
echo [INFO] Service verification successful.
echo [*] Triggering the service...
echo. >> "c:/tmp/sl5_record.trigger"
echo.
:: --- Final Success Message - ENTFERNT die doppelte Meldung ---
echo [SUCCESS] SL5 Aura is now running in the background.
echo This window will close automatically in a few seconds.
timeout /t 5 > nul
update.bat¶
@echo off
:: file: update.bat
:: Description: One-click updater for Windows users.
:: This script requests admin rights and then runs the main PowerShell update script.
if "%CI%"=="true" goto run_script
:: 1. Check for administrative privileges
net session >nul 2>&1
if %errorLevel% NEQ 0 (
echo Requesting administrative privileges to run the updater...
powershell -Command "Start-Process -FilePath '%0' -Verb RunAs"
exit /b
)
:: 2. Now that we have admin rights, run the actual PowerShell updater script
:: -ExecutionPolicy Bypass: Temporarily allows the script to run without changing system settings.
:: -File: Specifies the script to execute.
:run_script
echo Starting the update process...
powershell.exe -ExecutionPolicy Bypass -File "%~dp0update\update_for_windows_users.ps1"
echo.
echo The update script has finished. This window can be closed.
pause
install_hooks.sh¶
#!/bin/bash
cp githooks/pre-push .git/hooks/pre-push
chmod +x .git/hooks/pre-push
echo "pre-push hook installed!"
type_watcher.ahk¶
#Requires AutoHotkey v2.0
; type_watcher.ahk (v8.3 - Correct FileRead Syntax)
; #SingleInstance Force ; is buggy
; --- Configuration ---
watchDir := "C:\tmp\sl5_aura"
logDir := A_ScriptDir "\log"
autoEnterFlagPath := "C:\tmp\sl5_auto_enter.flag"
heartbeat_start_File := "C:\tmp\heartbeat_type_watcher_start.txt"
; --- Main Script Body ---
myUniqueID := A_TickCount . "-" . Random(1000, 9999)
try {
fileHandle := FileOpen(heartbeat_start_File, "w")
fileHandle.Write(myUniqueID)
fileHandle.Close()
} catch as e {
MsgBox("FATAL: Could not write heartbeat file: " . e.Message, "Error", 16)
ExitApp
}
; --- Global Variables ---
global pBuffer := Buffer(1024 * 16), hDir, pOverlapped := Buffer(A_PtrSize * 2 + 8, 0)
global CompletionRoutineProc
global watcherNeedsRearm := false
global fileQueue := [] ; The queue for files
global isProcessingQueue := false ; Flag to prevent simultaneous processing
Sleep(200) ; Give a potential double-clicked instance time to act
try {
if FileExist(heartbeat_start_File) {
lastUniqueID := Trim(FileRead(heartbeat_start_File))
if (lastUniqueID != myUniqueID) {
ExitApp ; other instance exists, I must exit.
}
}
} catch {
MsgBox("FATAL: " . e.Message, "Error", 16), ExitApp
}
SetTimer(CheckHeartbeatStart, 5000)
; =============================================================================
; SELF-TERMINATION VIA HEARTBEAT START
; =============================================================================
CheckHeartbeatStart() {
global heartbeat_start_File, myUniqueID
try {
local lastUniqueID := Trim(FileRead(heartbeat_start_File, "UTF-8"))
if (lastUniqueID != myUniqueID) {
Log("Newer instance detected (" . lastUniqueID . "). I am " . myUniqueID . ". Terminating self.")
ExitApp
}
} catch {
Log("Could not read heartbeat file. Terminating to be safe.")
ExitApp
}
}
DirCreate(watchDir)
DirCreate(logDir)
Log("--- Script Started (v8.3 - Correct FileRead Syntax) ---")
Log("Watching folder: " . watchDir)
CompletionRoutineProc := CallbackCreate(IOCompletionRoutine, "F", 3)
WatchFolder(watchDir) ; Initial arming
ProcessExistingFiles() ; Process initial files AND trigger the first queue run
; --- The Main Application Loop ---
Loop {
DllCall("SleepEx", "UInt", 0xFFFFFFFF, "Int", true)
if (watcherNeedsRearm) {
Log("MainLoop: Detected re-arm flag. Calling ReArmWatcher.")
watcherNeedsRearm := false
ReArmWatcher()
}
}
Log("--- FATAL: Main loop exited unexpectedly. ---")
ExitApp
; =============================================================================
; LOGGING FUNCTION
; =============================================================================
Log(message) {
static logFile := logDir "\type_watcher.log"
try {
FileAppend(A_YYYY "-" A_MM "-" A_DD " " A_Hour ":" A_Min ":" A_Sec " - " . message . "`n", logFile)
} catch as e {
MsgBox("CRITICAL LOGGING FAILURE!`n`nCould not write to: " . logFile . "`n`nReason: " . e.Message, "Logging Error", 16)
ExitApp
}
}
; =============================================================================
; INITIAL SCAN FOR EXISTING FILES
; =============================================================================
ProcessExistingFiles() {
Log("Scanning for existing files to queue...")
Loop Files, watchDir "\tts_output_*.txt" {
QueueFile(A_LoopFileName)
}
Log("Initial scan complete. " . fileQueue.Length . " files queued.")
TriggerQueueProcessing()
}
; =============================================================================
; FILE QUEUING FUNCTION
; =============================================================================
QueueFile(filename) {
if InStr(filename, "tts_output_") {
fullPath := watchDir "\" . filename
; --- FIX: Check if file is already in the queue ---
for index, queuedPath in fileQueue {
if (queuedPath = fullPath) {
Log("-> File is already in queue. Ignoring duplicate add. -> " . filename)
return ; Exit the function, do not add again
}
}
; --- End of FIX ---
Log("Queuing file -> " . filename)
fileQueue.Push(fullPath)
} else {
Log("Ignored non-target file -> " . filename)
}
}
; =============================================================================
; MASTER FUNCTION TO START QUEUE PROCESSING
; =============================================================================
TriggerQueueProcessing() {
global isProcessingQueue
if (isProcessingQueue) {
return
}
isProcessingQueue := true
Log(">>> Starting queue processing loop...")
ProcessQueue()
Log("<<< Queue processing loop finished.")
isProcessingQueue := false
}
; =============================================================================
; QUEUE PROCESSING LOOP (WITH CORRECT v2 FILEREAD SYNTAX)
; =============================================================================
ProcessQueue() {
while (fileQueue.Length > 0) {
local fullPath := fileQueue[1]
Log("Attempting to process from queue: " . fullPath)
static stabilityDelay := 50
local content := ""
local isReadyForProcessing := false
try {
if !FileExist(fullPath) {
Log("-> File no longer exists. Removing from queue.")
fileQueue.RemoveAt(1)
continue
}
size1 := FileGetSize(fullPath), Sleep(stabilityDelay), size2 := FileGetSize(fullPath)
if (size1 != size2 or size1 = 0) {
Log("-> File is unstable/empty. Deleting it.")
FileDelete(fullPath)
fileQueue.RemoveAt(1)
continue
}
; --- THE DEFINITIVE FIX IS HERE ---
; Using the correct AutoHotkey v2 syntax for FileRead.
content := FileRead(fullPath, "UTF-8")
content := Trim(content)
isReadyForProcessing := true
Log("-> File is stable and readable.")
} catch as e {
Log("-> CRITICAL ERROR while reading file. Removing to prevent blocking. Error: " . e.Message)
fileQueue.RemoveAt(1) ; Remove blocking file
continue ; Try next file
}
if (isReadyForProcessing) {
fileQueue.RemoveAt(1)
try {
FileDelete(fullPath)
Log("-> File successfully deleted.")
if (content != "") {
Log("--> Sending content: '" . content . "'")
SendText(content)
; --- Conditional Enter Key ---
; Check if the auto-enter plugin is enabled
if FileExist(autoEnterFlagPath) {
flagState := Trim(FileRead(autoEnterFlagPath))
if (flagState = "true") {
SendInput("{Enter}")
}
}
; --- End of Conditional Block ---
} else {
Log("-> File was empty.")
}
} catch as e {
Log("-> ERROR during FINAL delete/send step: " . e.Message)
}
}
}
}
; =============================================================================
; WATCHER INITIALIZATION & RE-ARMING
; =============================================================================
WatchFolder(pFolder) {
global hDir
hDir := DllCall("CreateFile", "Str", pFolder, "UInt", 1, "UInt", 7, "Ptr", 0, "UInt", 3, "UInt", 0x42000000, "Ptr", 0, "Ptr")
if (hDir = -1) {
local errMsg := "FATAL: Could not watch directory: " . pFolder
Log(errMsg), MsgBox(errMsg, "Error", 16), ExitApp
}
Log("Successfully opened handle for directory: " . pFolder)
ReArmWatcher()
}
ReArmWatcher() {
global hDir, pBuffer, pOverlapped, CompletionRoutineProc
static notifyFilter := 0x1
DllCall("msvcrt\memset", "Ptr", pOverlapped.Ptr, "Int", 0, "Ptr", pOverlapped.Size)
local success := DllCall("ReadDirectoryChangesW", "Ptr", hDir, "Ptr", pBuffer, "UInt", pBuffer.Size, "Int", false, "UInt", notifyFilter, "Ptr", 0, "Ptr", pOverlapped, "Ptr", CompletionRoutineProc)
if (success) {
Log("Arming watcher successful.")
} else {
Log("--- WARNING: ReArmWatcher failed! Error: " . A_LastError . ". Flag will be re-checked. ---")
watcherNeedsRearm := true
}
}
; =============================================================================
; COMPLETION ROUTINE TRIGGERS PROCESSING
; =============================================================================
IOCompletionRoutine(dwErrorCode, dwNumberOfBytesTransfered, lpOverlapped) {
global pBuffer, watcherNeedsRearm
try {
if (dwErrorCode != 0) {
Log("-> ERROR in IOCompletionRoutine. Code: " . dwErrorCode)
} else if (dwNumberOfBytesTransfered > 0) {
Log("==> Event TRIGGERED!")
local pCurrent := pBuffer.Ptr
Loop {
local NextEntryOffset := NumGet(pCurrent, 0, "UInt")
local Action := NumGet(pCurrent + 4, "UInt")
local FileName := StrGet(pCurrent + 12, NumGet(pCurrent + 8, "UInt") / 2, "UTF-16")
Log("--> Event data: Action=" . Action . ", FileName=" . FileName)
if (Action = 1) {
QueueFile(FileName)
}
if (!NextEntryOffset) {
break
}
pCurrent += NextEntryOffset
}
TriggerQueueProcessing()
}
} catch as e {
Log("--- FATAL ERROR in IOCompletionRoutine: " . e.Message . " ---")
}
watcherNeedsRearm := true
}
type_watcher.sh¶
#!/bin/bash
# type_watcher.sh
set -euo pipefail
DIR_TO_WATCH="/tmp/sl5_aura"
LOCKFILE="/tmp/type_watcher.lock"
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
LOG_DIR="$SCRIPT_DIR/log"
LOGFILE="$LOG_DIR/type_watcher.log"
AUTO_ENTER_FLAG="/tmp/sl5_auto_enter.flag" # The flag file for auto-enter
speak_file_path="$HOME/projects/py/TTS/speak_file.py"
if [ -e $speak_file_path ]
then
echo " ok $speak_file_path exist"
else
speak_file_path=''
echo "$speak_file_path dont exist"
fi
# Ensure log directory exists
mkdir -p "$LOG_DIR"
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOGFILE"
}
log_message "Hello from Watcher"
# --- Lockfile-Logic
if [ -e "$LOCKFILE" ]; then
pid=$(cat "$LOCKFILE" 2>/dev/null)
if [ -n "$pid" ] && ps -p "$pid" > /dev/null; then
msg="Watcher already runs (PID: $pid). Exiting."
echo "$msg"
log_message "$msg"
exit 0
fi
rm -f "$LOCKFILE"
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
# --- Wait for the directory to be created by the main service ---
while [ ! -d "$DIR_TO_WATCH" ]; do
sleep 0.5
done
# Function to get the title of the active window
get_active_window_title() {
active_window_id=$(xdotool getactivewindow)
xdotool getwindowname "$active_window_id"
}
OS_TYPE=$(uname -s)
if [[ "$OS_TYPE" == "Darwin" ]]; then
# --- macOS Logic ---
echo "✅ Watcher starting in macOS mode (using fswatch and osascript)."
log_message "Watcher starting in macOS mode (using fswatch and osascript)."
fswatch -0 "$DIR_TO_WATCH" | while read -d "" file; do
if [[ "$file" == *tts_output_*.txt ]]; then
osascript -e "tell application \"System Events\" to keystroke \"$(cat "$file")\""
rm -f "$file"
fi
done
elif [[ "$OS_TYPE" == "Linux" ]]; then
# --- Linux Logic ---
echo "✅ Watcher starting in Linux mode (using inotifywait and xdotool)."
log_message "Watcher starting in Linux mode (using inotifywait and xdotool)."
while true; do
inotifywait -q -e create,close_write "$DIR_TO_WATCH" --format '%f' | grep -q "tts_output_"
sleep 0.1
# Use null-separated find for safer file handling (handles spaces/newlines)
while IFS= read -r -d '' f; do
[ -f "$f" ] || continue
# Read lines into an array
mapfile -t lines < "$f"
for line in "${lines[@]}"; do
trimmed_line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
GAME_WINDOW_ID=$(xdotool search --name "0 A.D." | head -1 || true)
if [[ -n "$GAME_WINDOW_ID" ]]; then
log_message "GAME_WINDOW_ID = $GAME_WINDOW_ID"
if [[ "$trimmed_line" == 'alt+i' ]]; then
LC_ALL=C.UTF-8 xdotool windowactivate "$GAME_WINDOW_ID"
sleep 0.2
log_message "Sende ALT+i explizit mit keydown/keyup"
LC_ALL=C.UTF-8 xdotool keydown --window "$GAME_WINDOW_ID" 64
sleep 0.05
LC_ALL=C.UTF-8 xdotool key --window "$GAME_WINDOW_ID" 31
sleep 0.05
LC_ALL=C.UTF-8 xdotool keyup --window "$GAME_WINDOW_ID" 64
sleep 0.1
log_message "Fertig mit ALT+i Sequenz"
log_message "Sent alt+i"
if [[ -n "$speak_file_path" ]]; then
python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
sleep 0.01
fi
rm -f "$f"
continue
elif [[ "$trimmed_line" == 'alt+w' ]]; then
xte "keydown Alt_L" "keydown w" "keyup w" "keyup Alt_L"
log_message "Sent alt+w"
if [[ -n "$speak_file_path" ]]; then
python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
sleep 0.01
fi
rm -f "$f"
continue
elif [[ "$trimmed_line" == 'ctrl+c' ]]; then
LC_ALL=C.UTF-8 xdotool key ctrl+c clearmodifiers
log_message "Sent ctrl+c"
if [[ -n "$speak_file_path" ]]; then
python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
sleep 0.01
fi
rm -f "$f"
continue
elif [[ "$trimmed_line" == 'baue Haus' ]]; then
LC_ALL=C.UTF-8 xdotool key h
sleep 0.15
xdotool click --delay 10 --repeat 8 1
log_message "baue Haus"
if [[ -n "$speak_file_path" ]]; then
python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
sleep 0.01
fi
rm -f "$f"
continue
elif [[ "$trimmed_line" == 'baue Lagerhaus' ]]; then
LC_ALL=C.UTF-8 xdotool key s
sleep 0.15
xdotool click --delay 10 --repeat 8 1
log_message "baue Lagerhaus"
if [[ -n "$speak_file_path" ]]; then
python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
sleep 0.01
fi
rm -f "$f"
continue
elif [[ "$trimmed_line" == 'select iddle' ]]; then
xdotool keydown alt
xdotool type '#'
xdotool keyup alt
sleep 0.15
# xdotool click --delay 10 --repeat 8 1
log_message "select iddle"
if [[ -n "$speak_file_path" ]]; then
python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
sleep 0.01
fi
rm -f "$f"
continue
elif [[ "$trimmed_line" == 'baue Baracke' ]]; then
LC_ALL=C.UTF-8 xdotool key b
sleep 0.15
xdotool click --delay 10 --repeat 8 1
sleep 4
log_message "baue Baracke"
if [[ -n "$speak_file_path" ]]; then
python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
sleep 0.01
fi
rm -f "$f"
continue
fi
fi
# Fallback: type file content (if not a special command)
if [ -z "${CI:-}" ]; then
LC_ALL=C.UTF-8 xdotool type --clearmodifiers --delay 0 --file "$f"
log_message "type --file $f"
# When you also want to have a voice feedback (means STT + littleAI + TTS )
# Then you could use this repository:
# https://github.com/sl5net/gemini-tts/blob/main/speak_file.py
# You could start the STT Service like so:
# ~/projects/py/TTS/scripts/restart_venv_and_run-server.sh
# And add to type_watcher.sh the following line here:
if [[ -n "$speak_file_path" ]]; then
python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
sleep 0.01
fi
rm -f "$f"
continue
fi
done
rm -f "$f"
# --- Conditional Enter Key ---
window_title=$(get_active_window_title)
if [[ -f "$AUTO_ENTER_FLAG" ]]; then
regexLine=$(cat "$AUTO_ENTER_FLAG")
if echo "$window_title" | grep -Eq "$regexLine"; then
log_message "INFO: Auto-Enter is enabled. Pressing Return."
if [ -z "${CI:-}" ]; then
LC_ALL=C.UTF-8 xdotool key Return
fi
fi
fi
done < <(find "$DIR_TO_WATCH" -maxdepth 1 -type f -name 'tts_output_*.txt' -print0)
done
else
echo "ERROR: Unsupported operating system '$OS_TYPE'. Exiting."
exit 1
fi