Utility-Skripte

Automatisch generierte Übersicht der Hilfsskripte.

Setup-Skripte

setup/SystemCheck.ps1
# SystemCheck.ps1
Write-Host "--- STARTE SYSTEM-DIAGNOSE ---" -ForegroundColor Cyan

# 1. PFAD DEFINITIONEN
# $tempDir = "$env:TEMP\VoskTTS_Check"
$tempDir = "C:\tmp\sl5_aura"
$testFile = "$tempDir\write_test.txt"
$pythonPath = ".\.venv\Scripts\python.exe"

# 2. SCHREIBTEST (Simuliert Vosk Service)
Write-Host "[1/3] Prüfe Schreibrechte für Python..." -NoNewline
if (-not (Test-Path $tempDir)) { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null }

try {
    # Versuch, eine Datei zu schreiben
    "Test-Inhalt" | Out-File -FilePath $testFile -Encoding utf8
    if (Test-Path $testFile) {
        Write-Host " OK (Ordner und Datei erzeugbar)" -ForegroundColor Green
    } else {
        throw "Datei konnte nicht erstellt werden."
    }
} catch {
    Write-Host " FEHLER" -ForegroundColor Red
    Write-Host "   Ursache: Windows Defender oder fehlende Rechte blockieren das Schreiben in $tempDir" -ForegroundColor Yellow
    Write-Host "   Lösung: Fügen Sie den Ordner zu den Ausnahmen im Defender hinzu."
    exit
}

# 3. AHK ADMIN CHECK
Write-Host "[2/3] Prüfe AHK Berechtigungen..."
# Wir prüfen, ob der aktuelle Prozess Admin-Rechte hat.
# AHK erbt diese Rechte, wenn es von hier gestartet wird.
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")

if ($isAdmin) {
    Write-Host " OK (Läuft als Admin)" -ForegroundColor Green
} else {
Write-Host " WARNUNG: Skript läuft NICHT als Admin." -ForegroundColor Yellow
    Write-Host "   Das bedeutet: AHK darf NICHT in Programme schreiben, die als Administrator gestartet wurden." -ForegroundColor Yellow
    Write-Host "   Lösung: Starten Sie ALLES (Vosk & AHK) als Administrator." -ForegroundColor Yellow

}

# 4. TYPING TEST (Visueller Test)
Write-Host "[3/3] Simuliere Tippen..."
Write-Host "   Ein Notepad Fenster öffnet sich gleich. Wenn dort Text erscheint, funktioniert alles." -ForegroundColor Cyan
Start-Sleep -Seconds 2

# Notepad öffnen
$notepad = Start-Process notepad.exe -PassThru
Start-Sleep -Seconds 1

# SendKeys via PowerShell nutzen (ähnlich wie AHK, um zu testen ob Windows Input blockt)
try {
    [void] [System.Reflection.Assembly]::LoadWithPartialName("'System.Windows.Forms")
    [System.Windows.Forms.SendKeys]::SendWait("Test OK")
#    Write-Host "   Wurde Text in Notepad geschrieben? (Ja/Nein)"
} catch {
    Write-Host "   FEHLER beim Senden von Tastenanschlägen." -ForegroundColor Red
    Pause
}

Write-Host "--- DIAGNOSE ENDE ---"
Start-Sleep -Seconds 1

# Fenster schließen mit Alt+F4
#[System.Windows.Forms.SendKeys]::SendWait("%{F4}")
# Kill all notepad without save !!
#Get-Process notepad -ErrorAction SilentlyContinue | Stop-Process -Force # thats also has killed the Terminals

#$procs = Get-Process -Name notepad -ErrorAction SilentlyContinue
#$procs | ForEach-Object { Stop-Process -Id $_.Id -Force }


Add-Type -AssemblyName System.Windows.Forms
# Alt+F4 (schließt Notepad) und dann 'n' für "Nicht speichern"
[System.Windows.Forms.SendKeys]::SendWait("%{F4}")
Start-Sleep -Milliseconds 120
[System.Windows.Forms.SendKeys]::SendWait("n")


#Pause
setup/UNINSTALL.txt
--------------------------------------------------------------------------------
ENGLISH
--------------------------------------------------------------------------------
How to uninstall SL5 Aura:

1. Stop the application:
   Close the running Python window or terminate the process.

2. Remove the Main System:
   Simply DELETE the entire folder containing this file.
   SL5 Aura is portable and runs within its own virtual environment.
   It does not leave registry entries or hidden system files for the core engine.

3. Optional - Remove Client Tools:
   If you installed the optional helper tools (AutoHotkey & CopyQ) via our setup,
   you can run the script located at:
   setup/uninstall_ahk_copyq.bat

--------------------------------------------------------------------------------
DEUTSCH
--------------------------------------------------------------------------------
So deinstallieren Sie SL5 Aura:

1. Anwendung beenden:
   Schließen Sie das laufende Python-Fenster oder beenden Sie den Prozess.

2. Hauptsystem entfernen:
   LÖSCHEN Sie einfach den gesamten Ordner, in dem sich diese Datei befindet.
   SL5 Aura ist "portable" und läuft in seiner eigenen virtuellen Umgebung.
   Es hinterlässt keine Registry-Einträge oder versteckte Systemdateien für die Core-Engine.

3. Optional - Client-Tools entfernen:
   Falls Sie die optionalen Hilfsprogramme (AutoHotkey & CopyQ) über unser Setup
   installiert haben, können Sie dieses Skript ausführen:
   setup/uninstall_ahk_copyq.bat
setup/fix_permissions.bat
@echo off

:: setup/fix_permissions.bat



pushd "%~dp0"
cd ..
set "TARGET_DIR=%CD%"
popd

echo Ziel: "%TARGET_DIR%"

:: Wir setzen die Rechte NUR auf den Hauptordner.
:: (OI)(CI) sorgt dafuer, dass alle Unterordner/Dateien das automatisch erben.
:: Das dauert nur 1 Sekunde.

:: Deutsch
icacls "%TARGET_DIR%" /grant:r "Benutzer":(OI)(CI)F /grant:r "Jeder":(OI)(CI)F

:: Englisch / International
icacls "%TARGET_DIR%" /grant:r "Users":(OI)(CI)F /grant:r "Everyone":(OI)(CI)F



set "TARGET_DIR=C:\tmp\sl5_aura"

:: 1. Verzeichnis erstellen (falls nicht vorhanden)
if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%"

:: 2. Rechte setzen
:: /grant      = Rechte gewähren
:: *S-1-5-32-545 = SID für die Gruppe "Benutzer" (lokal) - sicherer als "Jeder"
:: (OI)        = Object Inherit (Dateien erben Rechte)
:: (CI)        = Container Inherit (Unterordner erben Rechte)
:: F           = Full Control (Vollzugriff - Lesen, Schreiben, Löschen)
:: /t          = Rekursiv auf bereits existierende Unterelemente anwenden

:: /grant *S-1-1-0:(OI)(CI)F = JEDER (Everyone) bekommt Vollzugriff auf Dateien und Unterordner

:: icacls "%TARGET_DIR%" /grant *S-1-1-0:(OI)(CI)F /t /c /q
icacls "%TARGET_DIR%" /grant *S-1-5-32-545:(OI)(CI)F /t

net session >nul 2>&1
if %errorLevel% neq 0 (
    echo ACHTUNG: Keine Admin-Rechte!
    echo Bitte prüfen ob das Script in C:\tmp\sl5_aura schreiben kann.
    echo Eventuell noch mal starten so: Rechtsklick auf die Datei -> "Als Administrator ausfuehren"
    timeout /t 4

)

echo Rechte für %TARGET_DIR% verarbeitet.




echo.
echo ========================================================
echo Fertig! Rechte wurden (hoffentlich) per Vererbung gesetzt.
echo ========================================================
setup/glogg_installer.bat
@echo off
cd /d "%~dp0"

set INSTALLER=glogg-latest-x86_64-setup.exe
set URL=http://glogg.bonnefon.org/files/%INSTALLER%
if exist "%ProgramFiles%\glogg\glogg.exe" (
    echo glogg is already installed.
    exit /b
)
echo Downloading %INSTALLER%...
powershell -Command "Invoke-WebRequest -Uri '%URL%' -OutFile '%INSTALLER%'"
echo Installing glogg...
:: /S = Silent
:: /D=Path = Optional output directory
"%INSTALLER%" /S
del "%INSTALLER%"
echo glogg installation complete.

reg add "HKCR\*\shell\Open with glogg" /ve /d "Open with glogg" /f
reg add "HKCR\*\shell\Open with glogg\command" /ve /d "\"C:\Program Files\glogg\glogg.exe\" \"%1\"" /f
setup/install_ahk_copyq.ps1
# install_ahk_copyq.ps1
# script_name: setup/install_ahk_copyq.ps1

param([string]$ProjectRootPath)

$downloadDir = "$env:USERPROFILE\Downloads"
# AHK specific variables
$ahkInstallerName = "ahk-install.exe"
$ahkLocalPath = Join-Path -Path $downloadDir -ChildPath $ahkInstallerName
$ahkInstalledPath = "$env:ProgramFiles\AutoHotkey\v2\AutoHotkey.exe"

$ProjectRootPath = Split-Path -Path $PSScriptRoot -Parent
function Register-AuraStartupTask {
    param(
        [string]$ProjectRootPath
    )
    $AhkExePath = Join-Path $env:ProgramFiles "AutoHotkey\v2\AutoHotkey64.exe"
    if (-not (Test-Path $AhkExePath)) {
        $AhkExePath = Join-Path $env:ProgramFiles "AutoHotkey\v2\AutoHotkey.exe"
    }
    $AhkScriptPath = Join-Path $ProjectRootPath "trigger-hotkeys.ahk"
    if (-not (Test-Path $AhkExePath)) {
        Write-Error "AutoHotkey executable not found at '$AhkExePath'. Cannot create scheduled task."
        return
    }
    $TaskName = "AuraDictation_Hotkeys"
    $TaskDescription = "Starts the AutoHotkey script for Aura Dictation with highest privileges on user logon."
    $TaskAction = New-ScheduledTaskAction -Execute $AhkExePath -Argument "`"$AhkScriptPath`""
    $TaskTrigger = New-ScheduledTaskTrigger -AtLogOn
    $TaskPrincipal = New-ScheduledTaskPrincipal -UserId (Get-CimInstance Win32_ComputerSystem).Username -LogonType Interactive -RunLevel Highest
    # Alte Aufgabe löschen, falls vorhanden, für eine saubere Neuinstallation
    Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
    # Neue Aufgabe registrieren

    $TaskSettings = New-ScheduledTaskSettingsSet `
        -AllowStartIfOnBatteries `
        -DontStopIfGoingOnBatteries `
        -StartWhenAvailable `
        -DontStopOnIdleEnd `
        -ExecutionTimeLimit 0

    try {
        Register-ScheduledTask -TaskName $TaskName `
        -Action $TaskAction  `
        -Trigger $TaskTrigger  `
        -Principal $TaskPrincipal  `
        -Settings $TaskSettings `
        -Description $TaskDescription
        Write-Host "Successfully created scheduled task '$TaskName'."
    }
    catch {
        Write-Error "Failed to create scheduled task. Error: $_"
    }
}

Write-Host "Starting setup for client tools..." -ForegroundColor Cyan

# --- TASK 1: AutoHotkey V2 ---
Write-Host "`n[1/2] Checking AutoHotkey V2..." -ForegroundColor Yellow

if (Test-Path -Path $ahkInstalledPath) {
    Write-Host "AutoHotkey V2 is already installed. Skipping." -ForegroundColor Green
} else {
    # Not installed, check local download
    if (Test-Path -Path $ahkLocalPath) {
        Write-Host "Found installer in Downloads folder ($ahkInstallerName). Installing from local file..." -ForegroundColor Cyan
        try {
            # Start local installer silent
            $process = Start-Process -FilePath $ahkLocalPath -ArgumentList "/silent" -Wait -PassThru
            if ($process.ExitCode -eq 0) {
                Write-Host "AutoHotkey installed from local file successfully." -ForegroundColor Green
            } else {
                Write-Warning "Local installer exited with code $($process.ExitCode). Trying Winget as fallback..."
                winget install --id "AutoHotkey.AutoHotkey" -e --source winget --accept-package-agreements --accept-source-agreements
            }
        } catch {
            Write-Error "Failed to run local installer. $_"
        }
    } else {
        # Not installed, no local file -> Use Winget
        Write-Host "No local installer found. Installing via Winget..." -ForegroundColor Cyan
        winget install --id "AutoHotkey.AutoHotkey" -e --source winget --accept-package-agreements --accept-source-agreements
    }
}


if ($ProjectRootPath) {
        Register-AuraStartupTask -ProjectRootPath $ProjectRootPath
} else {
    Write-Warning "ProjectRootPath path not provided. Cannot create scheduled task."
}








# --- TASK 2: CopyQ ---
Write-Host "`n[2/2] Checking CopyQ..." -ForegroundColor Yellow

# We rely on Winget's internal check here, as CopyQ installer filenames vary by version.
# Winget will detect if it's already installed and skip/update accordingly.
winget install --id "hluk.CopyQ" -e --source winget --accept-package-agreements --accept-source-agreements

if ($LASTEXITCODE -eq 0) {
    Write-Host "CopyQ check/install completed successfully." -ForegroundColor Green
} else {
    Write-Warning "CopyQ setup finished with exit code $LASTEXITCODE. It might be already installed or cancelled."
}

winget install --id "hluk.CopyQ" -e --source winget --accept-package-agreements --accept-source-agreements





# Write-Host "CopyQ check/install completed successfully." -ForegroundColor Green

# --- NEU: Konfiguration des Hotkeys ---
Write-Host "    -> Configuring Global Hotkey (Ctrl+Q)..."

# 1. Pfad suchen (Da PATH evtl. noch nicht aktuell ist)
$copyqExe = "copyq" # Fallback
if (Test-Path "C:\Program Files\CopyQ\copyq.exe") {
   $copyqExe = "C:\Program Files\CopyQ\copyq.exe"
} elseif (Test-Path "C:\Program Files (x86)\CopyQ\copyq.exe") {
   $copyqExe = "C:\Program Files (x86)\CopyQ\copyq.exe"
}

# Mit Call-Operator (Achtung: Pfad muss quoted werden)
& "$copyqExe" config global_shortcuts/show "Ctrl+1"
if ($LASTEXITCODE -eq 0) { "OK" } else {
    "Error: $LASTEXITCODE"
    # Source - https://stackoverflow.com/a/22362868
    # Posted by Michael Sorens
    # Retrieved 2026-01-22, License - CC BY-SA 3.0
#    Read-Host -Prompt "Press Enter to continue"
    Start-Sleep -Seconds 2
}

# setup/install_ahk_copyq.ps1:76
#& ".\.venv\Scripts\python.exe" "tools\map_tagger.py" "--yes"


# Ensure relative to the installer script directory
$scriptDir  = Split-Path -Parent $MyInvocation.MyCommand.Path
$pythonPath = Join-Path $scriptDir '..\.venv\Scripts\python.exe'
$pythonPath = (Resolve-Path $pythonPath).Path

# setup/install_ahk_copyq.ps1:87
Write-Host "Running map_tagger.py..."
$mapTagger  = Join-Path $scriptDir '..\tools\map_tagger.py'
$mapTagger  = (Resolve-Path $mapTagger).Path
& $pythonPath $mapTagger --yes
if ($LASTEXITCODE -eq 0) {
    "OK"
} else {
    "Error: $LASTEXITCODE"
    Read-Host -Prompt "Press Enter to continue"
}

Write-Host "Running export_to_copyq.py..."
Start-Process "py" -ArgumentList "-3", "..\tools\export_to_copyq.py" -WorkingDirectory (Get-Location) -Wait -PassThru
if ($LASTEXITCODE -eq 0) { "OK" } else {
    "Error: $LASTEXITCODE"
    Read-Host -Prompt "Press Enter to continue"
}


# Source - https://stackoverflow.com/a/22362868
# Posted by Michael Sorens
# Retrieved 2026-01-22, License - CC BY-SA 3.0
# Read-Host -Prompt "Press Enter to continue"


# map_tagger.py --yes

#& ".\.venv\Scripts\python.exe" "tools\export_to_copyq.py"




# --- Notepad++ ---
# if exist "C:\Program Files\Notepad++\notepad++.exe" (
if (Test-Path "C:\Program Files\Notepad++\notepad++.exe")
{
    echo [OK] Notepad++ ist schon da.
} else
{
    echo [..] Installiere Notepad++...
    winget install -e --id Notepad++.Notepad++ --silent --accept-source-agreements --accept-package-agreements
}

# --- Double Commander ---
if (Test-Path "C:\Program Files\Double Commander\doublecmd.exe")
{
    echo [OK] Double Commander ist schon da.
} else
{
    echo [..] Installiere Double Commander...
    winget install -e --id DoubleCommander.DoubleCommander --silent --accept-source-agreements --accept-package-agreements
}





Write-Host "`nClient tools setup sequence finished." -ForegroundColor Cyan
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

eval $(python3 scripts/py/setup_config.py)
echo "Wahl: $SELECTED_LANG | Zweit: $SECOND_LANG | Ohne: $EXCLUDE_LANGUAGES"

should_remove_zips_after_unpack=true

if [ -z "$EXCLUDE_LANGUAGES" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
    EXCLUDE_LANGUAGES="de,en"
    echo "--> GitHub Actions detected. Auto-excluding large models: $EXCLUDE_LANGUAGES"
fi

# --- 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
# --- Filter ARCHIVE_CONFIG based on EXCLUDE_LANGUAGES ---
INSTALL_CONFIG=()
for config_line in "${ARCHIVE_CONFIG[@]}"; do
    read -r base_name final_name dest_path <<< "$config_line"
    IS_MANDATORY=false
    IS_EXCLUDED=false
    if [[ "$base_name" == "LanguageTool-6.6" ]] || [[ "$base_name" == "lid.176" ]]; then
        IS_MANDATORY=true
    fi
    if [ "$EXCLUDE_LANGUAGES" == "all" ] && [ "$IS_MANDATORY" = false ]; then
        IS_EXCLUDED=true
    elif [ "$IS_MANDATORY" = false ]; then
        if [[ "$base_name" =~ vosk-model-de- ]] && [[ "$EXCLUDE_LANGUAGES" =~ de ]]; then
            IS_EXCLUDED=true
        fi
        if [[ "$base_name" =~ vosk-model-en-us- ]] && [[ "$EXCLUDE_LANGUAGES" =~ en ]]; then
            IS_EXCLUDED=true
        fi



            if [[ "$base_name" == "vosk-model-en-us-0.22" ]] && ([[ "$EXCLUDE_LANGUAGES" =~ en ]] || [[ "$CI" == "true" ]]); then
                echo "    -> Excluding large in CI (en): $base_name"
                IS_EXCLUDED=true
            fi



    fi
    if [ "$IS_EXCLUDED" = false ]; then
        INSTALL_CONFIG+=("$config_line")
    fi
done
# --- End Filter ---

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

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

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

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

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

    if [ -n "$EXCLUDE_LANGUAGES" ]; then
        ./.venv/bin/python tools/download_all_packages.py --exclude "$EXCLUDE_LANGUAGES"
    else
        ./.venv/bin/python tools/download_all_packages.py
    fi
    echo "    -> Downloader finished. Retrying extraction..."

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

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

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

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


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



# --- Install fzf (Fuzzy Finder) ---
if ! command -v fzf &> /dev/null; then
    echo "[INFO] fzf not found. Installing..."
    # Prüfen, ob brew installiert ist
    if command -v brew &> /dev/null; then
        brew install fzf
    else
        echo "[ERROR] Homebrew not found. Please install Homebrew first."
    fi
else
    echo "[INFO] fzf is already installed."
fi








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



# --- Automatisches Setzen des Standard-Modells ---
echo "--> Configuring default model in config/model_name.txt..."
if [ "$CI" == "true" ]; then
    echo "vosk-model-small-en-us-0.15" > config/model_name.txt
elif [ "$SELECTED_LANG" == "de" ]; then
    echo "vosk-model-de-0.21" > config/model_name.txt
else
    echo "Please set a vosk-model in config/model_name.txt e.g. vosk-model-en-us-0.22 and check https://alphacephei.com/vosk/models"
fi




# --- 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

eval $(python3 scripts/py/setup_config.py)
echo "Wahl: $SELECTED_LANG | Zweit: $SECOND_LANG | Ohne: $EXCLUDE_LANGUAGES"




echo ""
echo "--- Setup for Manjaro/Arch is complete! ---"
echo ""

echo "Optional: If you are running Wayland (e.g. KDE Plasma 6, CachyOS),"
echo "  'dotool' is REQUIRED for system-wide text input."
echo "  On X11 dotool is optional but recommended for better compatibility."
echo "  Install with:"

echo "  yay -S dotool"
echo "  sudo gpasswd -a \$USER input"
echo "  echo 'KERNEL==\"uinput\", GROUP=\"input\", MODE=\"0660\", OPTIONS+=\"static_node=uinput\"' | sudo tee /etc/udev/rules.d/80-dotool.rules"
echo "  sudo udevadm control --reload-rules && sudo udevadm trigger"
echo "  (Re-login required after group change)"
echo "  Then set x11_input_method_OVERRIDE = 'dotool' in config/settings_local.py"
echo ""
echo "To start Aura:"
echo "  ./scripts/restart_venv_and_run-server.sh"
echo ""















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

sudo pacman -S --needed sdl2 sdl2_mixer sdl2_ttf sdl2_image

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

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

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




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

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

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



# --- Filter Configuration based on EXCLUDE_LANGUAGES ---
INSTALL_CONFIG=()
if [ -z "$EXCLUDE_LANGUAGES" ]; then
    # Keine Ausschlüsse, die gesamte MASTER-Liste wird installiert.
    INSTALL_CONFIG=("${ARCHIVE_CONFIG[@]}")
else
    # Ausschlüsse aktiv, die Liste wird gefiltert.
    for config_line in "${ARCHIVE_CONFIG[@]}"; do
        read -r base_name final_name dest_path <<< "$config_line"

        IS_MANDATORY=false
        IS_EXCLUDED=false

        # Komponenten, die immer benötigt werden (z.B. LanguageTool Core)
        if [[ "$base_name" == "LanguageTool-6.6" ]] || [[ "$base_name" == "lid.176" ]]; then
            IS_MANDATORY=true
        fi

        # 1. Ausschluss-Check: exclude=all
        if [ "$EXCLUDE_LANGUAGES" == "all" ] && [ "$IS_MANDATORY" = false ]; then
            echo "    -> Excluding (all): $base_name"
            IS_EXCLUDED=true
        fi

        # 2. Ausschluss-Check: Spezifische Sprachen (z.B. de, en)
        if [ "$IS_EXCLUDED" = false ]; then
            # Test auf 'de' im Namen und in der Ausschlussliste
            if [[ "$base_name" =~ vosk-model-de- ]] && [[ "$EXCLUDE_LANGUAGES" =~ de ]]; then
                echo "    -> Excluding (de): $base_name"
                IS_EXCLUDED=true
            fi
            # Test auf 'en' im Namen und in der Ausschlussliste
            if [[ "$base_name" =~ vosk-model.*en-us ]] && [[ "$EXCLUDE_LANGUAGES" =~ en ]]; then
                echo "    -> Excluding (en): $base_name"
                IS_EXCLUDED=true
            fi
            # Hinzufügen weiterer spezifischer Exklusionsregeln nach Bedarf...
        fi

        # Nur hinzufügen, wenn nicht ausgeschlossen
        if [ "$IS_EXCLUDED" = false ]; then
            INSTALL_CONFIG+=("$config_line")
        fi
    done
fi
# --- End Filter Configuration ---


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

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

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

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

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

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

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

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

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

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

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



# --- Install fzf (Fuzzy Finder) ---
if ! command -v fzf &> /dev/null; then
    echo "[INFO] fzf not found. Installing..."
    sudo pacman -S --noconfirm fzf
else
    echo "[INFO] fzf is already installed."
fi


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


# --- Automatisches Setzen des Standard-Modells ---
echo "--> Configuring default model in config/model_name.txt..."
if [ "$CI" == "true" ]; then
    echo "vosk-model-small-en-us-0.15" > config/model_name.txt
elif [ "$SELECTED_LANG" == "de" ]; then
    echo "vosk-model-de-0.21" > config/model_name.txt
else
    echo "Please set a vosk-model in config/model_name.txt e.g. vosk-model-en-us-0.22"
fi




# --- dotool setup ---
if ! command -v dotool &> /dev/null; then
    echo "--> Installing dotool via AUR..."
    yay -S --noconfirm dotool || echo "WARNING: dotool install failed. See docs/LINUX_WAYLAND_dotool.md"
fi
sudo usermod -aG input $USER
echo 'KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"' \
  | sudo tee /etc/udev/rules.d/80-dotool.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
echo "NOTE: Re-login required for input group to take effect."
echo "See docs/LINUX_WAYLAND_dotool.md for details."
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"

eval $(python3 scripts/py/setup_config.py)
echo "Wahl: $SELECTED_LANG | Zweit: $SECOND_LANG | Ohne: $EXCLUDE_LANGUAGES"





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

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

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

set -e

# --- 1. System Dependencies ---
# (This section remains unchanged)
echo "--> Checking for a compatible Java version (>=17)..."
JAVA_OK=0
if command -v java &> /dev/null; then
    VERSION=$(java -version 2>&1 | awk -F[\".] '/version/ {print ($2 == "1") ? $3 : $2}')
    if [ "$VERSION" -ge 17 ]; then
        echo "    -> Found compatible Java version $VERSION. OK."
        JAVA_OK=1
    else
        echo "    -> Found Java version $VERSION, but we need >=17."
    fi
else
    echo "    -> No Java executable found."
fi
if [ "$JAVA_OK" -eq 0 ]; then
    echo "    -> Installing a modern JDK (>=17)..."
    # SUSE CHANGE: Use zypper instead of apt
    sudo zypper refresh && sudo zypper -n install java-21-openjdk
fi
echo "--> Installing other core dependencies..."
# SUSE CHANGE: Use zypper and adjust package names
sudo zypper -n install \
    inotify-tools wget unzip portaudio-devel python3-pip


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

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

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




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

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

# --- Configuration ---
PREFIX="Z_"

if [ "$GITHUB_ACTIONS" == "true" ]; then
  # Format: "BaseName FinalDirName DestinationPath"
  ARCHIVE_CONFIG=(
      "LanguageTool-6.6 LanguageTool-6.6 ."
      "vosk-model-small-en-us-0.15 vosk-model-small-en-us-0.15 ./models"
      "lid.176 lid.176.bin ./models"
  )
  echo "--> GitHub CI detected: Large models (0.21, 0.22) excluded to prevent 502 errors."
else
  # 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"
  )
fi

DOWNLOAD_REQUIRED=false

# --- Filter Configuration based on EXCLUDE_LANGUAGES ---
INSTALL_CONFIG=()
if [ -z "$EXCLUDE_LANGUAGES" ]; then
    # Keine Ausschlüsse, die gesamte MASTER-Liste wird installiert.
    INSTALL_CONFIG=("${ARCHIVE_CONFIG[@]}")
else
    # Ausschlüsse aktiv, die Liste wird gefiltert.
    for config_line in "${ARCHIVE_CONFIG[@]}"; do
        read -r base_name final_name dest_path <<< "$config_line"

        IS_MANDATORY=false
        IS_EXCLUDED=false

        # Komponenten, die immer benötigt werden (z.B. LanguageTool Core)
        if [[ "$base_name" == "LanguageTool-6.6" ]] || [[ "$base_name" == "lid.176" ]]; then
            IS_MANDATORY=true
        fi

        # 1. Ausschluss-Check: exclude=all
        if [ "$EXCLUDE_LANGUAGES" == "all" ] && [ "$IS_MANDATORY" = false ]; then
            echo "    -> Excluding (all): $base_name"
            IS_EXCLUDED=true
        fi

        # 2. Ausschluss-Check: Spezifische Sprachen (z.B. de, en)
        if [ "$IS_EXCLUDED" = false ]; then
            # Test auf 'de' im Namen und in der Ausschlussliste
            if [[ "$base_name" =~ vosk-model-de- ]] && [[ "$EXCLUDE_LANGUAGES" =~ de ]]; then
                echo "    -> Excluding (de): $base_name"
                IS_EXCLUDED=true
            fi
            # Test auf 'en' im Namen und in der Ausschlussliste
            if [[ "$base_name" =~ vosk-model.*en-us ]] && [[ "$EXCLUDE_LANGUAGES" =~ en ]]; then
                echo "    -> Excluding (en): $base_name"
                IS_EXCLUDED=true
            fi


            if [[ "$base_name" == "vosk-model-en-us-0.22" ]] && ([[ "$EXCLUDE_LANGUAGES" =~ en ]] || [[ "$CI" == "true" ]]); then
                echo "    -> Excluding large in CI (en): $base_name"
                IS_EXCLUDED=true
            fi



            # Hinzufügen weiterer spezifischer Exklusionsregeln nach Bedarf...
        fi

        # Nur hinzufügen, wenn nicht ausgeschlossen
        if [ "$IS_EXCLUDED" = false ]; then
            INSTALL_CONFIG+=("$config_line")
        fi
    done
fi
# --- End Filter Configuration ---


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

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

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

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

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

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

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

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

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

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

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




# --- Install fzf (Fuzzy Finder) ---
if ! command -v fzf &> /dev/null; then
    echo "[INFO] fzf not found. Installing..."
    sudo zypper install -y fzf
else
    echo "[INFO] fzf is already installed."
fi




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


# --- dotool setup ---
if ! command -v dotool &> /dev/null; then
    echo "--> Installing dotool..."
    sudo zypper install -y dotool || echo "WARNING: dotool not found in repos. Install manually. See docs/LINUX_WAYLAND_dotool.md"
fi
sudo usermod -aG input $USER
echo 'KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"' \
  | sudo tee /etc/udev/rules.d/80-dotool.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
echo "NOTE: Re-login required for input group to take effect."
echo "See docs/LINUX_WAYLAND_dotool.md for details."


# --- Automatisches Setzen des Standard-Modells ---
echo "--> Configuring default model in config/model_name.txt..."
if [ "$CI" == "true" ]; then
    echo "vosk-model-small-en-us-0.15" > config/model_name.txt
elif [ "$SELECTED_LANG" == "de" ]; then
    echo "vosk-model-de-0.21" > config/model_name.txt
else
    echo "Please set a vosk-model in config/model_name.txt e.g. vosk-model-en-us-0.22 and check https://alphacephei.com/vosk/models"
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

eval $(python3 scripts/py/setup_config.py)
echo "Wahl: $SELECTED_LANG | Zweit: $SECOND_LANG | Ohne: $EXCLUDE_LANGUAGES"

# --- 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 ---"

sudo apt-get update -y

# 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 \
    ffmpeg libnotify-bin xclip xvfb

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

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

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


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

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

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



# --- Filter Configuration based on EXCLUDE_LANGUAGES ---
INSTALL_CONFIG=()
if [ -z "$EXCLUDE_LANGUAGES" ]; then
    # Keine Ausschlüsse, die gesamte MASTER-Liste wird installiert.
    INSTALL_CONFIG=("${ARCHIVE_CONFIG[@]}")
else
    # Ausschlüsse aktiv, die Liste wird gefiltert.
    for config_line in "${ARCHIVE_CONFIG[@]}"; do
        read -r base_name final_name dest_path <<< "$config_line"

        IS_MANDATORY=false
        IS_EXCLUDED=false

        # Komponenten, die immer benötigt werden (z.B. LanguageTool Core)
        if [[ "$base_name" == "LanguageTool-6.6" ]] || [[ "$base_name" == "lid.176" ]]; then
            IS_MANDATORY=true
        fi

        # 1. Ausschluss-Check: exclude=all
        if [ "$EXCLUDE_LANGUAGES" == "all" ] && [ "$IS_MANDATORY" = false ]; then
            echo "    -> Excluding (all): $base_name"
            IS_EXCLUDED=true
        fi

        # 2. Ausschluss-Check: Spezifische Sprachen (z.B. de, en)
        if [ "$IS_EXCLUDED" = false ]; then
            # Test auf 'de' im Namen und in der Ausschlussliste
            if [[ "$base_name" =~ vosk-model-de- ]] && [[ "$EXCLUDE_LANGUAGES" =~ de ]]; then
                echo "    -> Excluding (de): $base_name"
                IS_EXCLUDED=true
            fi
            # Test auf 'en' im Namen und in der Ausschlussliste
            if [[ "$base_name" =~ vosk-model-en-us- ]] && [[ "$EXCLUDE_LANGUAGES" =~ en ]]; then
                echo "    -> Excluding (en): $base_name"
                IS_EXCLUDED=true
            fi

            if [[ "$base_name" == "vosk-model-en-us-0.22" ]] && ([[ "$EXCLUDE_LANGUAGES" =~ en ]] || [[ "$CI" == "true" ]]); then
                echo "    -> Excluding large in CI (en): $base_name"
                IS_EXCLUDED=true
            fi


            # Hinzufügen weiterer spezifischer Exklusionsregeln nach Bedarf...
        fi

        # Nur hinzufügen, wenn nicht ausgeschlossen
        if [ "$IS_EXCLUDED" = false ]; then
            INSTALL_CONFIG+=("$config_line")
        fi
    done
fi
# --- End Filter Configuration ---


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

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

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

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

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

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

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

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

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

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

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








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


# --- Install fzf (Fuzzy Finder) ---
if ! command -v fzf &> /dev/null; then
    echo "[INFO] fzf not found. Installing..."
    # We use apt for simplicity in the setup script
    sudo apt-get update && sudo apt-get install -y fzf

    # Optional: If you want the latest version with full shell bindings:
    # git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
    # ~/.fzf/install --all
else
    echo "[INFO] fzf is already installed."
fi






# --- 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



# --- dotool setup ---
if ! command -v dotool &> /dev/null; then
    echo "--> Installing dotool..."
    sudo apt-get install -y dotool || echo "WARNING: dotool not in apt repos. Install manually. See docs/LINUX_WAYLAND_dotool.md"
fi
sudo usermod -aG input $USER
echo 'KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"' \
  | sudo tee /etc/udev/rules.d/80-dotool.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
echo "NOTE: Re-login required for input group to take effect."
echo "See docs/LINUX_WAYLAND_dotool.md for details."

# --- Automatisches Setzen des Standard-Modells ---
echo "--> Configuring default model in config/model_name.txt..."
if [ "$CI" == "true" ]; then
    echo "vosk-model-small-en-us-0.15" > config/model_name.txt
elif [ "$SELECTED_LANG" == "de" ]; then
    echo "vosk-model-de-0.21" > config/model_name.txt
else
    echo "Please set a vosk-model in config/model_name.txt e.g. vosk-model-en-us-0.22 and check https://alphacephei.com/vosk/models"
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/uninstall_ahk_copyq.bat
REM uninstall_ahk_copyq.bat
REM script_name: setup/uninstall_ahk_copyq.bat
@echo off
ECHO Starting Uninstallation of Client Tools (AutoHotkey + CopyQ)...

REM --- ADMIN CHECK START ---
REM Uninstallations via Winget/MSI usually require Admin privileges.
FSUTIL dirty query %systemdrive% >nul
IF %ERRORLEVEL% NEQ 0 (
    ECHO Requesting Administrator privileges...
    powershell -Command "Start-Process cmd -ArgumentList '/c \"\"%~dpnx0\"\" %* ' -Verb RunAs"
    EXIT /B
)
ECHO [OK] Admin privileges confirmed.
REM --- ADMIN CHECK END ---


REM Ensure we are in the script directory to find the ps1 file
CD /D "%~dp0"

ECHO Running uninstallation script...
powershell.exe -ExecutionPolicy Bypass -File "%~dp0\uninstall_ahk_copyq.ps1"

ECHO.
ECHO Uninstallation process completed.
PAUSE
setup/uninstall_ahk_copyq.ps1
# uninstall_ahk_copyq.ps1
# script_name: setup/uninstall_ahk_copyq.ps1

Write-Host "Starting uninstallation of optional client tools..." -ForegroundColor Cyan

# Check if winget is available
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
    Write-Error "Winget command not found. Cannot proceed with automated uninstallation."
    exit 1
}

# --- STEP 1: Stop running processes ---
Write-Host "Stopping running instances of CopyQ and AutoHotkey..." -ForegroundColor Yellow
Stop-Process -Name "copyq" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "AutoHotkey" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "AutoHotkey64" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "AutoHotkey32" -Force -ErrorAction SilentlyContinue

# --- STEP 2: Uninstall CopyQ ---
Write-Host "Attempting to uninstall CopyQ..." -ForegroundColor Yellow
# We check if it is installed to avoid unnecessary error messages
$copyqCheck = winget list --id "hluk.CopyQ"
if ($copyqCheck) {
    winget uninstall --id "hluk.CopyQ" --silent --accept-source-agreements
    if ($LASTEXITCODE -eq 0) {
        Write-Host "CopyQ uninstalled successfully." -ForegroundColor Green
    } else {
        Write-Warning "CopyQ uninstallation finished with exit code $LASTEXITCODE."
    }
} else {
    Write-Host "CopyQ does not appear to be installed via Winget." -ForegroundColor DarkGray
}

# --- STEP 3: Uninstall AutoHotkey ---
Write-Host "Attempting to uninstall AutoHotkey..." -ForegroundColor Yellow
$ahkCheck = winget list --id "AutoHotkey.AutoHotkey"
if ($ahkCheck) {
    winget uninstall --id "AutoHotkey.AutoHotkey" --silent --accept-source-agreements
    if ($LASTEXITCODE -eq 0) {
        Write-Host "AutoHotkey uninstalled successfully." -ForegroundColor Green
    } else {
        Write-Warning "AutoHotkey uninstallation finished with exit code $LASTEXITCODE."
    }
} else {
    Write-Host "AutoHotkey does not appear to be installed via Winget." -ForegroundColor DarkGray
}

Write-Host "Uninstallation sequence finished." -ForegroundColor Cyan
setup/windows11_setup.bat
REM windows11_setup.bat
@echo off
ECHO start Windows 11 Setup-Script...

REM PowerShell, use Execution Policy this time
REM %* passes all arguments (e.g., -Exclude "en") given to the BAT file to the PowerShell script.
powershell.exe -ExecutionPolicy Bypass -File "%~dp0\windows11_setup.ps1" %*

REM NOTE: The Python script call should ideally be inside the PowerShell script,
REM as the virtual environment (.\.venv) is only guaranteed to be set up *after* the PS1 script finishes.
REM Assuming the PS1 script now handles all necessary setup and model downloading/filtering.

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

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

#  how to start:
#  .\setup\windows11_setup.ps1 -Exclude "en" or .\setup\windows11_setup.ps1 -Exclude "de" or .\setup\windows11_setup.ps1 -Exclude "all".

# --- Argument Parsing for Exclusion ---

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

python scripts/py/setup_config.py | iex

Write-Host "--> Auswahl: $SELECTED_LANG | $SECOND_LANG  | Ohne: $EXCLUDE_LANGUAGES"


if (-not [string]::IsNullOrEmpty($EXCLUDE_LANGUAGES)) {
    Write-Host "--> Exclusion list detected: $EXCLUDE_LANGUAGES" -ForegroundColor Yellow
} else {
    Write-Host "No exclusion list provided."
}
# --- End Argument Parsing ---


$ErrorActionPreference = "Stop"

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



# Only check for admin rights if NOT running in a CI environment (like GitHub Actions)
# $PSCommandPath is an automatic variable in PowerShell that contains the full path and filename
# of the script that is currently executing.
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


        Start-Process -FilePath "powershell.exe" -ArgumentList @(
            '-NoProfile',
            '-ExecutionPolicy', 'Bypass',
            '-File', "`"$PSCommandPath`""  # <--- Note the escaped quotes here
        ) -Verb RunAs



        # Exit the current (non-admin) script
        exit
    }
}
Write-Host "[SUCCESS] Running with Administrator privileges."






<#
Java-Balken always show (2-3 Sekunden) – thats normal

Wenn der Balken für 160 MB in 2-3 Sekunden durchläuft, lädt Windows die Datei nicht wirklich herunter.
Was passiert da? winget prüft die vorhandene Installation und vergleicht den "Hash" (Fingerabdruck) der Datei.
Es merkt: "Datei ist schon da und korrekt", und setzt den Fortschritt sofort auf 100%.
Fazit: please ignore it. The Das System works.
22.1.'26 16:07 Thu
#>


# 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."
    }

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



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

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

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


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


# --- 5. Python Requirements ---
Write-Host "--> Installing Python requirements into the virtual environment..."

#.\.venv\Scripts\pip.exe install --upgrade pip # falsch innerhalbe eines setupa
python -m pip install --upgrade pip

#.\.venv\Scripts\pip.exe install -r requirements.txt
& ".\.venv\Scripts\python.exe" -m pip 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

# setup/windows11_setup.ps1:183
# Execute the downloader and check its exit code
#  It downloads all required ZIPs to the project root.
.\.venv\Scripts\python.exe tools/download_all_packages.py --exclude "$EXCLUDE_LANGUAGES"

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

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

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








# setup/windows11_setup.ps1:207
# --- Configuration: Base List of Archives ---
$Prefix = "Z_"
$MasterConfig = @(
    @{ BaseName = "LanguageTool-6.6";              FinalName = "LanguageTool-6.6"; Dest = "." },
    @{ BaseName = "vosk-model-en-us-0.22";         FinalName = "vosk-model-en-us-0.22"; Dest = ".\models" },
    @{ BaseName = "vosk-model-small-en-us-0.15";   FinalName = "vosk-model-small-en-us-0.15"; Dest = ".\models" },
    @{ BaseName = "vosk-model-de-0.21";            FinalName = "vosk-model-de-0.21"; Dest = ".\models" },
    @{ BaseName = "lid.176";                       FinalName = "lid.176.bin"; Dest = ".\models" }
)

# --- Filter Configuration based on EXCLUDE_LANGUAGES (NEW) ---
$INSTALL_CONFIG = @()
$ExcludeList = @($EXCLUDE_LANGUAGES.Split(',') | ForEach-Object { $_.Trim().ToLower() })

if ($ExcludeList.Count -eq 0 -or $ExcludeList[0] -eq "") {
    # No exclusion, use master list
    $INSTALL_CONFIG = $MasterConfig
} else {
    # Exclusion active, filter the list
    foreach ($ConfigItem in $MasterConfig) {
        $IsMandatory = $ConfigItem.BaseName -like "LanguageTool-*" -or $ConfigItem.BaseName -eq "lid.176"
        $IsExcluded = $false

        # 1. Exclusion Check: exclude=all
        if ($ExcludeList -contains "all" -and -not $IsMandatory) {
            Write-Host "    -> Excluding (all): $($ConfigItem.BaseName)"
            $IsExcluded = $true
        }

        # 2. Exclusion Check: Specific Languages
        if (-not $IsExcluded) {
            # Check for 'de' exclusion
            if ($ExcludeList -contains "de" -and $ConfigItem.BaseName -like "*vosk-model-de-*") {
                Write-Host "    -> Excluding (de): $($ConfigItem.BaseName)"
                $IsExcluded = $true
            }
            # Check for 'en' exclusion (covers both en-us models)
            if ($ExcludeList -contains "en" -and $ConfigItem.BaseName -like "*vosk-model*en-us*") {
                Write-Host "    -> Excluding (en): $($ConfigItem.BaseName)"
                $IsExcluded = $true
            }
            # Add other specific language checks here if needed...
        }

        # Only add if not excluded
        if (-not $IsExcluded) {
            $INSTALL_CONFIG += $ConfigItem
        }
    }
}

# Now, create the ArchiveConfig from the filtered INSTALL_CONFIG
$ArchiveConfig = $INSTALL_CONFIG | ForEach-Object {
    [PSCustomObject]@{
        Zip  = "$($Prefix)$($_.BaseName).zip"
        Dir  = $_.BaseName
        Dest = $_.Dest
    }
}
# --- End Filter Configuration ---

# setup/windows11_setup.ps1:269
# Function to extract and clean up
function Expand-And-Cleanup {
    param (
        [string]$ZipFile,
        [string]$DestinationPath,
        [string]$ExpectedDirName,
        [bool]$CleanupAfterExtraction
    )

    # FIX 1: Use absolute path based on ProjectRoot (Prevents System32 errors)
    $AbsDest = Join-Path -Path $ProjectRoot -ChildPath $DestinationPath
    $FinalFullPath = Join-Path -Path $AbsDest -ChildPath $ExpectedDirName

    # FIX 2: Check for file/folder existence (Handle lid.176 vs lid.176.bin)
    $TargetExists = $false
    if (Test-Path $FinalFullPath) {
        $TargetExists = $true
    } elseif (Test-Path "$FinalFullPath.bin") {
        # This catches the case where config says "lid.176" but file is "lid.176.bin"
        $TargetExists = $true
    }

    if ($TargetExists) {
        Write-Host "    -> Target '$ExpectedDirName' already exists. Skipping extraction."
        return
    }

    # Look for the downloaded ZIP
    $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..."

    # Ensure destination directory exists
    if (-not (Test-Path $AbsDest)) {
        New-Item -ItemType Directory -Force -Path $AbsDest | Out-Null
    }

    Expand-Archive -Path $FinalZipPath -DestinationPath $AbsDest -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\tts_output"

@($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
}



# --- set standard-Modell ---
Write-Host "--> Configuring default model in config/model_name.txt..."
$modelFile = "config\model_name.txt"

if ($env:CI -eq "true") {
    "vosk-model-small-en-us-0.15" | Set-Content -Path $modelFile
} elseif ($SELECTED_LANG -eq "de") {
    "vosk-model-de-0.21" | Set-Content -Path $modelFile
} else {
    Write-Host "Please check config/model_name.txt if you prefer a larger model. and read https://alphacephei.com/vosk/models"
}


# --- 10. Completion ---
Write-Host ""
Write-Host "------------------------------------------------------------------" -ForegroundColor Green
Write-Host "------- setup/windows11_setup.ps1:413 ---" -ForegroundColor Green
Write-Host "Setup for Windows completed successfully." -ForegroundColor Green
Write-Host "------------------------------------------------------------------" -ForegroundColor Green


<#
Source - https://superuser.com/a/882297
Posted by chingNotCHing
Retrieved 2026-01-21, License - CC BY-SA 3.0
#>

Start-Process -FilePath "$PSScriptRoot\fix_permissions.bat" -Wait

#& .\SystemCheck.ps1
& "$PSScriptRoot\SystemCheck.ps1"
setup/windows11_setup_with_ahk_copyq_fzf_glogg.bat
REM setup/windows11_setup_with_ahk_copyq.bat
REM script_name: setup/windows11_setup_with_ahk_copyq.bat
@echo off
ECHO Starting Setup Variant: Core System + AutoHotkey + CopyQ...

REM --- ADMIN CHECK START ---
REM Force the script to run as Administrator.
REM If FSUTIL fails, we are not Admin -> Relaunch via PowerShell.
FSUTIL dirty query %systemdrive% >nul
IF %ERRORLEVEL% NEQ 0 (
    ECHO Requesting Administrator privileges...
    REM Restart this batch file with RunAs (Admin) and pass all original arguments
    powershell -Command "Start-Process cmd -ArgumentList '/c \"\"%~dpnx0\"\" %* ' -Verb RunAs"
    EXIT /B
)
ECHO Admin privileges confirmed.
REM --- ADMIN CHECK END ---

cd /d "%~dp0"

REM 1. Call the existing core setup script
REM Now running with Admin rights, so the PS1 won't try to crash-elevate itself.

@REM  -Exclude 'en'"
 
CALL "%~dp0windows11_setup.bat" -Exclude 'en'
CALL "%~dp0windows11_setup.bat" %*

REM Check if the core setup failed
IF %ERRORLEVEL% NEQ 0 (
    ECHO setup/windows11_setup_with_ahk_copyq.bat:25
    ECHO Core setup encountered errors. Skipping client tools installation.
@REM     PAUSE
@REM     EXIT /B %ERRORLEVEL%
)

ECHO Core setup completed. Moving to AHK and CopyQ installation...
REM 2. Run the specific client tools installation script
@REM setup/windows11_setup_with_ahk_copyq.bat:34
powershell.exe -ExecutionPolicy Bypass -File "%~dp0install_ahk_copyq.ps1"





call "%~dp0glogg_installer.bat"


REM --- FZF INSTALLATION START ---
ECHO Installing fzf (Fuzzy Finder)...
winget install -e --id junegunn.fzf --source winget --accept-package-agreements --accept-source-agreements
IF %ERRORLEVEL% NEQ 0 (
    ECHO fzf was already installed or an error occurred.
) ELSE (
    ECHO fzf installed successfully.
)
REM --- FZF INSTALLATION END ---




ECHO.
ECHO ========================================================
ECHO Installation finished.
ECHO start "Aura Dictation" (maybe ~ 30s)...
ECHO ========================================================

@REM CALL "%~dp0..\start_aura.bat"
START "" explorer "%~dp0..\start_aura.bat"

Config-Skripte

config/BERICHT_trigger_e2e_test.md
# Abschlussbericht: SL5 Aura – Trigger End-to-End Test

**Datum:** 2026-03-15  
**Datei:** `scripts/py/func/checks/test_trigger_end_to_end.py`

---

## 1. Der Plan

Ein echter End-to-End Test der das bekannte Problem untersucht:
**Bei manchen Aufnahmen fehlt das letzte Wort im Output.**

Der Test sollte:
1. Eine WAV-Datei als virtuelles Mikrofon einspeisen
2. Aura per `touch /tmp/sl5_record.trigger` starten — genau wie im echten Betrieb
3. Mit zweitem Trigger stoppen
4. Den Output mit dem YouTube-Transcript vergleichen
5. Feststellen ob ein Wort am Ende fehlt

---

## 2. Was erreicht wurde ✅

- Aura reagiert auf den Trigger korrekt
- LT läuft und ist erreichbar (`http://127.0.0.1:8082`)
- `_wait_for_output()` findet die `tts_output_*.txt` Datei
- `_fetch_yt_transcript_segment()` holt den Referenz-Text korrekt
- Der grundlegende Testaufbau ist solide und funktioniert konzeptionell

---

## 3. Das ungelöste Problem 🔴

### Kern-Problem: `manage_audio_routing` überschreibt alles

Beim Session-Start ruft Aura intern auf:
```python
manage_audio_routing(SYSTEM_DEFAULT)
```

Diese Funktion macht als erstes:
```python
subprocess.run(["pactl", "unload-module", "module-loopback"], capture_output=True)
subprocess.run(["pactl", "unload-module", "module-null-sink"], capture_output=True)
```

**Sie löscht jeden Sink den wir vorher erstellt haben.**

Danach erstellt sie keinen neuen Sink weil `mode == 'SYSTEM_DEFAULT'` (nicht `MIC_AND_DESKTOP`).

### Versuchte Lösungen

| Versuch | Problem |
|---|---|
| PulseAudio Virtual Source erstellen | PipeWire ignoriert `module-virtual-source` |
| `settings_local.py` auf `MIC_AND_DESKTOP` setzen | Datei wurde mit mehrfachen Einträgen korrumpiert |
| Markierten Override-Block ans Ende schreiben | Aura lädt Settings nicht schnell genug neu bevor Trigger kommt |
| `_create_mic_and_desktop_sink()` direkt im Test | Wird von `manage_audio_routing` beim Session-Start gelöscht |
| `pw-loopback` | Erscheint als Source aber Aura hört nicht darauf |

### Warum `settings_local.py` Override nicht funktioniert

`dynamic_settings.py` überwacht die Datei und lädt sie nach — aber mit einem Intervall. Der Trigger kommt zu schnell nach dem Schreiben. Aura startet die Session noch mit dem alten Wert `SYSTEM_DEFAULT`.

Außerdem: selbst wenn Aura `MIC_AND_DESKTOP` lädt, erstellt es den Sink erst beim **nächsten** Session-Start — nicht sofort.

---

## 4. Mögliche Lösungswege

### Option A — Längeres Warten nach Settings-Änderung
```python
_set_audio_input_device("MIC_AND_DESKTOP")
time.sleep(5.0)   # statt 1.5s — mehr Zeit für dynamic_settings reload
TRIGGER_FILE.touch()
```
Risiko: Nicht zuverlässig, timing-abhängig.

### Option B — Aura neu starten nach Settings-Änderung
```python
_set_audio_input_device("MIC_AND_DESKTOP")
subprocess.run(["./scripts/restart_venv_and_run-server.sh"])
time.sleep(60)   # warten bis LT bereit
TRIGGER_FILE.touch()
```
Nachteil: Test dauert über 1 Minute. Aber zuverlässig.

### Option C — `manage_audio_routing` direkt im Test aufrufen
```python
from scripts.py.func.manage_audio_routing import manage_audio_routing
manage_audio_routing("MIC_AND_DESKTOP", logger=null_logger)
```
Dann existiert der Sink bevor der Trigger kommt — und `manage_audio_routing` beim Session-Start erkennt `is_mic_and_desktop_sink_active() == True` und überspringt das Setup.

Das ist wahrscheinlich die **sauberste Lösung**.

### Option D — `process_text_in_background` direkt aufrufen (kein Trigger)
Wie in `test_youtube_audio_regression.py` — Vosk-Output direkt in die Pipeline übergeben, ohne den echten Trigger-Mechanismus. Dann testet man die Pipeline aber nicht das Abschneiden des letzten Wortes.

### Option E — Aura mit `run_mode_override=TEST` starten
Falls Aura einen Test-Modus hat der das Audio-Routing überspringt.

---

## 5. Empfehlung

**Option C** zuerst probieren — einen Import-Test machen:

```bash
python3 -c "from scripts.py.func.manage_audio_routing import manage_audio_routing; print('OK')"
```

Wenn das funktioniert:
```python
from scripts.py.func.manage_audio_routing import manage_audio_routing

manage_audio_routing("MIC_AND_DESKTOP", logger=null_logger)
time.sleep(0.5)
TRIGGER_FILE.touch()
```

Dann erkennt Aura beim Session-Start `is_mic_and_desktop_sink_active() == True` und lässt den Sink in Ruhe.

---

## 6. Was dieser Test langfristig bringt

Sobald er läuft, kann man:
- `SPEECH_PAUSE_TIMEOUT` Werte testen (1.0, 2.0, 4.0s) und sehen ob das letzte Wort abgeschnitten wird
- `transcribe_audio_with_feedback.py` Parameter optimieren
- Regressionen erkennen wenn sich das Audio-Handling ändert
- Beweisen dass ein Fix wirklich hilft

---

---

# Final Report: SL5 Aura – Trigger End-to-End Test

**Date:** 2026-03-15  
**File:** `scripts/py/func/checks/test_trigger_end_to_end.py`

---

## 1. The Plan

A real end-to-end test to investigate the known problem:
**In some recordings, the last word is cut off in the output.**

The test should:
1. Feed a WAV file as a virtual microphone
2. Start Aura via `touch /tmp/sl5_record.trigger` — exactly like real usage
3. Stop with a second trigger
4. Compare output with the YouTube transcript
5. Detect if a word is missing at the end

---

## 2. What was achieved ✅

- Aura responds to the trigger correctly
- LT is running and reachable (`http://127.0.0.1:8082`)
- `_wait_for_output()` finds the `tts_output_*.txt` file
- `_fetch_yt_transcript_segment()` fetches the reference text correctly
- The basic test structure is solid and works conceptually

---

## 3. The Unsolved Problem 🔴

### Core problem: `manage_audio_routing` overwrites everything

At session start, Aura internally calls:
```python
manage_audio_routing(SYSTEM_DEFAULT)
```

This function first does:
```python
subprocess.run(["pactl", "unload-module", "module-loopback"], capture_output=True)
subprocess.run(["pactl", "unload-module", "module-null-sink"], capture_output=True)
```

**It deletes any sink we created beforehand.**

Then it creates no new sink because `mode == 'SYSTEM_DEFAULT'` (not `MIC_AND_DESKTOP`).

### Attempted solutions

| Attempt | Problem |
|---|---|
| Create PulseAudio Virtual Source | PipeWire ignores `module-virtual-source` |
| Set `settings_local.py` to `MIC_AND_DESKTOP` | File was corrupted with multiple entries |
| Write marked override block to end of file | Aura doesn't reload settings fast enough before trigger fires |
| `_create_mic_and_desktop_sink()` directly in test | Deleted by `manage_audio_routing` at session start |
| `pw-loopback` | Appears as source but Aura doesn't listen to it |

---

## 4. Recommended Next Step

Call `manage_audio_routing` directly from the test before the trigger:

```python
from scripts.py.func.manage_audio_routing import manage_audio_routing

manage_audio_routing("MIC_AND_DESKTOP", logger=null_logger)
time.sleep(0.5)
TRIGGER_FILE.touch()
```

When Aura starts the session it checks `is_mic_and_desktop_sink_active()` — if `True`, it skips the setup and leaves the sink alone. This is the cleanest solution.

---

## 5. What this test will enable long-term

Once running:
- Test `SPEECH_PAUSE_TIMEOUT` values (1.0, 2.0, 4.0s) and detect word cutoff
- Optimize `transcribe_audio_with_feedback.py` parameters
- Catch regressions when audio handling changes
- Prove that a fix actually works
config/__init__.py

config/settings.py
# config/settings.py
# Central configuration for the application
# please see also: settings_local.py_Example.txt
from scripts.py.func.determine_current_user import determine_current_user
import os


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

SHOW_SEARCH_ON_STARTUP = True
# set False, for "silent"

current_user,_ = determine_current_user()
print(f'hi, hallo, welcome: {current_user}')
current_user = user_name = USER_NAME = str(current_user) # fallbacks
# logger.info("Current user successfully determined in a cross-platform manner.") # Add logger import if needed

# Set to True to disable certain production checks for local development,
# e.g., the wrapper script enforcement.
# DEV_MODE = 1  # ⚠️ Requires LOG_ONLY filter! See docs/dev_mode_setup.md
DEV_MODE = 0
DEV_MODE_all_processing = 0
DEV_MODE_show_window_title_stuff = 0
DEV_MODE_show_when_private_map_found = 0
DEV_MODE_memory = 0
DEV_MODE_zip_processing = 0

# --- Input Settings ---
# Default for X11 is 'xdotool'. Default for Wayland is 'dotool'.
# Set this to "dotool" if you have layout issues on X11.
# Comment it out to use the system default.
x11_input_method_OVERRIDE = "dotool" # intall examples: 1: pamac build dotool , yay -S dotool 2: sudo gpasswd -a $USER input 3: echo 'KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"' | sudo tee /etc/udev/rules.d/80-dotool.rules 4: sudo udevadm control --reload-rules && sudo udevadm trigger 5: reloggin

# x11_input_method_OVERRIDE = '' # fallback is xdotool. e.g. when you not set dotool

# test:
# dotoold &
# echo "type Hallo" | dotool

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

ENABLE_AUTO_LANGUAGE_DETECTION = False # Deprecated . Better set it to False

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


SIGNATURE_COOLDOWN = 50 # 600

# 🗣️🌐 (symbols and icons are probably cut out later by )
# sometimes e.g.in twitch: gelöscht: Nightbot: @seeh74 -> Sorry, you can't post links without permission!
#🗣ടㄴ⠄de╱Aura SL5.de/Aura
signatur=' #FreedomLadder #FreeSoftware #FSF #SL5Aura SL5.de/Aura'

# signatur='🗣SL5net ⟫ Aura'
# signatur='🗣[ SL5net Aura ]'
# signatur='🗣SL5net Aura'
# signatur='🗣ടㄴ5⠄de╱Aura'
# signatur='🗣Sㄴ5⠄de╱Aura' # this l is unvisable in gemini
#signatur='🗣SL5⠄de╱Aura'
# signatur='🗣SL5⠠de╱Aura' # i like this 11.11.'25 09:58 Tue
#now (original:'jetzt', ).
#signatur=',🗣SL5。de╱Aura' # i like this 11.11.'25 09:58 Tue
signatur=''
signatur='🗣SL5net ⟫ Aura'
signatur1=f'{signatur}' # (Powered by

LANGUAGE_PREFIXES = {
    "pt-br": "Tradução de Voz",
    "en": "Voice Translation",
    "ar": "تحدثت الترجمة",
    "ja": "話し言葉の翻訳",
    "de": "Sprachübersetzung",
    "DEFAULT": "" # Fallback
}




# Format: { "Regex-Pattern": ( "Signatur-Text", Cooldown_in_Sekunden ) }
SIGNATURE_MAPPING = {
    r"0 a\.d\.": ("SL5net >> Aura", 1),        # 14400 = 4 h autocivp/moddata/autocivP_IconNames_README.txt
    r"Matrix|Discord": ("🗣SL5net ⟫ Aura", 3600),   # 1 Stunde für Chat
    r"Outlook|Mail": ("-- Sent via Aura --", 86400), # 1 Tag für E-Mails
    r"|gmail": ("", 86400), # 1 Tag für E-Mails
    r"\.py|Konsole|kate|commander|Google AI Studio|google ai studio|google ai studio": ("", 999999),
    r".*": ("🗣[ SL5net Aura ]", 1800)             # 30 Min Fallback
}


# --- 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"]


if os.environ.get('CI') == 'true':
    # In CI alwais use the smaller english Modell
    PRELOAD_MODELS = ["vosk-model-small-en-us-0.15"]



# config/settings.py
PLUGIN_HELPER_TTS_ENABLED = True

PIPER_SERVER_PATH = "~/projects/py/TTS"
PIPER_SERVER_URL = "http://127.0.0.1:5002/speak"
# Timeout Server-anser (in Sec):
PIPER_SERVER_TIMEOUT = 2.0

USE_AS_PRIMARY_SPEAK = "piper"
USE_AS_PRIMARY_SPEAK = "ESPEAK"
USE_ESPEAK_FALLBACK = True
ESPEAK_FALLBACK_AMPLITUDE = 80

INPUT_METHOD="dotool"

#testtesttest


# --- 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://127.0.0.1:8081"

# config/settings.py
# Settings for our internal server (if used)


# --- 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.

# if true call iteratively all rules
default_mode_is_all = True
#  1 zwei drei vier 5
#

LT_SKIP_RATIO_THRESHOLD = 20
"""
Explanation of the Ratio Logic:
ratio = original_text_length / made_a_change: Calculates how many characters (on average) correspond to one change.

if ratio < lt_skip_ratio_threshold: If the ratio is low (less than the safe threshold), we skip LanguageTool.
"""

show_PLUGINS_ENABLED = True

PLUGINS_ENABLED = {
    "standard_actions/wikipedia_local": False,
    "standard_actions/count_loud": False,
    "game": False,
    "empty_all": False,
}
# count_loud▉

# 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


# Default is SYSTEM_DEFAULT, can be set to an integer (device index)
# or a string

# Select the audio input source for the STT engine:
# ------------------------------------------------
# None:
# Standard mode. Uses the system's default input device (e.g., your headset).
#
# 'UNIFIED_AUDIO_INPUT':
# Advanced Linux-only mode. Captures both the microphone AND the desktop
# audio (extern use of Aura, system, Radio, TV … sounds) simultaneously.
# It creates a virtual "Unified_Sink" and manages routing automatically
# via 'pactl' (PulseAudio/PipeWire).
# Note: 🐧 On Linux, this will temporarily bridge your audio sources.

PRE_RECORDING_TIMEOUT = 12
# SPEECH_PAUSE_TIMEOUT = 0.6


# AUDIO_INPUT_DEVICE = 'SYSTEM_DEFAULT'
# INITIAL_WAIT_TIMEOUT = initial_silence_timeout
# SPEECH_PAUSE_TIMEOUT = 2.0 # Standardwert
SPEECH_PAUSE_TIMEOUT = 1
# SPEECH_PAUSE_TIMEOUT = 999999
# may set if you want have permanent recording

# Standardwert 2

#

# test start:
# 1.2.'26 16:35 Sun cpu 6%,
# okeineneineneineneineneineneineneineneinen

AUDIO_INPUT_DEVICE = 'MIC_AND_DESKTOP'
# PRE_RECORDING_TIMEOUT = 12
# SPEECH_PAUSE_TIMEOUT = 5

# SPEECH_PAUSE_TIMEOUT
# Hebel, um Aura spürbar zu beschleunigen:
# Reaktionszeit (Der wichtigste Hebel): Reduziere den SPEECH_PAUSE_TIMEOUT in der settings_local.py (z. B. auf 0.5 oder 0.8). Aura wartet dann kürzer nach dem Ende deines Satzes, bevor sie loslegt. Das fühlt sich sofort "snappy" an.




SAMPLE_RATE = 16000

# System
CRITICAL_THRESHOLD_MB = 1024 * 2

LANGUAGETOOL_PORT = 8082

# 8081 is for eg. used for LG by Eloquent. before it was 8082

# LanguageTool Server Configuration
LANGUAGETOOL_BASE_URL = f"http://127.0.0.1:{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)"
# TODO implement for windows


# Auto-detected Java path
JAVA_EXECUTABLE_PATH = r"/usr/bin/java"
config/settings_local.py_Example.txt
# config/settings_local.py
# ============================================================
# Personal settings — overrides config/settings.py
# This file is ignored by Git (your changes are safe).
#
# GETTING STARTED:
#   1. Copy this file to config/settings_local.py
#   2. Uncomment and adjust the settings you want to change.
#   All other settings will use the defaults from config/settings.py
# ============================================================

import os
import pwd

# --- User Detection (do not remove — used by integrity checks) ---
current_user = pwd.getpwuid(os.getuid())[0]

# --- Service ---
# 0 = Start normally
SERVICE_START_OPTION = 0

SHOW_SEARCH_ON_STARTUP = True
# set False, for "silent"


# Kill competing LanguageTool or Eloquent instances on startup
KILL_COMPETING_LT_AND_ELOQUENT_ON_START = 0

# Enable or disable the type watcher (client-side typing integration)
TYPE_WATCHER_ENABLED = True

# --- Language Model ---
# Which Vosk model(s) should be preloaded at startup?
# Available models are located in the models/ folder.
# Preloading uses more RAM but reduces response time on first use.
PRELOAD_MODELS = ["vosk-model-de-0.21"]
# PRELOAD_MODELS = ["vosk-model-en-us-0.22"]
# PRELOAD_MODELS = ["vosk-model-de-0.21", "vosk-model-en-us-0.22"]

# --- Recording Timing ---
# How long Aura waits after the hotkey before starting to record (seconds)
PRE_RECORDING_TIMEOUT = 6

# How long Aura waits after a speech pause before processing (seconds)
# Lower = faster response, but more sensitive to mid-sentence pauses.
# Recommended: 1.0 – 2.0
SPEECH_PAUSE_TIMEOUT = 1.5

# --- Audio Input ---
# 'SYSTEM_DEFAULT' uses your system's default microphone.
# 'MIC_AND_DESKTOP' captures microphone + desktop audio (Linux only).
AUDIO_INPUT_DEVICE = 'SYSTEM_DEFAULT'

# --- Notifications ---
# 0 = Silent, 1 = Essential messages only, 2 = Verbose
NOTIFICATION_LEVEL = 1

# --- Sound Feedback ---
# 1 = enabled (recommended: audio cues for start/stop recording)
soundMute = 1
soundUnMute = 1
soundProgramLoaded = 1

# --- Text Output ---
# Character appended to the end of each dictated sentence.
# Set to "" to disable.
ADD_TO_SENCTENCE = "."

# --- Auto-Enter After Dictation ---
# Regex pattern of window titles where Aura automatically presses Enter
# after dictation. Use "(ExampleAplicationThatNotExist)" to disable.
AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "(ExampleAplicationThatNotExist)"

# --- Signature ---
# Text appended to dictated output in certain applications (e.g. chat, email).
# Leave empty to disable.
signatur = ""
signatur1 = f"{signatur}"
signatur_en = f""
signatur_ar = f""
signatur_ja = f""
signatur_pt_br = f""

# --- Memory ---
# Aura will stop loading models if free RAM drops below this threshold (MB).
CRITICAL_THRESHOLD_MB = 1024 * 2

# --- Text-to-Speech (optional) ---
# Path to a local Piper TTS server for voice output.
PIPER_SERVER_PATH = "~/path/to/piper"
PLUGIN_HELPER_TTS_ENABLED = False
USE_ESPEAK_FALLBACK = True
ESPEAK_FALLBACK_AMPLITUDE = 80

# --- LanguageTool Corrections ---
# Override which correction categories are enabled.
# Import defaults first, then update selectively.
try:
    from .settings import CORRECTIONS_ENABLED
except ImportError:
    CORRECTIONS_ENABLED = {}

# Example: enable git-specific corrections, disable style advice
# CORRECTIONS_ENABLED.update({
#     "git": True,
# })

# --- Plugins ---
# Show active plugins in log output
show_PLUGINS_ENABLED = False

# Enable or disable specific plugins.
# Keys not listed here use their defaults from config/settings.py
PLUGINS_ENABLED = {
    "empty_all": False,
    "git": False,
    "numbers_to_digits": False,      # e.g. "hundred" -> 100
    "digits_to_numbers": False,
    "web-radio-funk": False,
    "wannweil": False,
    "CCC_tue": False,
    "vsp_rt": False,
    "ki-maker-space": False,
    "volkshochschule_tue": False,
    "ethiktagung": False,
    "it-begriffe": False,
    "it-begriffe/php/codeigniter": False,
    "it-begriffe.php.codeigniter": False,
    "standard_actions/wikipedia_local/": False,
    "game/": False,
    "game/0ad": False,
    "game/dealers_choice": False,
}

# --- Developer Mode (advanced users only) ---
# Enables additional log output and internal checks.
# See docs/dev_mode_setup.md for setup instructions.
DEV_MODE = False
DEV_MODE_all_processing = False
DEV_MODE_memory = False
DEV_MODE_zip_processing = False
DEV_MODE_show_window_title_stuff = False
DEV_MODE_show_when_private_map_found = False
config/settings_local.py_Example_user_seeh-2026-0322-1820.txt
# config.settings_local.py
TYPE_WATCHER_ENABLED = True

import os
import pwd

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

KILL_COMPETING_LT_AND_ELOQUENT_ON_START= 1

PIPER_SERVER_PATH = "~/projects/py/TTS"


SERVICE_START_OPTION = 0

NOTIFICATION_LEVEL = 1

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

# Get username
current_user = pwd.getpwuid(os.getuid())[0]
if current_user == 'seeh':
    DEV_MODE = True

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

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

PLUGIN_HELPER_TTS_ENABLED = True

signatur=''
signatur1=f''
signatur_pt_br=f''
signatur_en=f''
signatur_ar=f''
signatur_ja=f''


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

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


CORRECTIONS_ENABLED.update({
})


PRE_RECORDING_TIMEOUT = 12
SPEECH_PAUSE_TIMEOUT = 1

# for more poweruser mor in hurry:

AUDIO_INPUT_DEVICE = 'SYSTEM_DEFAULT'


# needs NO restart:
PRE_RECORDING_TIMEOUT = 6
SPEECH_PAUSE_TIMEOUT = 2

#test (original:'test', 🗣SL5。de╱Aura).
if current_user == 'sl5':

    # needs NO restart:
    PRE_RECORDING_TIMEOUT = 4
    SPEECH_PAUSE_TIMEOUT = 1



# examples:
# AUTO_ENTER_AFTER_DICTATION_REGEX_APPS = "."

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

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

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

USE_ESPEAK_FALLBACK = True
ESPEAK_FALLBACK_AMPLITUDE = 80

show_PLUGINS_ENABLED = True

# --- Custom Client-Side Plugins ---
# Enable or disable specific client-side behaviors (plugins).
# The logic is handled by client scripts (e.g., type_watcher.sh, AutoKey).
# These settings tell the backend service what to expect or how to format output.
####     "github_corrections": True,         # Example from before ???
PLUGINS_ENABLED = {
    "empty_all": False,
    "git": True,
    "wannweil": True,

    "game/dealers_choice": False,
    "game/0ad": False,
    "game/": False,
    "standard_actions/wikipedia_local/": False,
    "ethiktagung": False,
    "it-begriffe.php.codeigniter": False,

    "volkshochschule_tue": True,
    "CCC_tue": True,
    "vsp_rt": True,
    "ki-maker-space": True,
    "numbers_to_digits": True, # hundert|einhundert --> 100
    "digits_to_numbers": False,
    "web-radio-funk": True,
    "it-begriffe": False,
    "it-begriffe/php/codeigniter": True,
}
config/unmatched_list.txt
Blumen fressen|aber einschalten|aura einschmelzen|aura einschwenken|aura entfalten|cobra einschalten horror eintreffen hurra einschalten aura einschalten|einen|einschalten|eure einschalten|horror in schweden|hurra ein schmied|hurra einschalten|hurra einschl|hurra einschränken|hurra entscheiden|hurra ritschel|ich habe es jetzt korrigiert es gab zwei fehler|irgendwas|jetzt funktionen|kaputt|oh ein einschalten|rohre eins 12|rohre einschalten|uva einschalten|zora einschalten

Githooks-Skripte

githooks/pre-commit_BACKUP
#!/bin/bash
# .git/hooks/pre-commit

# -----------------------------------------------------------------------------
# 1. QUALITY CHECK (Flake8)
# -----------------------------------------------------------------------------

echo "Running ruff auto-fix..."
if [ -f .venv/bin/ruff ]; then
  .venv/bin/ruff check --fix ./aura_engine.py ./scripts ./config 2>/dev/null || true
fi

# In pre-commit, nach ruff:
echo "Running pylint (warnings only)..."

# In pre-commit, statt ./aura_engine.py ./scripts ./config
# only staged files prüfen:
staged=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$' | tr '\n' ' ')
if [ -n "$staged" ]; then
  .venv/bin/pylint $staged \
    --disable=all,W0718 \
    --enable=W0702,W0703,W0611,W0612 \
    --exit-zero  # blockiert NIE den commit, nur Ausgabe
fi


echo "Running flake8 code quality check..."

# Activate virtual environment if it exists
if [ -f .venv/bin/activate ]; then
  source .venv/bin/activate
fi

# Run flake8 on specific directories
# flake8 ./aura_engine.py ./scripts ./config
./.venv/bin/flake8 ./aura_engine.py ./scripts ./config

# Abort commit if flake8 finds errors
if [ $? -ne 0 ]; then
  echo "COMMIT REJECTED: flake8 found issues. May use noqa: "
  echo "# noqa: "
  exit 1
fi

echo "flake8 check passed."




# convert staged .bat files to CRLF and restage them

# find staged .bat files (cached)
staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.bat$' || true)
if [ -z "$staged_files" ]; then
  echo "No .bat files staged. Skipping CRLF conversion."
fi

# convert staged .bat files to CRLF and restage them
staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.bat$' || true)
if [ -n "$staged_files" ]; then
  for f in $staged_files; do
    if [ -f "$f" ]; then
      unix2dos "$f" >/dev/null 2>&1 || { echo "unix2dos failed on $f"; exit 1; }
      git add "$f"
    fi
  done
else
  echo "No .bat files staged. Skipping CRLF conversion."
fi



















# -----------------------------------------------------------------------------
# 2. SETUP FOR REQUIREMENTS GENERATION
# -----------------------------------------------------------------------------

# Check if pipreqs is available in the venv
if [ ! -x ".venv/bin/pipreqs" ]; then
    echo "pipreqs is not installed in .venv. Please run '.venv/bin/pip install pipreqs' inside your virtual environment."
    exit 1
fi

TMPDIR=".pipreqs_temp"

# Clean up and create a fresh temp directory
rm -rf "$TMPDIR"
mkdir "$TMPDIR"

# Copy all relevant Python sources into the temp directory
cp aura_engine.py "$TMPDIR/"
# cp get_suggestions.py "$TMPDIR/"
# Copy the entire 'scripts' directory to preserve import paths
cp -r scripts "$TMPDIR/"
cp -r tools "$TMPDIR/"


echo "INFO: Generating requirements.txt (taking into account .pipreqs-whitelist.txt) ..."

# -----------------------------------------------------------------------------
# 3. GENERATE BASE REQUIREMENTS
# -----------------------------------------------------------------------------
# We use --savepath to tell pipreqs to save the file in the current
# directory while analyzing the temp directory.
WARNINGS=$(.venv/bin/pipreqs "$TMPDIR" --savepath ./requirements.txt --force --encoding=utf-8 2>&1 >/dev/null)

# Filter warnings. Only show those NOT in the whitelist.
if [ -s ".pipreqs-whitelist.txt" ]; then
    echo "$WARNINGS" | grep -v -f .pipreqs-whitelist.txt | grep -v "Please, verify manually"
else
    echo "$WARNINGS"
fi

# -----------------------------------------------------------------------------
# 4. PATCHING & CLEANUP (The "Patching Strategy")
# -----------------------------------------------------------------------------

# A) General Cleanup
# Remove known false positives like our own 'scripts' directory
sed -i '/^scripts==/d' requirements.txt

# Convert package names to lowercase to avoid duplicates like "Psutil" vs "psutil"
# alt
# awk -F'==' '/==/ {print tolower($1) "==" $2}' requirements.txt > requirements.txt.tmp && mv requirements.txt.tmp requirements.txt


# neu 2026-0331-2141
awk '/==/ {
    idx = index($0, "==")
    pkg = substr($0, 1, idx-1)
    rest = substr($0, idx+2)
    print tolower(pkg) "==" rest
}
!/==/ {print}' requirements.txt > requirements.txt.tmp && mv requirements.txt.tmp requirements.txt












# B) RELAXATION FIX (Fix for psutil, requests, etc.)
# We remove the strict entries generated by pipreqs and add relaxed versions (>=)
# to prevent conflicts (e.g. 7.1.3 vs 7.0.0).
sed -i '/^psutil/d' requirements.txt
sed -i '/^requests/d' requirements.txt
sed -i '/^tqdm/d' requirements.txt
sed -i '/^colorama/d' requirements.txt

# Re-add them with loose versioning
echo "psutil>=7.0.0" >> requirements.txt
echo "requests>=2.28.0" >> requirements.txt
echo "tqdm>=4.60.0" >> requirements.txt
echo "colorama>=0.4.0" >> requirements.txt
echo "pygame" >> requirements.txt

# C) PLATFORM-SPECIFIC FIXES

# Fix Fasttext: Remove generic, add platform specific
echo "INFO: Applying Fasttext platform-specific fix..."
sed -i '/^fasttext==/d' requirements.txt
echo "fasttext==0.9.3; sys_platform != 'win32'" >> requirements.txt
echo "fasttext-wheel==0.9.2; sys_platform == 'win32'" >> requirements.txt

# Fix Windows Audio & Control (remove generic, add win32 only)
sed -i '/^comtypes/d' requirements.txt
sed -i '/^pycaw/d' requirements.txt
echo "comtypes==1.4.11; sys_platform == 'win32'" >> requirements.txt
echo "pycaw==20240210; sys_platform == 'win32'" >> requirements.txt

# D) EXPLICIT EXTRAS
# Add packages that might be missed by static analysis
echo "wikipedia-api" >> requirements.txt
echo "bs4" >> requirements.txt

echo "pyperclip" >> requirements.txt
echo "cologne_phonetics" >> requirements.txt
echo "jellyfish" >> requirements.txt

echo "nltk==3.9.1" >> requirements.txt


# E) DEDUPLICATION & SORT
# Remove duplicate lines, keep sys_platform lines at the bottom
grep -v "sys_platform" requirements.txt | sort -u > requirements.txt.tmp
grep "sys_platform" requirements.txt | sort -u >> requirements.txt.tmp
mv requirements.txt.tmp requirements.txt

echo "-r requirements.txt" > requirements-dev.txt
echo "flake8" >> requirements-dev.txt
echo "ruff" >> requirements-dev.txt
echo "pylint" >> requirements-dev.txt
echo "pipreqs" >> requirements-dev.txt


# -----------------------------------------------------------------------------
# 5. CLEANUP
# -----------------------------------------------------------------------------
rm -rf "$TMPDIR"

# Backup the hook for reference
if [ "$CI" != "true" ]; then
  cp .git/hooks/pre-commit githooks/pre-commit_BACKUP
fi

Update-Skripte

update/update_for_linux_users.sh
#!/bin/bash

# file: update/update_for_linux_users.sh

# Description: Downloads the latest version and updates the application
#              while preserving user settings. For non-developer use.

# Stoppt das Script sofort bei einem Fehler
set -e

# Farbdefinitionen für die Konsole
COLOR_CYAN='\e[36m'
COLOR_GREEN='\e[32m'
COLOR_RED='\e[31m'
COLOR_YELLOW='\e[33m'
COLOR_RESET='\e[0m'

# Variablen
repoUrl="https://github.com/sl5net/SL5-aura-service/archive/refs/heads/master.zip"

# Den Installationspfad ermitteln (zwei Ebenen über dem Script-Pfad)
# Annahme: Script liegt in 'installDir/update/'
scriptDir="$(dirname "$0")"
installDir="$(dirname "$scriptDir")"
tempDir="/tmp/sl5_update_temp"
localSettingsPath="$installDir/config/settings_local.py"
backupPath="$tempDir/settings_local.py.bak"
zipPath="$tempDir/latest.zip"

# Funktion zur Fehlerbehandlung
cleanup_on_error() {
    echo -e "${COLOR_RED}FATAL: Ein Fehler ist während des Updates aufgetreten.${COLOR_RESET}" >&2
    if [ -d "$tempDir" ]; then
        echo -e "${COLOR_YELLOW}INFO: Lösche temporären Ordner...${COLOR_RESET}"
        rm -rf "$tempDir"
    fi
    read -p "Drücken Sie die Eingabetaste, um das Script zu beenden."
    exit 1
}

# Fange Fehler ab (trap)
trap cleanup_on_error ERR

echo -e "${COLOR_CYAN}--- SL5 Aura Updater ---${COLOR_RESET}"
echo "Dies lädt die neueste Version herunter und ersetzt alle Anwendungsdateien."
echo "Ihre persönlichen Einstellungen in 'config/settings_local.py' werden gespeichert."
if [ "$CI" != "true" ]; then
    echo "Bitte schließen Sie die Hauptanwendung, falls sie läuft."
    read -p "Drücken Sie die Eingabetaste, um fortzufahren, oder CTRL+C, um abzubrechen"
fi

# 1. Alte temporäre Dateien bereinigen und neuen Ordner erstellen
if [ -d "$tempDir" ]; then
    echo "INFO: Entferne alten temporären Update-Ordner..."
    rm -rf "$tempDir"
fi
mkdir -p "$tempDir"

# 2. Lokale Einstellungen sichern, falls vorhanden
if [ -f "$localSettingsPath" ]; then
    echo -e "${COLOR_GREEN}INFO: Sichern Ihrer lokalen Einstellungen...${COLOR_RESET}"
    cp "$localSettingsPath" "$backupPath"
fi

# 3. Neueste Version herunterladen (curl oder wget erforderlich)
echo "INFO: Lade neueste Version von GitHub herunter..."
curl -L -o "$zipPath" "$repoUrl"

# 4. Archiv entpacken (unzip erforderlich)
echo "INFO: Entpacke Update..."
unzip -q "$zipPath" -d "$tempDir"

# Den extrahierten Ordner finden (der normalerweise auf '-master' endet)
extractedFolder=$(find "$tempDir" -maxdepth 1 -type d -name "*-master" | head -n 1)
if [ -z "$extractedFolder" ]; then
    echo -e "${COLOR_RED}FATAL: Konnte den extrahierten '*-master' Ordner nicht finden.${COLOR_RESET}"
    exit 1
fi

# 5. Lokale Einstellungen in die neue Version wiederherstellen
if [ -f "$backupPath" ]; then
    echo -e "${COLOR_GREEN}INFO: Stelle Ihre lokalen Einstellungen in die neue Version wieder her...${COLOR_RESET}"
    cp "$backupPath" "$extractedFolder/config/"
fi

# 6. Dateien ersetzen (rsync erforderlich)
# Mit rsync die Dateien aus dem temporären Ordner in den Installationsordner kopieren und die alten Dateien löschen.
echo "INFO: Finalisiere Update und ersetze Dateien. Bitte warten..."
# Die '-a' Option bewahrt Berechtigungen, Zeiten etc.
# Die '--delete' Option entfernt Dateien, die im Quellverzeichnis nicht vorhanden sind.
# Der abschließende Schrägstrich bei "$extractedFolder/" ist wichtig, um den *Inhalt* zu kopieren.
# rsync -a --delete --force "$extractedFolder/" "$installDir"
#    --exclude='__pycache__' \
#    --exclude='*.pyc' \

rsync -a --delete --force \
    --max-size=100M \
    --exclude='.venv' \
    --exclude='models' \
    --exclude='LanguageTool*' \
    --exclude='.git' \
    "$extractedFolder/" "$installDir"


# 7. Aufräumen
echo "INFO: Bereinige temporäre Dateien..."
rm -rf "$tempDir"

source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txt

echo ""
echo -e "${COLOR_GREEN}Update abgeschlossen! Sie können die Anwendung jetzt neu starten.${COLOR_RESET}"

# Keine Fehlerbehandlung notwendig, wenn das Script bis hierher kommt
trap - ERR
read -p "Drücken Sie die Eingabetaste, um das Script zu beenden."
exit 0
update/update_for_windows_users.ps1
# file: update/update_for_windows_users.ps1                                       
# Description: Downloads the latest version and updates the application
#              while preserving user settings. For non-developer use.

$ErrorActionPreference = 'Stop'
$repoUrl = "https://github.com/sl5net/SL5-aura-service/archive/refs/heads/master.zip"

$installDir = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$tempDir = Join-Path $env:TEMP "sl5_update_temp"

Write-Host "--- SL5 Aura Updater ---" -ForegroundColor Cyan
Write-Host "This will download the latest version and replace all application files."
Write-Host "This will download the latest version and replace all application files."
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."
Write-Host "Your personal settings in 'config\settings_local.py' will be saved."
Write-Host "Your personal settings in 'config\settings_local.py' will be saved."
Write-Host "Your personal settings in 'config\settings_local.py' will be saved."
Write-Host "Your personal settings in 'config\settings_local.py' will be saved."
Write-Host "Your personal settings in 'config\settings_local.py' will be saved."

if (-not ($env:CI -eq 'true'))
{
    Write-Host "this will take some time ..."
#    Write-Host "Please close the main application if it is running."
#    Read-Host -Prompt "Press Enter to continue or CTRL+C to cancel"
}
try {
    # 1. Clean up previous temporary files if they exist
    if (Test-Path $tempDir) {
        Write-Host "INFO: Removing old temporary update folder..."
        Remove-Item -Path $tempDir -Recurse -Force
    }
    New-Item -Path $tempDir -ItemType Directory | Out-Null

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

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

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

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

    # 5. Restore local settings into the new version



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

    }
    # 6. Create a final batch script to perform the file replacement and update dependencies
#    $installerName = "setup\windows11_setup.bat"
    $installerName = "setup\windows11_setup_with_ahk_copyq.bat"

    $batchScript = @'
@echo off
echo Finalizing update, please wait...
timeout /t 3 /nobreak > nul

:: 1. Dateien verschieben
robocopy "{0}" "{1}" /E /MOVE /NFL /NDL /NJH /NJS > nul

:: 2. In Zielordner wechseln
cd /d "{1}"

:: 3. Abhängigkeiten aktualisieren
echo.
echo ---------------------------------------------------
echo Updating dependencies (pip install)...
echo ---------------------------------------------------
:: 3. Installer aufrufen (Dort passiert das pip upgrade und requirements install)
if exist "{2}" (
    echo Starting installer script...
    call "{2}"
) else (

    color 4f
    echo.
    echo ===================================================
    echo FATAL ERROR: SETUP SCRIPT NOT FOUND!
    echo Expected: {2} in %CD%
    echo ===================================================

    echo WARNING: "{2}" not found. Trying manual pip upgrade...
    :: Fallback, falls keine install.bat da ist:
    if exist ".venv\Scripts\activate.bat" (
        call .venv\Scripts\activate.bat
        python.exe -m pip install --upgrade pip
        pip install -r requirements.txt
        color 07
    ) else (
        echo The update cannot proceed. Please verify the filename.
        pause
        exit /b 1
    )
)

echo.
echo Update complete! You can now restart the application.
timeout /t 10 > nul
del "%~f0"
'@ -f $extractedFolder.FullName, $installDir, $installerName

    $batchPath = Join-Path $installDir "_finalize_update.bat"
    Set-Content -Path $batchPath -Value $batchScript
    # update/update_for_windows_users.ps1:110
    # 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="aura_engine"
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


# Kill any existing watcher instances to prevent ghost processes and lockfile conflicts
pkill -f "type_watcher.sh" || true
pkill -f "type_watcher_keep_alive.sh" || true

# guarantee that no stale
# processes from old paths or failed lockfile cleanups interfere with the new # instance. It is the most reliable way to handle path renames without leaving background processes behind.


if [ "$detected_os" = "windows" ]; then
  echo "please start type_watcher.ahk"
  echo "please start trigger-hotkeys.ahk"
else
  # Dynami User-ID D-Bus
  USER_ID=$(id -u)

  # ony when DBUS_SESSION_BUS_ADDRESS
  if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then
      export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus"
  fi

  # DISPLAY XAUTHORITY
  export DISPLAY="${DISPLAY:-:0}"
  export XAUTHORITY="${XAUTHORITY:-$HOME/.Xauthority}"

  $PROJECT_ROOT/scripts/sh/type_watcher_keep_alive.sh &
fi



set -e


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

# export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/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

echo "Activating virtual environment at '$PROJECT_ROOT/venv'..."


if [ ! -f "$PROJECT_ROOT/.venv/bin/python3" ]; then
    echo "Creating virtual environment..."
    python3 -m venv "$PROJECT_ROOT/.venv"
fi


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..."

#    RSS (Resident Set Size): Das ist der tatsächliche RAM-Verbrauch in htop oder top. Mit mimalloc sollte dieser Wert nach einem anfänglichen Anstieg sehr stabil bleiben und bei Inaktivität des Services sogar leicht sinken (weil mimalloc Speicher aggressiver ans OS zurückgibt als die Standard-glibc).

# Installation: sudo pacman -S mimalloc

# export PYTHONDONTWRITEBYTECODE=1

# Python environment settings
export PYTHONUNBUFFERED=1
export PYTHONDONTWRITEBYTECODE=1

# --- Memory Allocator Logic (mimalloc) ---
MIMALLOC_FOUND=false

# Check if we are on Windows (Git Bash / MSYS)
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
    # On Windows, LD_PRELOAD does not work like on Linux.
    # To use mimalloc on Windows, Python usually needs to be linked during compilation.
    echo "Note: Memory allocator override (mimalloc) is skipped on Windows."
else
    # Potential paths for mimalloc on various Linux distributions
    # 1. Arch Linux: /usr/lib/libmimalloc.so
    # 2. Ubuntu/Debian: /usr/lib/x86_64-linux-gnu/libmimalloc.so.2 (or .so)
    # 3. Manual installs: /usr/local/lib/libmimalloc.so
    POSSIBLE_MIMALLOC_PATHS=(
        "/usr/lib/libmimalloc.so"
        "/usr/lib/x86_64-linux-gnu/libmimalloc.so"
        "/usr/lib/x86_64-linux-gnu/libmimalloc.so.2"
        "/usr/local/lib/libmimalloc.so"
    )

    for path in "${POSSIBLE_MIMALLOC_PATHS[@]}"; do
        if [ -f "$path" ]; then
            export LD_PRELOAD="$path"
            MIMALLOC_FOUND=true
            echo "Info: Using mimalloc for improved memory management ($path)."
            break
        fi
    done

    if [ "$MIMALLOC_FOUND" = false ]; then
        echo "Warning: mimalloc library not found. Falling back to default allocator."
        echo "Hint: Install it with 'sudo apt install libmimalloc-dev' (Ubuntu) or 'sudo pacman -S mimalloc' (Arch)."
    fi
fi

echo "LD_PRELOAD=$LD_PRELOAD"  # ← zur Bestätigung

# --- Start the Service ---
echo "Starting $SCRIPT_TO_START..."
python3 "$SCRIPT_TO_START" &
scripts/notification_watcher.ahk
#Requires AutoHotkey v2.0

#SingleInstance Force ; is buggy, using Heartbeat mechanism instead

/**
 * @file notification_watcher.ahk
 * @description Displays non-blocking system notifications and feedback
 * from the STT background service.
 * https://www.autohotkey.com/docs/v2/Language.htm#comments
 */


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 aura_engine.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="aura_engine.py"

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

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



echo "Requesting restart for all services..."

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

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

    echo SCRIPT_DIR=$SCRIPT_DIR
    echo PROJECT_ROOT=$PROJECT_ROOT

    echo "Activating virtual environment at '$PROJECT_ROOT/venv'..."
    cd $PROJECT_ROOT

    # Sometimes it can help for performance (original:'manchmal kann es helfen aus performance Gründen').
    # to loose all cash files (original:'wenn alle cash files entgernt werden').
    find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf

    python3 -m venv .venv
    source .venv/bin/activate
    end_aura_enginePY="$PROJECT_ROOT/scripts/py/end_aura_engine.py"
    echo end_aura_enginePY=$end_aura_enginePY
    python3 "$end_aura_enginePY" &



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

sleep 1

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

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

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

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

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

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

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

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

LOG_FILE="$PROJECT_ROOT/type_watcher.log"

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

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

Root-Verzeichnis Skripte

Folgende Skripte sind hier dokumentiert:

  • start_aura.bat

  • update.bat

  • install_hooks.sh

  • type_watcher.ahk

  • scripts/sh/type_watcher_keep_alive.sh

start_aura.bat
@echo off
setlocal
title SL5 Aura - One-Click Starter

:: cmd /k "echo on & call start_aura.bat"


echo DBG1: before cd
cd /d "%~dp0"



:: start this script like: & .\start_aura.bat
:: Version: v2.1
;;

:: --- 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...
        REM update test: 2026-0122-1421
        REM  powershell -Command "Start-Process '%~f0' -Verb RunAs"

        REM powershell -Command "Start-Process cmd -ArgumentList '/c, %~f0' -Verb RunAs"

        REM Start-Process -FilePath "powershell.exe" -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File', $PSCommandPath) -Verb RunAs

        REM Start-Process -FilePath "powershell.exe" -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File', $PSCommandPath -Verb RunAs
        REM powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0\start_aura.ps1"
        powershell -Command "Start-Process cmd -ArgumentList '/c, """%~f0"""' -Verb RunAs"


        exit /b
    )
)
















echo [SUCCESS] Running with Administrator privileges.

echo DBG8: about to check venv
if not exist ".\.venv\Scripts\python.exe" (
  echo DBG9: venv missing
  pause
  powershell.exe -ExecutionPolicy Bypass -File ".\setup\windows11_setup.ps1" -Exclude "en"
  if not exist ".\.venv\Scripts\python.exe" (
    echo DBG10: venv still missing
    pause
    exit /b
  )
  echo DBG11: venv created
)
echo DBG12: after venv block

echo DBG13: stop processes via powershell 1

powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and ($_.CommandLine -like '*type_watcher.ahk*') } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"

echo DBG14: after ps1

powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and ($_.CommandLine -like '*notification_watcher.ahk*') } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"

echo DBG15: after ps2

powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and ($_.CommandLine -like '*trigger-hotkeys.ahk*') } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"

echo DBG16: after ps3

set "AHK_EXE="
if exist "%ProgramFiles%\AutoHotkey\v2\AutoHotkey64.exe" set "AHK_EXE=%ProgramFiles%\AutoHotkey\v2\AutoHotkey64.exe"
if exist "%ProgramFiles%\AutoHotkey\AutoHotkey.exe" if not defined AHK_EXE set "AHK_EXE=%ProgramFiles%\AutoHotkey\AutoHotkey.exe"
if not defined AHK_EXE (
    echo [WARNING] AutoHotkey executable not found in %ProgramFiles%. Trying file association instead.
)


echo DBG17: about to start AHK processes
start "" "%AHK_EXE%" "%~dp0scripts\notification_watcher.ahk" >nul 2>&1
echo DBG18: started notification_watcher
start "" "%AHK_EXE%" "%~dp0trigger-hotkeys.ahk" >nul 2>&1
echo DBG19: started trigger-hotkeys
start "" "%AHK_EXE%" "%~dp0type_watcher.ahk" >nul 2>&1
echo DBG20: started type_watcher

echo DBG21: checking errorlevel=%errorlevel%
if "%errorlevel%"=="0" (
  echo [INFO] Admin-Hotkeys are already running.
) else (
  echo [INFO] Starting Hotkeys in User-Mode...
  timeout /t 2 /nobreak >nul
  start "" "%~dp0trigger-hotkeys.ahk"
)
echo DBG22: after hotkey-start
echo [INFO] Background watchers have been started.


:: there should be a task (when you not admin) AuraDictation_Hotkeys (for use keys like F10) that is already installed during installation as admin.
:: maybe we should check this also in the start script?
:: maybe we should check this also in the start script?


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

call .venv\Scripts\activate

:: python -u aura_engine.py
python -X utf8 -u aura_engine.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\aura_engine.log" >nul 2>&1
IF "%errorlevel%"=="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...
type nul > "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.

pushd "%~dp0"


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

:: ---------------------------------------------------------
:: 1. SCHREIB-TEST (Write Permission Check)
:: ---------------------------------------------------------
:: Wir versuchen, eine unsichtbare Test-Datei zu erstellen.
echo test > ".write_permission_check.tmp" 2>nul

:: update.bat:17
if exist ".write_permission_check.tmp" (
    del ".write_permission_check.tmp"
    echo Schreibrechte vorhanden. Starten ohne Admin-Rechte...
    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"
    Start-Sleep -Seconds 2.5
)

call "%~dp0setup\fix_permissions.bat"


:: 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.

powershell -c "[System.Media.SystemSounds]::Asterisk.Play()"

call "%~dp0..\start_aura.bat"
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

#SingleInstance Off
; #SingleInstance Force ; is buggy, using Heartbeat mechanism instead
/**
 *
„Could not close the previous instance“ tritt genau dann auf, wenn ein Skript in einem Alertable State (SleepEx) oder einem tiefen DllCall (wie Folder-Watcher) feststeckt. AHKs Standardbefehl kann den Prozess dann nicht sauber beenden.
*/


/**
 * @file type_watcher.ahk
 * @description Monitors the STT output folder and types incoming text.
 * Includes a "Zombie Map" state machine to prevent double-processing
 * due to redundant Windows file system events.
 * https://www.autohotkey.com/docs/v2/Language.htm#comments
 *
 *
 * planed:
 * ListLines(False)
 *
 */

; --- Configuration ---

watchDir := "C:\tmp\sl5_aura\tts_output"
logDir := A_ScriptDir "\log"
autoEnterFlagPath := "C:\tmp\sl5_aura\sl5_auto_enter.flag"

heartbeat_start_File := "C:\tmp\heartbeat_type_watcher_start.txt"

global processedZombies := Map() ; Key: FullPath, Value: Timestamp
global isProcessingQueue := false


global fileStates := Map() ; Mögliche Zustände: "queued", "processing", "done"




; Am Anfang des Skripts (bevor der Watcher startet)
CleanTempFolder() {
    global watchDir
    Log("Cleaning up old session files...")
    Loop Files, watchDir "\tts_output_*.txt" {
        try {
            FileDelete(A_LoopFileFullPath)
        }
    }
}
CleanTempFolder() ; Einmal beim Start ausführen


; --- 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
}

; --- SendMode Event is safer than InputThenPlay on new Windows builds ---
SendMode('Event')
SetKeyDelay(10, 10)

; type_watcher.ahk:29
; --- Global Variables ---
global pBuffer := Buffer(1024 * 16), hDir, pOverlapped := Buffer(A_PtrSize * 2 + 8, 0)
global CompletionRoutineProc
global watcherNeedsRearm := false
global fileQueue := []
global isProcessingQueue := false

Sleep(200)
try {
    if FileExist(heartbeat_start_File) {
        lastUniqueID := Trim(FileRead(heartbeat_start_File))
        if (lastUniqueID != myUniqueID) {
            ExitApp ; other instance exists
        }
    }
} catch {
    ExitApp
}

SetTimer(CheckHeartbeatStart, 5000)

CheckHeartbeatStart() {
    global heartbeat_start_File, myUniqueID
    try {
        local lastUniqueID := Trim(FileRead(heartbeat_start_File, "UTF-8"))
        if (lastUniqueID != myUniqueID) {
            Log("Newer instance detected. Terminating self.")
            ExitApp
        }
    } catch {
        ExitApp
    }
}

DirCreate(watchDir)
DirCreate(logDir)
Log("--- Script Started (v9.0 - Fixed Callback Freeze) ---")
Log("Watching folder: " . watchDir)

; type_watcher.ahk:69

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

ProcessExistingFiles()

; --- The Main Application Loop ---
Loop {
    ; This puts the script in "Alertable State" so callbacks can fire.
    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
; =============================================================================
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)
    }
}

; =============================================================================
; FILE QUEUING
; =============================================================================
QueueFile(filename) {
    global fileStates ; <--- Diese Zeile oben in der Funktion hinzufügen, falls sie fehlt
    if InStr(filename, "tts_output_") {
        fullPath := watchDir "\" . filename

        ; Wenn der Pfad in der Map ist (als "queued", "processing" oder Zeitstempel),
        ; ignorieren wir das Event.
        if fileStates.Has(fullPath) {
            return
        }

        fileStates[fullPath] := "queued"
        Log("Queuing new file -> " . filename)
        fileQueue.Push(fullPath)
    }
}


ProcessExistingFiles() {
    Log("Scanning for existing files...")
    Loop Files, watchDir "\tts_output_*.txt" {
        QueueFile(A_LoopFileName)
    }
    ScheduleQueueProcessing() ; Use the safe scheduler
}


; =============================================================================
; Instead of running logic inside the callback, we set a Timer.
; This allows the callback to finish instantly, unfreezing the thread.
ScheduleQueueProcessing() {
    ; Run ProcessQueue once (-1) after 10ms
    SetTimer(ProcessQueue, -10)
}

; =============================================================================
; QUEUE PROCESSING LOOP
; =============================================================================
; --- Ganz oben im Skript ---
global processedHistory := Map()







CleanupZombies() {
    global fileStates
    if (fileStates.Count == 0)
        return

    toDelete := []
    now := A_TickCount

    for fullPath, timestamp in fileStates {
        ; WICHTIG: Wenn val ein Text ist ("queued" oder "processing"),
        ; überspringen wir die Zeitberechnung, da die Datei noch in Arbeit ist.
        if !IsNumber(timestamp) {
            continue
        }

        ; 1. Versuche zu löschen, wenn die Datei noch da ist
        if FileExist(fullPath) {
            try {
                FileDelete(fullPath)
                ; Wenn erfolgreich, markieren wir den Map-Eintrag zum Löschen
                ; aber erst nach einer Sicherheitsmarge von 10 Sekunden
                if (now - timestamp > 10000) {
                    toDelete.Push(fullPath)
                }
            } catch {
                ; Noch gesperrt...
                ; Datei ist noch gesperrt (wahrscheinlich von Python)
                ; Wir lassen sie in der Map, damit sie nicht doppelt getippt wird.
            }
        } else {
            ; Datei ist physisch schon weg
            if (now - timestamp > 10000) {
                toDelete.Push(fullPath)
            }
        }

        ; 2. Aus der Map löschen, wenn Datei weg UND 10 Sek um
        if !FileExist(fullPath) && (now - timestamp > 10000) {
            toDelete.Push(fullPath)
        }

        ; 3. Sicherheits-Timeout (5 Min), falls Datei niemals gelöscht werden kann
        if (now - timestamp > 300000) {
            toDelete.Push(fullPath)
        }
    }

    for path in toDelete {
        if FileExist(path){
        fileStates.Delete(path)
        }
    }
}






; --- 2. Anpassung in ProcessQueue ---
ProcessQueue() {
    global isProcessingQueue, fileQueue, fileStates

    if (isProcessingQueue)
        return
    isProcessingQueue := true

    while (fileQueue.Length > 0) {
        local fullPath := fileQueue[1]
        ; fileQueue.RemoveAt(1) ; <--- WICHTIG: Sofort aus der Liste nehmen!

        ; Nur verarbeiten, wenn sie im Status "queued" ist
        if (!fileStates.Has(fullPath) || fileStates[fullPath] != "queued") {
            continue
        }

        ; Status auf "processing" setzen
        fileStates[fullPath] := "processing"

        try {
            ; Stabilitäts-Check (Punkt 3 der Experten: Sicherstellen, dass Python fertig ist)
            size1 := FileGetSize(fullPath), Sleep(100), size2 := FileGetSize(fullPath)
            if (size1 != size2 || size1 == 0) {
                fileStates[fullPath] := "queued" ; Zurücksetzen für nächsten Versuch
                isProcessingQueue := false
                return
            }

            content := Trim(FileRead(fullPath, "UTF-8"))
            if (content != "") {
                fileStates[fullPath] := A_TickCount
                Log("Typing content from " . fullPath)

                SendText(content)

                ; Erfolg! Status auf "done" setzen
                ; fileStates[fullPath] := "done"
                ; fileStates["last_processed_time_" . fullPath] := A_TickCount ; Für Debouncing
            }

            fileQueue.RemoveAt(1)

        } catch as e {
            Log("Error: " . e.Message)
            fileStates[fullPath] := "queued" ; Bei Fehler zurück in Warteschlange
            fileQueue.RemoveAt(1)
        }
    }

    CleanupZombies() ; Hier nutzen wir weiterhin fileStates statt processedZombies
    isProcessingQueue := false
} ; Ende von ProcessQueue()



; Hilfsfunktion für rückstandsloses Löschen
SecureDelete(filePath) {
    Loop 100 { ; Versuche es bis zu 10-mal (ca. 1 Sekunde lang)
        try {
            if !FileExist(filePath)
                return
            FileDelete(filePath)
            return ; Erfolg!
        } catch {
            Sleep(100) ; Warte 100ms, falls Datei noch gesperrt ist
        }
    }
    Log("CRITICAL: Could not delete sensitive file after 10 attempts: " filePath)
}
; =============================================================================
; WATCHER LOGIC
; =============================================================================
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) {
        MsgBox("FATAL: Could not watch directory.", "Error", 16), ExitApp
    }
    ReArmWatcher()
}

ReArmWatcher() {
    global hDir, pBuffer, pOverlapped, CompletionRoutineProc, watcherNeedsRearm
    static notifyFilter := 0x11 ; 0x1 (Name) + 0x10 (LastWrite) - Better for file updates
    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("ReArmWatcher failed! Error: " . A_LastError)
        watcherNeedsRearm := true
    }
}

IOCompletionRoutine(dwErrorCode, dwNumberOfBytesTransfered, lpOverlapped) {
    global pBuffer, watcherNeedsRearm

    if (dwErrorCode != 0) {
        Log("IOCompletionRoutine Error: " . dwErrorCode)
        watcherNeedsRearm := true
        return
    }

    if (dwNumberOfBytesTransfered > 0) {
        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")

            ; Action 1 = Added, Action 3 = Modified
            if (Action = 1 or Action = 3) {
                QueueFile(FileName)
            }

            if (!NextEntryOffset)
                break
            pCurrent += NextEntryOffset
        }

        ; --- CRITICAL CHANGE: DO NOT PROCESS HERE ---
        ; Instead of calling ProcessQueue() directly, we schedule it.
        ScheduleQueueProcessing()
    }

    watcherNeedsRearm := true
}
scripts/sh/type_watcher_keep_alive.sh
#!/bin/bash
# type_watcher_keep_alive.sh
# Stoppen: pkill -f type_watcher_keep_alive.sh

# --- PURPOSE OF THIS WRAPPER SCRIPT ---
#
# The main 'type_watcher.sh' script is generally stable, but has been
# observed to crash under specific, hard-to-reproduce circumstances,
# likely related to race conditions or heavy load (e.g., after processing
# many consecutive inputs).
#
# This wrapper script acts as a simple and robust "keep-alive" watchdog.
# It runs 'type_watcher.sh' in an infinite loop. If the main script
# ever crashes or exits unexpectedly, this watchdog will automatically
# restart it after a short delay. This ensures high availability without
# making the main script overly complex.

clear

# This script acts as a watchdog for type_watcher.sh.
# It runs it in an infinite loop, so if it ever crashes, it will be
# automatically restarted after a 1-second delay.
# scripts/sh/type_watcher_keep_alive.sh:23
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
LOG_DIR="$SCRIPT_DIR/../../log"
LOGFILE="$LOG_DIR/type_watcher_keep_alive.log"

CONFIG_FILE_1="$SCRIPT_DIR/config/settings.py"
CONFIG_FILE_2="$SCRIPT_DIR/config/settings_local.py"

# Initial timestamps
ts1_old=$(stat -c %Y "$CONFIG_FILE_1" 2>/dev/null || echo 0)
ts2_old=$(stat -c %Y "$CONFIG_FILE_2" 2>/dev/null || echo 0)


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

echo "Starting watchdog for type_watcher.sh"
log_message "Starting watchdog for type_watcher.sh"


while true; do
    # Check if the process is running by looking for a process with the script's name.
    # We use 'pgrep -f' and exclude the watchdog script itself.

    # Check config changes
    ts1_new=$(stat -c %Y "$CONFIG_FILE_1" 2>/dev/null || echo 0)
    ts2_new=$(stat -c %Y "$CONFIG_FILE_2" 2>/dev/null || echo 0)


    if [ "$ts1_old" != "$ts1_new" ] || [ "$ts2_old" != "$ts2_new" ]; then
        log_message "Config changed - killing type_watcher for restart"
        pkill -f "type_watcher.sh"
        ts1_old=$ts1_new
        ts2_old=$ts2_new
    fi


    if pgrep -f "type_watcher.sh" | grep -qv "$$"; then
        # It's running, do nothing.
        :
    else
        # It's not running, so it must have crashed. Start it.
        log_message "WATCHDOG: 'type_watcher.sh' is not running. Starting it now."
        log_message "$SCRIPT_DIR/../../type_watcher.sh"
        echo log_message "WATCHDOG: 'type_watcher.sh' is not running. Starting it now "
        echo $SCRIPT_DIR/../../type_watcher.sh
        $SCRIPT_DIR/../../type_watcher.sh
    fi

    # Wait for a few seconds before checking again.
    sleep 5
done