Utility-Skripte

Automatisch generierte Übersicht der Hilfsskripte.

Setup-Skripte

setup/macos_setup.sh
#!/bin/bash
#
# setup/macos_setup.sh
# Run this setup script from the project's root directory.
#

SCRIPT_NAME=$(basename "$0")
# Check if the script is run from the project root.
# This check is more robust than changing directory.
if [ ! -f "requirements.txt" ]; then
    echo "ERROR: Please run this script from the project's root directory."
    echo ""
    echo "cd .. ; ./setup/$SCRIPT_NAME"
    exit 1
fi


should_remove_zips_after_unpack=true

# --- Make script location-independent ---
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(dirname "$SCRIPT_DIR")
cd "$PROJECT_ROOT"

echo "--> Running setup from project root: $(pwd)"
set -e
echo "--- Starting STT Setup for macOS ---"
# setup/macos_setup.sh
# --- 1. System Dependencies ---
if ! command -v brew &> /dev/null; then
    echo "ERROR: Homebrew not found. Please install it from https://brew.sh"
    exit 1
fi
echo "--> Checking for a compatible Java version (>=17)..."

JAVA_OK=0
if command -v java &> /dev/null; then
    VERSION=$(java -version 2>&1 | awk -F'[."]' '/version/ {print $2}')
    if [ "$VERSION" -ge 17 ]; then
        echo "    -> Found compatible Java version $VERSION. OK."
        JAVA_OK=1
    else
        echo "    -> Found Java version $VERSION, but we need >=17."
    fi
else
    echo "    -> No Java executable found."
fi
if [ "$JAVA_OK" -eq 0 ]; then
    echo "    -> Installing a modern JDK via Homebrew..."
    brew install openjdk
fi
echo "--> Installing other core dependencies..."
brew install fswatch wget unzip portaudio


# --- 2. Python Virtual Environment ---
# We check if the venv directory exists before creating it.
if [ ! -d ".venv" ]; then
    echo "--> Creating Python virtual environment in './.venv'..."
    python3 -m venv .venv
else
    echo "--> Virtual environment already exists. Skipping creation."
fi

# --- 3. Python Requirements ---
echo "--> Preparing requirements for macOS..."
# The macOS equivalent, 'fswatch', is already installed via Homebrew.
sed -i.bak '/inotify-tools/d' requirements.txt
echo "--> Installing Python requirements into the virtual environment..."
if ! ./.venv/bin/pip install -r requirements.txt; then
    echo "ERROR: Failed to install requirements. Trying to fix other common version issues..."
    # Example: Fix vosk version, then retry
    sed -i.bak 's/vosk==0.3.45/vosk/' requirements.txt
    # We run the command again after the potential fixes
    ./.venv/bin/pip install -r requirements.txt
fi
# --- 4. Project Structure and Configuration ---
echo "--> Setting up project directories and initial files..."
python3 "scripts/py/func/create_required_folders.py" "$(pwd)"
# ==============================================================================
# --- 5. Download and Extract Required Components ---
# This block intelligently handles downloads and extractions.
# ==============================================================================

echo "--> Checking for required components (LanguageTool, Vosk-Models)..."

# --- Configuration ---
PREFIX="Z_"
# Format: "BaseName FinalDirName DestinationPath"
ARCHIVE_CONFIG=(
    "LanguageTool-6.6 LanguageTool-6.6 ."
    "vosk-model-en-us-0.22 vosk-model-en-us-0.22 ./models"
    "vosk-model-small-en-us-0.15 vosk-model-small-en-us-0.15 ./models"
    "vosk-model-de-0.21 vosk-model-de-0.21 ./models"
    "lid.176 lid.176.bin ./models"
)
DOWNLOAD_REQUIRED=false

# --- Phase 1: Check and attempt to restore from local ZIP cache ---
echo "    -> Phase 1: Checking and trying to restore from local cache..."
for config_line in "${ARCHIVE_CONFIG[@]}"; do
    read -r base_name final_name dest_path <<< "$config_line"
    target_path="$dest_path/$final_name"
    zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"

    # If the component already exists, we're good for this one.
    if [ -e "$target_path" ]; then
        continue
    fi

    # The component is missing. Let's see if we can unzip it from a local cache.
    echo "    -> Missing: '$target_path'. Searching for '$zip_file'..."
    if [ -f "$zip_file" ]; then
        echo "    -> Found ZIP cache. Extracting '$zip_file'..."
        unzip -q "$zip_file" -d "$dest_path"
    else
        # The ZIP is not there. We MUST run the downloader.
        echo "    -> ZIP cache not found. A download is required."
        DOWNLOAD_REQUIRED=true
    fi
done

# --- Phase 2: Download if necessary ---
if [ "$DOWNLOAD_REQUIRED" = true ]; then
    echo "    -> Phase 2: Running Python downloader for missing components..."

    # Create the models directory before attempting to download files into it.
    mkdir -p ./models

    ./.venv/bin/python tools/download_all_packages.py
    echo "    -> Downloader finished. Retrying extraction..."

    # After downloading, we must re-check and extract anything that's still missing.
    for config_line in "${ARCHIVE_CONFIG[@]}"; do
        read -r base_name final_name dest_path <<< "$config_line"
        target_path="$dest_path/$final_name"
        zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"

        if [ -e "$target_path" ]; then
            continue
        fi

        if [ -f "$zip_file" ]; then
            echo "    -> Extracting newly downloaded '$zip_file'..."
            unzip -q "$zip_file" -d "$dest_path"
        else
            echo "    -> FATAL: Downloader ran but '$zip_file' is still missing. Aborting."
            exit 1
        fi
    done
fi

echo "--> All components are present and correctly placed."

# ==============================================================================
# --- End of Download/Extract block ---
# ==============================================================================






source "$(dirname "${BASH_SOURCE[0]}")/../scripts/sh/get_lang.sh"


# --- 6. Completion ---
echo ""
echo "--- Setup for macOS is complete! ---"
echo ""
echo "IMPORTANT NEXT STEPS:"
echo ""
echo "1. Configure Java PATH:"
echo "   To make Java available, you may need to add it to your shell's PATH."
echo "   Run this command in your terminal:"
echo '   export PATH="$(brew --prefix openjdk@21)/bin:$PATH"'
echo "   (Consider adding this line to your ~/.zshrc or ~/.bash_profile file to make it permanent)."
echo ""
echo "2. Activate Environment and Run:"
echo "   To start the application, use the following commands:"
echo "   source .venv/bin/activate"
echo "   ./scripts/restart_venv_and_run-server.sh"
echo ""
echo "3. Potential macOS Permissions:"
echo "   The 'xdotool' utility may require you to grant Accessibility permissions"
echo "   to your Terminal app in 'System Settings -> Privacy & Security -> Accessibility'."
echo ""
setup/manjaro_arch_setup.sh
#!/bin/bash
#
# setup/manjaro_arch_setup.sh
# Run this setup script from the project's root directory.
#

SCRIPT_NAME=$(basename "$0")
# Check if the script is run from the project root.
# This check is more robust than changing directory.
if [ ! -f "requirements.txt" ]; then
    echo "ERROR: Please run this script from the project's root directory."
    echo ""
    echo "cd .. ; ./setup/$SCRIPT_NAME"
    exit 1
fi


should_remove_zips_after_unpack=true


SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(dirname "$SCRIPT_DIR")
cd "$PROJECT_ROOT"

echo "--> Running setup from project root: $(pwd)"
# --- End of location-independent block ---

set -e

echo "--- Starting STT Setup for Manjaro/Arch Linux ---"

# setup/manjaro_arch_setup.sh
# --- 1. System Dependencies ---
echo "--> Checking for a compatible Java version (>=17)..."

JAVA_OK=0
if command -v java &> /dev/null; then
    VERSION=$(java -version 2>&1 | awk -F[\".] '/version/ {print ($2 == "1") ? $3 : $2}')
    if [ "$VERSION" -ge 17 ]; then
        echo "    -> Found compatible Java version $VERSION. OK."
        JAVA_OK=1
    else
        echo "    -> Found Java version $VERSION, but we need >=17."
    fi
else
    echo "    -> No Java executable found."
fi
if [ "$JAVA_OK" -eq 0 ]; then
    echo "    -> Installing a modern JDK to satisfy the requirement..."
    sudo pacman -S --noconfirm --needed jdk-openjdk
fi
echo "--> Installing other core dependencies..."
sudo pacman -S --noconfirm --needed \
    inotify-tools wget unzip portaudio xdotool

# --- 2. Python Virtual Environment ---
# We check if the venv directory exists before creating it.
if [ ! -d ".venv" ]; then
    echo "--> Creating Python virtual environment in './.venv'..."
    python3 -m venv .venv
else
    echo "--> Virtual environment already exists. Skipping creation."
fi

# --- 3. Python Requirements ---
# We call pip from the venv directly. This is more robust than sourcing 'activate'.
echo "--> Installing Python requirements into the virtual environment..."
./.venv/bin/pip install -r requirements.txt

# --- 4. Project Structure and Configuration ---
echo "--> Setting up project directories and initial files..."
# THIS IS THE KEY CHANGE. We call the Python script and pass the current
# working directory (which is the project root) as an argument.
# This one command replaces all old 'mkdir' and 'touch' commands for the project structure.
python3 "scripts/py/func/create_required_folders.py" "$(pwd)"




# ==============================================================================
# --- 5. Download and Extract Required Components ---
# This block intelligently handles downloads and extractions.
# ==============================================================================

echo "--> Checking for required components (LanguageTool, Vosk-Models)..."

# --- Configuration ---
PREFIX="Z_"
# Format: "BaseName FinalDirName DestinationPath"
ARCHIVE_CONFIG=(
    "LanguageTool-6.6 LanguageTool-6.6 ."
    "vosk-model-en-us-0.22 vosk-model-en-us-0.22 ./models"
    "vosk-model-small-en-us-0.15 vosk-model-small-en-us-0.15 ./models"
    "vosk-model-de-0.21 vosk-model-de-0.21 ./models"
    "lid.176 lid.176.bin ./models"
)
DOWNLOAD_REQUIRED=false

# --- Phase 1: Check and attempt to restore from local ZIP cache ---
echo "    -> Phase 1: Checking and trying to restore from local cache..."
for config_line in "${ARCHIVE_CONFIG[@]}"; do
    read -r base_name final_name dest_path <<< "$config_line"
    target_path="$dest_path/$final_name"
    zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"

    # If the component already exists, we're good for this one.
    if [ -e "$target_path" ]; then
        continue
    fi

    # The component is missing. Let's see if we can unzip it from a local cache.
    echo "    -> Missing: '$target_path'. Searching for '$zip_file'..."
    if [ -f "$zip_file" ]; then
        echo "    -> Found ZIP cache. Extracting '$zip_file'..."
        unzip -q "$zip_file" -d "$dest_path"
    else
        # The ZIP is not there. We MUST run the downloader.
        echo "    -> ZIP cache not found. A download is required."
        DOWNLOAD_REQUIRED=true
    fi
done

# --- Phase 2: Download if necessary ---
if [ "$DOWNLOAD_REQUIRED" = true ]; then
    echo "    -> Phase 2: Running Python downloader for missing components..."

    # Create the models directory before attempting to download files into it.
    mkdir -p ./models

    ./.venv/bin/python tools/download_all_packages.py
    echo "    -> Downloader finished. Retrying extraction..."

    # After downloading, we must re-check and extract anything that's still missing.
    for config_line in "${ARCHIVE_CONFIG[@]}"; do
        read -r base_name final_name dest_path <<< "$config_line"
        target_path="$dest_path/$final_name"
        zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"

        if [ -e "$target_path" ]; then
            continue
        fi

        if [ -f "$zip_file" ]; then
            echo "    -> Extracting newly downloaded '$zip_file'..."
            unzip -q "$zip_file" -d "$dest_path"
        else
            echo "    -> FATAL: Downloader ran but '$zip_file' is still missing. Aborting."
            exit 1
        fi
    done
fi

echo "--> All components are present and correctly placed."

# ==============================================================================
# --- End of Download/Extract block ---
# ==============================================================================

# Function to extract and clean up
expand_and_cleanup() {
    local zip_file=$1
    local expected_dir=$2
    local dest_path=$3

    # Check if final directory already exists
    if [ -d "$dest_path/$expected_dir" ]; then
        echo "    -> Directory '$expected_dir' already exists. Skipping."
        return
    fi

    # Check if the downloaded zip exists
    if [ ! -f "$zip_file" ]; then
        echo "    -> FATAL: Expected archive not found: '$zip_file'"
        exit 1
    fi

    echo "    -> Extracting $zip_file to $dest_path..."
    unzip -q "$zip_file" -d "$dest_path"

    # Clean up the zip file
    if [ "$should_remove_zips_after_unpack" = true ] ; then
        rm "$zip_file"
    fi

    echo "    -> Cleaned up ZIP file: $zip_file"
}

# Execute extraction for each archive
for config_line in "${BASE_CONFIG[@]}"; do
    # Read the space-separated values into variables
    read -r base_name final_name dest_path <<< "$config_line"

    # CONSTRUCT THE FILENAMES, including the prefix for the zip file
    zip_file="${PREFIX}${base_name}.zip"
    expected_dir="${base_name}" # The final directory name has no prefix

    if [ ! -e "$dest_path/$final_name" ]; then
        echo "    -> MISSING: '$dest_path/$final_name'. Download is required."
        download_needed=true
        break # Ein fehlendes Teil reicht, Prüfung kann stoppen
    fi

    expand_and_cleanup "$zip_file" "$expected_dir" "$dest_path"
done

echo "    -> Extraction and cleanup successful."





source "$(dirname "${BASH_SOURCE[0]}")/../scripts/sh/get_lang.sh"

# --- 5. Project Configuration ---
# Ensures Python can treat 'config' directories as packages.
echo "--> Creating Python package markers (__init__.py)..."
touch config/__init__.py
touch config/languagetool_server/__init__.py

# --- User-Specific Configuration ---
# This part is about user config, so it's fine for it to stay here.
CONFIG_FILE="$HOME/.config/sl5-stt/config.toml"
mkdir -p "$(dirname "$CONFIG_FILE")"
# Only write the file if it doesn't exist to avoid overwriting user settings
if [ ! -f "$CONFIG_FILE" ]; then
    echo "[paths]" > "$CONFIG_FILE"
    echo "project_root = \"$(pwd)\"" >> "$CONFIG_FILE"
fi

# --- 6. Completion ---
echo ""
echo "--- Setup for Manjaro/Arch is complete! ---"
echo ""
echo "To activate the environment and run the server, use the following commands:"
echo "  source .venv/bin/activate"
echo "  ./scripts/restart_venv_and_run-server.sh"
echo ""
setup/suse_setup.sh
#!/bin/bash
# setup/suse_setup.sh
# Run this setup script from the project's root directory.

SCRIPT_NAME=$(basename "$0")
# Check if the script is run from the project root.
# This check is more robust than changing directory.
if [ ! -f "requirements.txt" ]; then
    echo "ERROR: Please run this script from the project's root directory."
    echo ""
    echo "cd .. ; ./setup/$SCRIPT_NAME"
    exit 1
fi

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(dirname "$SCRIPT_DIR")
#cd "$PROJECT_ROOT"

should_remove_zips_after_unpack=true


echo "--- Starting STT Setup for openSUSE ---"
# --- End of location-independent block ---

set -e

# --- 1. System Dependencies ---
# (This section remains unchanged)
echo "--> Checking for a compatible Java version (>=17)..."
JAVA_OK=0
if command -v java &> /dev/null; then
    VERSION=$(java -version 2>&1 | awk -F[\".] '/version/ {print ($2 == "1") ? $3 : $2}')
    if [ "$VERSION" -ge 17 ]; then
        echo "    -> Found compatible Java version $VERSION. OK."
        JAVA_OK=1
    else
        echo "    -> Found Java version $VERSION, but we need >=17."
    fi
else
    echo "    -> No Java executable found."
fi




if [ "$JAVA_OK" -eq 0 ]; then
    echo "    -> Installing a modern JDK (>=17)..."
    # SUSE CHANGE: Use zypper instead of apt
    sudo zypper refresh && sudo zypper -n install java-21-openjdk
fi
echo "--> Installing other core dependencies..."
# SUSE CHANGE: Use zypper and adjust package names
sudo zypper -n install \
    inotify-tools wget unzip portaudio-devel python3-pip


# --- 2. Python Virtual Environment ---
# (This section remains unchanged)
if [ ! -d ".venv" ]; then
    echo "--> Creating Python virtual environment in './.venv'..."
    python3 -m venv .venv
else
    echo "--> Virtual environment already exists. Skipping creation."
fi

# --- 3. Python Requirements ---
# (This section remains unchanged)
echo "--> Installing Python requirements into the virtual environment..."
./.venv/bin/pip install -r requirements.txt

# --- 4. Project Structure and Configuration ---
echo "--> Setting up project directories and initial files..."
# THIS IS THE KEY CHANGE. We call the Python script and pass the current
# working directory (which is the project root) as an argument.
# This one command replaces all old 'mkdir' and 'touch' commands for the project structure.
python3 "scripts/py/func/create_required_folders.py" "$(pwd)"




# ==============================================================================
# --- 5. Download and Extract Required Components ---
# This block intelligently handles downloads and extractions.
# ==============================================================================

echo "--> Checking for required components (LanguageTool, Vosk-Models)..."

# --- Configuration ---
PREFIX="Z_"
# Format: "BaseName FinalDirName DestinationPath"
ARCHIVE_CONFIG=(
    "LanguageTool-6.6 LanguageTool-6.6 ."
    "vosk-model-en-us-0.22 vosk-model-en-us-0.22 ./models"
    "vosk-model-small-en-us-0.15 vosk-model-small-en-us-0.15 ./models"
    "vosk-model-de-0.21 vosk-model-de-0.21 ./models"
    "lid.176 lid.176.bin ./models"
)
DOWNLOAD_REQUIRED=false

# --- Phase 1: Check and attempt to restore from local ZIP cache ---
echo "    -> Phase 1: Checking and trying to restore from local cache..."
for config_line in "${ARCHIVE_CONFIG[@]}"; do
    read -r base_name final_name dest_path <<< "$config_line"
    target_path="$dest_path/$final_name"
    zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"

    # If the component already exists, we're good for this one.
    if [ -e "$target_path" ]; then
        continue
    fi

    # The component is missing. Let's see if we can unzip it from a local cache.
    echo "    -> Missing: '$target_path'. Searching for '$zip_file'..."
    if [ -f "$zip_file" ]; then
        echo "    -> Found ZIP cache. Extracting '$zip_file'..."
        unzip -q "$zip_file" -d "$dest_path"
    else
        # The ZIP is not there. We MUST run the downloader.
        echo "    -> ZIP cache not found. A download is required."
        DOWNLOAD_REQUIRED=true
    fi
done

# --- Phase 2: Download if necessary ---
if [ "$DOWNLOAD_REQUIRED" = true ]; then
    echo "    -> Phase 2: Running Python downloader for missing components..."

    # Create the models directory before attempting to download files into it.
    mkdir -p ./models

    ./.venv/bin/python tools/download_all_packages.py
    echo "    -> Downloader finished. Retrying extraction..."

    # After downloading, we must re-check and extract anything that's still missing.
    for config_line in "${ARCHIVE_CONFIG[@]}"; do
        read -r base_name final_name dest_path <<< "$config_line"
        target_path="$dest_path/$final_name"
        zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"

        if [ -e "$target_path" ]; then
            continue
        fi

        if [ -f "$zip_file" ]; then
            echo "    -> Extracting newly downloaded '$zip_file'..."
            unzip -q "$zip_file" -d "$dest_path"
        else
            echo "    -> FATAL: Downloader ran but '$zip_file' is still missing. Aborting."
            exit 1
        fi
    done
fi

echo "--> All components are present and correctly placed."

# ==============================================================================
# --- End of Download/Extract block ---
# ==============================================================================



source "$(dirname "${BASH_SOURCE[0]}")/../scripts/sh/get_lang.sh"

# --- 5. Project Configuration ---
# Ensures Python can treat 'config' directories as packages.
echo "--> Creating Python package markers (__init__.py)..."
touch config/__init__.py
touch config/languagetool_server/__init__.py

# --- User-Specific Configuration ---
# This part is about user config, so it's fine for it to stay here.
CONFIG_FILE="$HOME/.config/sl5-stt/config.toml"
echo "--> Ensuring user config file exists at $CONFIG_FILE..."
mkdir -p "$(dirname "$CONFIG_FILE")"
# Only write the file if it doesn't exist to avoid overwriting user settings
if [ ! -f "$CONFIG_FILE" ]; then
    echo "[paths]" > "$CONFIG_FILE"
    echo "project_root = \"$(pwd)\"" >> "$CONFIG_FILE"
fi

# --- 6. Completion ---
echo ""
echo "--- Setup for openSUSE is complete! ---"
echo ""
echo "To activate the environment and run the server, use the following commands:"
echo "  source .venv/bin/activate"
echo "  ./scripts/restart_venv_and_run-server.sh"
echo ""
setup/ubuntu_setup.sh
#!/bin/bash
#
# setup/ubuntu_setup.sh
# Run this setup script from the project's root directory.
#

SCRIPT_NAME=$(basename "$0")
# Check if the script is run from the project root.
# This check is more robust than changing directory.
if [ ! -f "requirements.txt" ]; then
    echo "ERROR: Please run this script from the project's root directory."
    echo ""
    echo "cd .. ; ./setup/$SCRIPT_NAME"
    exit 1
fi


should_remove_zips_after_unpack=true

# --- Make script location-independent ---
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(dirname "$SCRIPT_DIR")
cd "$PROJECT_ROOT"

echo "--> Running setup from project root: $(pwd)"
# --- End of location-independent block ---

set -e

echo "--- Starting STT Setup for Debian/Ubuntu ---"

# setup/ubuntu_setup.sh
# --- 1. System Dependencies ---
# (This section remains unchanged)
echo "--> Checking for a compatible Java version (>=17)..."
JAVA_OK=0
if command -v java &> /dev/null; then
    VERSION=$(java -version 2>&1 | awk -F[\".] '/version/ {print ($2 == "1") ? $3 : $2}')
    if [ "$VERSION" -ge 17 ]; then
        echo "    -> Found compatible Java version $VERSION. OK."
        JAVA_OK=1
    else
        echo "    -> Found Java version $VERSION, but we need >=17."
    fi
else
    echo "    -> No Java executable found."
fi
if [ "$JAVA_OK" -eq 0 ]; then
    echo "    -> Installing a modern JDK (>=17)..."
    sudo apt-get update && sudo apt-get install -y openjdk-21-jdk
fi
echo "--> Installing other core dependencies..."
sudo apt-get install -y \
    inotify-tools wget unzip portaudio19-dev python3-pip

# --- 2. Python Virtual Environment ---
# (This section remains unchanged)
if [ ! -d ".venv" ]; then
    echo "--> Creating Python virtual environment in './.venv'..."
    python3 -m venv .venv
else
    echo "--> Virtual environment already exists. Skipping creation."
fi

# --- 3. Python Requirements ---
# (This section remains unchanged)
echo "--> Installing Python requirements into the virtual environment..."
./.venv/bin/pip install -r requirements.txt

# --- 4. Project Structure and Configuration ---
echo "--> Setting up project directories and initial files..."
# THIS IS THE KEY CHANGE. We call the Python script and pass the current
# working directory (which is the project root) as an argument.
# This one command replaces all old 'mkdir' and 'touch' commands for the project structure.
python3 "scripts/py/func/create_required_folders.py" "$(pwd)"




# ==============================================================================
# --- 5. Download and Extract Required Components ---
# This block intelligently handles downloads and extractions.
# ==============================================================================

echo "--> Checking for required components (LanguageTool, Vosk-Models)..."

# --- Configuration ---
PREFIX="Z_"
# Format: "BaseName FinalDirName DestinationPath"
ARCHIVE_CONFIG=(
    "LanguageTool-6.6 LanguageTool-6.6 ."
    "vosk-model-en-us-0.22 vosk-model-en-us-0.22 ./models"
    "vosk-model-small-en-us-0.15 vosk-model-small-en-us-0.15 ./models"
    "vosk-model-de-0.21 vosk-model-de-0.21 ./models"
    "lid.176 lid.176.bin ./models"
)
DOWNLOAD_REQUIRED=false

# --- Phase 1: Check and attempt to restore from local ZIP cache ---
echo "    -> Phase 1: Checking and trying to restore from local cache..."
for config_line in "${ARCHIVE_CONFIG[@]}"; do
    read -r base_name final_name dest_path <<< "$config_line"
    target_path="$dest_path/$final_name"
    zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"

    # If the component already exists, we're good for this one.
    if [ -e "$target_path" ]; then
        continue
    fi

    # The component is missing. Let's see if we can unzip it from a local cache.
    echo "    -> Missing: '$target_path'. Searching for '$zip_file'..."
    if [ -f "$zip_file" ]; then
        echo "    -> Found ZIP cache. Extracting '$zip_file'..."
        unzip -q "$zip_file" -d "$dest_path"
    else
        # The ZIP is not there. We MUST run the downloader.
        echo "    -> ZIP cache not found. A download is required."
        DOWNLOAD_REQUIRED=true
    fi
done

# --- Phase 2: Download if necessary ---
if [ "$DOWNLOAD_REQUIRED" = true ]; then
    echo "    -> Phase 2: Running Python downloader for missing components..."

    # Create the models directory before attempting to download files into it.
    mkdir -p ./models

    ./.venv/bin/python tools/download_all_packages.py
    echo "    -> Downloader finished. Retrying extraction..."

    # After downloading, we must re-check and extract anything that's still missing.
    for config_line in "${ARCHIVE_CONFIG[@]}"; do
        read -r base_name final_name dest_path <<< "$config_line"
        target_path="$dest_path/$final_name"
        zip_file="$PROJECT_ROOT/${PREFIX}${base_name}.zip"

        if [ -e "$target_path" ]; then
            continue
        fi

        if [ -f "$zip_file" ]; then
            echo "    -> Extracting newly downloaded '$zip_file'..."
            unzip -q "$zip_file" -d "$dest_path"
        else
            echo "    -> FATAL: Downloader ran but '$zip_file' is still missing. Aborting."
            exit 1
        fi
    done
fi

echo "--> All components are present and correctly placed."

# ==============================================================================
# --- End of Download/Extract block ---
# ==============================================================================







source "$(dirname "${BASH_SOURCE[0]}")/../scripts/sh/get_lang.sh"

# --- 5. Project Configuration ---
# Ensures Python can treat 'config' directories as packages.
echo "--> Creating Python package markers (__init__.py)..."
touch config/__init__.py
touch config/languagetool_server/__init__.py

# --- User-Specific Configuration ---
# This part is about user config, so it's fine for it to stay here.
CONFIG_FILE="$HOME/.config/sl5-stt/config.toml"
echo "--> Ensuring user config file exists at $CONFIG_FILE..."
mkdir -p "$(dirname "$CONFIG_FILE")"
# Only write the file if it doesn't exist to avoid overwriting user settings
if [ ! -f "$CONFIG_FILE" ]; then
    echo "[paths]" > "$CONFIG_FILE"
    echo "project_root = \"$(pwd)\"" >> "$CONFIG_FILE"
fi

# --- 6. Completion ---
echo ""
echo "--- Setup for Ubuntu is complete! ---"
echo ""
echo "To activate the environment and run the server, use the following commands:"
echo "  source .venv/bin/activate"
echo "  ./scripts/restart_venv_and_run-server.sh"
echo ""
setup/windows11_setup.bat
@echo off
ECHO start Windows 11 Setup-Script...

REM PowerShell, use Execution Policy this time
powershell.exe -ExecutionPolicy Bypass -File "%~dp0\windows11_setup.ps1"

call .\.venv\Scripts\python.exe scripts/py/func/setup_initial_model.py

ECHO
ECHO Script is ended.
ECHO You can close the window
pause
setup/windows11_setup.ps1
# setup/windows11_setup.ps1

# --- Make script location-independent ---
$ProjectRoot = Split-Path -Path $PSScriptRoot -Parent
Set-Location -Path $ProjectRoot
Write-Host "--> Running setup from project root: $(Get-Location)"




$ErrorActionPreference = "Stop"

# Configuration: Set to $false to keep ZIP files after extraction or use $true
$should_remove_zips_after_unpack = $true
# --- 1. Admin Rights Check ---
Write-Host "[*] Checking for Administrator privileges"



# Only check for admin rights if NOT running in a CI environment (like GitHub Actions)
if ($env:CI -ne 'true') {
    # Check if the current user is an Administrator
    $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

    if (-not $isAdmin) {
        Write-Host "[ERROR] Administrator privileges are required. Re-launching..."
        # Re-run the current script with elevated privileges for GitHub Actions
        Start-Process -FilePath $PSCommandPath -Verb RunAs
        # Exit the current (non-admin) script
        exit
    }
}
Write-Host "[SUCCESS] Running with Administrator privileges."





# Only check for java if NOT running in a CI environment (like GitHub Actions)
if ($env:CI -ne 'true')
{
    # --- 2. Java Installation Check ---
    Write-Host "--> Checking Java installation..."
    $JavaVersion = $null
    try
    {
        $JavaOutput = & java -version 2>&1
        if ($JavaOutput -match 'version "(\d+)\.')
        {
            $JavaVersion = [int]$matches[1]
        }
        elseif ($JavaOutput -match 'version "1\.(\d+)\.')
        {
            $JavaVersion = [int]$matches[1]
        }
    }
    catch
    {
        Write-Host "    -> Java not found in PATH."
    }

    if ($JavaVersion -and $JavaVersion -ge 17)
    {
        Write-Host "    -> Java $JavaVersion detected. OK." -ForegroundColor Green
    }
    else
    {
        Write-Host "    -> Java 17+ not found. Installing OpenJDK 17..." -ForegroundColor Yellow
        try
        {
            winget install --id Microsoft.OpenJDK.17 --silent --accept-source-agreements --accept-package-agreements
            if ($LASTEXITCODE -eq 0)
            {
                Write-Host "    -> OpenJDK 17 installed successfully." -ForegroundColor Green
                # Refresh PATH for current session
                $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User")
            }
            else
            {
                Write-Host "ERROR: Failed to install OpenJDK 17. Please install manually." -ForegroundColor Red
                # exit 1 # the script works anywas usuallly. dont exit here!
            }
        }
        catch
        {
            Write-Host "ERROR: Failed to install Java. Please install Java 17+ manually." -ForegroundColor Red
            exit 1
        }
    }
}



# --- 3. Python Installation Check ---
Write-Host "--> Checking Python installation..."
$PythonVersion = $null
try {
    $PythonOutput = & python --version 2>&1
    if ($PythonOutput -match 'Python (\d+)\.(\d+)') {
        $PythonMajor = [int]$matches[1]
        $PythonMinor = [int]$matches[2]
        if ($PythonMajor -eq 3 -and $PythonMinor -ge 8) {
            $PythonVersion = "$PythonMajor.$PythonMinor"
        }
    }
} catch {
    Write-Host "    -> Python not found in PATH."
}

if ($PythonVersion) {
    Write-Host "    -> Python $PythonVersion detected. OK." -ForegroundColor Green
} else {
    Write-Host "    -> Python 3.8+ not found. Installing Python 3.11..." -ForegroundColor Yellow
    try {
        winget install --id Python.Python.3.11 --silent --accept-source-agreements --accept-package-agreements
        if ($LASTEXITCODE -eq 0) {
            Write-Host "    -> Python 3.11 installed successfully." -ForegroundColor Green
            # Refresh PATH for current session
            $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User")
        } else {
            Write-Host "ERROR: Failed to install Python. Please install manually." -ForegroundColor Red
            exit 1
        }
    } catch {
        Write-Host "ERROR: Failed to install Python. Please install Python 3.8+ manually." -ForegroundColor Red
        exit 1
    }
}

# --- 3. Python Virtual Environment ---
Write-Host "--> Creating Python virtual environment in '.\.venv'..."
if (-not (Test-Path -Path ".\.venv")) {
    python -m venv .venv
} else {
    Write-Host "    -> Virtual environment already exists. Skipping creation."
}


# --- PATCH: Replace fasttext with fasttext-wheel in requirements.txt ---
Write-Host "--> Patching requirements.txt for Windows fasttext-wheel compatibility..."
(Get-Content requirements.txt) -replace '^fasttext.*$', 'fasttext-wheel' | Set-Content requirements.txt


# --- 5. Python Requirements ---
Write-Host "--> Installing Python requirements into the virtual environment..."
.\.venv\Scripts\pip.exe install -r requirements.txt

# --- 5. External Tools & Models (from Releases) ---
$LtDir = "LanguageTool-6.6"





# --- 6. External Tools & Models (using the robust Python downloader) ---
Write-Host "--> Downloading external tools and models via Python downloader..."

# Create the models directory before attempting to download files into it.
New-Item -ItemType Directory -Path ".\models" -Force | Out-Null

# Execute the downloader and check its exit code
#  It downloads all required ZIPs to the project root.
.\.venv\Scripts\python.exe tools/download_all_packages.py

# $LASTEXITCODE contains the exit code of the last program that was run.
# 0 means success. Anything else is an error.
if ($LASTEXITCODE -ne 0) {
    Write-Host "FATAL: The Python download script failed. Halting setup." -ForegroundColor Red
    # Wir benutzen 'exit 1', um das ganze Skript sofort zu beenden.
    exit 1
}

Write-Host "    -> Python downloader completed successfully." -ForegroundColor Green

# --- Now, extract the downloaded archives ---
Write-Host "--> Extracting downloaded archives..."

$Prefix = "Z_"
$BaseConfig = @(
    @{ BaseName = "LanguageTool-6.6";              Dest = "." },
    @{ BaseName = "vosk-model-en-us-0.22";         Dest = ".\models" },
    @{ BaseName = "vosk-model-small-en-us-0.15";   Dest = ".\models" },
    @{ BaseName = "vosk-model-de-0.21";            Dest = ".\models" },
    @{ BaseName = "lid.176";                   Dest = ".\models" }
)
$ArchiveConfig = $BaseConfig | ForEach-Object {
    [PSCustomObject]@{
        Zip  = "$($Prefix)$($_.BaseName).zip"
        Dir  = $_.BaseName
        Dest = $_.Dest
    }
}

# Function to extract and clean up
function Expand-And-Cleanup {
    param (
        [string]$ZipFile,
        [string]$DestinationPath,
        [string]$ExpectedDirName,
        [bool]$CleanupAfterExtraction
    )

    # Check if final directory already exists
    $FinalFullPath = Join-Path -Path $DestinationPath -ChildPath $ExpectedDirName
    if (Test-Path $FinalFullPath) {
        Write-Host "    -> Directory '$ExpectedDirName' already exists. Skipping extraction."
        return
    }

    # Look for the downloaded ZIP (Python script creates final ZIPs without Z_ prefix)
    $FinalZipPath = Join-Path -Path $ProjectRoot -ChildPath $ZipFile

    if (-not (Test-Path $FinalZipPath)) {
        Write-Host "FATAL: Expected archive not found: '$FinalZipPath'" -ForegroundColor Red
        Write-Host "       The Python download script should have created this file." -ForegroundColor Red
        exit 1
    }

    Write-Host "    -> Extracting $ZipFile to $DestinationPath..."

    Expand-Archive -Path $FinalZipPath -DestinationPath $DestinationPath -Force

    if ($CleanupAfterExtraction) {
        Remove-Item $FinalZipPath -Force
        Write-Host "    -> Cleaned up ZIP file: $ZipFile"
    } else {
        Write-Host "    -> Keeping ZIP file: $ZipFile"
    }
}

# Execute extraction for each archive
foreach ($Config in $ArchiveConfig) {
    Expand-And-Cleanup -ZipFile $Config.Zip -DestinationPath $Config.Dest -ExpectedDirName $Config.Dir -CleanupAfterExtraction $should_remove_zips_after_unpack
}

Write-Host "    -> Extraction and cleanup successful." -ForegroundColor Green





# --- Run initial model setup ---
Write-Host "INFO: Setting up initial models based on system language..."

# Get the 2-letter language code (e.g., "de", "fr") from Windows
$LangCode = (Get-Culture).Name.Substring(0, 2)

# Define the path to the Python script
$SetupModelScriptPath = ".\scripts\py\func\setup_initial_model.py"

# Check if the Python script exists
if (Test-Path $SetupModelScriptPath) {
    Write-Host "    -> Running setup_initial_model.py with language code: $LangCode"

    # Execute the Python script with the detected language code
    .\.venv\Scripts\python.exe $SetupModelScriptPath $LangCode

    if ($LASTEXITCODE -eq 0) {
        Write-Host "INFO: Initial model setup completed successfully." -ForegroundColor Green
    } else {
        Write-Host "WARNING: Initial model setup failed with exit code: $LASTEXITCODE" -ForegroundColor Yellow
    }
} else {
    Write-Host "WARNING: Initial model setup script not found at: $SetupModelScriptPath" -ForegroundColor Yellow
}

# --- Create required directories ---
$tmpPath = "C:\tmp"
$sl5DictationPath = "C:\tmp\sl5_aura"

@($tmpPath, $sl5DictationPath) | ForEach-Object {
    if (!(Test-Path $_)) {
        New-Item -ItemType Directory -Path $_ | Out-Null
        Write-Host "Created directory: $_"
    } else {
        Write-Host "Directory already exists: $_"
    }
}

# --- Create central config file ---
Write-Host "--> Creating central config file..."
$ConfigDir = Join-Path -Path $env:USERPROFILE -ChildPath ".config\sl5-stt"
if (-not (Test-Path -Path $ConfigDir)) {
    Write-Host "    -> Creating config directory at $ConfigDir"
    New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
}

# Korrigiert, um Backslashes für Windows zu verwenden und dann für TOML zu normalisieren
$ProjectRootPath = (Get-Location).Path
$ConfigContent = @"
[paths]
project_root = "$($ProjectRootPath.Replace('\', '/'))"
"@
$ConfigFile = Join-Path -Path $ConfigDir -ChildPath "config.toml"
Set-Content -Path $ConfigFile -Value $ConfigContent

# --- 9. Project Configuration ---
Write-Host "--> Creating Python package markers (__init__.py)..."

# Create log directory if it doesn't exist
if (-not (Test-Path "log")) {
    New-Item -Name "log" -ItemType Directory | Out-Null
}

# Create __init__.py files
@("log/__init__.py", "config/__init__.py", "config/languagetool_server/__init__.py") | ForEach-Object {
    $Directory = Split-Path -Path $_ -Parent
    if ($Directory -and (-not (Test-Path $Directory))) {
        New-Item -ItemType Directory -Path $Directory -Force | Out-Null
    }
    New-Item -Path $_ -ItemType File -Force | Out-Null
}

# --- 10. Completion ---
Write-Host ""
Write-Host "------------------------------------------------------------------" -ForegroundColor Green
Write-Host "Setup for Windows completed successfully." -ForegroundColor Green
Write-Host "------------------------------------------------------------------" -ForegroundColor Green

Config-Skripte

config/__init__.py

config/dynamic_settings.py
# config/dynamic_settings.py
import collections.abc # Corrected import to collections.abc
import importlib
import sys
import os
from datetime import datetime
from pathlib import Path
from threading import RLock
from config import settings
from config.settings_local import DEV_MODE
# Get a logger instance instead of direct print statements for better control
import logging

class CustomFormatter(logging.Formatter):
    def formatTime(self, record, datefmt=None):
        dt_object = datetime.fromtimestamp(record.created)

        # Das Standardformat des logging-Moduls für asctime ist '%Y-%m-%d %H:%M:%S,f'
        # Hier formatieren wir nur den H:M:S Teil und fügen die Millisekunden an
        time_str_without_msecs = dt_object.strftime("%H:%M:%S")

        milliseconds = int(record.msecs)
        # Die 03d sorgt dafür, dass die Millisekunden immer dreistellig sind (z.B. 001, 010, 123)
        formatted_time = f"{time_str_without_msecs},{milliseconds:03d}"

        return formatted_time

PROJECT_ROOT = Path(__file__).resolve().parent.parent
LOG_FILE = PROJECT_ROOT / "log/dynamic_settings.log"

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Clear any pre-existing handlers to prevent duplicates.
if len(logger.handlers) > 0:
    logger.handlers.clear()

# Create a shared formatter with the custom formatTime function.

log_formatter = CustomFormatter('%(asctime)s - %(levelname)-8s - %(message)s')

# Create, configure, and add the File Handler.
file_handler = logging.FileHandler(f'{PROJECT_ROOT}/log/dynamic_settings.log', mode='w')
file_handler.setFormatter(log_formatter)
logger.addHandler(file_handler)

logger.info(f"👀 dynamic_settings.py: DEV_MODE={DEV_MODE}, settings.DEV_MODE = {settings.DEV_MODE}")


class DynamicSettings:
    _instance = None
    _lock = RLock()

    _last_modified_time = 0

    _settings_module = None

    _settings_local_module = None

    _last_base_modified_time = None
    _last_local_modified_time = None

    _settings_file_path = None
    _settings_local_file_path = None

    def __new__(cls):
        with cls._lock:
            if cls._instance is None:
                cls._instance = super(DynamicSettings, cls).__new__(cls)
                logger.info(f"👀 dynamic_settings.py: DEV_MODE={DEV_MODE}, settings.DEV_MODE = {settings.DEV_MODE}")

                if settings.DEV_MODE:
                    print("👀 DEBUG: DynamicSettings.__new__ called, initializing instance.")
                cls._instance._init_settings()
        return cls._instance

    def _init_settings(self):
        self._settings_file_path = os.path.join(
            os.path.dirname(__file__), "settings.py"
        )
        self._settings_local_file_path = os.path.join(
            os.path.dirname(__file__), "settings_local.py"
        )

        self._last_base_modified_time = os.path.getmtime(self._settings_file_path)
        self._last_local_modified_time = os.path.getmtime(self._settings_local_file_path)

        logger.info(f"👀 dynamic_settings.py: settings.DEV_MODE = {settings.DEV_MODE}")

        if settings.DEV_MODE:
            print(f"👀 DEBUG: DynamicSettings._init_settings called. Base settings file: {self._settings_file_path}")
            print(f"👀 DEBUG: DynamicSettings._init_settings called. Local settings file: {self._settings_local_file_path}")
            logger.info(f"👀 DEBUG: DynamicSettings._init_settings called. Base settings file: {self._settings_file_path}")
        self.reload_settings(force=False)

    def reload_settings(self, force=False):
        # config/dynamic_settings.py:44
        logger.info(f"👀 dynamic_settings.py:reload_settings():44 DEV_MODE={DEV_MODE}, settings.DEV_MODE = {settings.DEV_MODE}")
        print(f"👀 dynamic_settings.py:reload_settings():44 DEV_MODE={DEV_MODE}, settings.DEV_MODE = {settings.DEV_MODE}")


        if settings.DEV_MODE:
            print("👀 DEBUG: reload_settings called.")
        with self._lock:
            if settings.DEV_MODE:
                print("👀 DEBUG: Lock acquired for settings reload.")

            current_base_modified_time = os.path.getmtime(self._settings_file_path) if os.path.exists(
                self._settings_file_path) else 0
            current_local_modified_time = 0
            if self._settings_local_file_path and os.path.exists(self._settings_local_file_path):
                current_local_modified_time = os.path.getmtime(self._settings_local_file_path)

            any_file_modified = (
                    current_base_modified_time > self._last_base_modified_time or
                    current_local_modified_time > self._last_local_modified_time
            )
            if force or self._settings_module is None or any_file_modified:

                if settings.DEV_MODE:

                    logger.info(
                        f"👀 Triggering full settings reload. Reasons: force={force}, _settings_module is None={self._settings_module is None}, any_file_modified={any_file_modified}")
                    logger.info(
                        f"👀【┘】⌚ ⏳ self._settings_file_path: {self._settings_file_path} | self._settings_local_file_path={self._settings_local_file_path} | any_file_modified={any_file_modified}")
                    logger.info(
                        f"👀【┘】⌚ ⏳ current_base_modified_time: {current_base_modified_time} | current_local_modified_time: {current_local_modified_time}")
                    logger.info(
                        f"👀【┘】⌚ ⏳ _last_base_modified_time: {self._last_base_modified_time} | _last_local_modified_time: {self._last_local_modified_time}")
                self._last_base_modified_time = current_base_modified_time
                self._last_local_modified_time = current_local_modified_time



                # --- Reloading base settings (config.settings) ---
                if 'config.settings' in sys.modules:
                    if settings.DEV_MODE:
                        print("👀 DEBUG: Calling importlib.reload(sys.modules['config.settings'])")
                    self._settings_module = importlib.reload(sys.modules['config.settings'])
                else:
                    if settings.DEV_MODE:
                        print("👀 DEBUG: Calling importlib.import_module('config.settings')")
                    self._settings_module = importlib.import_module('config.settings')
                if settings.DEV_MODE:
                    print("👀 DEBUG: Base settings loaded.")

                # --- Reloading local settings (config.settings_local) ---
                try:
                    if os.path.exists(self._settings_local_file_path):
                        if 'config.settings_local' in sys.modules:
                            if settings.DEV_MODE:
                                print("👀 DEBUG: Calling importlib.reload(sys.modules['config.settings_local'])")
                            self._settings_local_module = importlib.reload(sys.modules['config.settings_local'])
                        else:
                            if settings.DEV_MODE:
                                print("👀 DEBUG: Calling importlib.import_module('config.settings_local')")
                            self._settings_local_module = importlib.import_module('config.settings_local')
                        if settings.DEV_MODE:
                            print("👀 DEBUG: Local settings loaded.")
                    else:
                        print("👀 INFO: config.settings_local.py does not exist. Skipping local settings load.")
                        self._settings_local_module = None
                except ModuleNotFoundError:
                    print("👀 WARNING: config.settings_local module not found. This might indicate a path issue or missing file.")
                    self._settings_local_module = None
                except Exception as e:
                    print(f"👀 CRITICAL ERROR: Exception during config.settings_local import/reload: {e}")
                    import traceback
                    traceback.print_exc()
                    raise

                if settings.DEV_MODE:
                    print("👀 DEBUG: --- Merging settings ---")
                # Clear existing attributes to ensure a clean merge
                for attr in list(self.__dict__.keys()):
                    # IMPORTANT: Do not delete 'settings' itself or internal attributes like '_instance', '_lock', etc.
                    # Ensure that we only delete dynamically added configuration attributes.
                    # A robust way might be to keep track of which attributes were added,
                    # but for now, checking for standard internal attributes should be sufficient.
                    if not attr.startswith('_') and attr not in ['settings', '_settings_module', '_settings_local_module']:
                        delattr(self, attr)

                # Apply base settings
                if self._settings_module:
                    for attr in dir(self._settings_module):
                        if not attr.startswith('__'):
                            value = getattr(self._settings_module, attr)
                            setattr(self, attr, value)
                    if settings.DEV_MODE:
                        print("👀 DEBUG: Base settings attributes applied to DynamicSettings instance.")

                # Apply/Merge local settings
                if self._settings_local_module:
                    for attr in dir(self._settings_local_module):
                        if not attr.startswith('__'):
                            local_value = getattr(self._settings_local_module, attr)


                        # --- START MODIFICATION ---
                            # Special handling for PRELOAD_MODELS: always override
                            if attr == "PRELOAD_MODELS":
                                setattr(self, attr, local_value)
                                if settings.DEV_MODE:
                                    print(f"👀 DEBUG: Overrode PRELOAD_MODELS with local value: {local_value}")
                            # --- END MODIFICATION ---
                            elif hasattr(self, attr) and isinstance(getattr(self, attr), collections.abc.MutableMapping) and isinstance(local_value, collections.abc.MutableMapping):
                                merged_dict = getattr(self, attr)
                                merged_dict.update(local_value)
                                setattr(self, attr, merged_dict)
                                if settings.DEV_MODE:
                                    print(f"👀 DEBUG: Merged dictionary setting '{attr}': {getattr(self, attr)}")
                            elif hasattr(self, attr) and isinstance(getattr(self, attr), collections.abc.MutableSequence) and not isinstance(getattr(self, attr), (str, bytes)) and isinstance(local_value, collections.abc.MutableSequence) and not isinstance(local_value, (str, bytes)):
                                merged_list = getattr(self, attr)
                                # Only append items if they are not already in the list
                                for item in local_value:
                                    if item not in merged_list:
                                        merged_list.append(item)
                                setattr(self, attr, merged_list)
                                if settings.DEV_MODE:
                                    print(f"👀 DEBUG: Merged list setting '{attr}': {getattr(self, attr)}")
                            else:
                                # Default: override with local value
                                setattr(self, attr, local_value)
                                if settings.DEV_MODE:
                                    print(f"👀 DEBUG: Overrode setting '{attr}' with local value: {local_value}")
                                    print("👀 DEBUG: Local settings attributes applied/merged to DynamicSettings   instance.")

settings = DynamicSettings() # noqa: F811
config/settings.py
# file: config/settings.py
# Central configuration for the application
# please see also: settings_local.py_Example.txt
import os

SERVICE_START_OPTION = 0
# Option 1: Start the service only on when there is an internet connection.

# Get username
current_user = os.environ.get('USERNAME', 'default')

# Set to True to disable certain production checks for local development,
# e.g., the wrapper script enforcement.
DEV_MODE = False

soundMute = 1  # 1 is really recommandet. to know when your recording is ended.
soundUnMute = 1
soundProgramLoaded = 1

ENABLE_AUTO_LANGUAGE_DETECTION = False

# --- Notification Settings ---
# Default for new users is the most verbose level.
NOTIFICATION_LEVEL = 0 # 0=Silent, 1=Essential, 2=Verbose

# --- Language Model Preloading ---
# A list of Vosk model folder names to preload at startup if memory allows.
PRELOAD_MODELS = ["vosk-model-de-0.21", "vosk-model-en-us-0.22"] # e.g. ["vosk-model-de-0.21", "vosk-model-en-us-0.22"]
# PRELOAD_MODELS = ["vosk-model-de-0.21"]

if current_user == 'SL5.de':
    PRELOAD_MODELS = ["vosk-model-de-0.21"]


# --- LanguageTool Server ---
# Set to True to use an existing LT server. AT YOUR OWN RISK!
# The application will not start its own server and will not stop the external one.
USE_EXTERNAL_LANGUAGETOOL = False # Default: False

# URL for the external server if the option above is True.
EXTERNAL_LANGUAGETOOL_URL = "http://localhost:8081"

# Settings for our internal server (if used)
LANGUAGETOOL_PORT = 8082

# --- Text Correction Settings ---
# This dictionary controls which categories of LanguageTool rules are enabled.
# The application will use these settings to enable/disable rule categories
# when checking text. Set a category to False to ignore its suggestions.
#
# You can override these in your config/settings_local.py file.
CORRECTIONS_ENABLED = {
    # Core Corrections
    "spelling": True,          # Basic spell checking (e.g., "Rechtschreibung")
    "punctuation": True,       # Missing/incorrect commas, periods, etc.
    "grammar": True,           # Grammatical errors (e.g., subject-verb agreement)
    "casing": True,            # Incorrect capitalization (e.g., "berlin" -> "Berlin")
    "style": True,             # Stylistic suggestions (e.g., wordiness, passive voice)
    "colloquialisms": True,    # Flags informal or colloquial language

    # Specialized Dictionaries/Rules
    # These are disabled by default as they may not be relevant for all users.
    # Set to True in settings_local.py to enable them.
    "medical": False,          # Rules related to medical terminology
    "law_rules": False,        # Rules related to legal terminology

    "git": False,        # git Basic commands

    # Add other custom categories here as needed.
    # "academic_writing": False,
}


PLUGINS_ENABLED = {}

# needs restart. implemented in the python part:
ADD_TO_SENCTENCE = "."
# set ADD_TO_SENCTENCE = "" when you dont want it.


# Recording & Transcription
SUSPICIOUS_TIME_WINDOW = 90
SUSPICIOUS_THRESHOLD = 3

# INITIAL_WAIT_TIMEOUT = initial_silence_timeout
# SPEECH_PAUSE_TIMEOUT = 2.0 # Standardwert

PRE_RECORDING_TIMEOUT = 12
SPEECH_PAUSE_TIMEOUT = 0.6


SAMPLE_RATE = 16000

# System
CRITICAL_THRESHOLD_MB = 1024 * 2

# LanguageTool Server Configuration
LANGUAGETOOL_BASE_URL = f"http://localhost:{LANGUAGETOOL_PORT}"
LANGUAGETOOL_CHECK_URL = f"{LANGUAGETOOL_BASE_URL}/v2/check"
LANGUAGETOOL_RELATIVE_PATH = "LanguageTool-6.6/languagetool-server.jar"

NOTIFY_SEND_PATH = "/usr/bin/notify-send"
XDOTOOL_PATH = "/usr/bin/xdotool"

TRIGGER_FILE_PATH = "/tmp/sl5_record.trigger"

# Auto-detected Java path
JAVA_EXECUTABLE_PATH = r"/usr/bin/java"

# needs NO restart. implemented in the sh part. TODO implemt for windows:
# use . for all Windows. Other examples:
# AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "."
AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "(ExampleAplicationThatNotExist|Pi, your personal AI)"
# TODO implement for windows
config/settings_local.py
# config/settings_local.py

# My personal settings for SL5 Aura
# This file is ignored by Git.

SERVICE_START_OPTION = 1
# Option 1: Start the service only on when there is an internet connection.

NOTIFICATION_LEVEL = 0 # 0=Silent, 1=Essential, 2=Verbose

soundMute = 1  # 1 is really recomanded. to know when your recording is endet.
soundUnMute = 1
soundProgramLoaded = 1

# Set to True to disable certain production checks for local development,
# e.g., the wrapper script enforcement.
DEV_MODE = True
# DEV_MODE = False
DEV_MODE_memory = False


# may yo want to overwrite the PRELOAD_MODELS settings from settings.py here
# PRELOAD_MODELS = ["vosk-model-de-0.21"]

# PRELOAD_MODELS = ["vosk-model-de-0.21", "vosk-model-en-us-0.22"] # e.g. ["vosk-model-de-0.21", "vosk-model-en-us-0.22"]

# PRELOAD_MODELS = ["vosk-model-de-0.21", "vosk-model-en-us-0.22"]

#PRELOAD_MODELS = ["vosk-model-en-us-0.22"]
PRELOAD_MODELS = ["vosk-model-de-0.21", "vosk-model-en-us-0.22"]


CRITICAL_THRESHOLD_MB = 1024 * 2
# CRITICAL_THRESHOLD_MB = 28000 # (also 28 GB)


# --- Custom Client-Side Plugins ---
# Enable or disable specific client-side behaviors (plugins).
# The logic is handled by client scripts (e.g., type_watcher.sh, AutoKey).
# These settings tell the backend service what to expect or how to format output.


PLUGINS_ENABLED = {
    "git": True,
    "wannweil": True,
    "game-dealers_choice": False,
    "0ad": False,
    "ethiktagung": True,
    "volkshochschule_tue": False,
    "CCC_tue": True,
    "vsp_rt": True,
    "ki-maker-space": True,
    "numbers_to_digits": True,
    "digits_to_numbers": False,
    "web-radio-funk": True,
}

# needs restart. implemented in the python part:
ADD_TO_SENCTENCE = "."
# set ADD_TO_SENCTENCE = "" when you dont want it.


# needs NO restart:
PRE_RECORDING_TIMEOUT = 8
SPEECH_PAUSE_TIMEOUT = 2

# needs NO restart. implemented in the sh part. TODO implemt for windows:
# use . for all windos. Other examples:
# AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "."
AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "(ExampleAplicationThatNotExist|Pi, your personal AI)"
# TODO implement for windows

config/settings_local.py_Example.txt
# config/settings_local.py

# My personal settings Example
# This file is ignored by Git.


SERVICE_START_OPTION = 0

NOTIFICATION_LEVEL = 1

soundMute = 1  # 1 is really recommandet. to know when your recording is endet.
soundUnMute = 1
soundProgramLoaded = 1

# Set to True to disable certain production checks for local development,
# e.g., the wrapper script enforcement.
DEV_MODE = False
# DEV_MODE = False
DEV_MODE_memory = False


PRELOAD_MODELS = ["vosk-model-de-0.21"]

# --- Custom Correction Settings ---
# Import the default dictionary from the main settings file.
try:
    from .settings import CORRECTIONS_ENABLED
except ImportError:
    CORRECTIONS_ENABLED = {} # Fallback in case the import fails

# Update the dictionary with my personal preferences.
# I want specialized legal and medical checks, but I don't want style advice.
CORRECTIONS_ENABLED.update({
    "git": True,
})


CORRECTIONS_ENABLED.update({
})


PRE_RECORDING_TIMEOUT = 12
SPEECH_PAUSE_TIMEOUT = 1

# examples:
# AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "."

# needs NO restart. implemented in the sh part. TODO implemt for windows:
AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "(ExampleAplicationThatNotExist|Pi, your personal AI)"

ADD_TO_SENCTENCE = "."
# set ADD_TO_SENCTENCE = "" when you dont want it.

CRITICAL_THRESHOLD_MB = 1024 * 2
# CRITICAL_THRESHOLD_MB = 28000 # (also 28 GB)


# --- Custom Client-Side Plugins ---
# Enable or disable specific client-side behaviors (plugins).
# The logic is handled by client scripts (e.g., type_watcher.sh, AutoKey).
# These settings tell the backend service what to expect or how to format output.
####     "github_corrections": True,         # Example from before ???
PLUGINS_ENABLED = {
    "game-dealers_choice": False,             # For Andy's poker game
    "wannweil": False,
    "git": False,
    "0ad": False,
    "ethiktagung": True
    "ethiktagung": True,
    "volkshochschule_tue": True,
    "CCC_tue": True,
    "vsp_rt": True,
    "numbers_to_digits": True,
    # "digits_to_numbers": False, deprecated
    "ki-maker-space": False,
    "web-radio-funk": False,
}

Githooks-Skripte

githooks/pre-push
#!/bin/bash
set -e


SRC1="$HOME/.config/autokey/data/stt/autokey-scripts/"
SRC2="scripts/autokey-scripts/"

# Sync newer files from SRC1 to SRC2 
rsync -au --delete "$SRC1" "$SRC2"

# Sync newer files from SRC2  to SRC1
rsync -au --delete "$SRC2" "$SRC1"


# Temporary directory for requirements generation
TMPDIR=".pipreqs_temp"
FILES=("dictation_service.py" "get_suggestions.py")

# Clean up any old temp dirs
rm -rf "$TMPDIR"
mkdir "$TMPDIR"

# Copy only the relevant files
for f in "${FILES[@]}"; do
    cp "$f" "$TMPDIR/"
done

# Generate requirements.txt from only those files
pipreqs "$TMPDIR" --force

# Replace the actual requirements.txt
mv "$TMPDIR/requirements.txt" requirements.txt

# Convert all package names to lowercase (keep version numbers)
awk -F'==' '{print tolower($1) "==" $2}' requirements.txt > requirements.txt.tmp && mv requirements.txt.tmp requirements.txt

# Clean up temp directory
rm -rf "$TMPDIR"

Update-Skripte

update/update_for_windows_users.ps1
# file: update/update_for_windows_users.ps1                                       
# Description: Downloads the latest version and updates the application
#              while preserving user settings. For non-developer use.

$ErrorActionPreference = 'Stop'
$repoUrl = "https://github.com/sl5net/SL5-aura-service/archive/refs/heads/master.zip"
$installDir = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$tempDir = Join-Path $env:TEMP "sl5_update_temp"

Write-Host "--- SL5 Aura Updater ---" -ForegroundColor Cyan
Write-Host "This will download the latest version and replace all application files."
Write-Host "Your personal settings in 'config\settings_local.py' will be saved."
if (-not ($env:CI -eq 'true'))
{
    Write-Host "Please close the main application if it is running."
    Read-Host -Prompt "Press Enter to continue or CTRL+C to cancel"
}
try {
    # 1. Clean up previous temporary files if they exist
    if (Test-Path $tempDir) {
        Write-Host "INFO: Removing old temporary update folder..."
        Remove-Item -Path $tempDir -Recurse -Force
    }
    New-Item -Path $tempDir -ItemType Directory | Out-Null

    # 2. Backup local settings if they exist
    $localSettingsPath = Join-Path $installDir "config\settings_local.py"

    $backupPath = Join-Path $tempDir "settings_local.py.bak"
    if (Test-Path $localSettingsPath) {
        Write-Host "INFO: Backing up your local settings..." -ForegroundColor Green
        Copy-Item -Path $localSettingsPath -Destination $backupPath
    }

    # 3. Download the latest version
    $zipPath = Join-Path $tempDir "latest.zip"
    Write-Host "INFO: Downloading latest version from GitHub..."
    Invoke-WebRequest -Uri $repoUrl -OutFile $zipPath

    # 4. Extract the archive
    Write-Host "INFO: Extracting update..."
    Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force
    $extractedFolder = Get-ChildItem -Path $tempDir -Directory | Where-Object { $_.Name -like '*-master' } | Select-Object -First 1
    if (-not $extractedFolder) { throw "Could not find extracted '*-master' folder." }

    # 5. Restore local settings into the new version



    if (Test-Path $backupPath) {
        Write-Host "INFO: Restoring your local settings into the new version..." -ForegroundColor Green
        Copy-Item -Path $backupPath -Destination (Join-Path $extractedFolder.FullName "config\")
    }

    # 6. Create a final batch script to perform the file replacement
    $batchScript = @'
@echo off
echo Finalizing update, please wait...
timeout /t 3 /nobreak > nul
robocopy "{0}" "{1}" /E /MOVE /NFL /NDL /NJH /NJS > nul
echo.
echo Update complete! You can now restart the application.
timeout /t 5 > nul
del "%~f0"
'@ -f $extractedFolder.FullName, $installDir

    $batchPath = Join-Path $installDir "_finalize_update.bat"
    Set-Content -Path $batchPath -Value $batchScript

    # 7. Launch the batch script and exit this PowerShell script
    Write-Host "INFO: Handing over to final updater script. This window will close." -ForegroundColor Yellow
    Start-Process cmd.exe -ArgumentList "/C `"$batchPath`""

} catch {
    Write-Host "FATAL: An error occurred during the update." -ForegroundColor Red
    Write-Host $_.Exception.Message -ForegroundColor Red
    Read-Host -Prompt "Press Enter to exit."
}

Scripts-Skripte

scripts/__init__.py

scripts/activate-venv_and_run-server.sh
#!/bin/bash
# scripts/activate-venv_and_run-server.sh
# Exit immediately if a command fails

SCRIPT_firstName="dictation_service"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )

PROJECT_ROOT="$SCRIPT_DIR/.."

os_type=$(uname -s)
if [[ "$os_type" == "MINGW"* || "$os_type" == "CYGWIN"* || "$os_type" == "MSYS"* ]]; then
    # This is a Windows-based shell environment
    detected_os="windows"
else
    # This is any other OS (Linux, macOS, FreeBSD, etc.)
    detected_os="other"
fi

if [ "$detected_os" = "windows" ]; then
  echo "please start type_watcher.ahk"
  echo "please start trigger-hotkeys.ahk"
else
  $PROJECT_ROOT/type_watcher.sh &
fi


set -e


HEARTBEAT_FILE="/tmp/$SCRIPT_firstName.heartbeat"
SCRIPT_TO_START="$SCRIPT_DIR/../$SCRIPT_firstName.py"
MAX_STALE_SECONDS=5

export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
export DISPLAY=:0
export XAUTHORITY=${HOME}/.Xauthority
export DICTATION_SERVICE_STARTED_CORRECTLY="true"

if [ -f "$HEARTBEAT_FILE" ]
then
    last_update=$(cat "$HEARTBEAT_FILE")
    current_time=$(date +%s)
    age=$((current_time - last_update))

    if [ "$age" -lt "$MAX_STALE_SECONDS" ]
    then
        echo "Service appears to be running and healthy."
        exit 0 
    else
        echo "Service heartbeat is stale. Attempting to restart."
    fi
else
    echo "Service is not running."
fi


python3 -m venv .env

echo "Activating virtual environment at '$PROJECT_ROOT/venv'..."
python3 -m venv .venv
source "$PROJECT_ROOT/.venv/bin/activate"

echo "Starting Python server from '$PROJECT_ROOT'..."
# We run the python script using its absolute path to be safe

echo "Starting service..."

python3 "$SCRIPT_TO_START" & 
scripts/notification_watcher.ahk
#Requires AutoHotkey v2.0
#SingleInstance Force

A_IconTip := "SL5 Aura Notifier"

; Create a borderless, always-on-top GUI window for our notification
noteGui := Gui("+AlwaysOnTop -Caption +ToolWindow")
noteGui.SetFont("s10", "Segoe UI")
titleCtrl := noteGui.Add("Text", "w300")
noteGui.SetFont("s9", "Segoe UI")
bodyCtrl := noteGui.Add("Text", "w300 y+5")

SetTimer(WatchForNotification, 250)

WatchForNotification() {
    static notifyFile := "C:\tmp\notification.txt"

    if FileExist(notifyFile) {
        content := Trim(FileRead(notifyFile))
        FileDelete(notifyFile)
        if (content = "")
            return

        parts := StrSplit(content, "|")
        titleCtrl.Text := parts[1]
        bodyCtrl.Text := parts.Has(2) ? parts[2] : ""

        ; Position and show the GUI
        noteGui.Show("NA")
        ; Hide it after 5 seconds
        SetTimer(HideGui, -5000)
    }
}

HideGui() {
    noteGui.Hide()
}
scripts/restart_venv_and_run-server.ahk
#Requires AutoHotkey v2.0
; restart_venv_and_run-server.ahk

FileReadLine(File, LineNumber)
{
    FileRead FileContents, %File%
    FileContents := ""
    Loop Parse, FileContents, "`n", "`r"
    {
        If (A_Index = LineNumber)
        return Trim(A_LoopField)
    }
    return ""
}

; Setzen Sie den Pfad zum Skript-Verzeichnis
; ScriptDir := FileGetDir(A_ScriptFullPath)
ScriptDir := A_ScriptDir

; Setzen Sie den Pfad zur Konfigurationsdatei
ConfigFile := ScriptDir . "\..\config\server.conf"

; Setzen Sie den Pfad zum Server-Skript
ServerScript := ScriptDir . "\activate-venv_and_run-server.sh"

; Lese die Konfigurationsdatei und lade die Variable PORT
FileRead ConfigFile, "r"
PORT := Trim(FileReadLine(ConfigFile, 1), " `t")

; Starte den Server neu
MsgBox "Restarting TTS Server on Port " PORT "..."
Run "pkill -f dictation_service.py"
Run "pkill -f type_watcher.sh"
; DO NOT kill LanguageTool server here. Run "pkill -f languagetool-server.jar"

Sleep 1000

; Überprüfe, ob ein Prozess auf dem Port läuft
PID := ""
PID := Run("lsof -t -i :" PORT, "", "", Output)
if (PID != "")
{
    Run "kill " PID
    Sleep 1000
}

; Starte das Server-Skript neu
Run ServerScript
scripts/restart_venv_and_run-server.sh
#!/bin/bash
#
# restart_venv_and_run-server.sh
#
# Final version: Correctly terminates ALL associated processes (main service and watcher)
# and reliably waits for them to disappear before starting a new instance.

# --- Configuration ---
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )

SERVER_SCRIPT="$SCRIPT_DIR/activate-venv_and_run-server.sh"
SERVICE_NAME_MAIN="dictation_service.py"

os_type=$(uname -s)
if [[ "$os_type" == "MINGW"* || "$os_type" == "CYGWIN"* || "$os_type" == "MSYS"* ]]; then
    # This is a Windows-based shell environment
    detected_os="windows"
else
    # This is any other OS (Linux, macOS, FreeBSD, etc.)
    detected_os="other"
fi

if [ "$detected_os" = "windows" ]; then
  echo "please start type_watcher.ahk"
  echo "please start trigger-hotkeys.ahk"
else
  SERVICE_NAME_WATCHER="type_watcher.sh"
fi



echo "Requesting restart for all services..."

# --- Step 1: Check if any of the target processes are running ---
# The -f flag for pgrep searches the full command line.
if ! pgrep -f "$SERVICE_NAME_MAIN" > /dev/null && ! pgrep -f "$SERVICE_NAME_WATCHER" > /dev/null; then
    echo "Info: No running server or watcher processes found. Starting fresh."
else
    # --- Step 2: Kill ALL old processes (Main Service AND Watcher) ---
    echo "Stopping old processes..."
    # Use -f to match the full command line, just like in pgrep.
    pkill -f "$SERVICE_NAME_MAIN"
    pkill -f "$SERVICE_NAME_WATCHER"
    echo pkill -f "$SERVICE_NAME_MAIN"
    echo pkill -f "$SERVICE_NAME_WATCHER"

    # realpath /tmp/../tmp/../tmp
    # /tmp
    # PROJECT_ROOT=realpath $SCRIPT_DIR/..
    PROJECT_ROOT=$(realpath "$SCRIPT_DIR/..")

    echo SCRIPT_DIR=$SCRIPT_DIR
    echo PROJECT_ROOT=$PROJECT_ROOT

    echo "Activating virtual environment at '$PROJECT_ROOT/venv'..."
    cd $PROJECT_ROOT
    python3 -m venv .venv
    source .venv/bin/activate
    end_dictation_servicePY="$PROJECT_ROOT/scripts/py/end_dictation_service.py"
    echo end_dictation_servicePY=$end_dictation_servicePY
    python3 "$end_dictation_servicePY" &



    # --- Step 3: Reliably wait for BOTH processes to terminate ---
    echo -n "Waiting for all processes to shut down "
    TIMEOUT_SECONDS=10
    for (( i=0; i<TIMEOUT_SECONDS; i++ )); do
        # The loop continues as long as EITHER the main service OR the watcher is found.
        if ! pgrep -f "$SERVICE_NAME_MAIN" > /dev/null && ! pgrep -f "$SERVICE_NAME_WATCHER" > /dev/null; then
            echo -e "\nInfo: All services have been terminated."
            break # Exit the loop, we are done waiting.
        fi
        echo -n "."
        sleep 1
    done
fi

sleep 1

# --- Step 4: Start the new server instance ---
echo "Starting new server and watcher..."
if [ -x "$SERVER_SCRIPT" ]; then
    "$SERVER_SCRIPT"
else
    echo "Error: Server script is not executable: $SERVER_SCRIPT"
    echo "Please run: chmod +x $SERVER_SCRIPT"
    exit 1
fi

echo "Server started or closed now."
scripts/type_watcher_Stash_Wird_bald_geloescht.sh
#!/bin/bash
# type_watcher.sh (Version 4 - Final)

# --- Set FULL Environment explicitly for background tools ---
export DISPLAY=:0
export XAUTHORITY=${HOME}/.Xauthority
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus"

# ... (der Rest des Skripts bleibt exakt gleich) ...

# --- Dependency Check ---
if ! command -v inotifywait &> /dev/null || ! command -v xdotool &> /dev/null; then
    exit 1
fi

DIR_TO_WATCH="/tmp"
# DIR_TO_WATCH="${HOME}/.sl5_stt_tmp"

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
PROJECT_ROOT="$SCRIPT_DIR/.."

FILE_PATTERN_BASE="tts_output_"
# LOCKFILE="/tmp/type_watcher.lock"
LOCKFILE="${DIR_TO_WATCH}/type_watcher.lock" # Lockfile also moves

LOG_FILE="$PROJECT_ROOT/type_watcher.log"

if [ -e "$LOCKFILE" ] && ps -p "$(cat "$LOCKFILE")" > /dev/null; then
    exit 0
fi
echo $$ > "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT

inotifywait -m -q -e create --format '%f' "$DIR_TO_WATCH" | while read -r FILE; do
    if [[ "$FILE" == ${FILE_PATTERN_BASE}* ]]; then
        FULL_PATH="${DIR_TO_WATCH}/${FILE}"
        sleep 0.05
        if [ -s "$FULL_PATH" ]; then
            TEXT=$(cat "$FULL_PATH")
            rm "$FULL_PATH"
            [ -n "$TEXT" ] && xdotool type --clearmodifiers "$TEXT"
        fi
    fi
done

Root-Verzeichnis Skripte

Folgende Skripte sind hier dokumentiert:

  • start_dictation_v2.0.bat

  • update.bat

  • install_hooks.sh

  • type_watcher.ahk

  • type_watcher.sh

start_dictation_v2.0.bat
:: start this script like: & .\start_dictation_v2.0.bat
:: Version: v2.1

@echo off
setlocal
title SL5 Aura - One-Click Starter

:: --- Step 1: Set correct working directory ---
cd /d "%~dp0"







# --- 2. Admin Rights Check ---
echo [*] Checking for Administrator privileges

REM Only check for admin rights if NOT running in a CI environment
if /I NOT "%CI%"=="true" (
    net session >nul 2>&1
    if %errorLevel% neq 0 (
        echo [ERROR] Re-launching with Admin rights...
        powershell -Command "Start-Process '%~f0' -Verb RunAs"
        exit /b
    )
)

echo [SUCCESS] Running with Administrator privileges.













:: --- Step 3: VEREINFACHT - Check if venv exists, otherwise run full setup ---
if not exist ".\.venv\Scripts\python.exe" (
    echo [WARNING] Virtual environment is missing or incomplete.
    echo [ACTION] Running full setup. This may take a moment...
    powershell.exe -ExecutionPolicy Bypass -File ".\setup\windows11_setup.ps1"

    if not exist ".\.venv\Scripts\python.exe" (
        echo [FATAL] Automated setup failed. Please check setup script.
        pause
        exit /b
    )
    echo [SUCCESS] Virtual environment has been set up successfully.
)
echo.

:: --- Step 4: Start background components ---
start "SL5 Type Watcher" type_watcher.ahk
start "SL5 Notification Watcher" scripts\notification_watcher.ahk
echo [INFO] Background watchers have been started.
echo.

:: --- Step 5: Activate venv and start the main service with auto-repair ---
echo [INFO] Activating virtual environment...
call .\.venv\Scripts\activate.bat

set REPAIR_ATTEMPTED=

:START_SERVICE_LOOP
echo [INFO] Starting the Python STT backend service...

python -u dictation_service.py

echo [INFO] Waiting 5 seconds for the service to initialize...
timeout /t 5 >nul

echo [INFO] Verifying service status via log file...
findstr /C:"Setup validation successful" "log\dictation_service.log" >nul 2>&1
IF %ERRORLEVEL% EQU 0 goto :CONTINUE_SCRIPT

:: --- ERROR HANDLING & REPAIR ---
echo [WARNING] Service verification failed. Log does not contain success signal.
if defined REPAIR_ATTEMPTED (
    echo [FATAL] The automatic repair attempt has failed. Please check setup manually.
    pause
    exit /b 1
)
echo [ACTION] Attempting automatic repair by reinstalling dependencies...
set REPAIR_ATTEMPTED=true
call .\.venv\Scripts\python.exe -m pip install -r requirements.txt
echo [INFO] Repair finished. Retrying service start...
echo.
goto :START_SERVICE_LOOP

:CONTINUE_SCRIPT
echo [INFO] Service verification successful.
echo [*] Triggering the service...
echo. >> "c:/tmp/sl5_record.trigger"
echo.

:: --- Final Success Message - ENTFERNT die doppelte Meldung ---
echo [SUCCESS] SL5 Aura is now running in the background.
echo This window will close automatically in a few seconds.
timeout /t 5 > nul
update.bat
@echo off
:: file: update.bat
:: Description: One-click updater for Windows users.
:: This script requests admin rights and then runs the main PowerShell update script.

if "%CI%"=="true" goto run_script

:: 1. Check for administrative privileges
net session >nul 2>&1
if %errorLevel% NEQ 0 (
    echo Requesting administrative privileges to run the updater...
    powershell -Command "Start-Process -FilePath '%0' -Verb RunAs"
    exit /b
)

:: 2. Now that we have admin rights, run the actual PowerShell updater script
::    -ExecutionPolicy Bypass: Temporarily allows the script to run without changing system settings.
::    -File: Specifies the script to execute.
:run_script
echo Starting the update process...
powershell.exe -ExecutionPolicy Bypass -File "%~dp0update\update_for_windows_users.ps1"

echo.
echo The update script has finished. This window can be closed.
pause
install_hooks.sh
#!/bin/bash
cp githooks/pre-push .git/hooks/pre-push
chmod +x .git/hooks/pre-push
echo "pre-push hook installed!"
type_watcher.ahk
#Requires AutoHotkey v2.0
; type_watcher.ahk (v8.3 - Correct FileRead Syntax)

; #SingleInstance Force ; is buggy

; --- Configuration ---
watchDir := "C:\tmp\sl5_aura"
logDir := A_ScriptDir "\log"
autoEnterFlagPath := "C:\tmp\sl5_auto_enter.flag"

heartbeat_start_File := "C:\tmp\heartbeat_type_watcher_start.txt"
; --- Main Script Body ---
myUniqueID := A_TickCount . "-" . Random(1000, 9999)

try {
    fileHandle := FileOpen(heartbeat_start_File, "w")
    fileHandle.Write(myUniqueID)
    fileHandle.Close()
} catch as e {
    MsgBox("FATAL: Could not write heartbeat file: " . e.Message, "Error", 16)
    ExitApp
}

; --- Global Variables ---
global pBuffer := Buffer(1024 * 16), hDir, pOverlapped := Buffer(A_PtrSize * 2 + 8, 0)
global CompletionRoutineProc
global watcherNeedsRearm := false
global fileQueue := []           ; The queue for files
global isProcessingQueue := false ; Flag to prevent simultaneous processing



Sleep(200)           ; Give a potential double-clicked instance time to act

try {
    if FileExist(heartbeat_start_File) {
        lastUniqueID := Trim(FileRead(heartbeat_start_File))
        if (lastUniqueID != myUniqueID) {
            ExitApp ; other instance exists, I must exit.
        }
    }
} catch {
    MsgBox("FATAL: " . e.Message, "Error", 16), ExitApp
}


SetTimer(CheckHeartbeatStart, 5000)

; =============================================================================
; SELF-TERMINATION VIA HEARTBEAT START
; =============================================================================
CheckHeartbeatStart() {
    global heartbeat_start_File, myUniqueID
    try {
        local lastUniqueID := Trim(FileRead(heartbeat_start_File, "UTF-8"))
        if (lastUniqueID != myUniqueID) {
            Log("Newer instance detected (" . lastUniqueID . "). I am " . myUniqueID . ". Terminating self.")
            ExitApp
        }
    } catch {
        Log("Could not read heartbeat file. Terminating to be safe.")
        ExitApp
    }
}

DirCreate(watchDir)
DirCreate(logDir)
Log("--- Script Started (v8.3 - Correct FileRead Syntax) ---")
Log("Watching folder: " . watchDir)

CompletionRoutineProc := CallbackCreate(IOCompletionRoutine, "F", 3)
WatchFolder(watchDir) ; Initial arming

ProcessExistingFiles() ; Process initial files AND trigger the first queue run

; --- The Main Application Loop ---
Loop {
    DllCall("SleepEx", "UInt", 0xFFFFFFFF, "Int", true)

    if (watcherNeedsRearm) {
        Log("MainLoop: Detected re-arm flag. Calling ReArmWatcher.")
        watcherNeedsRearm := false
        ReArmWatcher()
    }
}

Log("--- FATAL: Main loop exited unexpectedly. ---")
ExitApp

; =============================================================================
; LOGGING FUNCTION
; =============================================================================
Log(message) {
    static logFile := logDir "\type_watcher.log"
    try {
        FileAppend(A_YYYY "-" A_MM "-" A_DD " " A_Hour ":" A_Min ":" A_Sec " - " . message . "`n", logFile)
    } catch as e {
        MsgBox("CRITICAL LOGGING FAILURE!`n`nCould not write to: " . logFile . "`n`nReason: " . e.Message, "Logging Error", 16)
        ExitApp
    }
}

; =============================================================================
; INITIAL SCAN FOR EXISTING FILES
; =============================================================================
ProcessExistingFiles() {
    Log("Scanning for existing files to queue...")
    Loop Files, watchDir "\tts_output_*.txt" {
        QueueFile(A_LoopFileName)
    }
    Log("Initial scan complete. " . fileQueue.Length . " files queued.")
    TriggerQueueProcessing()
}

; =============================================================================
; FILE QUEUING FUNCTION
; =============================================================================
QueueFile(filename) {
    if InStr(filename, "tts_output_") {
        fullPath := watchDir "\" . filename

        ; --- FIX: Check if file is already in the queue ---
        for index, queuedPath in fileQueue {
            if (queuedPath = fullPath) {
                Log("-> File is already in queue. Ignoring duplicate add. -> " . filename)
                return ; Exit the function, do not add again
            }
        }
        ; --- End of FIX ---

        Log("Queuing file -> " . filename)
        fileQueue.Push(fullPath)
    } else {
        Log("Ignored non-target file -> " . filename)
    }
}

; =============================================================================
; MASTER FUNCTION TO START QUEUE PROCESSING
; =============================================================================
TriggerQueueProcessing() {
    global isProcessingQueue
    if (isProcessingQueue) {
        return
    }
    isProcessingQueue := true
    Log(">>> Starting queue processing loop...")
    ProcessQueue()
    Log("<<< Queue processing loop finished.")
    isProcessingQueue := false
}

; =============================================================================
; QUEUE PROCESSING LOOP (WITH CORRECT v2 FILEREAD SYNTAX)
; =============================================================================
ProcessQueue() {
    while (fileQueue.Length > 0) {
        local fullPath := fileQueue[1]
        Log("Attempting to process from queue: " . fullPath)

        static stabilityDelay := 50
        local content := ""
        local isReadyForProcessing := false

        try {
            if !FileExist(fullPath) {
                Log("-> File no longer exists. Removing from queue.")
                fileQueue.RemoveAt(1)
                continue
            }
            size1 := FileGetSize(fullPath), Sleep(stabilityDelay), size2 := FileGetSize(fullPath)
            if (size1 != size2 or size1 = 0) {
                Log("-> File is unstable/empty. Deleting it.")
                FileDelete(fullPath)
                fileQueue.RemoveAt(1)
                continue
            }

            ; --- THE DEFINITIVE FIX IS HERE ---
            ; Using the correct AutoHotkey v2 syntax for FileRead.
            content := FileRead(fullPath, "UTF-8")
            content := Trim(content)

            isReadyForProcessing := true
            Log("-> File is stable and readable.")
        } catch as e {
            Log("-> CRITICAL ERROR while reading file. Removing to prevent blocking. Error: " . e.Message)
            fileQueue.RemoveAt(1) ; Remove blocking file
            continue ; Try next file
        }

        if (isReadyForProcessing) {
            fileQueue.RemoveAt(1)
            try {
                FileDelete(fullPath)
                Log("-> File successfully deleted.")
                if (content != "") {
                    Log("--> Sending content: '" . content . "'")
                    SendText(content)

                    ; --- Conditional Enter Key ---
                    ; Check if the auto-enter plugin is enabled
                    if FileExist(autoEnterFlagPath) {
                        flagState := Trim(FileRead(autoEnterFlagPath))
                        if (flagState = "true") {
                            SendInput("{Enter}")
                        }
                    }
                    ; --- End of Conditional Block ---

                } else {
                    Log("-> File was empty.")
                }
            } catch as e {
                Log("-> ERROR during FINAL delete/send step: " . e.Message)
            }
        }
    }
}

; =============================================================================
; WATCHER INITIALIZATION & RE-ARMING
; =============================================================================
WatchFolder(pFolder) {
    global hDir
    hDir := DllCall("CreateFile", "Str", pFolder, "UInt", 1, "UInt", 7, "Ptr", 0, "UInt", 3, "UInt", 0x42000000, "Ptr", 0, "Ptr")
    if (hDir = -1) {
        local errMsg := "FATAL: Could not watch directory: " . pFolder
        Log(errMsg), MsgBox(errMsg, "Error", 16), ExitApp
    }
    Log("Successfully opened handle for directory: " . pFolder)
    ReArmWatcher()
}

ReArmWatcher() {
    global hDir, pBuffer, pOverlapped, CompletionRoutineProc
    static notifyFilter := 0x1
    DllCall("msvcrt\memset", "Ptr", pOverlapped.Ptr, "Int", 0, "Ptr", pOverlapped.Size)
    local success := DllCall("ReadDirectoryChangesW", "Ptr", hDir, "Ptr", pBuffer, "UInt", pBuffer.Size, "Int", false, "UInt", notifyFilter, "Ptr", 0, "Ptr", pOverlapped, "Ptr", CompletionRoutineProc)
    if (success) {
        Log("Arming watcher successful.")
    } else {
        Log("--- WARNING: ReArmWatcher failed! Error: " . A_LastError . ". Flag will be re-checked. ---")
        watcherNeedsRearm := true
    }
}

; =============================================================================
; COMPLETION ROUTINE TRIGGERS PROCESSING
; =============================================================================
IOCompletionRoutine(dwErrorCode, dwNumberOfBytesTransfered, lpOverlapped) {
    global pBuffer, watcherNeedsRearm

    try {
        if (dwErrorCode != 0) {
            Log("-> ERROR in IOCompletionRoutine. Code: " . dwErrorCode)
        } else if (dwNumberOfBytesTransfered > 0) {
            Log("==> Event TRIGGERED!")
            local pCurrent := pBuffer.Ptr
            Loop {
                local NextEntryOffset := NumGet(pCurrent, 0, "UInt")
                local Action := NumGet(pCurrent + 4, "UInt")
                local FileName := StrGet(pCurrent + 12, NumGet(pCurrent + 8, "UInt") / 2, "UTF-16")

                Log("--> Event data: Action=" . Action . ", FileName=" . FileName)

                if (Action = 1) {
                    QueueFile(FileName)
                }

                if (!NextEntryOffset) {
                    break
                }
                pCurrent += NextEntryOffset
            }
            TriggerQueueProcessing()
        }
    } catch as e {
        Log("--- FATAL ERROR in IOCompletionRoutine: " . e.Message . " ---")
    }

    watcherNeedsRearm := true
}
type_watcher.sh
#!/bin/bash
# type_watcher.sh

set -euo pipefail

DIR_TO_WATCH="/tmp/sl5_aura"
LOCKFILE="/tmp/type_watcher.lock"
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
LOG_DIR="$SCRIPT_DIR/log"
LOGFILE="$LOG_DIR/type_watcher.log"
AUTO_ENTER_FLAG="/tmp/sl5_auto_enter.flag" # The flag file for auto-enter

speak_file_path="$HOME/projects/py/TTS/speak_file.py"
if [ -e $speak_file_path ]
then
  echo " ok $speak_file_path exist"
else
    speak_file_path=''
    echo "$speak_file_path dont exist"
fi

# Ensure log directory exists
mkdir -p "$LOG_DIR"

log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOGFILE"
}
log_message "Hello from Watcher"

# --- Lockfile-Logic
if [ -e "$LOCKFILE" ]; then
    pid=$(cat "$LOCKFILE" 2>/dev/null)
    if [ -n "$pid" ] && ps -p "$pid" > /dev/null; then
        msg="Watcher already runs (PID: $pid). Exiting."
        echo "$msg"
        log_message "$msg"
        exit 0
    fi
    rm -f "$LOCKFILE"
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT

# --- Wait for the directory to be created by the main service ---
while [ ! -d "$DIR_TO_WATCH" ]; do
    sleep 0.5
done

# Function to get the title of the active window
get_active_window_title() {
    active_window_id=$(xdotool getactivewindow)
    xdotool getwindowname "$active_window_id"
}

OS_TYPE=$(uname -s)

if [[ "$OS_TYPE" == "Darwin" ]]; then
    # --- macOS Logic ---
    echo "✅ Watcher starting in macOS mode (using fswatch and osascript)."
    log_message "Watcher starting in macOS mode (using fswatch and osascript)."
    fswatch -0 "$DIR_TO_WATCH" | while read -d "" file; do
        if [[ "$file" == *tts_output_*.txt ]]; then
            osascript -e "tell application \"System Events\" to keystroke \"$(cat "$file")\""
            rm -f "$file"
        fi
    done
elif [[ "$OS_TYPE" == "Linux" ]]; then
    # --- Linux Logic ---
    echo "✅ Watcher starting in Linux mode (using inotifywait and xdotool)."
    log_message "Watcher starting in Linux mode (using inotifywait and xdotool)."

    while true; do
        inotifywait -q -e create,close_write "$DIR_TO_WATCH" --format '%f' | grep -q "tts_output_"
        sleep 0.1

        # Use null-separated find for safer file handling (handles spaces/newlines)
        while IFS= read -r -d '' f; do
            [ -f "$f" ] || continue

            # Read lines into an array
            mapfile -t lines < "$f"
            for line in "${lines[@]}"; do
                trimmed_line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
                GAME_WINDOW_ID=$(xdotool search --name "0 A.D." | head -1 || true)

                if [[ -n "$GAME_WINDOW_ID" ]]; then
                    log_message "GAME_WINDOW_ID = $GAME_WINDOW_ID"
                    if [[ "$trimmed_line" == 'alt+i' ]]; then
                        LC_ALL=C.UTF-8 xdotool windowactivate "$GAME_WINDOW_ID"
                        sleep 0.2
                        log_message "Sende ALT+i explizit mit keydown/keyup"
                        LC_ALL=C.UTF-8 xdotool keydown --window "$GAME_WINDOW_ID" 64
                        sleep 0.05
                        LC_ALL=C.UTF-8 xdotool key --window "$GAME_WINDOW_ID" 31
                        sleep 0.05
                        LC_ALL=C.UTF-8 xdotool keyup --window "$GAME_WINDOW_ID" 64
                        sleep 0.1
                        log_message "Fertig mit ALT+i Sequenz"
                        log_message "Sent alt+i"
                        if [[ -n "$speak_file_path" ]]; then
                          python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
                          sleep 0.01
                        fi

                        rm -f "$f"
                        continue
                    elif [[ "$trimmed_line" == 'alt+w' ]]; then
                        xte "keydown Alt_L" "keydown w" "keyup w" "keyup Alt_L"
                        log_message "Sent alt+w"
                        if [[ -n "$speak_file_path" ]]; then
                          python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
                          sleep 0.01
                        fi
                        rm -f "$f"
                        continue
                    elif [[ "$trimmed_line" == 'ctrl+c' ]]; then
                        LC_ALL=C.UTF-8 xdotool key ctrl+c clearmodifiers
                        log_message "Sent ctrl+c"
                        if [[ -n "$speak_file_path" ]]; then
                          python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
                          sleep 0.01
                        fi
                        rm -f "$f"
                        continue
                    elif [[ "$trimmed_line" == 'baue Haus' ]]; then
                        LC_ALL=C.UTF-8 xdotool key h
                        sleep 0.15
                        xdotool click --delay 10 --repeat 8 1
                        log_message "baue Haus"
                        if [[ -n "$speak_file_path" ]]; then
                          python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
                          sleep 0.01
                        fi
                        rm -f "$f"
                        continue
                    elif [[ "$trimmed_line" == 'baue Lagerhaus' ]]; then
                        LC_ALL=C.UTF-8 xdotool key s
                        sleep 0.15
                        xdotool click --delay 10 --repeat 8 1
                        log_message "baue Lagerhaus"
                        if [[ -n "$speak_file_path" ]]; then
                          python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
                          sleep 0.01
                        fi
                        rm -f "$f"
                        continue
                    elif [[ "$trimmed_line" == 'select iddle' ]]; then
                        xdotool keydown alt
                        xdotool type '#'
                        xdotool keyup alt
                        sleep 0.15
                        # xdotool click --delay 10 --repeat 8 1
                        log_message "select iddle"
                        if [[ -n "$speak_file_path" ]]; then
                          python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
                          sleep 0.01
                        fi
                        rm -f "$f"
                        continue
                    elif [[ "$trimmed_line" == 'baue Baracke' ]]; then
                        LC_ALL=C.UTF-8 xdotool key b
                        sleep 0.15
                        xdotool click --delay 10 --repeat 8 1
                        sleep 4
                        log_message "baue Baracke"
                        if [[ -n "$speak_file_path" ]]; then
                          python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
                          sleep 0.01
                        fi
                        rm -f "$f"
                        continue
                    fi
                fi

                # Fallback: type file content (if not a special command)
                if [ -z "${CI:-}" ]; then
                    LC_ALL=C.UTF-8 xdotool type --clearmodifiers --delay 0 --file "$f"
                    log_message "type --file $f"

                    # When you also want to have a voice feedback (means STT + littleAI + TTS )
                    #  Then you could use this repository:
                    # https://github.com/sl5net/gemini-tts/blob/main/speak_file.py
                    #  You could start the STT Service like so:
                    # ~/projects/py/TTS/scripts/restart_venv_and_run-server.sh
                    #  And add to type_watcher.sh the following line here:
                    if [[ -n "$speak_file_path" ]]; then
                      python3 "$speak_file_path" "$f" > /tmp/speak_error.log 2>&1
                      sleep 0.01
                    fi

                    rm -f "$f"
                    continue
                fi
            done



            rm -f "$f"

            # --- Conditional Enter Key ---
            window_title=$(get_active_window_title)
            if [[ -f "$AUTO_ENTER_FLAG" ]]; then
                regexLine=$(cat "$AUTO_ENTER_FLAG")
                if echo "$window_title" | grep -Eq "$regexLine"; then
                    log_message "INFO: Auto-Enter is enabled. Pressing Return."
                    if [ -z "${CI:-}" ]; then
                        LC_ALL=C.UTF-8 xdotool key Return
                    fi
                fi
            fi
        done < <(find "$DIR_TO_WATCH" -maxdepth 1 -type f -name 'tts_output_*.txt' -print0)
    done
else
    echo "ERROR: Unsupported operating system '$OS_TYPE'. Exiting."
    exit 1
fi