Marquee

v1.1 ยท contract v1 ยท base Qwen3-4B-Instruct-2507

Turns one item in a media library into Plex-style rows of other items in that same library. Built for self-hosted media servers.

IN:  "The Sopranos"

OUT:
  Shows like The Sopranos      Gomorrah, Boardwalk Empire, The Wire, ZeroZeroZero
  More from Tim Van Patten     Game of Thrones, Black Mirror, Boardwalk Empire
  More with Edie Falco         Nurse Jackie, Oz
  Shot by Alik Sakharov        Game of Thrones, House of Cards

Read this before downloading the weights

The model alone does nothing useful. It decides which rows to show, their order, and what to call them. It does not choose what goes in them -- a retriever does, by joining your library on shared cast, crew and franchise, and by embedding similarity.

The model never even sees item ids. It answers with index references into candidates it was handed, so it cannot invent a title that isn't in your library. Hallucination isn't low here, it's structurally impossible.

pip install marquee-ai

Download

hf download KernelMedia/marquee-ai --local-dir marquee
cd marquee && ollama create marquee -f Modelfile

~3.6 GB: the model plus a catalog of ~195,000 films and shows with full cast and crew.

Requirements

VRAM ~4.7 GB (Q6_K at 6144 context). Runs on a 6GB card, or CPU.
RAM ~2.5 GB for the 195k-title catalog index
Disk ~3.3 GB model + ~240 MB catalog + ~300 MB embeddings
Latency ~1s per request on an RTX 3060; cached responses are instant

Quickstart

marquee serve --library catalog.jsonl --port 8080

That's the whole setup -- catalog and embeddings ship precomputed.

API

POST /recommend

curl -X POST localhost:8080/recommend -H 'Content-Type: application/json' \
  -d '{"title": "The Sopranos", "row_size": 20}'
{
  "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:

{"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).

curl 'localhost:8080/metadata?tmdb_id=335984'
{
  "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"}
}
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.

curl 'localhost:8080/person?name=Roger%20Deakins&limit=3'
{"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

{"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 catalog. To mark what's actually on your server, so a UI can show a Request button on the rest:

marquee ingest --server library.jsonl --catalog catalog.jsonl \
               --out lib.jsonl --mode requestable
marquee serve --library lib.jsonl

Your export needs one line per item; tmdb_id and media_type are enough:

{"id": "myserver-04471", "tmdb_id": 335984, "media_type": "movie"}

Evaluation

50 unseen holdout examples, run through plain transformers.generate with no grammar constraint, on the unmerged adapter -- so this measures the weights, not a decoder forcing shape, and not the quantization the GGUF ships with.

Metric Base Qwen3-4B Marquee
JSON parses -- 100%
Fully contract-clean -- 100%
Hallucinated ids -- 0.0%
Row-order violations -- 0%

That number does not carry over unchanged to the shipped GGUF: merging a LoRA adapter and quantizing it are both lossy steps this harness doesn't exercise. Measured end-to-end through actual Ollama serving (20 held-out seeds, real greedy decoding) -- Q6_K: 19/20 clean. See Limitations below for what pushed us to Q6_K over a smaller quant.

What this is, honestly: the fine-tune buys format discipline, not taste. The retriever picks every item; the model decides which rows to show and writes one row title. It does not make recommendations smarter on its own -- that lives in the retriever's embeddings and joins.

Training

  • Base: Qwen3-4B-Instruct-2507, full-precision LoRA (bf16 base, not QLoRA) r=16, lr 2e-4, 1 epoch, 3,684 examples
  • Data: generated by Qwen3-8B-AWQ over the catalog below, normalised by a deterministic repair pass. 0.4% rejected.
  • Hardware: one RTX 3060 12GB, ~2h48m, 10.2 GB peak VRAM
  • Targets are ~30 tokens: the model emits index references, not item lists
  • Why full precision, not QLoRA: a QLoRA adapter is calibrated against its 4-bit base's numerical behaviour. Merging it into full-precision weights for GGUF export bakes in a dequantized representation the adapter never saw, which destabilized this model badly enough to periodically emit <tool_call> instead of JSON. Training directly on real bf16 weights removes that mismatch.

Limitations

  • Thematic rows are the weakest part. Built from bge-small embeddings blended with keyword and genre overlap; ~31% of the catalog has no plot summary to embed, so those titles lean on genre/subject tags alone.
  • Franchise coverage is thin (~2%) compared to a TMDB-sourced catalog -- series membership is sparse in the source data.
  • Same-medium only. A show returns shows, a film returns films.
  • Library-size sensitive. A director with one other credit present gets no row. Deliberate -- no padding.
  • Movies and TV only. English metadata.
  • No poster/image data. The catalog is text and structured facts only; nothing in it or the API returns image URLs. Pull artwork from your media server, or from TMDB/Fanart yourself using the external.tmdb/external.imdb ids in /metadata.

โš ๏ธ GGUF: the chat template is not optional

Ollama will not use the Jinja template embedded in GGUF metadata and falls back to a bare {{ .Prompt }} passthrough, so the model never receives the <|im_start|> markers it was trained on and reverts to base-model behaviour. The supplied Modelfile carries an explicit ChatML TEMPLATE block. Do not remove it.

Two things degrade output independently, and both are already handled in the shipped Modelfile/GGUF:

  • temperature must stay low -- 0.1 gave 8/8 clean over 8 test seeds without a grammar, 0.3 gave 5/8. The Modelfile ships 0.1.
  • Don't requantize below Q6_K. llama.cpp's Q4_K_M rounding alone -- independent of temperature, independent of the LoRA-merge issue above -- was enough to collapse this model into repeating <tool_call> tokens instead of JSON on most requests (1/20 clean vs Q6_K's 19/20, same weights, same greedy decoding, only the quant level changed). Q6_K is the smallest quant confirmed clean.

Verify any deployment by calling it without a JSON schema. A working fine-tune emits valid contract JSON unprompted. marquee serve runs this check at startup.

Data

The catalog is built entirely from Wikidata (CC0) and Wikipedia (plot summaries, embedded then discarded -- never redistributed as text, so CC BY-SA's share-alike terms are never triggered). No TMDB data is shipped or was used in training.

Not used: TMDB (API terms prohibit ML training and third-party redistribution of cached content), IMDb (non-transferable, non-sublicensable licence), Rotten Tomatoes (no public API).

Licence

Apache-2.0, inherited from Qwen3-4B-Instruct-2507. Catalog: CC0.

Downloads last month
10
GGUF
Model size
4B params
Architecture
qwen3
Hardware compatibility
Log In to add your hardware

6-bit

Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support

Model tree for KernelMedia/marquee-ai

Quantized
(32)
this model