# Single-container image for a Hugging Face Space (Docker SDK). # # Spaces expose exactly one public port, but Auralynq's default topology is # 7 containers behind a Caddy proxy (see containers/ and compose.yml). This # image instead runs the API (FastAPI/uvicorn, loopback-only) and the web UI # (Next.js standalone) as two processes in one container; Next.js itself # proxies /api/* to the API process (see web/next.config.js's rewrites()), # so no separate reverse proxy is needed. # # Build context must be the repository root: # podman build -f deploy/huggingface/Dockerfile -t auralynq-space . FROM docker.io/library/node:20-slim AS web-builder WORKDIR /web COPY web/package.json web/package-lock.json* ./ RUN npm install --no-audit --no-fund COPY web/ ./ # next.config.js's rewrites() resolves at `next build` time (baked into # .next/routes-manifest.json), so AURALYNQ_INTERNAL_API_URL must be set here, # not just in the runtime stage below — otherwise the /api proxy is silently # empty and every API request 404s through the web server. ENV NEXT_PUBLIC_API_BASE=/api \ NEXT_TELEMETRY_DISABLED=1 \ AURALYNQ_INTERNAL_API_URL=http://127.0.0.1:8000 RUN npm run build FROM docker.io/library/python:3.11-slim AS runtime ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 WORKDIR /app # poppler-utils: PDF page rendering for visual grounding. nodejs/npm: runtime # only for the pre-built Next.js standalone server (no build tools needed # here — the build already happened in the web-builder stage). RUN apt-get update \ && apt-get install -y --no-install-recommends curl poppler-utils nodejs npm ffmpeg libsndfile1 espeak-ng zstd \ && rm -rf /var/lib/apt/lists/* # ---- API (light extras only — no torch/GPU stack; see pyproject.toml) ---- # `ingest` = PDF/DOCX parsing + page rendering for visual grounding. `llm` = # the lightweight openai/anthropic/cohere HTTP SDKs (no torch, no langchain) — # openai is required for the `huggingface` provider (HF Inference Providers via # the OpenAI-compatible router). `eval` is deliberately excluded (ragas drags in # langchain/langgraph and ~doubles image size; only `make eval`/`bench` need it). COPY pyproject.toml README.md ./ COPY auralynq ./auralynq RUN pip install -e ".[ingest,llm]" # ---- Voice ASR (free, torch-free): faster-whisper (CTranslate2) + soundfile. ---- # The full `voice` extra pulls silero-vad (→torch, ~800MB) + librosa; we skip # those and disable VAD/diarization (VAD is only in the live-mic path, not the # uploaded-file path the web voice query uses). The Whisper model is baked into # an image layer so voice works instantly + offline at runtime (no per-cold-start # download on the free CPU tier). ENV HF_HOME=/app/hfcache # faster-whisper (ASR, small) + Kokoro (TTS, spoken replies). Install the # CPU-only torch wheel first so kokoro doesn't drag in the ~2GB CUDA build. # Prebake both models into image layers → voice is instant + offline at runtime. RUN pip install "faster-whisper>=1.0" "soundfile>=0.12" \ && pip install --index-url https://download.pytorch.org/whl/cpu torch \ && pip install "kokoro>=0.9" \ && mkdir -p /app/hfcache \ && python -c "from faster_whisper import WhisperModel; WhisperModel('base.en', device='cpu', compute_type='int8')" \ && python -c "from kokoro import KPipeline; p=KPipeline(lang_code='a'); [c for _,_,c in p('warm up', voice='af_heart')]; [c for _,_,c in p('warm up', voice='am_michael')]" # ---- Local generative LLM (free, in-container): llama-cpp-python + GGUF. ---- # Install a PREBUILT AVX2/FMA/F16C wheel (compiled in a matching python:3.11-slim # and committed under wheels/). HF's build node can't finish a source compile in # its time window, so we bring our own optimized binary — no toolchain in the # image, no build timeout. Qwen2.5-3B-Instruct Q4_K_M (~2.1 GB) follows the # "cite every claim with [n]" instruction far more reliably than 1.5B. Model # baked in for offline runtime. COPY wheels/llama_cpp_python-0.3.34-py3-none-linux_x86_64.whl /tmp/wheels/ RUN pip install --no-cache-dir /tmp/wheels/llama_cpp_python-0.3.34-py3-none-linux_x86_64.whl \ && rm -rf /tmp/wheels \ && python -c "from huggingface_hub import hf_hub_download; hf_hub_download('Qwen/Qwen2.5-3B-Instruct-GGUF', 'qwen2.5-3b-instruct-q4_k_m.gguf')" # ---- Ollama (GPU path): self-contained CUDA runtime, auto-detects the T4. ---- # The provider is `auto`: ollama (GPU) is preferred, with the CPU GGUF above as # a fallback if the GPU/ollama is ever unavailable. Model baked into an image # layer at OLLAMA_MODELS so runtime is offline. `ollama serve` is started by # entrypoint.sh. ENV OLLAMA_MODELS=/app/ollama_models RUN curl -fsSL https://github.com/ollama/ollama/releases/download/v0.32.1/ollama-linux-amd64.tar.zst -o /tmp/ollama.tar.zst \ && tar --use-compress-program=unzstd -xf /tmp/ollama.tar.zst -C /usr && rm /tmp/ollama.tar.zst \ && mkdir -p /app/ollama_models \ && bash -c 'OLLAMA_HOST=127.0.0.1:11434 /usr/bin/ollama serve & SRV=$!; \ for i in $(seq 1 30); do curl -fsS http://127.0.0.1:11434/api/tags >/dev/null 2>&1 && break; sleep 1; done; \ /usr/bin/ollama pull qwen2.5:3b; kill "$SRV"' COPY scripts ./scripts COPY examples ./examples # ---- Web (pre-built Next.js standalone output) ---- COPY --from=web-builder /web/.next/standalone ./web COPY --from=web-builder /web/.next/static ./web/.next/static COPY --from=web-builder /web/public ./web/public COPY deploy/huggingface/entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh \ && useradd -m -u 10001 auralynq \ && mkdir -p /data/auralynq \ && chown -R auralynq:auralynq /app /data LABEL org.opencontainers.image.title="auralynq-space" \ org.opencontainers.image.description="Auralynq single-container image for Hugging Face Spaces" \ org.opencontainers.image.source="https://github.com/MHHamdan/Auralynq" \ org.opencontainers.image.licenses="Apache-2.0" USER auralynq # Safe-by-default posture for a public Space — see env.example for the full # list and docs/getting-started/huggingface-space.md for the design notes. # Every one of these can be overridden via Space Variables/Secrets. # HF_HUB_OFFLINE: ASR/TTS models are baked into the image above — never let a # runtime model load block on (rate-limited, unauthenticated) hub HEAD calls. # A stalled load during a relaunch is exactly what "workload not healthy after # 30 min" looks like on the free tier. ENV HF_HUB_OFFLINE=1 \ AURALYNQ_HF_SPACE=true \ AURALYNQ_DEMO_MODE=true \ AURALYNQ_PUBLIC_DEMO=true \ AURALYNQ_ALLOW_UPLOADS=false \ AURALYNQ_DATA_DIR=/data/auralynq \ AURALYNQ_VECTOR__BACKEND=memory \ AURALYNQ_EMBEDDING__PROVIDER=hash \ AURALYNQ_LLM__PROVIDER=slm \ AURALYNQ_LLM__SLM_REPO=Qwen/Qwen2.5-3B-Instruct-GGUF \ AURALYNQ_LLM__SLM_FILENAME=qwen2.5-3b-instruct-q4_k_m.gguf \ AURALYNQ_LLM__MAX_TOKENS=512 \ AURALYNQ_VOICE__ASR_PROVIDER=faster_whisper \ AURALYNQ_VOICE__ASR_MODEL=base.en \ AURALYNQ_VOICE__VAD=false \ AURALYNQ_VOICE__DIARIZE=false \ AURALYNQ_VOICE__TTS_PROVIDER=kokoro \ AURALYNQ_VOICE__TTS_VOICE=af_heart \ AURALYNQ_VISUAL__ENABLED=true \ AURALYNQ_SERVE__API_KEY= \ AURALYNQ_INTERNAL_API_URL=http://127.0.0.1:8000 \ NEXT_PUBLIC_API_BASE=/api \ PORT=7860 EXPOSE 7860 HEALTHCHECK --interval=15s --timeout=10s --retries=6 --start-period=240s \ CMD curl -fsS "http://127.0.0.1:${PORT:-7860}/api/health" || exit 1 CMD ["/app/entrypoint.sh"]