Repository navigation
Scope Media-bin media to the service: code narrows, the model decides - #161
Conversation
v2.18.0 pulled in random media on the booth machine and never said which folders it used. A short folder name matched on one shared word -- a folder called "Youth" scored a perfect match against any runsheet that mentioned youth -- and with no template the whole bin was searched. Now five steps, drawn at the top of propresenter/media_match.py: code drops folders named for another known service unless a line mentions it; the model picks folders from their names (new parsing/folder_plan.py); code cuts those folders' files to 150 with a share per runsheet line; the model places files; code checks every pick. Folders saved per service (settings media_folders_by_service) skip the folder call and are final. A row applies only if it shares a word with the service's kind, so a "Sunday" row can't claim "Summit 2026 -- Sunday Night". No whole-bin fallback. A folder line under the template banner says which folders were used and who chose them; clicking it opens a recommendation-first picker that saves for the service and places the media again without re-parsing. Also: one tokeniser for "which service is this" (service_tokens), with template detection unchanged; settings.json read as UTF-8; a failed playlist-name lookup no longer cancels bin media; re-placing never clears earlier picks when ProPresenter doesn't answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
media_plan, lighting_plan and the new folder_plan each carried the same twenty lines: resolve the model, build a body that only goes to providers that keep nothing, add reasoning, call, check the status, dig the JSON out of the reply. parsing/ask.py is those lines once, as ask_json(). Each module keeps what differs -- its prompt, its timeout, what it accepts back. The guarantees now live in one place no caller can drop: data_collection deny, temperature 0, None rather than an exception on any failure, the key never logged. tests/test_ask.py states them directly. The three test files stubbed fetch_catalogue/resolve_model/chat by module name; they share one `openrouter` fixture in conftest.py now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Placing media again after a folder change cleared the bin's earlier picks before asking the model, so a dropped call -- or any error in the steps -- wiped every slide the parse had placed. The steps now run on a working view and the runsheet is only changed once there is an answer. That needs "the model didn't answer" told apart from "it answered and nothing fits": plan_media returns None for the first and [] for the second. A real answer that nothing fits still clears the old picks -- the operator un-ticked those folders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dragging out of the service field and letting go on the backdrop closed the picker and lost the ticks. It now closes only on a click that starts and ends on the backdrop. - Save didn't hold anything while placing media: Create could build the playlist from the old picks mid-way, a second Save raced the first, and an answer for the previous runsheet could overwrite a new one. Create, Save and Forget now wait, and an answer for an older runsheet is dropped. - A failed Save changed a grey hint far above the buttons, and errors showed raw "TypeError: Failed to fetch". Errors now appear beside the buttons in alert colours (role="alert") as plain sentences. - After a failed placement, reopening showed the old folders, so a second Save put them back. It now shows what was saved. - Banners are only rewritten when they change: identical redraws replayed the slide-in, re-announced the line and dropped keyboard focus. Focus follows to the new button when the line does change. - The folder line waited for nothing: it appeared before the match call finished, and Save then placed nothing. It now appears with the results. - On a short window (the minimum at 150% Windows scaling) Save sat outside the card; the list now gives way and the card scrolls past that. - Folder names were muted like their counts with a dead gap between rows (inherited label styles); counts read "38 files"; each group is labelled for screen readers; Enter in the service field saves; "Let AI choose" is now "Forget saved folders" and forgets the row that actually applied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-reviewRead the full diff against main, four commits. What I checked, and what it turned up. Found and fixed while reviewing. Re-placing media after a folder change cleared the bin's earlier picks before reading the bin and before asking the model. ProPresenter restarting, or one dropped OpenRouter call, between the parse and pressing Save would have left the runsheet with no media at all. Fixed in Precedence. A saved row is final: the folder call is skipped and the row's folders are used as-is, even one the model would never pick. A model suggestion is re-checked against Matching identity. Rows match the parse model's Blast radius. Every failure path in Security. Both new endpoints take an object body only, accept strings only (a list once got stringified into a saved row — fixed), cap every length and count, and need Live model behaviour. Four hard cases against GPT-4.1 mini, three runs each plus one after the helper refactor, identical every time. The first prompt failed two; the dated title and relevance-not-scarcity wording fixed them. Front end. A frontend-reviewer pass measured ten issues in the picker; the ones that cost a volunteer a Sunday are fixed and each re-verified in the browser ( Performance. Not covered. Never run against the real Media bin's folder names — ProPresenter wasn't running here. First thing to check on the booth machine; Checks: 1357 passed, 1 skipped locally. 🤖 Reviewed by Claude Code |
CodeQL #197: /api/media/place returned str(e) for UnreachableHost. The
message is operator-written, but the route didn't need to catch it at all —
propresenter_app.py already answers UnreachableHost for every route in the
standard {"ok": false, "error": ...} shape, as library.py relies on. Test
added for the SSRF guard on this route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Self-review: 98c5c89. One commit since the review above: 🤖 Reviewed by Claude Code |
Media-bin media scoped to the service: folders saved per service, picked by the model from their names otherwise, and a folder line under Step 1 saying which folders were used and who chose them (#161). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Why
On the booth machine, v2.18.0 pulled in the right Summit media — and some random media with it, and nothing said which folders it had used. Two causes, both in
media_match.py:_folder_scoredivided by the shorter name, so a folder calledYouthscored a perfect 1.0 against any runsheet that mentioned youth — and Summit is a youth event. Its whole contents became candidates.And the banner only named a folder when exactly one was used, so the multi-folder case was silent.
What changed
Code narrows, the model decides. The flow is drawn at the top of
propresenter/media_match.py:Sunday Service Media.parsing/folder_plan.pyasks which folders hold this service's media. It reads folder names (dozens), never the bin's thousands of files. Skipped when the operator has saved folders for the service.Saved folders per service.
settings["media_folders_by_service"], e.g. Sunday → Current media + Sunday media. Per service on purpose — a global "always use" list would leak Sunday media into Youth. A row applies only if it shares a word with the service's kind (service_type), not just its dated title, so aSundayrow can't claim "Summit 2026 — Sunday Night".The folder line and picker. A second line under the template banner says which folders were used and who chose them (your saved folders / AI / matched by name / none). Clicking it opens a picker listed recommendation-first: used for this runsheet, folders that look like this service, everything else. Saving stores the ticks for the service and places this runsheet's media again (
POST /api/media/place) without re-parsing.No whole-bin fallback. Nothing for this service is said, not papered over.
One helper for the model calls (second commit).
media_plan,lighting_planand the newfolder_planeach carried the same twenty lines to ask OpenRouter for JSON.parsing/ask.pyis those lines once, asask_json(), and holds the guarantees no caller can drop —data_collection: deny, temperature 0, None rather than an exception, the key never logged.tests/test_ask.pystates them directly; the three test files share oneopenrouterfixture instead of three copies. The live folder check gives identical answers through it.Smaller:
service_tokens(was_template_signal_tokens) is now the one tokeniser for "which service is this", with folder words passed in so template detection is unchanged;settings.jsonis read as UTF-8 (a migrated file with em-dashes decoded to mojibake on Windows without raising); a failed playlist-name lookup no longer cancels all bin media.Verified
The first prompt failed two of these (it skipped Sunday's own folder, and both Summit nights when the model wasn't shown the title); the prompt and the title are what changed.
_budgetedon 3,000 files × 60 lines: 35 ms.Found and fixed while building
plan_medianow returns None for "didn't answer" and[]for "answered, nothing fits"; a real "nothing fits" still clears the old picks, because the operator un-ticked those folders. Tests for both doors.service: ["Summit"]was stringified to"['Summit']"and saved as a row. Strings only now.Front-end review (fourth commit)
A frontend-reviewer pass drove the page headless and measured. Fixed:
TypeError: Failed to fetch. Now beside the buttons,role="alert", plain sentences.Each verified in the browser afterwards. Not changed, deliberately: no dialog in the app traps focus or sets
inert(an app-wide pattern, not this picker's), and.btn-grntext contrast is shared with Create — both worth their own change.Not covered
app.loglogsMedia bin: N of M items got media from K folder(s), <source>: ....Checks: 1357 passed, 1 skipped.
🤖 Generated with Claude Code