Refined picks better films but is chattier and occasionally breaks JSON; the
retry previously also used refined, so both passes could fail (seen as a 422).
Now distinguish the retry: a flat-but-parsed first pass retries refined
(sharper), while an unparseable first pass falls back to the more JSON-reliable
baseline. Makes refined safe as the Space default.
fix(oracle): retry when fewer than three films come back
Smaller models sometimes return one film despite "exactly 3", which reads as
broken. Treat a short film count as weak so the one bounded retry fires, and
when both passes are flagged prefer the one with more films.
fix(prompt): require the year field so it stops intermittently dropping
The previous "title without the year" wording let the model omit the year
entirely instead of moving it to the year field, so years vanished from the UI
at random. Now explicitly require a four-digit year in the year field, name-only
in title.
fix(prompt): strip year from title, harden JSON-only instruction
A/B on Qwen showed two issues: titles like "Before Sunrise (1995)" rendered
as "(1995) (1995)", and the refined prompt occasionally broke the JSON
contract. Strip a trailing (YYYY) from titles (using it as the year if absent),
and instruct the model to emit only the JSON object with escaped inner quotes.
fix(backend): first pass respects refined env flag; add A/B override
The first pass was hardcoded to baseline, so CHALCHITRA_REFINED_PROMPT was
ignored except on retry. Now the first pass uses the configured default
(refined=None reads the env). Adds an optional per-request `refined` override
(API + interpret) for a single clean pass — used to A/B baseline vs refined on
the same image against the live model. The frontend never sends it.
feat(space): wire Qwen2.5-VL-7B via hf_local provider (stage 2)
In-process model on ZeroGPU: model + processor load lazily to CPU, the forward
pass runs inside a @spaces.GPU-wrapped _generate (duration 180) that moves to
cuda. Translates our OpenAI-format content (image_url data URLs + text) into
Qwen2.5-VL chat messages via qwen-vl-utils. On the Space, default
CHALCHITRA_PROVIDER=hf_local and model=Qwen/Qwen2.5-VL-7B-Instruct (overridable
via Space Variables); local stays on LM Studio. Adds transformers/torchvision/
accelerate/qwen-vl-utils.
The client accumulates shown titles and sends them as `exclude`; the oracle
adds a "don't recommend these, find different films for this same moment"
clause. Adds a results button to re-roll other films for the current moment,
keeping the seen-list across "another moment" so a session never loops the
same recommendations. Titles are sanitized and capped. Stateless server.
- provider: bounded request timeout (CHALCHITRA_TIMEOUT) so a down/slow
model fails fast instead of hanging; oracle owns retries (max_retries=0)
- oracle: one retry now also covers a malformed/empty first reply, not just
a flat one, falling back to whichever pass is usable
- app: never leak exception detail to users; map model-server connectivity
errors to a friendly 503, log the rest server-side
feat(backend): sanitize and re-encode all input before the model
Never trust the client. Decode, validate, downscale, and re-encode every
image server-side to a known JPEG (≤896px), cap count (4) and source size
(12MB), and clean/cap the fragment (strip control chars, 600 chars). Bad
input raises InputError, which the API returns as a clean 400 instead of
letting it reach or crash the model. Adds Pillow to runtime deps.
feat(backend): retry once when an interpretation reads flat
Small models sometimes return a generic one-liner or describe "the photo"
despite the prompt. Detect that (min length + banned-phrase regex) and retry
once with the refined prompt and a hotter temperature, keeping whichever pass
is stronger. Adds an explicit `refined` override to system_prompt() and a
per-call temperature to providers.
feat(backend): make the typed fragment a primary signal
A present fragment now leads the reading instead of tinting it: the system
prompt weights the words as heavily as the images, and the user turn frames
them as the heart of the moment. Closing instruction adapts to whether a
fragment was given. Adds an A/B script showing the output pivots when a
personal line is present.
docs(backend): record proven ZeroGPU pattern, resolve spaces.GPU risk
Mirror the Mycelium Space approach for HFLocalProvider: module-level
@spaces.GPU warmup, a _gpu wrapper that's a no-op off-Spaces, model on
CPU at startup moved to cuda inside the wrapped call. Confirms the
earlier concern is resolved — @spaces.GPU allocates from FastAPI routes.
feat(backend): add pluggable LLM oracle for image and fragment input
- providers: OpenAI-compatible (LM Studio now, HF TGI/vLLM later) plus a
ZeroGPU/transformers hf_local seam for the eventual in-Space model
- prompt: the Chalchitra system prompt, with optional tested refinements
behind a flag so we start from the known-good baseline
- oracle: assemble the vision request and parse the frozen JSON contract,
tolerating fences, preamble, trailing commas, and key casing across models