-
Stefy Lanza (nextime / spora ) authored
fit the caption to the frame, install the font that draws it, and stop reading an unset field as a free GPU (v0.5.6) Four defects behind one report: vertical renders came back with caption lines running off both edges, in the wrong typeface, and speech-to-text answered 500/503 while a text model held the card. THE FONT WAS NEVER IN THE IMAGE. Dockerfile.oci and Dockerfile.update install fonts-montserrat; Dockerfile.oci-venv -- the one build_oci_image.sh actually builds -- installed no fonts at all, so no image built the normal way has ever had the family the compose API documents and defaults to. libass does not fail on a missing family, it substitutes: DejaVu is considerably wider, which is how a missing package became a layout bug with nothing but a log line to say so. All three Dockerfiles now install the fonts AND fontconfig (Dockerfile.oci had the fonts but not fc-match, so it could not even tell whether a family existed). And because an image rebuild is a poor way to add a brand font, fonts can now be supplied per installation: files dropped in <config>/fonts -- or wherever CODERAI_FONT_DIR points -- are registered at startup by writing a fontconfig snippet into the user config. The snippet matters more than it looks: captions are burned by libass inside an ffmpeg CHILD process, which does its own fontconfig lookup, so a font this process can find is not necessarily a font the renderer can use. The config volume outlives both a restart and a new image, which is the point. CAPTION SIZE CAME FROM THE FRAME HEIGHT. On a vertical video the height is the LONG side: at 1080x1920 the font was sized 0.052 * 1920 ~ 100px to be drawn across a 1080px frame. It is now sized from the short side, which leaves 16:9 byte-identical (there the short side IS the height) and gives 9:16 the same text-to-width ratio as 16:9. Line breaking upstream works on a word budget and cannot know how wide the words are, and WrapStyle is 2 -- no automatic wrapping -- so a line that still overflows was simply drawn past the edge. Each event's widest line is now measured against the real font file, and one that does not fit gets its own smaller size through an inline \fs override: the long line shrinks, not the whole video. Measured with the actual face rather than estimated per character, because the families involved differ in width by exactly the margin that decides the question. Clients scaling font_scale down by hand for 9:16 and 4:5 can stop. AN UNSET FIELD READ AS A FREE GPU. used_vram_gb=0 was honoured as authoritative -- yet zero is this estimator's own "could not determine" sentinel, and it is what the admin UI stores for a field the operator never filled in. A 27B Q4_K_P with used_vram_gb=0 and measured_vram_gb=18.0 therefore estimated 0.00 GB against 8852 MiB of free VRAM. Nothing was evicted, every layer was aimed at the GPU, the load failed and retried down a ladder -- all, 52, 39, 26 layers, re-reading the weights at each rung -- for 11m50s, and settled with 39 of 66 layers on the CPU. The request then generated for another 19 minutes, 31m04s in all, holding GPU ownership the whole time: every other model queued behind it, which is what made speech-to-text answer 500/503. The swap gate was working correctly; it swapped the moment the owner finished. A real measurement is better information than an unfilled field, so it now wins. An operator who means "no weights on the GPU" has n_gpu_layers=0, which is handled separately and still reserves nothing. Worth recording for the next time this looks like a quantization problem: it was not. Those weights are 4-bit on disk and the KV cache was already q4_0/q4_0 with flash attention -- about 2.2 GB for n_ctx=32000. Both were at their most aggressive setting while the model sat on the CPU. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
30f224e3