8000 perf(docker): fetch speech and object models at run time, not at build time by adeelahmad · Pull Request #183 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/configuration.md
8000
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,8 @@ video's sampled frames and on a document's or an image's pages ([API](api.md#obj
`video.object_engine`:

* `yolox`, the default: YOLOX on ONNX Runtime, both Apache-2.0. It needs ONNX Runtime (`pip install -e ".[objects]"`)
and a YOLOX `.onnx` model as YOLOX's releases publish them. The `lens:full` image has YOLOX-s in `/opt/lens/models`,
where Lens finds it without being told ([Deployment](deployment.md)).
and a YOLOX `.onnx` model as YOLOX's releases publish them. Lens fetches both, with YOLOX-s, the first time
a job needs them ([Components](components.md)), and also finds a model put in `/opt/lens/models`.
* `ultralytics`: Ultralytics YOLO (`pip install ultralytics`). It's AGPL-3.0: a server that lets others use it must
offer them its source, so it's in no image and no extra.
* `off`.
Expand All @@ -268,7 +268,7 @@ Set at startup only:

| Setting | Default | |
|---|---|---|
| `video.yolox_model` | the first `yolox*.onnx` in `/opt/lens/models` | a YOLOX model: YOLOX-s (35 MB, the image's), or YOLOX-Nano and YOLOX-Tiny (smaller and faster, less sure) |
| `video.yolox_model` | the first `yolox*.onnx` in `/opt/lens/models` or the data folder's models | a YOLOX model: YOLOX-s (35 MB, the one fetched), or YOLOX-Nano and YOLOX-Tiny (smaller and faster, less sure) |
| `video.ultralytics_model` | `yolov8n.pt` | Ultralytics weights, a file or a name Ultralytics downloads |

On a CPU, YOLOX-s takes about a tenth of a second a frame. What's found is kept per kind and resource: where it's seen
Expand Down
10 changes: 6 additions & 4 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ docker compose -f docker-compose.prod.yml logs backend | grep "setup code"
* **Backups.** `docker compose -f docker-compose.prod.yml exec backend lens backup` writes one of the database into
`data_dir/backups` ([Database](database.md#backups)); back up the `archive-data` volume as well.
* **Mail** for sign-in links and access requests: **Settings → Email** in the app (or `MAIL_*` in `.env`).
* **Workers.** Scale with `docker compose up -d --scale worker=3`. The images transcribe with faster-whisper on the
CPU (SenseVoice, the default engine, is used where it is installed; elsewhere the job log notes the fallback). To
use SenseVoice in Docker, set `EXTRAS=sensevoice` in `.env` and rebuild (`make dev`); it adds PyTorch. For GPU transcription, build with
* **Workers.** Scale with `docker compose up -d --scale worker=3`. The images carry no speech-to-text engine: a worker
fetches the configured one (SenseVoice by default) the first time a recording needs it ([Components](components.md)),
and cloud speech needs none. To bake one in instead, set `EXTRAS=whisper` (faster-whisper) or `EXTRAS=sensevoice`
(adds PyTorch) in `.env` and rebuild (`make run`). For GPU transcription, build with
`EXTRAS="sensevoice voices"` and give the worker the GPU; or run workers on other machines with `SURREAL_URL`
pointing at the database and `--steps` limited to what they can do.
* **Documents.** Docker Compose builds the full image, which reads Word, PowerPoint and spreadsheet files, text,
Markdown, saved web pages and emails as well as PDFs and images (`LENS_TARGET=lean` builds the smaller one that
reads only PDFs and images; a bare `docker build` makes the lean one unless given `--target full -t lens:full`): it adds LibreOffice, Chromium and fonts for most
scripts, and ONNX Runtime with the YOLOX-s model for the objects step ([Configuration](configuration.md#objects));
scripts (ONNX Runtime and the YOLOX-s model for the objects step are fetched at run time, on first use:
[Components](components.md));
add `EXTRAS="msg"` for Outlook `.msg` emails ([Configuration](configuration.md#documents-and-images)), and
`EXTRAS="doctr"` for docTR as the OCR engine (PyTorch: several GB;
[Configuration](configuration.md#text-on-screen-and-scans-ocr)).
Expand Down
20 changes: 6 additions & 14 deletions fastapi_backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Lens API and worker image. The same image runs `fastapi` (API) or `lens worker` (background jobs).
# Two targets: the default one (lean), and `full` (docker build --target full -t lens:full), which adds what makes
# Word, PowerPoint and spreadsheet files, text, web pages and emails into documents: LibreOffice, Chromium and fonts
# for most scripts; and what finds objects in frames and pages: ONNX Runtime and the YOLOX-s model (both Apache-2.0).
# for most scripts. Heavy Python extras and models (speech, voice IDs, faces, objects) are in neither: a worker fetches
# them in the background, at run time, when a job first needs them or they're turned on in Settings → Components.
# Outlook .msg emails also need the msg extra (extract-msg, GPL-3.0): --build-arg EXTRAS="msg".
FROM python:3.12-slim-bookworm AS base

Expand Down Expand Up @@ -37,15 +38,15 @@ ARG DEV=false
RUN case " $EXTRAS " in *" doctr "*) apt-get update \
&& apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*;; esac

# faster-whisper (the whisper extra) is always in: transcription works out of the box, on the CPU (SenseVoice, which
# needs PyTorch, is an extra)
# No speech-to-text engine is baked in (light by default: cloud speech users never need one). The worker fetches the
# configured one on first use (components.py); EXTRAS="whisper" bakes faster-whisper in, EXTRAS="sensevoice" SenseVoice.
COPY pyproject.toml uv.lock README.md ./
RUN --mount=type=cache,target=/root/.cache/uv env -u UV_NO_CACHE uv sync --frozen --no-install-project --extra whisper \
RUN --mount=type=cache,target=/root/.cache/uv env -u UV_NO_CACHE uv sync --frozen --no-install-project \
$( [ "$DEV" = "true" ] || echo --no-dev ) \
$(for e in $EXTRAS; do printf -- '--extra %s ' "$e"; done)

COPY . .
RUN --mount=type=cache,target=/root/.cache/uv env -u UV_NO_CACHE uv sync --frozen --extra whisper $( [ "$DEV" = "true" ] || echo --no-dev ) $(for e in $EXTRAS; do printf -- '--extra %s ' "$e"; done) \
RUN --mount=type=cache,target=/root/.cache/uv env -u UV_NO_CACHE uv sync --frozen $( [ "$DEV" = "true" ] || echo --no-dev ) $(for e in $EXTRAS; do printf -- '--extra %s ' "$e"; done) \
&& useradd --create-home --uid 1000 lens && mkdir -p /data && chown lens:lens /data

ENV ARCHIVE_CONFIG=/app/docker/archive.yaml
Expand All @@ -61,15 +62,6 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends libreoffice-writer-nogui libreoffice-calc-nogui libreoffice-impress-nogui \
chromium fonts-dejavu-core fonts-liberation2 fonts-noto-core fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
# objects: ONNX Runtime (the objects extra) and YOLOX-s, checked against its hash, where objects.py looks for it
ARG EXTRAS=""
ARG DEV=false
ARG YOLOX_URL=https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_s.onnx
ARG YOLOX_SHA256=c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063
RUN --mount=type=cache,target=/root/.cache/uv env -u UV_NO_CACHE uv sync --frozen --extra whisper --extra objects $( [ "$DEV" = "true" ] || echo --no-dev ) $(for e in $EXTRAS; do printf -- '--extra %s ' "$e"; done) \
&& mkdir -p /opt/lens/models \
&& python -c "import sys, urllib.request; urllib.request.urlretrieve(sys.argv[1], sys.argv[2])" "$YOLOX_URL" /opt/lens/models/yolox_s.onnx \
&& echo "$YOLOX_SHA256 /opt/lens/models/yolox_s.onnx" | sha256sum -c -
USER lens

# the default: everything above, without the converters
Expand Down
11 changes: 6 additions & 5 deletions fastapi_backend/app/domain/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,8 @@ def _make_engine(cfg, e):

def get_engine(cfg, log=None):
"""The configured engine; when it isn't installed here (or its packages are there but don't import, e.g. FunASR
without PyTorch), the first one that is (the Docker images and packages carry faster-whisper, not SenseVoice, the
default), so an import is transcribed rather than failing. With none at all, says how to add one."""
without PyTorch), the first one that is (say faster-whisper baked in with EXTRAS=whisper, while SenseVoice, the default, is
still being fetched), so an import is transcribed rather than failing. With none at all, says how to add one."""
e = cfg["transcribe"]["engine"]
if e not in ENGINE_MODULES:
return _make_engine(cfg, e)
Expand All @@ -387,9 +387,10 @@ def get_engine(cfg, log=None):
log(f" {e} isn't installed on this worker ({why[e]}); transcribing with {x}")
return engine
raise EngineMissing(
f"no speech-to-text engine is installed on this worker ({why[e]}). In Docker, rebuild the images "
"(make dev, or docker compose up --build --renew-anon-volumes): they carry faster-whisper, and "
"EXTRAS=sensevoice adds SenseVoice. Elsewhere: uv sync --extra whisper (or --extra sensevoice)"
f"no speech-to-text engine is installed on this worker ({why[e]}). Install it in Settings → Components "
"(or turn on components.auto), pick a cloud engine in Settings → Speech, or bake one into the Docker images "
"with EXTRAS=whisper (or sensevoice) in .env and rebuild (make run, or make dev). "
"Elsewhere: uv sync --extra whisper (or --extra sensevoice)"
)


Expand Down
4 changes: 2 additions & 2 deletions fastapi_backend/app/domain/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Engines (video.object_engine):
- yolox, the default: YOLOX on ONNX Runtime (both Apache-2.0). Its model is a YOLOX .onnx file: video.yolox_model, or
one in MODELS (the lens:full image has yolox_s there); `pip install "lens[objects]"` brings ONNX Runtime.
one in MODELS or the data folder's models (components.py fetches yolox_s there on first use); `pip install "lens[objects]"` brings ONNX Runtime.
- ultralytics: Ultralytics YOLO (video.ultralytics_model, such as yolov8n.pt). It's AGPL-3.0: a server that lets
others use it must offer them its source, so it's in no image or extra; `pip install ultralytics` to use it.
Without one the objects step is skipped, saying why.
Expand All @@ -23,7 +23,7 @@
from . import faces, jobs, keyring, store

R = store.R
MODELS = pathlib.Path("/opt/lens/models") # where the lens:full image keeps its models
MODELS = pathlib.Path("/opt/lens/models") # models put there by hand, or by older lens:full images
BOXES_MAX = 500 # boxes kept per kind and resource
COCO = (
"person bicycle car motorcycle airplane bus train truck boat traffic_light fire_hydrant stop_sign parking_meter bench "
Expand Down
4 changes: 2 additions & 2 deletions fastapi_backend/tests/domain/test_importing.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def test_paste_and_chunk_stitching(db, cfg):


def test_an_engine_that_isnt_installed_falls_back_to_one_that_is(monkeypatch):
"""The Docker images carry faster-whisper, not SenseVoice (the default engine): imports are transcribed anyway."""
"""faster-whisper is there (EXTRAS=whisper), SenseVoice (the default engine) not yet: imports are transcribed anyway."""
made = []

def sensevoice(cfg):
Expand Down Expand Up @@ -176,6 +176,6 @@ def no_engines(name, *a, **k):

monkeypatch.setattr(builtins, "__import__", no_engines)
cfg = {"transcribe": {"engine": "sensevoice", "device": "cpu", "sensevoice": {}}}
with pytest.raises(ingest.EngineMissing, match=r"no speech-to-text engine.*FunASR.*make dev") as e:
with pytest.raises(ingest.EngineMissing, match=r"no speech-to-text engine.*FunASR.*Settings → Components.*EXTRAS=whisper") as e:
ingest.get_engine(cfg)
assert not isinstance(e.value, SystemExit) # a job records it as a plain error, not "SystemExit: ..."
Loading
0