# Marquee Recommendation rows for a media server. ## Install ```bash pip install marquee-ai ``` You also need [Ollama](https://ollama.com) to run the model. ## Download ```bash hf download KernelMedia/marquee-ai --local-dir marquee cd marquee ollama create marquee -f Modelfile ``` ~3 GB: the model, plus a catalog of ~195,000 films and shows with full cast and crew. ## Run ```bash marquee serve --library catalog.jsonl --port 8080 ``` Loads in about 15 seconds. ## API ### `POST /recommend` ```bash curl -X POST localhost:8080/recommend -H 'Content-Type: application/json' \ -d '{"title": "The Sopranos", "row_size": 20}' ``` ```json { "seed": {"id": "wd:Q23628", "title": "The Sopranos", "year": 1999, "media_type": "tv", "rating": 0.0, "owned": true}, "rows": [ {"type": "thematic", "title": "Shows like The Sopranos", "items": [ {"id": "wd:Q...", "title": "Gomorrah", "year": 2014, "media_type": "tv", "owned": true}, {"id": "wd:Q...", "title": "Boardwalk Empire", "year": 2010, "media_type": "tv", "owned": true} ]}, {"type": "director", "title": "More from Tim Van Patten", "items": [ {"id": "wd:Q...", "title": "Game of Thrones", "year": 2011, "media_type": "tv", "owned": true} ]} ] } ``` Body: `seed_id` **or** `title`, plus optional `year`, `media_type`, `row_size`, `owned_only`, `semantic_k`. Rows arrive in a fixed order -- thematic, franchise, director, cast, crew. An ambiguous title returns **409** with the candidates rather than guessing. A catalog this size holds many remakes sharing a bare title: ```json {"error": "22 items match 'Macbeth'; pass seed_id, or narrow with year/media_type", "matches": [ {"id": "wd:Q15934383", "title": "Macbeth", "year": 2015, "media_type": "movie"}, {"id": "wd:Q2573008", "title": "Macbeth", "year": 1951, "media_type": "movie"} ]} ``` `{"title": "Macbeth", "year": 2015}` or `{"title": "Macbeth (2015)"}` resolves it. ### `GET /metadata` and `POST /metadata/batch` Full catalog record for one item, or up to 500 in one call. Pure index lookups -- no model, no GPU. Look up by `id`, `tmdb_id`, `imdb_id` (all exact), or `title` (fuzzy, can be ambiguous like `/recommend`). ```bash curl 'localhost:8080/metadata?tmdb_id=335984' ``` ```json { "id": "wd:Q21500755", "title": "Blade Runner 2049", "media_type": "movie", "year": 2017, "genres": ["cyberpunk", "dystopian film", "neo-noir"], "people": [{"name": "Denis Villeneuve", "role": "director"}, {"name": "Roger Deakins", "role": "cinematographer"}], "summary": "Blade Runner 2049 is a 2017 American science fiction film...", "franchise": "Blade Runner", "runtime_min": 164, "country": "United Kingdom", "language": "English", "external": {"wikidata": "Q21500755", "tmdb": "335984", "imdb": "tt1856101"} } ``` ```bash curl -X POST localhost:8080/metadata/batch -H 'Content-Type: application/json' -d '{ "queries": [{"tmdb_id": 335984}, {"imdb_id": "tt0141842"}] }' ``` Every query gets a result in the order sent -- found, ambiguous with candidates, or not found -- so you can zip the response against your input. ### `GET /search?q=` and `GET /person?name=` `/search` matches titles **and** people in one call -- "Scorsese" isn't a title but has 109 credits. `/person` returns everything someone worked on, films and shows together, ranked by a popularity-weighted score, deduplicated across roles. Both are index lookups, no model call. ```bash curl 'localhost:8080/person?name=Roger%20Deakins&limit=3' ``` ```json {"person": "Roger Deakins", "count": 3, "credits": [ {"id": "wd:...", "title": "The Shawshank Redemption", "year": 1994, "score": 8.66, "roles": ["cinematographer"]} ]} ``` `score` is what ranking actually uses -- popularity-weighted, so a well-known film outranks an obscure one even without a raw rating to sort by. ### `GET /health` and `DELETE /cache` ```json {"status": "ok", "contract_version": 1, "model_self_test": {"passed": true, "detail": "contract v1 OK (3/3 clean)"}, "library_items": 194509, "owned": 194509, "requestable": 0} ``` `status` is `degraded` if the self-test fails -- model and package disagree, or the chat template is wrong. Recommendations are cached per seed; call `DELETE /cache` after a library rescan or you'll serve rows referring to items you no longer have. ## Restricting results to your own library (optional) By default it recommends from the whole 195k-title catalog, whether or not you own it. To mark what is actually on your server -- so a UI can show a Request button on the rest -- export your library and join it: ```bash marquee ingest --server library.jsonl --catalog catalog.jsonl \ --out lib.jsonl --mode requestable marquee serve --library lib.jsonl ``` Your export needs one JSON object per line; `tmdb_id` and `media_type` are enough: ```json {"id": "myserver-04471", "tmdb_id": 335984, "media_type": "movie"} ``` Every item then carries `"owned": true|false`. Pass `"owned_only": true` on a request to drop the ones you do not have: ```bash curl -X POST localhost:8080/recommend -H 'Content-Type: application/json' \ -d '{"seed_id": "myserver-04471", "row_size": 20, "owned_only": true}' ``` `/health` reports the split: ```json {"library_items": 194509, "owned": 1670, "requestable": 192839} ``` **Jellyfin**: `/Items?Recursive=true&Fields=ProviderIds` gives `ProviderIds.Tmdb`. **Plex**: `/library/sections/{id}/all` -- the `guid` carries the TMDB id on recent agents. ## Requirements | | | |---|---| | VRAM | ~4.7 GB (Q6_K at 6144 context). A 6 GB card is fine; CPU works but is slower. | | RAM | ~2.5 GB for the 195k-title catalog | | Disk | ~3.6 GB | | Latency | ~1 s per request on an RTX 3060; cached responses are instant | ## Troubleshooting **Every row comes back typed `director`, titles look like item names.** The chat template is missing. Ollama ignores the template inside GGUF metadata and falls back to a raw passthrough, so the model never sees the tokens it was trained on: ```bash ollama show --template marquee # must NOT be just "{{ .Prompt }}" ``` Recreate from the supplied `Modelfile`, and do not remove its `TEMPLATE` block. **Output is valid sometimes and not others.** Two independent causes, both already fixed in the shipped model/Modelfile: - `temperature` too high -- structured output needs near-greedy decoding. 0.1 gave 8/8 clean in testing, 0.3 gave 5/8. The Modelfile ships 0.1. - Quantizing below Q6_K measurably hurts this model: on 20 held-out seeds through real Ollama serving, Q4_K_M gave 1/20 clean vs Q6_K's 19/20, same weights, same decoding -- llama.cpp's 4-bit rounding alone is enough to collapse it into repeating `` tokens instead of JSON. Don't requantize to Q4_K_M to save disk space. **Self-test fails at startup.** The model and the package are built against different contract versions. Both must be contract v1. **A title I own is missing.** The catalog covers ~195k titles from Wikidata. Genuinely obscure releases may not be there. `marquee harvest-wikidata --min-votes 0` (if rebuilding) widens coverage. ## Data The catalog is built from Wikidata (CC0) and Wikipedia (summaries, embedded then discarded -- never redistributed as text). No TMDB data is shipped. No poster/image URLs are included -- text and structured facts only. Pull artwork from your media server, or from TMDB/Fanart yourself using the `external.tmdb`/ `external.imdb` ids returned by `/metadata`. ## Licence Apache-2.0.