⭐
Log in to mark this repository.
Ingest video, and run it through the pipeline via an extracted frame
07a12d4 · gmorell · 2026-08-30 00:51
Message
{commit_body(@commit)}
Files changed
modified
Cargo.lock
+11
−0
@@ -4375,6 +4375,16 @@ version = "1.3.0"
| 4375 | 4375 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 4376 | 4376 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" |
| 4377 | 4377 | |
| 4378 | +[[package]] | |
| 4379 | +name = "signal-hook-registry" | |
| 4380 | +version = "1.4.8" | |
| 4381 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4382 | +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" | |
| 4383 | +dependencies = [ | |
| 4384 | + "errno", | |
| 4385 | + "libc", | |
| 4386 | +] | |
| 4387 | + | |
| 4378 | 4388 | [[package]] |
| 4379 | 4389 | name = "signature" |
| 4380 | 4390 | version = "2.2.0" |
@@ -4887,6 +4897,7 @@ dependencies = [
| 4887 | 4897 | "libc", |
| 4888 | 4898 | "mio", |
| 4889 | 4899 | "pin-project-lite", |
| 4900 | + "signal-hook-registry", | |
| 4890 | 4901 | "slab", |
| 4891 | 4902 | "socket2 0.6.0", |
| 4892 | 4903 | "tokio-macros", |
modified
Cargo.toml
+1
−1
@@ -19,7 +19,7 @@ path = "src/lib.rs"
| 19 | 19 | [dependencies] |
| 20 | 20 | axum = { version = "0.8.5", features = ["multipart"] } |
| 21 | 21 | log = "0.4.28" |
| 22 | −tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "sync"] } | |
| 22 | +tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "sync", "process", "time"] } | |
| 23 | 23 | tower-http = { version = "0.6.6", features = ["fs", "trace"] } |
| 24 | 24 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } |
| 25 | 25 | anyhow = "1.0.100" |
modified
Dockerfile
+4
−1
@@ -77,8 +77,11 @@ RUN curl -fL --retry 3 -o text-detection.rten https://ocrs-models.s3-accelerat
| 77 | 77 | # Stage 4 — runtime: minimal Debian slim with the binaries + assets + templates. |
| 78 | 78 | # --------------------------------------------------------------------------- |
| 79 | 79 | FROM debian:bookworm-slim AS runtime |
| 80 | +# ffmpeg is here for the `poster` worker, which lifts a still frame out of a | |
| 81 | +# video so the image-only categorizers have something to read. It is the | |
| 82 | +# heaviest thing in this layer; drop it only if video uploads go away. | |
| 80 | 83 | RUN apt-get update \ |
| 81 | − && apt-get install -y --no-install-recommends ca-certificates libssl3 \ | |
| 84 | + && apt-get install -y --no-install-recommends ca-certificates libssl3 ffmpeg \ | |
| 82 | 85 | && rm -rf /var/lib/apt/lists/* \ |
| 83 | 86 | && groupadd --system --gid 10001 dms \ |
| 84 | 87 | && useradd --system --uid 10001 --gid dms --home /app --shell /usr/sbin/nologin dms \ |
modified
Justfile
+1
−1
@@ -44,7 +44,7 @@ migrate:
| 44 | 44 | serve: |
| 45 | 45 | cargo run --bin webservice |
| 46 | 46 | |
| 47 | −# Run a categorizer worker. Usage: just worker thumbnail | ocr | describe | extract_tags | |
| 47 | +# Run a categorizer worker. Usage: just worker poster | thumbnail | ocr | describe | extract_tags | |
| 48 | 48 | worker kind: |
| 49 | 49 | cargo run --bin queue-worker -- --kind {{kind}} |
| 50 | 50 |
modified
README.md
+29
−3
@@ -111,9 +111,11 @@ All env-driven — defaults make sense for local dev.
| 111 | 111 | | `AUTO_MIGRATE` | `true` | Run pending migrations on webservice boot. Disable in environments that prefer a separate migration step. | |
| 112 | 112 | | `REGISTRATION_ENABLED` | `false` | When off, plain `/register` 404s; `/register?token=…` invite links still work. | |
| 113 | 113 | | `DMS_PUBLIC_URL` | (unset) | Override what the QR pairing URL encodes when behind a proxy. Falls back to the request's Host header, then the bind address. | |
| 114 | −| `WORKER_KIND` | (required for workers) | `thumbnail` / `ocr` / `describe` / `extract_tags`. Same as `--kind`. | | |
| 114 | +| `WORKER_KIND` | (required for workers) | `poster` / `thumbnail` / `ocr` / `describe` / `extract_tags`. Same as `--kind`. | | |
| 115 | 115 | | `WORKER_PREFETCH` | `4` | Per-worker concurrency (RabbitMQ basic_qos). Tune CPU-bound (OCR) lower than I/O-bound. | |
| 116 | 116 | | `WORKER_MAX_ATTEMPTS` | `100` | Hard cap on per-(upload, kind) DB-tracked attempts. Once exceeded, the worker DLQs the next redelivery instead of running it — protects the queue from a single poisoned message hogging consumers. Set `0` to disable. | |
| 117 | +| `FFMPEG_BIN` | `ffmpeg` | Binary the `poster` worker shells out to for video frame extraction. | | |
| 118 | +| `POSTER_TIMEOUT_SECS` | `120` | Hard cap on one frame extraction, so a video that hangs the decoder fails the job rather than parking a worker. | | |
| 117 | 119 | | `OCRS_DETECTION_MODEL` | `/opt/dms/models/text-detection.rten` | Bundled in the image; override to swap models. Required for OCR — outside Docker (e.g. `cargo run`), set this to a downloaded `.rten` file or the worker DLQs jobs with a clear log line. | |
| 118 | 120 | | `OCRS_RECOGNITION_MODEL` | `/opt/dms/models/text-recognition.rten` | Same as above. | |
| 119 | 121 |
@@ -184,8 +186,9 @@ hosts, which would put those gigabytes back in RAM.
| 184 | 186 | |
| 185 | 187 | Per entry: |
| 186 | 188 | |
| 187 | −- Only entries whose name resolves to an image type are stored. Everything | |
| 188 | − else is counted and skipped. | |
| 189 | +- Only entries whose name resolves to an image or video type are stored. | |
| 190 | + Everything else, such as an export's stray `.json` or `.txt`, is counted and | |
| 191 | + skipped rather than becoming an upload nobody asked for. | |
| 189 | 192 | - Directories, `__MACOSX/` resource forks, and dotfiles are ignored. Nested |
| 190 | 193 | paths flatten to the basename — storage is content-addressed, so the |
| 191 | 194 | archive's layout doesn't survive. |
@@ -228,6 +231,29 @@ a namespace wildcard. Bare values match across whichever sources are enabled.
| 228 | 231 | The whole pipeline is covered by unit tests for the parser plus integration tests |
| 229 | 232 | that exercise the SQL against a real Postgres (`tests/search_integration.rs`). |
| 230 | 233 | |
| 234 | +## Video | |
| 235 | + | |
| 236 | +A video upload cannot go straight through the categorizers: thumbnail, OCR and | |
| 237 | +describe all work on a decoded still, and none of them can open an `.mp4`. So a | |
| 238 | +video gets one job first, `poster`, which shells out to ffmpeg for a single | |
| 239 | +representative frame, stores it as `<stem>.poster.jpg` beside the original, and | |
| 240 | +then enqueues thumbnail/ocr/describe itself. From their point of view the video | |
| 241 | +has become an image; `workers::source_image_relative` is what quietly hands them | |
| 242 | +the frame instead of the original. | |
| 243 | + | |
| 244 | +ffmpeg's `thumbnail` filter picks the frame, scoring a window rather than taking | |
| 245 | +frame 0, which on a GIF-turned-mp4 is routinely a blank fade-in. Captions burned | |
| 246 | +into the picture survive, so OCR on an imgur-style export finds real text. | |
| 247 | + | |
| 248 | +Run the worker like any other: `just worker poster`. It needs ffmpeg on PATH | |
| 249 | +(`FFMPEG_BIN` overrides the binary, `POSTER_TIMEOUT_SECS` the 120s cap); the | |
| 250 | +runtime image installs it. | |
| 251 | + | |
| 252 | +Every kind that exists must appear in `queue::ALL_KINDS`. That list drives the | |
| 253 | +queue topology, and a kind missing from it has no queue, so anything published | |
| 254 | +for it is dropped by the broker without an error. A unit test keeps the list | |
| 255 | +exhaustive. | |
| 256 | + | |
| 231 | 257 | ## Pipelines |
| 232 | 258 | |
| 233 | 259 | Every upload row keeps a `processing_status`. Per-upload, per-kind state |
modified
assets/f/js/src.js
+16
−0
Click to load diff…
modified
compose.yml
+7
−0
Click to load diff…
modified
src/bin/queue-worker.rs
+3
−2
Click to load diff…
modified
src/bin/webservice.rs
+9
−0
Click to load diff…
modified
src/config.rs
+8
−0
Click to load diff…
modified
src/handlers/failed.rs
+1
−7
Click to load diff…
modified
src/handlers/uploads.rs
+46
−26
Click to load diff…
modified
src/queue/messages.rs
+60
−0
Click to load diff…
modified
src/queue/mod.rs
+3
−8
Click to load diff…
modified
src/queue/publisher.rs
+1
−8
Click to load diff…
modified
src/storage/local.rs
+15
−10
Click to load diff…
modified
src/storage/mod.rs
+29
−1
Click to load diff…
modified
src/telemetry.rs
+1
−1
Click to load diff…
modified
src/workers/describe.rs
+11
−3
Click to load diff…
modified
src/workers/mod.rs
+22
−1
Click to load diff…
modified
src/workers/ocr.rs
+1
−1
Click to load diff…
added
src/workers/poster.rs
+171
−0
Click to load diff…
modified
src/workers/thumbnail.rs
+20
−2
Click to load diff…
modified
templates/search.html
+6
−0
Click to load diff…
modified
templates/uploads/list.html
+6
−0
Click to load diff…
modified
templates/uploads/show.html
+7
−0
Click to load diff…
Parents: 6fa3ef9