From e500daa08fe4943b9894d7951689f0f645d809a9 Mon Sep 17 00:00:00 2001 From: Liam Pettigrew Date: Mon, 9 Feb 2026 17:28:53 +1100 Subject: [PATCH] usability and docker launch fixes --- .dockerignore | 1 + .gitignore | 1 + Dockerfile | 19 +++++++++------ Dockerfile_gpu | 59 +++++++++++++++++++++++++-------------------- app.py | 7 +++--- compose.yml | 17 +++++++++---- compose_gpu.yml | 15 +++++++++--- launch.sh | 50 +++++++++++++++++++++++--------------- readme.md | 1 + tools/search_web.py | 16 ++++++------ 10 files changed, 112 insertions(+), 74 deletions(-) diff --git a/.dockerignore b/.dockerignore index 9ac53b0..b9ee1a9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -59,6 +59,7 @@ data/token.json # Local Data and Models (mount separately) # ============================================================================= data/models/ +data/voices/ data/cache/ *.gguf diff --git a/.gitignore b/.gitignore index c3652b8..79ef1f7 100755 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,4 @@ data/voices/private !data/config.example.yml !.env.example\ publish.sh +searxng_data/settings.yml.new diff --git a/Dockerfile b/Dockerfile index 9fd8e4e..1de0ff6 100755 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,6 @@ FROM python:3.12-slim WORKDIR /app -# Point HuggingFace cache at the mounted data volume so from_pretrained() -# finds models downloaded by launch.sh into data/models/hub/ ENV HF_HOME=/app/data/models ENV HF_HUB_OFFLINE=1 @@ -15,20 +13,25 @@ RUN apt-get update && apt-get install -y \ ffmpeg \ git \ libportaudio2 \ + procps \ && rm -rf /var/lib/apt/lists/* # Install Python dependencies (Force transformers 4.57.3 for offline Qwen3 ASR to work) -RUN pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu130 +RUN pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt && \ pip install --no-deps git+https://github.com/rekuenkdr/Qwen3-TTS-streaming.git@97da215 +# Create non-root user +RUN useradd -m -u 1000 -s /bin/bash appuser +USER appuser + # Copy application code -COPY app.py . -COPY core/ core/ -COPY tools/ tools/ -COPY utils/ utils/ -COPY audio/ audio/ +COPY --chown=appuser:appuser app.py . +COPY --chown=appuser:appuser core/ core/ +COPY --chown=appuser:appuser tools/ tools/ +COPY --chown=appuser:appuser utils/ utils/ +COPY --chown=appuser:appuser audio/ audio/ # Run the app CMD ["python", "app.py"] diff --git a/Dockerfile_gpu b/Dockerfile_gpu index 08d2d86..1196125 100644 --- a/Dockerfile_gpu +++ b/Dockerfile_gpu @@ -1,42 +1,49 @@ -# Use PyTorch image with CUDA support (includes Python 3.11, PyTorch, CUDA) -FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-devel +# Stage 1: Build compiled CUDA extensions +FROM pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel AS builder + +ENV DEBIAN_FRONTEND=noninteractive +ENV CMAKE_ARGS="-DGGML_CUDA=on" +ENV FORCE_CMAKE=1 + +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt && \ + pip install --no-deps git+https://github.com/rekuenkdr/Qwen3-TTS-streaming.git@97da215 && \ + pip install flash-attn --no-build-isolation + +# Stage 2: Runtime (no CUDA compilers/headers) +FROM pytorch/pytorch:2.8.0-cuda12.8-cudnn9-runtime -# Avoid interactive prompts ENV DEBIAN_FRONTEND=noninteractive -# Install system dependencies +WORKDIR /app + +ENV HF_HOME=/app/data/models +ENV HF_HUB_OFFLINE=1 + +# Install runtime system dependencies RUN apt-get update && apt-get install -y \ - git \ sox \ libsox-dev \ libsox-fmt-all \ ffmpeg \ libportaudio2 \ + procps \ && rm -rf /var/lib/apt/lists/* -WORKDIR /app +# Copy Python environment with compiled packages from builder +COPY --from=builder /opt/conda /opt/conda -# Point HuggingFace cache at the mounted data volume so from_pretrained() -# finds models downloaded by launch.sh into data/models/hub/ -ENV HF_HOME=/app/data/models -ENV HF_HUB_OFFLINE=1 - -# Set Environment Variables for llama-cpp-python CUDA build -ENV CMAKE_ARGS="-DGGML_CUDA=on" -ENV FORCE_CMAKE=1 - -# Install Python dependencies (Force transformers 4.57.3 for offline Qwen3 ASR to work) -RUN pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu130 -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt && \ - pip install --no-deps git+https://github.com/rekuenkdr/Qwen3-TTS-streaming.git@97da215 && \ - pip install flash-attn --no-build-isolation +# Create non-root user +RUN useradd -m -u 1000 -s /bin/bash appuser +USER appuser # Copy application code -COPY app.py . -COPY core/ core/ -COPY tools/ tools/ -COPY utils/ utils/ -COPY audio/ audio/ +COPY --chown=appuser:appuser app.py . +COPY --chown=appuser:appuser core/ core/ +COPY --chown=appuser:appuser tools/ tools/ +COPY --chown=appuser:appuser utils/ utils/ +COPY --chown=appuser:appuser audio/ audio/ CMD ["python", "app.py"] diff --git a/app.py b/app.py index a36c45e..ddc9d43 100644 --- a/app.py +++ b/app.py @@ -11,11 +11,12 @@ Usage: """ import os -import warnings +import logging import yaml from pathlib import Path -warnings.filterwarnings("ignore", message="Setting `pad_token_id` to `eos_token_id`") +# Suppress noisy "Setting pad_token_id to eos_token_id" from transformers +logging.getLogger("transformers.generation.utils").setLevel(logging.ERROR) # Load configuration with open("./data/config.yml", "r") as f: @@ -32,8 +33,6 @@ os.environ["DO_NOT_TRACK"] = "1" os.environ["ANONYMIZED_TELEMETRY"] = "False" os.environ["VLLM_NO_USAGE_STATS"] = "1" -import logging - logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" diff --git a/compose.yml b/compose.yml index 7600534..333ca69 100755 --- a/compose.yml +++ b/compose.yml @@ -2,17 +2,28 @@ services: # Fulloch AI app: build: - context: . + context: . env_file: - .env container_name: fulloch-ai network_mode: host + restart: unless-stopped devices: - /dev/snd:/dev/snd group_add: - audio + environment: + - PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native volumes: - ./data:/app/data:rw + - ${XDG_RUNTIME_DIR}/pulse:${XDG_RUNTIME_DIR}/pulse:ro + - ${HOME}/.config/pulse/cookie:/home/appuser/.config/pulse/cookie:ro + healthcheck: + test: ["CMD-SHELL", "pgrep -f 'python app.py' || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 60s # Web Search - Searxng searxng: @@ -30,7 +41,3 @@ services: - SETGID - SETUID - DAC_OVERRIDE - -volumes: - searxng_data: {} - app_data: {} \ No newline at end of file diff --git a/compose_gpu.yml b/compose_gpu.yml index b0d76a0..bf5883d 100755 --- a/compose_gpu.yml +++ b/compose_gpu.yml @@ -8,12 +8,23 @@ services: - .env container_name: fulloch-ai network_mode: host + restart: unless-stopped devices: - /dev/snd:/dev/snd group_add: - audio + environment: + - PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native volumes: - ./data:/app/data:rw + - ${XDG_RUNTIME_DIR}/pulse:${XDG_RUNTIME_DIR}/pulse:ro + - ${HOME}/.config/pulse/cookie:/home/appuser/.config/pulse/cookie:ro + healthcheck: + test: ["CMD-SHELL", "pgrep -f 'python app.py' || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 60s deploy: resources: reservations: @@ -38,7 +49,3 @@ services: - SETGID - SETUID - DAC_OVERRIDE - -volumes: - searxng_data: {} - app_data: {} \ No newline at end of file diff --git a/launch.sh b/launch.sh index a502e73..99ab7a2 100755 --- a/launch.sh +++ b/launch.sh @@ -74,40 +74,44 @@ echo "✅ All dependencies found." echo "📂 Checking directory structure..." mkdir -p "$HUB_DIR" "$GRAMMAR_DIR" -# 2a. Check for config.yml +# 2a. Check for config.yml and .env, create from templates if missing CONFIG_FILE="$(pwd)/data/config.yml" CONFIG_EXAMPLE="$(pwd)/data/config.example.yml" +ENV_FILE="$(pwd)/.env" +ENV_EXAMPLE="$(pwd)/.env.example" + +CREATED_FILES=() if [ ! -f "$CONFIG_FILE" ]; then echo "📝 config.yml not found. Creating from template..." cp "$CONFIG_EXAMPLE" "$CONFIG_FILE" - echo "" - echo "⚠️ Please edit data/config.yml with your settings before continuing." - echo " See data/config.example.yml for documentation on each option." - echo "" - echo " Run ./launch.sh again when ready." - exit 0 + CREATED_FILES+=("data/config.yml") else echo "✅ config.yml exists." fi -# 2b. Check for .env -ENV_FILE="$(pwd)/.env" -ENV_EXAMPLE="$(pwd)/.env.example" - if [ ! -f "$ENV_FILE" ]; then echo "📝 .env not found. Creating from template..." cp "$ENV_EXAMPLE" "$ENV_FILE" - echo "" - echo "⚠️ Please edit .env with your credentials before continuing." - echo " See .env.example for documentation on each variable." - echo "" - echo " Run ./launch.sh again when ready." - exit 0 + CREATED_FILES+=(".env") else echo "✅ .env exists." fi +if [ ${#CREATED_FILES[@]} -gt 0 ]; then + echo "" + echo "📄 Created: ${CREATED_FILES[*]}" + echo "" + read -p "Continue with defaults or exit to edit these files first? (c)ontinue / (e)xit: " response + response=${response,,} + if [[ "$response" == "e" || "$response" == "exit" ]]; then + echo "" + echo " Edit the files and run ./launch.sh again when ready." + exit 0 + fi + echo "✅ Continuing with defaults." +fi + # 3. Check and Download json.gbnf if [ ! -f "$GRAMMAR_DIR/json.gbnf" ]; then if ask_download "json.gbnf (grammar file)"; then @@ -187,7 +191,7 @@ else fi # 7. Prompt the user -read -p "Are you using a GPU? (y/n): " response +read -p "Are you using an NVIDIA GPU? (y/n): " response response=${response,,} if [[ "$response" == "y" || "$response" == "yes" ]]; then COMPOSE_FILE="compose_gpu.yml" @@ -197,6 +201,14 @@ else echo "✅ Using default containers" fi -# 8. Launch Docker Compose +# 8. Prepare runtime environment +# Ensure XDG_RUNTIME_DIR is set with the correct UID (compose files reference it) +export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + +# Ensure PulseAudio cookie exists (PipeWire systems may not create one) +mkdir -p "${HOME}/.config/pulse" +[ -f "${HOME}/.config/pulse/cookie" ] || touch "${HOME}/.config/pulse/cookie" + +# 9. Launch Docker Compose echo "🚀 All files checked. Starting services..." docker compose -f "$COMPOSE_FILE" up -d diff --git a/readme.md b/readme.md index ff3329d..5c00c56 100644 --- a/readme.md +++ b/readme.md @@ -58,6 +58,7 @@ A privacy-focused voice assistant that runs speech recognition, text-to-speech, ## Prerequisites +- Linux-based OS - Python 3.10+ - CUDA-capable GPU (recommended) or CPU - ~4GB disk space for models diff --git a/tools/search_web.py b/tools/search_web.py index e0f614b..39da714 100755 --- a/tools/search_web.py +++ b/tools/search_web.py @@ -96,15 +96,15 @@ def external_information(query: str = "get me the latest news stories") -> str: today = datetime.now().strftime("%B %d, %Y") - prompt = f""" -Today is {today}. + lines = [f"Today is {today}.", ""] + if website_snippets: + lines.append("A web search has retrieved the following information:") + lines.extend(website_snippets) + lines.append("") + lines.append("User question:") + lines.append(query) -{f"A web search has retrieved the following information:\n{chr(10).join(website_snippets)}" if len(website_snippets) > 0 else ""} - -User question: -{query} -""" - return prompt.strip() + return "\n".join(lines) if __name__ == "__main__": print("Web Search")