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
# --- Argument Parsing for Exclusion ---
EXCLUDE_LANGUAGES=""
# --- Argument Parsing for Exclusion ---
EXCLUDE_LANGUAGES=""
# Parst Argumente in den Formaten: exclude=all, exclude=de, exclude=en, exclude=de,en
for arg in "$@"; do
# Prüft auf Formate: exclude=all, exclude=de, exclude=de,en (fängt alle Spracodes ab)
if [[ "$arg" =~ ^exclude=([a-zA-Z,]+)$ ]]; then
EXCLUDE_LANGUAGES="${BASH_REMATCH[1]}"
# Optional: Altes Format exclude:de,en beibehalten
elif [[ "$arg" =~ ^exclude:([a-zA-Z,]+)$ ]]; then
EXCLUDE_LANGUAGES="${BASH_REMATCH[1]}"
fi
done
if [ -n "$EXCLUDE_LANGUAGES" ]; then
echo "--> Exclusion list detected: $EXCLUDE_LANGUAGES"
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
# --- Filter Configuration based on EXCLUDE_LANGUAGES ---
INSTALL_CONFIG=()
if [ -z "$EXCLUDE_LANGUAGES" ]; then
# Keine Ausschlüsse, die gesamte MASTER-Liste wird installiert.
INSTALL_CONFIG=("${ARCHIVE_CONFIG[@]}")
else
# Ausschlüsse aktiv, die Liste wird gefiltert.
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
IS_MANDATORY=false
IS_EXCLUDED=false
# Komponenten, die immer benötigt werden (z.B. LanguageTool Core)
if [[ "$base_name" == "LanguageTool-6.6" ]] || [[ "$base_name" == "lid.176" ]]; then
IS_MANDATORY=true
fi
# 1. Ausschluss-Check: exclude=all
if [ "$EXCLUDE_LANGUAGES" == "all" ] && [ "$IS_MANDATORY" = false ]; then
echo " -> Excluding (all): $base_name"
IS_EXCLUDED=true
fi
# 2. Ausschluss-Check: Spezifische Sprachen (z.B. de, en)
if [ "$IS_EXCLUDED" = false ]; then
# Test auf 'de' im Namen und in der Ausschlussliste
if [[ "$base_name" =~ vosk-model-de- ]] && [[ "$EXCLUDE_LANGUAGES" =~ de ]]; then
echo " -> Excluding (de): $base_name"
IS_EXCLUDED=true
fi
# Test auf 'en' im Namen und in der Ausschlussliste
if [[ "$base_name" =~ vosk-model.*en-us ]] && [[ "$EXCLUDE_LANGUAGES" =~ en ]]; then
echo " -> Excluding (en): $base_name"
IS_EXCLUDED=true
fi
# Hinzufügen weiterer spezifischer Exklusionsregeln nach Bedarf...
fi
# Nur hinzufügen, wenn nicht ausgeschlossen
if [ "$IS_EXCLUDED" = false ]; then
INSTALL_CONFIG+=("$config_line")
fi
done
fi
# --- End Filter Configuration ---
# --- 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 "${INSTALL_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 --exclude "$EXCLUDE_LANGUAGES"
echo " -> Downloader finished. Retrying extraction..."
# After downloading, we must re-check and extract anything that's still missing.
for config_line in "${INSTALL_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"
# --- Argument Parsing for Exclusion ---
EXCLUDE_LANGUAGES=""
# --- Argument Parsing for Exclusion ---
EXCLUDE_LANGUAGES=""
# Parst Argumente in den Formaten: exclude=all, exclude=de, exclude=en, exclude=de,en
for arg in "$@"; do
# Prüft auf Formate: exclude=all, exclude=de, exclude=de,en (fängt alle Spracodes ab)
if [[ "$arg" =~ ^exclude=([a-zA-Z,]+)$ ]]; then
EXCLUDE_LANGUAGES="${BASH_REMATCH[1]}"
# Optional: Altes Format exclude:de,en beibehalten
elif [[ "$arg" =~ ^exclude:([a-zA-Z,]+)$ ]]; then
EXCLUDE_LANGUAGES="${BASH_REMATCH[1]}"
fi
done
if [ -n "$EXCLUDE_LANGUAGES" ]; then
echo "--> Exclusion list detected: $EXCLUDE_LANGUAGES"
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 ---
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
# --- Filter Configuration based on EXCLUDE_LANGUAGES ---
INSTALL_CONFIG=()
if [ -z "$EXCLUDE_LANGUAGES" ]; then
# Keine Ausschlüsse, die gesamte MASTER-Liste wird installiert.
INSTALL_CONFIG=("${ARCHIVE_CONFIG[@]}")
else
# Ausschlüsse aktiv, die Liste wird gefiltert.
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
IS_MANDATORY=false
IS_EXCLUDED=false
# Komponenten, die immer benötigt werden (z.B. LanguageTool Core)
if [[ "$base_name" == "LanguageTool-6.6" ]] || [[ "$base_name" == "lid.176" ]]; then
IS_MANDATORY=true
fi
# 1. Ausschluss-Check: exclude=all
if [ "$EXCLUDE_LANGUAGES" == "all" ] && [ "$IS_MANDATORY" = false ]; then
echo " -> Excluding (all): $base_name"
IS_EXCLUDED=true
fi
# 2. Ausschluss-Check: Spezifische Sprachen (z.B. de, en)
if [ "$IS_EXCLUDED" = false ]; then
# Test auf 'de' im Namen und in der Ausschlussliste
if [[ "$base_name" =~ vosk-model-de- ]] && [[ "$EXCLUDE_LANGUAGES" =~ de ]]; then
echo " -> Excluding (de): $base_name"
IS_EXCLUDED=true
fi
# Test auf 'en' im Namen und in der Ausschlussliste
if [[ "$base_name" =~ vosk-model.*en-us ]] && [[ "$EXCLUDE_LANGUAGES" =~ en ]]; then
echo " -> Excluding (en): $base_name"
IS_EXCLUDED=true
fi
# Hinzufügen weiterer spezifischer Exklusionsregeln nach Bedarf...
fi
# Nur hinzufügen, wenn nicht ausgeschlossen
if [ "$IS_EXCLUDED" = false ]; then
INSTALL_CONFIG+=("$config_line")
fi
done
fi
# --- End Filter Configuration ---
# --- 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 "${INSTALL_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 --exclude "$EXCLUDE_LANGUAGES"
echo " -> Downloader finished. Retrying extraction..."
# After downloading, we must re-check and extract anything that's still missing.
for config_line in "${INSTALL_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"
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
# --- Argument Parsing for Exclusion ---
EXCLUDE_LANGUAGES=""
# Parst Argumente wie 'exclude:de,en' oder 'exclude=all'
for arg in "$@"; do
if [[ "$arg" =~ ^exclude:([a-zA-Z,]+)$ ]]; then
EXCLUDE_LANGUAGES="${BASH_REMATCH[1]}"
elif [[ "$arg" == "exclude=all" ]]; then
EXCLUDE_LANGUAGES="all"
fi
done
if [ -n "$EXCLUDE_LANGUAGES" ]; then
echo "--> Exclusion list detected: $EXCLUDE_LANGUAGES"
fi
# --- End Argument Parsing ---
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
# --- Filter Configuration based on EXCLUDE_LANGUAGES ---
INSTALL_CONFIG=()
if [ -z "$EXCLUDE_LANGUAGES" ]; then
# Keine Ausschlüsse, die gesamte MASTER-Liste wird installiert.
INSTALL_CONFIG=("${ARCHIVE_CONFIG[@]}")
else
# Ausschlüsse aktiv, die Liste wird gefiltert.
for config_line in "${ARCHIVE_CONFIG[@]}"; do
read -r base_name final_name dest_path <<< "$config_line"
IS_MANDATORY=false
IS_EXCLUDED=false
# Komponenten, die immer benötigt werden (z.B. LanguageTool Core)
if [[ "$base_name" == "LanguageTool-6.6" ]] || [[ "$base_name" == "lid.176" ]]; then
IS_MANDATORY=true
fi
# 1. Ausschluss-Check: exclude=all
if [ "$EXCLUDE_LANGUAGES" == "all" ] && [ "$IS_MANDATORY" = false ]; then
echo " -> Excluding (all): $base_name"
IS_EXCLUDED=true
fi
# 2. Ausschluss-Check: Spezifische Sprachen (z.B. de, en)
if [ "$IS_EXCLUDED" = false ]; then
# Test auf 'de' im Namen und in der Ausschlussliste
if [[ "$base_name" =~ vosk-model-de- ]] && [[ "$EXCLUDE_LANGUAGES" =~ de ]]; then
echo " -> Excluding (de): $base_name"
IS_EXCLUDED=true
fi
# Test auf 'en' im Namen und in der Ausschlussliste
if [[ "$base_name" =~ vosk-model-en-us- ]] && [[ "$EXCLUDE_LANGUAGES" =~ en ]]; then
echo " -> Excluding (en): $base_name"
IS_EXCLUDED=true
fi
# Hinzufügen weiterer spezifischer Exklusionsregeln nach Bedarf...
fi
# Nur hinzufügen, wenn nicht ausgeschlossen
if [ "$IS_EXCLUDED" = false ]; then
INSTALL_CONFIG+=("$config_line")
fi
done
fi
# --- End Filter Configuration ---
# --- 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 "${INSTALL_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 --exclude "$EXCLUDE_LANGUAGES"
echo " -> Downloader finished. Retrying extraction..."
# After downloading, we must re-check and extract anything that's still missing.
for config_line in "${INSTALL_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"
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¶
REM windows11_setup.bat
@echo off
ECHO start Windows 11 Setup-Script...
REM PowerShell, use Execution Policy this time
REM %* passes all arguments (e.g., -Exclude "en") given to the BAT file to the PowerShell script.
powershell.exe -ExecutionPolicy Bypass -File "%~dp0\windows11_setup.ps1" %*
REM NOTE: The Python script call should ideally be inside the PowerShell script,
REM as the virtual environment (.\.venv) is only guaranteed to be set up *after* the PS1 script finishes.
REM Assuming the PS1 script now handles all necessary setup and model downloading/filtering.
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
# how to start:
# .\setup\windows11_setup.ps1 -Exclude "en" or .\setup\windows11_setup.ps1 -Exclude "de" or .\setup\windows11_setup.ps1 -Exclude "all".
# --- Argument Parsing for Exclusion (NEW) ---
param(
[string]$Exclude = ""
)
# --- Make script location-independent ---
$ProjectRoot = Split-Path -Path $PSScriptRoot -Parent
Set-Location -Path $ProjectRoot
Write-Host "--> Running setup from project root: $(Get-Location)"
$EXCLUDE_LANGUAGES = $Exclude
if (-not [string]::IsNullOrEmpty($EXCLUDE_LANGUAGES)) {
Write-Host "--> Exclusion list detected: $EXCLUDE_LANGUAGES" -ForegroundColor Yellow
}
# --- End Argument Parsing ---
$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 --upgrade pip
.\.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 --exclude "$EXCLUDE_LANGUAGES"
# $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..."
# --- Configuration: Base List of Archives ---
$Prefix = "Z_"
$MasterConfig = @(
@{ BaseName = "LanguageTool-6.6"; FinalName = "LanguageTool-6.6"; Dest = "." },
@{ BaseName = "vosk-model-en-us-0.22"; FinalName = "vosk-model-en-us-0.22"; Dest = ".\models" },
@{ BaseName = "vosk-model-small-en-us-0.15"; FinalName = "vosk-model-small-en-us-0.15"; Dest = ".\models" },
@{ BaseName = "vosk-model-de-0.21"; FinalName = "vosk-model-de-0.21"; Dest = ".\models" },
@{ BaseName = "lid.176"; FinalName = "lid.176.bin"; Dest = ".\models" }
)
# --- Filter Configuration based on EXCLUDE_LANGUAGES (NEW) ---
$INSTALL_CONFIG = @()
$ExcludeList = @($EXCLUDE_LANGUAGES.Split(',') | ForEach-Object { $_.Trim().ToLower() })
if ($ExcludeList.Count -eq 0 -or $ExcludeList[0] -eq "") {
# No exclusion, use master list
$INSTALL_CONFIG = $MasterConfig
} else {
# Exclusion active, filter the list
foreach ($ConfigItem in $MasterConfig) {
$IsMandatory = $ConfigItem.BaseName -like "LanguageTool-*" -or $ConfigItem.BaseName -eq "lid.176"
$IsExcluded = $false
# 1. Exclusion Check: exclude=all
if ($ExcludeList -contains "all" -and -not $IsMandatory) {
Write-Host " -> Excluding (all): $($ConfigItem.BaseName)"
$IsExcluded = $true
}
# 2. Exclusion Check: Specific Languages
if (-not $IsExcluded) {
# Check for 'de' exclusion
if ($ExcludeList -contains "de" -and $ConfigItem.BaseName -like "*vosk-model-de-*") {
Write-Host " -> Excluding (de): $($ConfigItem.BaseName)"
$IsExcluded = $true
}
# Check for 'en' exclusion (covers both en-us models)
if ($ExcludeList -contains "en" -and $ConfigItem.BaseName -like "*vosk-model*en-us*") {
Write-Host " -> Excluding (en): $($ConfigItem.BaseName)"
$IsExcluded = $true
}
# Add other specific language checks here if needed...
}
# Only add if not excluded
if (-not $IsExcluded) {
$INSTALL_CONFIG += $ConfigItem
}
}
}
# Now, create the ArchiveConfig from the filtered INSTALL_CONFIG
$ArchiveConfig = $INSTALL_CONFIG | ForEach-Object {
[PSCustomObject]@{
Zip = "$($Prefix)$($_.BaseName).zip"
Dir = $_.BaseName
Dest = $_.Dest
}
}
# --- End Filter Configuration ---
# 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
import importlib
#import pwd
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
def is_plugin_enabled(hierarchical_key, plugins_config):
"""
Prüft, ob ein Plugin aktiviert ist. Ein Plugin ist DEAKTIVIERT,
wenn es selbst oder irgendein übergeordnetes Modul in der Hierarchie
explizit auf `False` gesetzt ist. In allen anderen Fällen ist es AKTIVIERT.
"""
current_key_parts = hierarchical_key.split('/')
for i in range(len(current_key_parts)):
current_key = "/".join(current_key_parts[:i + 1])
if plugins_config.get(current_key) is False:
return False
return True
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_DIR = PROJECT_ROOT / "log"
LOG_FILE = LOG_DIR / "dynamic_settings.log"
if not LOG_DIR.exists():
LOG_DIR.mkdir(exist_ok=True)
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)
# Cross-platform logic to determine the current user's name:
if sys.platform.startswith('win'):
# On Windows, use the USERNAME environment variable.
current_user = os.environ.get('USERNAME')
# logger.info("Determined current_user using Windows environment variables.") # Add log later if needed
else:
# On Unix/Linux/Mac, try the 'pwd' module (it should work here)
# If the user is on a Unix-like system where 'pwd' is available,
# the original logic is more robust than environment variables alone.
try:
import pwd
current_user = pwd.getpwuid(os.getuid()).pw_name
# logger.info("Determined current_user using 'pwd' module (Unix-like system).")
except ImportError:
# Fallback for systems that look like Unix but might lack 'pwd'
# (e.g., some minimal environments or non-standard Python builds)
current_user = os.environ.get('USER')
# Final fallback
if not current_user:
current_user = "unknown_user"
logger.warning("Could not determine current_user, defaulting to 'unknown_user'.")
# Ensure 'current_user' is a string before proceeding
current_user = str(current_user)
logger.info(f"dynamic_settings.py: DEV_MODE={DEV_MODE}, settings.DEV_MODE = {settings.DEV_MODE}, current_user={current_user}")
# sys.exit(1)
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.")
if hasattr(self, 'PLUGINS_ENABLED') and isinstance(self.PLUGINS_ENABLED, dict):
if settings.DEV_MODE:
print("DEBUG: Resolving PLUGINS_ENABLED hierarchy...")
# Das zusammengeführte Dictionary, bevor es aufgelöst wird
raw_plugins_config = self.PLUGINS_ENABLED
# Ein neues Dictionary für die aufgelösten Zustände
resolved_plugins_config = {}
# Wir müssen über eine Kopie der Keys iterieren, falls wir das dict ändern
all_plugin_keys = list(raw_plugins_config.keys())
for key in all_plugin_keys:
# Wende unsere Hierarchie-Logik auf jeden Key an
resolved_status = is_plugin_enabled(key, raw_plugins_config)
resolved_plugins_config[key] = resolved_status
if settings.DEV_MODE:
print(f"DEBUG: Plugin '{key}' -> Resolved Status: {resolved_status}")
# Überschreibe das alte PLUGINS_ENABLED mit dem neuen, aufgelösten Dictionary
setattr(self, 'PLUGINS_ENABLED', resolved_plugins_config)
if settings.DEV_MODE:
print("DEBUG: PLUGINS_ENABLED has been updated with resolved statuses.")
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
import sys
SERVICE_START_OPTION = 0
# Option 1: Start the service only on when there is an internet connection.
# Get username
# Determine username in a cross-platform way
if sys.platform.startswith('win'):
# On Windows, use the USERNAME environment variable.
current_user = os.environ.get('USERNAME')
else:
# On Unix/Linux/Mac, try the 'pwd' module for robustness
try:
import pwd
# The original Unix-like system logic
current_user = pwd.getpwuid(os.getuid()).pw_name
except ImportError:
# Fallback for non-standard Unix environments
current_user = os.environ.get('USER')
# Final fallback to ensure current_user is always a string
if not current_user:
current_user = "unknown_user"
current_user = str(current_user)
# logger.info("Current user successfully determined in a cross-platform manner.") # Add logger import if needed
# 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 # Deprecated . Better set it to False
# --- Notification Settings ---
# Default for new users is the most verbose level.
NOTIFICATION_LEVEL = 0 # 0=Silent, 1=Essential, 2=Verbose
# 🗣️🌐 (symbols and icons are probably cut out later by )
# sometimes e.g.in twitch: gelöscht: Nightbot: @seeh74 -> Sorry, you can't post links without permission!
#🗣ടㄴ⠄de╱Aura SL5.de/Aura
signatur=' #FreedomLadder #FreeSoftware #FSF #SL5Aura SL5.de/Aura'
# signatur='🗣ടㄴ5⠄de╱Aura'
# signatur='🗣Sㄴ5⠄de╱Aura' # this l is unvisable in gemini
#signatur='🗣SL5⠄de╱Aura'
# signatur='🗣SL5⠠de╱Aura' # i like this 11.11.'25 09:58 Tue
#now (original:'jetzt', ).
#signatur=',🗣SL5。de╱Aura' # i like this 11.11.'25 09:58 Tue
signatur=''
#signatur1=f'{signatur}' # (Powered by
signatur_pt_br=f'Tradução de Voz{signatur}'
signatur_en=f'Voice Translation{signatur}'
signatur_en=f'{signatur}'
signatur_ar=f"تحدثت الترجمة{signatur} "
signatur_ja=f"話し言葉の翻訳{signatur} "
# --- 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"]
PLUGIN_HELPER_TTS_ENABLED = True
# USE_AS_PRIMARY_SPEAK = "piper"
USE_AS_PRIMARY_SPEAK = "ESPEAK"
USE_ESPEAK_FALLBACK = True
ESPEAK_FALLBACK_AMPLITUDE = 50
# --- 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,
}
# if true call iteratively all rules
default_mode_is_all = True
# 1 zwei drei vier 5
#
LT_SKIP_RATIO_THRESHOLD = 20
"""
Explanation of the Ratio Logic:
ratio = original_text_length / made_a_change: Calculates how many characters (on average) correspond to one change.
if ratio < lt_skip_ratio_threshold: If the ratio is low (less than the safe threshold), we skip LanguageTool.
"""
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
import os, sys
# My personal settings for SL5 Aura
# This file is ignored by Git.
SERVICE_START_OPTION = 0
# Option 1: Start the service only on when there is an internet connection.
# Get username
# Determine username in a cross-platform way
if sys.platform.startswith('win'):
# On Windows, use the USERNAME environment variable.
current_user = os.environ.get('USERNAME')
else:
# On Unix/Linux/Mac, try the 'pwd' module for robustness
try:
import pwd
# The original Unix-like system logic
current_user = pwd.getpwuid(os.getuid()).pw_name
except ImportError:
# Fallback for non-standard Unix environments
current_user = os.environ.get('USER')
# Final fallback to ensure current_user is always a string
if not current_user:
current_user = "unknown_user"
current_user = str(current_user)
# logger.info("Current user successfully determined in a cross-platform manner.") # Add logger import if needed
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
# best disable before run self-tester(DEV_MODE = True) rules like: match all to nothing. like: .+ -> or .* -> ''
# 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
DEV_MODE_all_processing = False
# needs NO restart:
PRE_RECORDING_TIMEOUT = 6
SPEECH_PAUSE_TIMEOUT = 2
# may yo want to overwrite the PRELOAD_MODELS settings from settings.py here
PRELOAD_MODELS = ["vosk-model-de-0.21"]
#test (original:'test', 🗣SL5。de╱Aura).
if current_user == 'seeh':
# needs NO restart:
PRE_RECORDING_TIMEOUT = 2
SPEECH_PAUSE_TIMEOUT = 1
signatur=''
#
if 1:
signatur=''
signatur1=f''
signatur_pt_br=f''
signatur_en=f''
signatur_ar=f''
signatur_ja=f''
DEV_MODE = True
# DEV_MODE = False
DEV_MODE_all_processing = 0
PRELOAD_MODELS = ["vosk-model-de-0.21"]
# PRELOAD_MODELS = ["vosk-model-en-us-0.22"]
# needs NO restart:
PRE_RECORDING_TIMEOUT = 2
SPEECH_PAUSE_TIMEOUT = 1
# 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_keep_alive.sh, AutoKey).
# These settings tell the backend service what to expect or how to format output.
#git status git status git status
USE_ESPEAK_FALLBACK = True
PLUGINS_ENABLED = {
"empty_all": False,
"git": True,
"wannweil": True,
"game": False,
"game/dealers_choice": True,
"game/0ad": False,
"ethiktagung": False,
"volkshochschule_tue": True,
"CCC_tue": True,
"vsp_rt": True,
"ki-maker-space": True,
"numbers_to_digits": True, # hundert|einhundert --> 100
"digits_to_numbers": False,
"web-radio-funk": True,
"it-begriffe": True,
"it-begriffe/php/codeigniter": True,
}
# geht status eins zwei doch
# needs restart. implemented in the python part:
ADD_TO_SENCTENCE = "."
# set ADD_TO_SENCTENCE = "" when you dont want it.
# 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
import os
import pwd
# My personal settings Example
# This file is (maybe) 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
# Get username
current_user = pwd.getpwuid(os.getuid())[0]
if current_user == 'seeh':
DEV_MODE = True
# 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
DEV_MODE_all_processing = False
PRELOAD_MODELS = ["vosk-model-de-0.21"]
signatur=''
signatur1=f''
signatur_pt_br=f''
signatur_en=f''
signatur_ar=f''
signatur_ja=f''
# --- 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
# for more poweruser mor in hurry:
# needs NO restart:
PRE_RECORDING_TIMEOUT = 6
SPEECH_PAUSE_TIMEOUT = 2
#test (original:'test', 🗣SL5。de╱Aura).
if current_user == 'sl5':
# needs NO restart:
PRE_RECORDING_TIMEOUT = 4
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)
USE_ESPEAK_FALLBACK = True
ESPEAK_FALLBACK_AMPLITUDE = 80
# --- 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 = {
"empty_all": False,
"git": True,
"wannweil": True,
"game/dealers_choice": False,
"game/0ad": False,
"ethiktagung": False,
"volkshochschule_tue": True,
"CCC_tue": True,
"vsp_rt": True,
"ki-maker-space": True,
"numbers_to_digits": True, # hundert|einhundert --> 100
"digits_to_numbers": False,
"web-radio-funk": True,
"it-begriffe": False,
"it-begriffe/php/codeigniter": True,
}
Githooks-Skripte¶
githooks/pre-commit¶
#!/bin/bash
# .git/hooks/pre-commit
# --- Start of flake8 check ---
echo "Running flake8 code quality check..."
# Activate virtual environment if it exists
if [ -f .venv/bin/activate ]; then
source .venv/bin/activate
fi
# Run flake8 on specific directories
flake8 ./dictation_service.py ./scripts ./config
# flake8 ./scripts ./config/languagetool_server/maps
# Abort commit if flake8 finds errors
if [ $? -ne 0 ]; then
echo "COMMIT REJECTED: flake8 found issues."
exit 1
fi
echo "flake8 check passed."
# --- End of flake8 check ---
# Check if pipreqs is available in the venv
if [ ! -x ".venv/bin/pipreqs" ]; then
echo "pipreqs is not installed in .venv. Please run '.venv/bin/pip install pipreqs' inside your virtual environment."
exit 1
fi
TMPDIR=".pipreqs_temp"
# Clean up and create a fresh temp directory
rm -rf "$TMPDIR"
mkdir "$TMPDIR"
# Copy all relevant Python sources into the temp directory
cp dictation_service.py "$TMPDIR/"
cp get_suggestions.py "$TMPDIR/"
# Das gesamte 'scripts'-Verzeichnis kopieren, damit Import-Pfade erhalten bleiben.
cp -r scripts "$TMPDIR/"
echo "INFO: Generating requirements.txt (take into account .pipreqs-whitelist.txt) ..."
# --- FINALE KORREKTUR HIER: ---
# Wir benutzen --savepath, um pipreqs anzuweisen, die Datei im aktuellen
# Verzeichnis zu speichern, während es das Temp-Verzeichnis analysiert.
# Die Warnungen fangen wir weiterhin für die Whitelist ab.
WARNINGS=$(.venv/bin/pipreqs "$TMPDIR" --savepath ./requirements.txt --force --encoding=utf-8 2>&1 >/dev/null)
# Filtere die Warnungen. Zeige nur die an, die NICHT in der Whitelist stehen.
if [ -s ".pipreqs-whitelist.txt" ]; then
# Zeige nur die Zeilen, die nicht in der Whitelist sind
echo "$WARNINGS" | grep -v -f .pipreqs-whitelist.txt | grep -v "Please, verify manually"
else
# Wenn die Whitelist leer ist, zeige alle Warnungen
echo "$WARNINGS"
fi
# Remove known false positives like our own 'scripts' directory
sed -i '/^scripts==/d' requirements.txt
# Convert package names to lowercase
awk -F'==' '/==/ {print tolower($1) "==" $2}' requirements.txt > requirements.txt.tmp && mv requirements.txt.tmp requirements.txt
# --- PLATFORM-SPECIFIC FASTTEXT FIX ---
# pipreqs generates 'fasttext==X.X.X'. We need to replace it with the
# platform-specific versions for Windows and Linux/MacOS.
echo "INFO: Applying Fasttext platform-specific fix..."
# 1. Remove the line generated by pipreqs
sed -i '/^fasttext==/d' requirements.txt
# 2. Append the platform-specific rules explicitly
echo "fasttext==0.9.3; sys_platform != 'win32'" >> requirements.txt
echo "fasttext-wheel==0.9.2; sys_platform == 'win32'" >> requirements.txt
# ------------------------------------
# B) WINDOWS AUDIO & CONTROL (Persistent on Linux)
# Remove potential entries to prevent duplicates or missing markers
sed -i '/^comtypes/d' requirements.txt
sed -i '/^pycaw/d' requirements.txt
# Append explicit Windows rules
echo "comtypes==1.4.11; sys_platform == 'win32'" >> requirements.txt
echo "pycaw==20240210; sys_platform == 'win32'" >> requirements.txt
# Clean up temp directory
rm -rf "$TMPDIR"
# add some explizit that e.g. used in little config/maps/plugin scripts
echo "wikipedia-api" >> requirements.txt
echo "bs4" >> requirements.txt
cp .git/hooks/pre-commit docs/pre-commit_backup.txt
Update-Skripte¶
update/update_for_linux_users.sh¶
#!/bin/bash
# file: update/update_for_linux_users.sh
# Description: Downloads the latest version and updates the application
# while preserving user settings. For non-developer use.
# Stoppt das Script sofort bei einem Fehler
set -e
# Farbdefinitionen für die Konsole
COLOR_CYAN='\e[36m'
COLOR_GREEN='\e[32m'
COLOR_RED='\e[31m'
COLOR_YELLOW='\e[33m'
COLOR_RESET='\e[0m'
# Variablen
repoUrl="https://github.com/sl5net/SL5-aura-service/archive/refs/heads/master.zip"
# Den Installationspfad ermitteln (zwei Ebenen über dem Script-Pfad)
# Annahme: Script liegt in 'installDir/update/'
scriptDir="$(dirname "$0")"
installDir="$(dirname "$scriptDir")"
tempDir="/tmp/sl5_update_temp"
localSettingsPath="$installDir/config/settings_local.py"
backupPath="$tempDir/settings_local.py.bak"
zipPath="$tempDir/latest.zip"
# Funktion zur Fehlerbehandlung
cleanup_on_error() {
echo -e "${COLOR_RED}FATAL: Ein Fehler ist während des Updates aufgetreten.${COLOR_RESET}" >&2
if [ -d "$tempDir" ]; then
echo -e "${COLOR_YELLOW}INFO: Lösche temporären Ordner...${COLOR_RESET}"
rm -rf "$tempDir"
fi
read -p "Drücken Sie die Eingabetaste, um das Script zu beenden."
exit 1
}
# Fange Fehler ab (trap)
trap cleanup_on_error ERR
echo -e "${COLOR_CYAN}--- SL5 Aura Updater ---${COLOR_RESET}"
echo "Dies lädt die neueste Version herunter und ersetzt alle Anwendungsdateien."
echo "Ihre persönlichen Einstellungen in 'config/settings_local.py' werden gespeichert."
if [ "$CI" != "true" ]; then
echo "Bitte schließen Sie die Hauptanwendung, falls sie läuft."
read -p "Drücken Sie die Eingabetaste, um fortzufahren, oder CTRL+C, um abzubrechen"
fi
# 1. Alte temporäre Dateien bereinigen und neuen Ordner erstellen
if [ -d "$tempDir" ]; then
echo "INFO: Entferne alten temporären Update-Ordner..."
rm -rf "$tempDir"
fi
mkdir -p "$tempDir"
# 2. Lokale Einstellungen sichern, falls vorhanden
if [ -f "$localSettingsPath" ]; then
echo -e "${COLOR_GREEN}INFO: Sichern Ihrer lokalen Einstellungen...${COLOR_RESET}"
cp "$localSettingsPath" "$backupPath"
fi
# 3. Neueste Version herunterladen (curl oder wget erforderlich)
echo "INFO: Lade neueste Version von GitHub herunter..."
curl -L -o "$zipPath" "$repoUrl"
# 4. Archiv entpacken (unzip erforderlich)
echo "INFO: Entpacke Update..."
unzip -q "$zipPath" -d "$tempDir"
# Den extrahierten Ordner finden (der normalerweise auf '-master' endet)
extractedFolder=$(find "$tempDir" -maxdepth 1 -type d -name "*-master" | head -n 1)
if [ -z "$extractedFolder" ]; then
echo -e "${COLOR_RED}FATAL: Konnte den extrahierten '*-master' Ordner nicht finden.${COLOR_RESET}"
exit 1
fi
# 5. Lokale Einstellungen in die neue Version wiederherstellen
if [ -f "$backupPath" ]; then
echo -e "${COLOR_GREEN}INFO: Stelle Ihre lokalen Einstellungen in die neue Version wieder her...${COLOR_RESET}"
cp "$backupPath" "$extractedFolder/config/"
fi
# 6. Dateien ersetzen (rsync erforderlich)
# Mit rsync die Dateien aus dem temporären Ordner in den Installationsordner kopieren und die alten Dateien löschen.
echo "INFO: Finalisiere Update und ersetze Dateien. Bitte warten..."
# Die '-a' Option bewahrt Berechtigungen, Zeiten etc.
# Die '--delete' Option entfernt Dateien, die im Quellverzeichnis nicht vorhanden sind.
# Der abschließende Schrägstrich bei "$extractedFolder/" ist wichtig, um den *Inhalt* zu kopieren.
# rsync -a --delete --force "$extractedFolder/" "$installDir"
# --exclude='__pycache__' \
# --exclude='*.pyc' \
rsync -a --delete --force \
--max-size=100M \
--exclude='.venv' \
--exclude='models' \
--exclude='LanguageTool*' \
--exclude='.git' \
"$extractedFolder/" "$installDir"
# 7. Aufräumen
echo "INFO: Bereinige temporäre Dateien..."
rm -rf "$tempDir"
echo ""
echo -e "${COLOR_GREEN}Update abgeschlossen! Sie können die Anwendung jetzt neu starten.${COLOR_RESET}"
# Keine Fehlerbehandlung notwendig, wenn das Script bis hierher kommt
trap - ERR
read -p "Drücken Sie die Eingabetaste, um das Script zu beenden."
exit 0
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 and update dependencies
# WICHTIG: Ersetze 'install.bat' durch den echten Namen deiner Installations-Datei!
$installerName = "install.bat"
$batchScript = @'
@echo off
echo Finalizing update, please wait...
timeout /t 3 /nobreak > nul
:: 1. Dateien verschieben
robocopy "{0}" "{1}" /E /MOVE /NFL /NDL /NJH /NJS > nul
:: 2. In Zielordner wechseln
cd /d "{1}"
:: 3. Abhängigkeiten aktualisieren
echo.
echo ---------------------------------------------------
echo Updating dependencies (pip install)...
echo ---------------------------------------------------
:: 3. Installer aufrufen (Dort passiert das pip upgrade und requirements install)
if exist "{2}" (
echo Starting installer script...
call "{2}"
) else (
echo WARNING: "{2}" not found. Trying manual pip upgrade...
:: Fallback, falls keine install.bat da ist:
if exist ".venv\Scripts\activate.bat" (
call .venv\Scripts\activate.bat
python.exe -m pip install --upgrade pip
pip install -r requirements.txt
)
)
echo.
echo Update complete! You can now restart the application.
timeout /t 10 > nul
del "%~f0"
'@ -f $extractedFolder.FullName, $installDir, $installerName
$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_keep_alive.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_keep_alive.sh"
SERVICE_NAME_WATCHER2="type_watcher_keep_alive.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 -9 "$SERVICE_NAME_MAIN"
pkill -f "$SERVICE_NAME_WATCHER"
echo pkill -9 -f "$SERVICE_NAME_MAIN"
echo pkill -9 -f "$SERVICE_NAME_WATCHER"
echo pkill -9 -f "$SERVICE_NAME_WATCHER2"
# 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
# Sometimes it can help for performance (original:'manchmal kann es helfen aus performance Gründen').
# to loose all cash files (original:'wenn alle cash files entgernt werden').
find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf
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_keep_alive.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.batupdate.batinstall_hooks.shtype_watcher.ahktype_watcher_keep_alive.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.ahk" type_watcher.ahk
start "SL5 Notification Watcher.ahk" scripts\notification_watcher.ahk
start "trigger-hotkeys.ahk" trigger-hotkeys.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
python -X utf8 -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_keep_alive.sh¶
#!/bin/bash
# type_watcher_keep_alive.sh
# Stoppen: pkill -f type_watcher_keep_alive.sh
# --- PURPOSE OF THIS WRAPPER SCRIPT ---
#
# The main 'type_watcher.sh' script is generally stable, but has been
# observed to crash under specific, hard-to-reproduce circumstances,
# likely related to race conditions or heavy load (e.g., after processing
# many consecutive inputs).
#
# This wrapper script acts as a simple and robust "keep-alive" watchdog.
# It runs 'type_watcher.sh' in an infinite loop. If the main script
# ever crashes or exits unexpectedly, this watchdog will automatically
# restart it after a short delay. This ensures high availability without
# making the main script overly complex.
clear
# This script acts as a watchdog for type_watcher.sh.
# It runs it in an infinite loop, so if it ever crashes, it will be
# automatically restarted after a 1-second delay.
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
LOG_DIR="$SCRIPT_DIR/log"
LOGFILE="$LOG_DIR/type_watcher_keep_alive.sh"
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOGFILE"
}
echo "Starting watchdog for type_watcher.sh"
log_message "Starting watchdog for type_watcher.sh"
while true; do
# Check if the process is running by looking for a process with the script's name.
# We use 'pgrep -f' and exclude the watchdog script itself.
if pgrep -f "type_watcher.sh" | grep -qv "$$"; then
# It's running, do nothing.
:
else
# It's not running, so it must have crashed. Start it.
log_message "WATCHDOG: 'type_watcher.sh' is not running. Starting it now."
log_message "$SCRIPT_DIR/type_watcher.sh"
echo log_message "WATCHDOG: 'type_watcher.sh' is not running. Starting it now "
echo $SCRIPT_DIR/type_watcher.sh
$SCRIPT_DIR/type_watcher.sh
fi
# Wait for a few seconds before checking again.
sleep 5
done