-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathDockerfile
More file actions
158 lines (138 loc) · 8.53 KB
/
Copy pathDockerfile
File metadata and controls
158 lines (138 loc) · 8.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# Stage 0: Build the Obsidian plugin bundle + downloadable zip from source, so
# the dashboard always serves what's in obsidian-plugin/main.ts rather than a
# stale hand-built artifact.
FROM node:20-slim AS obsidian-plugin-builder
WORKDIR /plugin
RUN apt-get update && apt-get install -y zip && rm -rf /var/lib/apt/lists/*
COPY obsidian-plugin/package.json obsidian-plugin/package-lock.json ./
RUN npm ci
COPY obsidian-plugin/main.ts obsidian-plugin/manifest.json obsidian-plugin/esbuild.config.mjs obsidian-plugin/tsconfig.json ./
RUN npm run build && zip fulloch.zip manifest.json main.js
# Stage 1: Build compiled CUDA extensions
FROM pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel AS builder
ENV DEBIAN_FRONTEND=noninteractive
# Cap flash-attn's parallel nvcc jobs so the build doesn't exhaust RAM on small
# CI runners (each job needs a few GB). Override with --build-arg MAX_JOBS=N.
ARG MAX_JOBS=4
ENV MAX_JOBS=${MAX_JOBS}
ARG HIGGS_TTS_REPO=https://github.com/liampetti/HiggsTTS.cpp.git
ARG HIGGS_TTS_REF=061134ad8c3dc544ebf1362f12882fd26a879f8f
ARG LLAMA_CPP_REF=e3546c7948e3af463d0b401e6421d5a4c2faf565
RUN apt-get update && apt-get install -y git cmake && rm -rf /var/lib/apt/lists/*
# HiggsTTS.cpp runs in its own process. Clone the Fulloch fork at an immutable
# commit so images are reproducible without vendoring its ggml runtime.
RUN git clone "${HIGGS_TTS_REPO}" /tmp/higgs-tts && \
git -C /tmp/higgs-tts checkout --detach "${HIGGS_TTS_REF}" && \
test "$(git -C /tmp/higgs-tts rev-parse HEAD)" = "${HIGGS_TTS_REF}" && \
cmake -S /tmp/higgs-tts -B /tmp/higgs-tts/build -DGGML_CUDA=ON && \
cmake --build /tmp/higgs-tts/build --target higgs_server -j "${MAX_JOBS}" && \
mkdir -p /opt/higgs-tts && \
cp /tmp/higgs-tts/build/bin/higgs_server /opt/higgs-tts/ && \
cp /tmp/higgs-tts/build/ggml/src/libggml.so.0.* /opt/higgs-tts/libggml.so.0 && \
cp /tmp/higgs-tts/build/ggml/src/libggml-base.so.0.* /opt/higgs-tts/libggml-base.so.0 && \
cp /tmp/higgs-tts/build/ggml/src/libggml-cpu.so.0.* /opt/higgs-tts/libggml-cpu.so.0 && \
cp /tmp/higgs-tts/build/ggml/src/ggml-cuda/libggml-cuda.so.0.* /opt/higgs-tts/libggml-cuda.so.0 && \
rm -rf /tmp/higgs-tts
# All local GGUF models run through this matching upstream llama-server build.
RUN git init /tmp/llama.cpp && \
git -C /tmp/llama.cpp remote add origin https://github.com/ggml-org/llama.cpp.git && \
git -C /tmp/llama.cpp fetch --depth 1 origin "${LLAMA_CPP_REF}" && \
git -C /tmp/llama.cpp checkout --detach FETCH_HEAD && \
cmake -S /tmp/llama.cpp -B /tmp/llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=ON && \
cmake --build /tmp/llama.cpp/build --target llama-server -j "${MAX_JOBS}" && \
mkdir -p /opt/llama-cpp && \
cp /tmp/llama.cpp/build/bin/llama-server /opt/llama-cpp/ && \
rm -rf /tmp/llama.cpp
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt && \
pip install --no-deps git+https://github.com/liampetti/Qwen3-TTS-streaming.git@97da215 && \
pip install flash-attn --no-build-isolation && \
python -m spacy download en_core_web_sm
# Stage 2: Runtime (no CUDA compilers/headers)
FROM pytorch/pytorch:2.8.0-cuda12.8-cudnn9-runtime
ENV DEBIAN_FRONTEND=noninteractive
ARG CRISPASR_RELEASE_TAG=v0.8.7
WORKDIR /app
ENV HF_HOME=/app/data/models
# HF_HUB_OFFLINE is managed at runtime by app.py (online for the first-run
# wizard download, offline once models are cached) — not pinned here.
# Image variant — the wizard offers all tiers on the GPU image.
ENV FULLOCH_VARIANT=gpu
# Native llama-server is separate from Python, so it does not receive PyTorch's
# internal CUDA-library discovery. Make the CUDA wheels' shared libraries visible
# to its dynamic linker as well.
ENV LD_LIBRARY_PATH=/opt/conda/lib/python3.11/site-packages/nvidia/cuda_runtime/lib:/opt/conda/lib/python3.11/site-packages/nvidia/cublas/lib:/opt/conda/lib/python3.11/site-packages/nvidia/nccl/lib:${LD_LIBRARY_PATH}
# Install runtime system dependencies
# - gcc: needed by Triton JIT for TTS kernels
# - libasound2-plugins + libpulse0: ALSA→PulseAudio bridge so PortAudio's ALSA
# backend transparently routes through pulseaudio (handles resampling +
# device selection regardless of what mic is plugged in)
RUN apt-get update && apt-get install -y \
gcc \
sox \
libsox-dev \
libsox-fmt-all \
ffmpeg \
espeak-ng \
libportaudio2 \
libasound2-plugins \
libpulse0 \
procps \
curl \
&& rm -rf /var/lib/apt/lists/*
# CrispASR publishes its Python glue and CUDA native runtime separately. Keep
# the assembled runtime outside /app/data so setup only downloads model weights.
RUN set -eux; \
curl -L "https://github.com/CrispStrobe/CrispASR/releases/download/${CRISPASR_RELEASE_TAG}/crispasr-python-linux-x86_64.tar.gz" -o /tmp/crispasr-python.tar.gz; \
curl -L "https://github.com/CrispStrobe/CrispASR/releases/download/${CRISPASR_RELEASE_TAG}/libcrispasr-linux-x86_64-cuda.tar.gz" -o /tmp/crispasr-cuda.tar.gz; \
tar -xzf /tmp/crispasr-python.tar.gz -C /tmp; \
mv /tmp/crispasr-python-linux-x86_64 /opt/crispasr-python-cuda; \
tar -xzf /tmp/crispasr-cuda.tar.gz -C /tmp; \
rm -f /opt/crispasr-python-cuda/crispasr/libcrispasr.so /opt/crispasr-python-cuda/crispasr/libggml*.so*; \
cp /tmp/libcrispasr-linux-x86_64-cuda/src/libcrispasr.so.0.8.7 /opt/crispasr-python-cuda/crispasr/libcrispasr.so; \
cp /tmp/libcrispasr-linux-x86_64-cuda/ggml/src/libggml.so.0.10.2 /opt/crispasr-python-cuda/crispasr/libggml.so.0; \
cp /tmp/libcrispasr-linux-x86_64-cuda/ggml/src/libggml-cpu.so.0.10.2 /opt/crispasr-python-cuda/crispasr/libggml-cpu.so.0; \
cp /tmp/libcrispasr-linux-x86_64-cuda/ggml/src/libggml-base.so.0.10.2 /opt/crispasr-python-cuda/crispasr/libggml-base.so.0; \
cp /tmp/libcrispasr-linux-x86_64-cuda/ggml/src/libggml-cuda.so.0.10.2 /opt/crispasr-python-cuda/crispasr/libggml-cuda.so.0; \
rm -rf /tmp/crispasr-python.tar.gz /tmp/crispasr-cuda.tar.gz /tmp/libcrispasr-linux-x86_64-cuda
# Route ALSA's default device through PulseAudio so the host's selected
# input/output devices and resampling are used automatically
RUN printf 'pcm.!default { type pulse }\nctl.!default { type pulse }\n' > /etc/asound.conf
# Copy Python environment with compiled packages from builder
COPY --from=builder /opt/conda /opt/conda
COPY --from=builder /opt/higgs-tts /opt/higgs-tts
COPY --from=builder /opt/llama-cpp /opt/llama-cpp
# Create non-root user
RUN useradd -m -u 1000 -s /bin/bash appuser
USER appuser
# Copy application code
COPY --chown=appuser:appuser app.py fulloch.png parloch.png ./
COPY --chown=appuser:appuser core/ core/
COPY --chown=appuser:appuser tools/ tools/
COPY --chown=appuser:appuser utils/ utils/
COPY --chown=appuser:appuser audio/ audio/
COPY --chown=appuser:appuser server/ server/
# First-run seeds (copied into the empty ./data volume by core/bootstrap.py):
# the config template the wizard fills in, the app's own GBNF grammar (which
# the wizard's downloader can't fetch), and the timer alert tone. Weights stay
# OUT of the image — the wizard pulls them on first run.
COPY --chown=appuser:appuser data/config.example.yml /app/seed/config.example.yml
COPY --chown=appuser:appuser data/models/grammars/agent.gbnf /app/seed/grammars/agent.gbnf
COPY --chown=appuser:appuser data/wav/ /app/seed/wav/
# Obsidian plugin zip, built from source in the obsidian-plugin-builder stage
# above — served by GET /api/obsidian/plugin.zip (server/dashboard.py).
COPY --from=obsidian-plugin-builder --chown=appuser:appuser /plugin/fulloch.zip /app/obsidian-plugin/fulloch.zip
# Entrypoint chowns the data dir to appuser on every boot — needed because
# Docker-named volumes start root-owned, and a fresh volume would otherwise
# fail the app's first write to /app/data. Idempotent (no-op when the dir
# is already correctly owned) and safe on read-only mounts.
COPY --chown=root:root --chmod=755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# Liveness probe: the dashboard's GET /ready returns 200 as soon as the
# web server is up. A user who hits the URL right now sees the wizard,
# download progress, or an actionable error — never connection-refused.
# `docker compose ps` flips this to "healthy" the moment the UI is
# reachable. Task 7a of docs/ease-of-use-tasks.md.
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \
CMD python -c "import ssl,urllib.request,sys; ctx=ssl._create_unverified_context(); sys.exit(0 if urllib.request.urlopen('https://127.0.0.1:8765/ready', context=ctx, timeout=2).status == 200 else 1)"
CMD ["python", "app.py"]