Metadata-Version: 2.4
Name: marquee-ai
Version: 1.1.0
Summary: Marquee -- Plex-style recommendation rows for a media server library
Author: KernelMedia
License: Apache-2.0
Project-URL: Homepage, https://huggingface.co/KernelMedia
Keywords: recommendations,media-server,plex,jellyfin,llm
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Multimedia :: Video
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.24
Requires-Dist: requests>=2.31
Requires-Dist: sentence-transformers>=3.0
Provides-Extra: api
Requires-Dist: fastapi>=0.110; extra == "api"
Requires-Dist: uvicorn>=0.27; extra == "api"
Requires-Dist: pydantic>=2.6; extra == "api"
Provides-Extra: harvest
Requires-Dist: tqdm>=4.66; extra == "harvest"

# Marquee

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
```

## Two halves, both required

A **retriever** decides what goes in the rows, by joining your library on shared cast,
crew and franchise and by embedding similarity. A small fine-tuned **model** decides
which rows are worth showing, their order, and what to call them.

The model never 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.

## Install

```bash
pip install marquee-ai
```

Plus [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
```

That is the whole setup.

```bash
curl -X POST localhost:8080/recommend \
  -H 'Content-Type: application/json' \
  -d '{"title": "The Sopranos", "row_size": 20}'
```

Or from the command line:

```bash
marquee recommend --library catalog.jsonl --seed "Blade Runner 2049" --row-size 20
marquee person  --library catalog.jsonl --name "Roger Deakins"
```

## 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.


## Use it as a library

The HTTP API is optional. If your server is Python, skip it:

```python
from marquee import Library
from marquee.contract import messages, parse, repair, expand_rows

lib = Library.load("lib.jsonl")
lib.build_embeddings(cache=Path("lib.emb.npz"))     # once at startup

request = lib.build_request(seed_id, semantic_k=14, row_size=20)
raw = call_your_llm(messages(request))              # any OpenAI-compatible endpoint
rows = expand_rows(repair(parse(raw), request), request, 20)
```

## Requirements

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

Full setup and troubleshooting: [SETUP.md](https://huggingface.co/KernelMedia/marquee-ai/blob/main/SETUP.md)

## Data

The catalog is built from Wikidata (CC0) and Wikipedia (summaries, embedded then
discarded -- never redistributed as text). No TMDB data is shipped.

## Licence

Apache-2.0.
