flickpick 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flickpick-0.0.2/.gitattributes +1 -0
- flickpick-0.0.2/.gitignore +121 -0
- flickpick-0.0.2/LICENSE +21 -0
- flickpick-0.0.2/PKG-INFO +75 -0
- flickpick-0.0.2/README.md +49 -0
- flickpick-0.0.2/docs/architecture.md +88 -0
- flickpick-0.0.2/docs/artifact-format.md +24 -0
- flickpick-0.0.2/docs/core-contract.md +65 -0
- flickpick-0.0.2/docs/research/README.md +18 -0
- flickpick-0.0.2/docs/research/brand-name-report.md +489 -0
- flickpick-0.0.2/docs/research/browser-first-feasibility-licensing-agents-prior-art.md +297 -0
- flickpick-0.0.2/docs/research/inhouse-inventory.md +116 -0
- flickpick-0.0.2/docs/research/movie-posters-and-media-sources.md +166 -0
- flickpick-0.0.2/docs/research/movie-recommender-research-synthesis.md +296 -0
- flickpick-0.0.2/docs/research/original-request.md +33 -0
- flickpick-0.0.2/docs/research/prompts/browser-ml-and-extras.md +18 -0
- flickpick-0.0.2/docs/research/prompts/posters.md +14 -0
- flickpick-0.0.2/docs/research/prompts/recsys.md +16 -0
- flickpick-0.0.2/docs/research/prompts/semantic-data-and-features.md +16 -0
- flickpick-0.0.2/docs/research/prompts/streaming-and-ratings-apis.md +15 -0
- flickpick-0.0.2/docs/research/recsys-methods-libraries-evaluation.md +310 -0
- flickpick-0.0.2/docs/research/semantic-movie-data-and-filter-features.md +228 -0
- flickpick-0.0.2/docs/research/streaming-availability-and-ratings-apis.md +206 -0
- flickpick-0.0.2/flickpick/__init__.py +108 -0
- flickpick-0.0.2/flickpick/__main__.py +15 -0
- flickpick-0.0.2/flickpick/artifacts.py +264 -0
- flickpick-0.0.2/flickpick/build.py +180 -0
- flickpick-0.0.2/flickpick/data/examples/movie_ratings_various.csv +117 -0
- flickpick-0.0.2/flickpick/data.py +129 -0
- flickpick-0.0.2/flickpick/defaults.py +67 -0
- flickpick-0.0.2/flickpick/ease.py +99 -0
- flickpick-0.0.2/flickpick/embedding.py +82 -0
- flickpick-0.0.2/flickpick/importers.py +188 -0
- flickpick-0.0.2/flickpick/ratings.py +166 -0
- flickpick-0.0.2/flickpick/science.py +351 -0
- flickpick-0.0.2/flickpick/score.py +367 -0
- flickpick-0.0.2/flickpick/tools.py +207 -0
- flickpick-0.0.2/js/.gitignore +3 -0
- flickpick-0.0.2/js/package-lock.json +3114 -0
- flickpick-0.0.2/js/package.json +79 -0
- flickpick-0.0.2/js/scripts/prepare.mjs +20 -0
- flickpick-0.0.2/js/src/artifacts.ts +267 -0
- flickpick-0.0.2/js/src/cli.ts +159 -0
- flickpick-0.0.2/js/src/defaults.ts +111 -0
- flickpick-0.0.2/js/src/embed.ts +102 -0
- flickpick-0.0.2/js/src/importers.ts +330 -0
- flickpick-0.0.2/js/src/index.ts +19 -0
- flickpick-0.0.2/js/src/science.ts +395 -0
- flickpick-0.0.2/js/src/scoring.ts +356 -0
- flickpick-0.0.2/js/src/shims.d.ts +7 -0
- flickpick-0.0.2/js/src/types.ts +160 -0
- flickpick-0.0.2/js/test/artifacts.test.ts +76 -0
- flickpick-0.0.2/js/test/cli.test.ts +84 -0
- flickpick-0.0.2/js/test/embed.test.ts +55 -0
- flickpick-0.0.2/js/test/fixture.test.ts +90 -0
- flickpick-0.0.2/js/test/helpers/synthetic.ts +130 -0
- flickpick-0.0.2/js/test/importers.test.ts +181 -0
- flickpick-0.0.2/js/test/science.test.ts +183 -0
- flickpick-0.0.2/js/test/scoring.test.ts +152 -0
- flickpick-0.0.2/js/tsconfig.json +21 -0
- flickpick-0.0.2/js/tsup.config.ts +15 -0
- flickpick-0.0.2/js/vitest.config.ts +10 -0
- flickpick-0.0.2/pyproject.toml +180 -0
- flickpick-0.0.2/tests/conftest.py +31 -0
- flickpick-0.0.2/tests/fixtures/artifacts_small/LICENSE-DATA.md +7 -0
- flickpick-0.0.2/tests/fixtures/artifacts_small/catalog.json +1 -0
- flickpick-0.0.2/tests/fixtures/artifacts_small/cf_indices.i32 +0 -0
- flickpick-0.0.2/tests/fixtures/artifacts_small/cf_indptr.i32 +0 -0
- flickpick-0.0.2/tests/fixtures/artifacts_small/cf_values.f32 +0 -0
- flickpick-0.0.2/tests/fixtures/artifacts_small/embeddings.f32 +0 -0
- flickpick-0.0.2/tests/fixtures/artifacts_small/manifest.json +60 -0
- flickpick-0.0.2/tests/fixtures/make_fixtures.py +149 -0
- flickpick-0.0.2/tests/fixtures/parity/LICENSE-DATA.md +3 -0
- flickpick-0.0.2/tests/fixtures/parity/expected_evaluate.json +525 -0
- flickpick-0.0.2/tests/fixtures/parity/expected_recommend.json +1077 -0
- flickpick-0.0.2/tests/test_artifacts.py +123 -0
- flickpick-0.0.2/tests/test_ease.py +50 -0
- flickpick-0.0.2/tests/test_import.py +33 -0
- flickpick-0.0.2/tests/test_importers.py +173 -0
- flickpick-0.0.2/tests/test_science.py +174 -0
- flickpick-0.0.2/tests/test_score.py +164 -0
- flickpick-0.0.2/web/.gitignore +6 -0
- flickpick-0.0.2/web/.npmrc +1 -0
- flickpick-0.0.2/web/README.md +48 -0
- flickpick-0.0.2/web/index.html +14 -0
- flickpick-0.0.2/web/package-lock.json +4119 -0
- flickpick-0.0.2/web/package.json +56 -0
- flickpick-0.0.2/web/scripts/build-core.mjs +40 -0
- flickpick-0.0.2/web/scripts/prepare-data.mjs +30 -0
- flickpick-0.0.2/web/src/App.test.tsx +69 -0
- flickpick-0.0.2/web/src/App.tsx +134 -0
- flickpick-0.0.2/web/src/assets/tmdb-logo.svg +1 -0
- flickpick-0.0.2/web/src/charts/agreement-chart.tsx +123 -0
- flickpick-0.0.2/web/src/charts/chart-frame.tsx +103 -0
- flickpick-0.0.2/web/src/charts/interval-chart.tsx +97 -0
- flickpick-0.0.2/web/src/charts/scale.ts +21 -0
- flickpick-0.0.2/web/src/commands/commands.test.ts +157 -0
- flickpick-0.0.2/web/src/commands/deps.ts +44 -0
- flickpick-0.0.2/web/src/commands/index.ts +449 -0
- flickpick-0.0.2/web/src/commands/registry.ts +24 -0
- flickpick-0.0.2/web/src/components/artifacts-gate.tsx +48 -0
- flickpick-0.0.2/web/src/components/movie-card.tsx +86 -0
- flickpick-0.0.2/web/src/components/poster.tsx +88 -0
- flickpick-0.0.2/web/src/components/rating-control.tsx +77 -0
- flickpick-0.0.2/web/src/components/route-link.tsx +22 -0
- flickpick-0.0.2/web/src/components/tmdb-attribution.tsx +23 -0
- flickpick-0.0.2/web/src/defaults.ts +114 -0
- flickpick-0.0.2/web/src/embed/client.ts +75 -0
- flickpick-0.0.2/web/src/embed/embed.worker.ts +64 -0
- flickpick-0.0.2/web/src/embed/protocol.ts +18 -0
- flickpick-0.0.2/web/src/index.css +145 -0
- flickpick-0.0.2/web/src/main.tsx +21 -0
- flickpick-0.0.2/web/src/pages/about-page.tsx +66 -0
- flickpick-0.0.2/web/src/pages/movie-page.tsx +226 -0
- flickpick-0.0.2/web/src/pages/ratings-page.tsx +278 -0
- flickpick-0.0.2/web/src/pages/recommend-page.tsx +279 -0
- flickpick-0.0.2/web/src/pages/science-page.tsx +247 -0
- flickpick-0.0.2/web/src/pages/settings-page.tsx +120 -0
- flickpick-0.0.2/web/src/route.test.ts +93 -0
- flickpick-0.0.2/web/src/route.ts +136 -0
- flickpick-0.0.2/web/src/settings/schema.test.ts +63 -0
- flickpick-0.0.2/web/src/settings/schema.ts +95 -0
- flickpick-0.0.2/web/src/sources/tmdb.ts +129 -0
- flickpick-0.0.2/web/src/state/hooks.tsx +41 -0
- flickpick-0.0.2/web/src/state/providers.ts +56 -0
- flickpick-0.0.2/web/src/state/recommend-form.ts +84 -0
- flickpick-0.0.2/web/src/state/schemas.ts +92 -0
- flickpick-0.0.2/web/src/state/store.ts +77 -0
- flickpick-0.0.2/web/src/test-helpers/services.ts +58 -0
- flickpick-0.0.2/web/src/test-setup.ts +15 -0
- flickpick-0.0.2/web/src/ui/button.tsx +34 -0
- flickpick-0.0.2/web/src/ui/card.tsx +34 -0
- flickpick-0.0.2/web/src/ui/chip.tsx +25 -0
- flickpick-0.0.2/web/src/ui/edit-buffer.ts +32 -0
- flickpick-0.0.2/web/src/ui/feedback.tsx +67 -0
- flickpick-0.0.2/web/src/ui/ghost-slider.tsx +95 -0
- flickpick-0.0.2/web/src/ui/input.tsx +15 -0
- flickpick-0.0.2/web/src/ui/switch.tsx +37 -0
- flickpick-0.0.2/web/tsconfig.app.json +30 -0
- flickpick-0.0.2/web/tsconfig.json +4 -0
- flickpick-0.0.2/web/tsconfig.node.json +21 -0
- flickpick-0.0.2/web/vite.config.ts +33 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.ipynb linguist-documentation
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
.claude/handoffs/
|
|
2
|
+
.claude/scratch/
|
|
3
|
+
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
.DS_Store
|
|
11
|
+
# C extensions
|
|
12
|
+
*.so
|
|
13
|
+
|
|
14
|
+
# TLS certificates
|
|
15
|
+
## Ignore all PEM files anywhere
|
|
16
|
+
*.pem
|
|
17
|
+
## Also ignore any certs directory
|
|
18
|
+
certs/
|
|
19
|
+
|
|
20
|
+
# Distribution / packaging
|
|
21
|
+
.Python
|
|
22
|
+
build/
|
|
23
|
+
develop-eggs/
|
|
24
|
+
dist/
|
|
25
|
+
downloads/
|
|
26
|
+
eggs/
|
|
27
|
+
.eggs/
|
|
28
|
+
lib/
|
|
29
|
+
lib64/
|
|
30
|
+
parts/
|
|
31
|
+
sdist/
|
|
32
|
+
var/
|
|
33
|
+
wheels/
|
|
34
|
+
*.egg-info/
|
|
35
|
+
.installed.cfg
|
|
36
|
+
*.egg
|
|
37
|
+
MANIFEST
|
|
38
|
+
_build
|
|
39
|
+
|
|
40
|
+
# PyInstaller
|
|
41
|
+
# Usually these files are written by a python script from a template
|
|
42
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
43
|
+
*.manifest
|
|
44
|
+
*.spec
|
|
45
|
+
|
|
46
|
+
# Installer logs
|
|
47
|
+
pip-log.txt
|
|
48
|
+
pip-delete-this-directory.txt
|
|
49
|
+
|
|
50
|
+
# Unit test / coverage reports
|
|
51
|
+
htmlcov/
|
|
52
|
+
.tox/
|
|
53
|
+
.coverage
|
|
54
|
+
.coverage.*
|
|
55
|
+
.cache
|
|
56
|
+
nosetests.xml
|
|
57
|
+
coverage.xml
|
|
58
|
+
*.cover
|
|
59
|
+
.hypothesis/
|
|
60
|
+
.pytest_cache/
|
|
61
|
+
|
|
62
|
+
# Translations
|
|
63
|
+
*.mo
|
|
64
|
+
*.pot
|
|
65
|
+
|
|
66
|
+
# Django stuff:
|
|
67
|
+
*.log
|
|
68
|
+
local_settings.py
|
|
69
|
+
db.sqlite3
|
|
70
|
+
|
|
71
|
+
# Flask stuff:
|
|
72
|
+
instance/
|
|
73
|
+
.webassets-cache
|
|
74
|
+
|
|
75
|
+
# Scrapy stuff:
|
|
76
|
+
.scrapy
|
|
77
|
+
|
|
78
|
+
# Sphinx documentation
|
|
79
|
+
docs/_build/
|
|
80
|
+
|
|
81
|
+
# PyBuilder
|
|
82
|
+
target/
|
|
83
|
+
|
|
84
|
+
# Jupyter Notebook
|
|
85
|
+
.ipynb_checkpoints
|
|
86
|
+
|
|
87
|
+
# pyenv
|
|
88
|
+
.python-version
|
|
89
|
+
|
|
90
|
+
# celery beat schedule file
|
|
91
|
+
celerybeat-schedule
|
|
92
|
+
|
|
93
|
+
# SageMath parsed files
|
|
94
|
+
*.sage.py
|
|
95
|
+
|
|
96
|
+
# Environments
|
|
97
|
+
.env
|
|
98
|
+
.venv
|
|
99
|
+
env/
|
|
100
|
+
venv/
|
|
101
|
+
ENV/
|
|
102
|
+
env.bak/
|
|
103
|
+
venv.bak/
|
|
104
|
+
|
|
105
|
+
# Spyder project settings
|
|
106
|
+
.spyderproject
|
|
107
|
+
.spyproject
|
|
108
|
+
|
|
109
|
+
# Rope project settings
|
|
110
|
+
.ropeproject
|
|
111
|
+
|
|
112
|
+
# mkdocs documentation
|
|
113
|
+
/site
|
|
114
|
+
|
|
115
|
+
# mypy
|
|
116
|
+
.mypy_cache/
|
|
117
|
+
|
|
118
|
+
# PyCharm
|
|
119
|
+
.idea
|
|
120
|
+
|
|
121
|
+
web/public/data/
|
flickpick-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thor Whalen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
flickpick-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: flickpick
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Personal movie recommender: browser-first app, TypeScript core, Python build pipeline; AI agents pick movies for you
|
|
5
|
+
Project-URL: Homepage, https://github.com/thorwhalen/flickpick
|
|
6
|
+
Project-URL: Repository, https://github.com/thorwhalen/flickpick
|
|
7
|
+
Project-URL: Documentation, https://thorwhalen.github.io/flickpick
|
|
8
|
+
Author: Thor Whalen
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: cw<0.2,>=0.1
|
|
13
|
+
Requires-Dist: numpy>=1.24
|
|
14
|
+
Requires-Dist: pandas>=2.0
|
|
15
|
+
Requires-Dist: scipy>=1.10
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
20
|
+
Provides-Extra: docs
|
|
21
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
|
|
22
|
+
Requires-Dist: sphinx>=6.0; extra == 'docs'
|
|
23
|
+
Provides-Extra: embed
|
|
24
|
+
Requires-Dist: sentence-transformers>=2.2; extra == 'embed'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# flickpick
|
|
28
|
+
|
|
29
|
+
A personal movie recommender you can run in a browser, built so that AI agents can pick movies for you. It learns from your own ratings, scores the whole catalogue client-side, lets you ask for a mood ("slow-burn melancholic sci-fi, nothing gory"), and shows where a film streams and how the public rated it. A "for scientists" page compares your taste with the population's and measures how well the recommender does on your own history.
|
|
30
|
+
|
|
31
|
+
## Try it in two commands
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install "flickpick[embed]"
|
|
35
|
+
python -m flickpick build --sample --out ./artifacts # a small artifact set from MovieLens ml-latest-small
|
|
36
|
+
python -m flickpick recommend --artifacts ./artifacts --ratings my_ratings.csv --mood "slow-burn melancholic sci-fi" --exclude-genre Horror
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`my_ratings.csv` can be a Letterboxd, IMDb or MovieLens export, or flickpick's own format (see `flickpick/data/examples/movie_ratings_various.csv`). Drop `--sample` to build from MovieLens ml-32m (about 10,000 films; a few minutes). Drop `[embed]` and add `--no-embeddings` if you do not want mood search.
|
|
40
|
+
|
|
41
|
+
The same core exists in TypeScript (`js/`, npm `flickpick`) and runs in the browser; the web app is in `web/`. From a fresh clone (Node 20 or later):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd js && npm ci && npm test && npm run build # the TypeScript core; CLI: node dist/cli.js recommend --artifacts ../tests/fixtures/artifacts_small --ratings <csv>
|
|
45
|
+
cd web && npm install && npm run dev # the web app; dev, build and test build ../js first when it is missing or stale
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
- **Offline build (Python).** MovieLens ratings train an EASE item-item model (a closed-form linear recommender that matches or beats most neural models on this kind of data), sparsified to the top neighbours per film. Each film's genres and community tags are embedded with a small sentence-embedding model. Everything is written as plain typed-array files plus JSON (`docs/artifact-format.md`).
|
|
51
|
+
- **Scoring (TypeScript in the browser, or Python).** Your liked films select rows of the item-item matrix; their sum is your collaborative score for every film. A mood is embedded on the fly and compared with every film by cosine. Structured filters apply, the components are rank-normalised and combined, and each result carries its reasons ("because you liked …", "close to …").
|
|
52
|
+
- **The LLM, when you add one, parses and explains. It never ranks.** The ranking is deterministic and inspectable.
|
|
53
|
+
- **Science.** Hold-out evaluation of the recommender on your own ratings with confidence intervals; agreement statistics between your scores and the population's; cross-validated fits.
|
|
54
|
+
|
|
55
|
+
The design decisions and the research behind them are in `docs/architecture.md` and `docs/research/` (start with the synthesis).
|
|
56
|
+
|
|
57
|
+
## Data and licensing
|
|
58
|
+
|
|
59
|
+
Shipped artifacts are built only from openly usable sources: MovieLens (research, non-commercial, share-alike on transformations), Wikidata (CC0) and Wikipedia (CC BY-SA) in later versions. Posters, overviews, certifications and streaming availability come live from TMDB (and JustWatch through TMDB) with **your own API key**, are shown for display only, are cached locally for at most six months, and are never fed to the recommender or to an LLM, because TMDB's API terms exclude ML/AI applications. This product uses the TMDB API but is not endorsed or certified by TMDB.
|
|
60
|
+
|
|
61
|
+
Your ratings stay in your browser (or in the file you point the CLI at) unless you export them.
|
|
62
|
+
|
|
63
|
+
## Layout
|
|
64
|
+
|
|
65
|
+
| Path | What |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `flickpick/` | Python package: build pipeline, EASE, scorer, importers, science, CLI (`python -m flickpick`) |
|
|
68
|
+
| `js/` | TypeScript core (npm `flickpick`): the same scorer, importers and science for browser and Node, with a CLI |
|
|
69
|
+
| `web/` | Vite + React web app over `js/` |
|
|
70
|
+
| `tests/` | Python tests and the small fixture artifact set |
|
|
71
|
+
| `docs/` | architecture, artifact format, core contract, research |
|
|
72
|
+
|
|
73
|
+
## Status
|
|
74
|
+
|
|
75
|
+
v1: Python and TypeScript cores with parity tests, CLI on both, the web app, MovieLens-only catalogue. Next: Wikidata and Wikipedia catalogue, LLM "vibe profiles", MCP server and in-app assistant, remote MCP for claude.ai connectors, int8 artifacts, a published data package.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# flickpick
|
|
2
|
+
|
|
3
|
+
A personal movie recommender you can run in a browser, built so that AI agents can pick movies for you. It learns from your own ratings, scores the whole catalogue client-side, lets you ask for a mood ("slow-burn melancholic sci-fi, nothing gory"), and shows where a film streams and how the public rated it. A "for scientists" page compares your taste with the population's and measures how well the recommender does on your own history.
|
|
4
|
+
|
|
5
|
+
## Try it in two commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install "flickpick[embed]"
|
|
9
|
+
python -m flickpick build --sample --out ./artifacts # a small artifact set from MovieLens ml-latest-small
|
|
10
|
+
python -m flickpick recommend --artifacts ./artifacts --ratings my_ratings.csv --mood "slow-burn melancholic sci-fi" --exclude-genre Horror
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`my_ratings.csv` can be a Letterboxd, IMDb or MovieLens export, or flickpick's own format (see `flickpick/data/examples/movie_ratings_various.csv`). Drop `--sample` to build from MovieLens ml-32m (about 10,000 films; a few minutes). Drop `[embed]` and add `--no-embeddings` if you do not want mood search.
|
|
14
|
+
|
|
15
|
+
The same core exists in TypeScript (`js/`, npm `flickpick`) and runs in the browser; the web app is in `web/`. From a fresh clone (Node 20 or later):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd js && npm ci && npm test && npm run build # the TypeScript core; CLI: node dist/cli.js recommend --artifacts ../tests/fixtures/artifacts_small --ratings <csv>
|
|
19
|
+
cd web && npm install && npm run dev # the web app; dev, build and test build ../js first when it is missing or stale
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
- **Offline build (Python).** MovieLens ratings train an EASE item-item model (a closed-form linear recommender that matches or beats most neural models on this kind of data), sparsified to the top neighbours per film. Each film's genres and community tags are embedded with a small sentence-embedding model. Everything is written as plain typed-array files plus JSON (`docs/artifact-format.md`).
|
|
25
|
+
- **Scoring (TypeScript in the browser, or Python).** Your liked films select rows of the item-item matrix; their sum is your collaborative score for every film. A mood is embedded on the fly and compared with every film by cosine. Structured filters apply, the components are rank-normalised and combined, and each result carries its reasons ("because you liked …", "close to …").
|
|
26
|
+
- **The LLM, when you add one, parses and explains. It never ranks.** The ranking is deterministic and inspectable.
|
|
27
|
+
- **Science.** Hold-out evaluation of the recommender on your own ratings with confidence intervals; agreement statistics between your scores and the population's; cross-validated fits.
|
|
28
|
+
|
|
29
|
+
The design decisions and the research behind them are in `docs/architecture.md` and `docs/research/` (start with the synthesis).
|
|
30
|
+
|
|
31
|
+
## Data and licensing
|
|
32
|
+
|
|
33
|
+
Shipped artifacts are built only from openly usable sources: MovieLens (research, non-commercial, share-alike on transformations), Wikidata (CC0) and Wikipedia (CC BY-SA) in later versions. Posters, overviews, certifications and streaming availability come live from TMDB (and JustWatch through TMDB) with **your own API key**, are shown for display only, are cached locally for at most six months, and are never fed to the recommender or to an LLM, because TMDB's API terms exclude ML/AI applications. This product uses the TMDB API but is not endorsed or certified by TMDB.
|
|
34
|
+
|
|
35
|
+
Your ratings stay in your browser (or in the file you point the CLI at) unless you export them.
|
|
36
|
+
|
|
37
|
+
## Layout
|
|
38
|
+
|
|
39
|
+
| Path | What |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `flickpick/` | Python package: build pipeline, EASE, scorer, importers, science, CLI (`python -m flickpick`) |
|
|
42
|
+
| `js/` | TypeScript core (npm `flickpick`): the same scorer, importers and science for browser and Node, with a CLI |
|
|
43
|
+
| `web/` | Vite + React web app over `js/` |
|
|
44
|
+
| `tests/` | Python tests and the small fixture artifact set |
|
|
45
|
+
| `docs/` | architecture, artifact format, core contract, research |
|
|
46
|
+
|
|
47
|
+
## Status
|
|
48
|
+
|
|
49
|
+
v1: Python and TypeScript cores with parity tests, CLI on both, the web app, MovieLens-only catalogue. Next: Wikidata and Wikipedia catalogue, LLM "vibe profiles", MCP server and in-app assistant, remote MCP for claude.ai connectors, int8 artifacts, a published data package.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Movie recommender — proposed architecture (v1)
|
|
2
|
+
|
|
3
|
+
*2026-09-30 · written by the project lead session after the research round · inputs: the seven reports in the sibling folders, the `architecture-first` and `frontend` skills · status: PROPOSAL, awaiting the user's name choice and licensing posture*
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
Build a **browser-first app over a TypeScript core**, with **Python as the offline build pipeline** (catalogue, EASE matrix, embeddings) and, later, a thin `qh` service for the three things a static site cannot do (remote MCP for claude.ai connectors, optional sync, keyed proxies). The research settles the split: compute is not the constraint (the whole recommender is 15 to 25 MB of client-side artifacts); data licensing is. So the shipped artifacts are built from open sources (Wikidata CC0, Wikipedia CC BY-SA, MovieLens ml-32m non-commercial), and TMDB, JustWatch-via-TMDB, OMDb and MDBList are live, display-only enrichments fetched with the user's own keys. The LLM parses requests and explains results; a deterministic ranker over catalogue IDs does the ranking.
|
|
8
|
+
|
|
9
|
+
The name must be free on PyPI and npm because the project ships two packages: the TS core (npm) and the Python build pipeline plus MCP/qh adapters (PyPI). Both under the same name.
|
|
10
|
+
|
|
11
|
+
## Repository shape
|
|
12
|
+
|
|
13
|
+
One repo, `$PP/tt/<name>/` (frontend-ish, so `tt/`), holding three deliverables that share one vocabulary and one artifact format:
|
|
14
|
+
|
|
15
|
+
| Part | Language | Ships to | Purpose |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `src/` core library | TypeScript | npm `<name>` | Loads artifacts, scores a user (EASE product, iALS fold-in), semantic search (brute-force cosine over int8 vectors), structured filters, fusion/rerank, personal evaluation, agreement statistics. Runs in browser and Node. No UI, no fetch policy: everything comes in as arguments. |
|
|
18
|
+
| `web/` app | Vite + React + TS | static hosting (GitHub Pages or Cloudflare Pages) | The product: rating UI, recommendations, movie pages with live enrichment, the "for scientists" page, the about page (with the user's 2004-to-now recommender story), settings (BYO keys). zodal schema-driven UI, zustand/immer, acture commands. |
|
|
19
|
+
| `<name>/` Python package | Python | PyPI `<name>` | Offline build pipeline (`build` CLI via `cw`): Wikidata + Wikipedia + MovieLens ml-32m → catalogue Parquet, EASE top-k matrix, item vectors via `ef`, ID crosswalk. Later: `py2mcp` local MCP and a `qh` service (remote MCP, proxies). |
|
|
20
|
+
| data package | none | Hugging Face Hub dataset `<name>-data` | The built artifacts, versioned, under their own licence (MovieLens share-alike, non-commercial; Wikipedia CC BY-SA attribution). Never inside the code repo. |
|
|
21
|
+
|
|
22
|
+
The first two are the v1. The Python pipeline is v1 too, because nothing runs without artifacts, but only its `build` command. MCP and qh are v1.5.
|
|
23
|
+
|
|
24
|
+
## Concerns extracted (each its own module; each could be reused by another app)
|
|
25
|
+
|
|
26
|
+
1. **Ratings importers** (`importers/`): Letterboxd, IMDb, MovieLens and Netflix exports → one canonical `Rating` schema (Zod). Pure functions over text; no host knowledge.
|
|
27
|
+
2. **Data-source clients** (`sources/`): TMDB, MDBList, Movie of the Night, Wikidata. Each client owns its terms: cache TTL (TMDB ≤ 6 months), attribution strings, "display-only" flag, key requirement. The licensing rule lives in code next to the fetch, not in a README.
|
|
28
|
+
3. **Personal evaluation and agreement statistics** (`science/`): temporal split, full-catalogue ranking metrics with confidence intervals, rank correlations with population ratings, fitted models with cross-validation. Pure numeric code over arrays.
|
|
29
|
+
4. **BYO keys, settings, assistant**: not built here. `zodal-dials` for the settings schema (one section per provider), `acture` for commands, `acture-ai` for the assistant. The app mounts their sections.
|
|
30
|
+
|
|
31
|
+
## Seam table (v1)
|
|
32
|
+
|
|
33
|
+
| # | Seam | v1 default (no new dependency) | Replacement I can point at |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| 1 | where artifacts are loaded from (`artifactSource`) | `fetch()` from a static base URL (HF Hub dataset or the site's own `data/`) | local files in Node, or a `qh` endpoint (`tt/app_ef` is the template; browser-first report §2) |
|
|
36
|
+
| 2 | where the user's data lives (`store`) | `@zodal/store-localstorage` (IndexedDB-backed provider) | `@zodal/store-http` against the qh service (both in the manifest) |
|
|
37
|
+
| 3 | how a free-text request becomes a structured query (`queryParser`) | the structured form itself: mood text is embedded client-side, everything else is fields | an LLM parser with the user's key via `acture-ai` (the brief asks for it; acture exists) |
|
|
38
|
+
|
|
39
|
+
Two rows that were in the proposal were **demoted to seam candidates** after the v1 review (2026-10-06), because their replacement exists only in the research, not in code, which fails the evidence test: the **scorer** (`recommend` calls EASE directly in both languages; iALS fold-in and item-kNN would be one keyword argument when the pipeline emits their artifacts) and the **enrichment provider** (only the HTTP transport is injected; Movie of the Night and MDBList would be a second module behind one argument). Both carry a `seam candidate` comment at the call site.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Surface for v1 (built): web app + Node CLI over the TS core; Python `build|recommend|evaluate` CLI.
|
|
43
|
+
(MCP, remote MCP, shipped skills, qh: questions answered below, not built)
|
|
44
|
+
NOT seams: embedding model (bge-small-en-v1.5 int8 or snowflake-arctic-embed-xs, fixed per artifact version),
|
|
45
|
+
fusion formula (weights are config, the formula is code), chart library, URL state shape,
|
|
46
|
+
artifact file format (Parquet + typed-array binaries, versioned).
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Surface questions, answered for v1 (one line each).** CLI: the core takes arrays and returns arrays of IDs with scores and reasons, so a Node CLI is a thin wrapper. MCP: every command is an acture command with a Zod schema; `recommend`, `searchCatalog`, `getTitle`, `rateTitle`, `whereToWatch`, `explain` are the tool list, and `getTitle` output for agents is built from open data only. HTTP: the core has no request state; the qh service would wrap the Python pipeline's outputs and the same tool list. Skills: each verb has a docstring and a trigger sentence. Frontend: it is the product, so the order inverts (web first), but the Node CLI is written first anyway as the proof that the core is not fused to the UI.
|
|
50
|
+
|
|
51
|
+
## The one-command test (v1 definition)
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
python -m <name> build --sample # small artifact set from a MovieLens subset + Wikidata sample
|
|
55
|
+
npx <name> recommend --ratings my_letterboxd.csv --mood "slow-burn melancholic sci-fi" --exclude-genre horror --k 10
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Passes when it prints ten catalogue titles with scores and one-line reasons, none of them already rated, in under a second on the sample artifacts. This is `test_smoke` for both packages and must still pass after every seam swap.
|
|
59
|
+
|
|
60
|
+
The web app's equivalent: open the site, import the same CSV, type the same mood, see the same ten films with posters.
|
|
61
|
+
|
|
62
|
+
## How the pieces answer the brief
|
|
63
|
+
|
|
64
|
+
- **Browser vs Python**: browser for everything the user touches; Python only offline (build) and for the remote MCP/proxy service later. Detail: browser-first report §1-2.
|
|
65
|
+
- **Streaming availability**: TMDB `/watch/providers` (JustWatch-sourced, 180+ countries, attribution to both) as default; Movie of the Night as the second provider. Display-only, cached ≤ 6 months, user's key. Detail: streaming report §1.
|
|
66
|
+
- **Ratings**: IMDb `title.ratings` (built into the catalogue at build time for the user's local use, personal/non-commercial), TMDB votes live, MDBList for RT/Metacritic/Letterboxd live, Wikidata snapshots as a dated seed. Detail: streaming report §2.
|
|
67
|
+
- **Scientists page**: MovieLens as the reproducible population baseline; live scores as dated features; methods in recsys report §4-5 (full-catalogue ranking, temporal split, confidence intervals, rank correlations, cross-validated fits). Charts follow the `dataviz` skill.
|
|
68
|
+
- **Semantic descriptions**: Wikipedia plot sections + an LLM-generated structured vibe profile per film (cheap model, fixed vocabulary, $3 to $127 for 30k films as an estimate), embedded with a 384-d model, shipped int8. Negation and constraints go through structured fields, not the embedding. Detail: semantic-data report §1, §3-4.
|
|
69
|
+
- **Filterable features**: Wikidata facts (genre, runtime, language, country, year, director, awards), TMDB certifications live, LLM-inferred content flags labelled as inferred. Detail: semantic-data report §5.
|
|
70
|
+
- **Posters**: TMDB image CDN, hotlinked, keyless at the image layer, attributed, never stored in the repo or the data package; generated placeholder when missing. Detail: posters report.
|
|
71
|
+
- **What the brief did not ask**: the TMDB AI clause; MovieLens share-alike on the shipped matrix; Letterboxd's API refusing recommender projects (their export file is the only path); Trakt's paywall; ANN indexes are unnecessary at 30k items; LLM rankers are worse than CF when ratings exist; a personal benchmark with 50 test events cannot separate methods that differ by less than about ten points of hit rate.
|
|
72
|
+
|
|
73
|
+
## Decisions taken by the lead (from the synthesis' conflict list)
|
|
74
|
+
|
|
75
|
+
- **Canonical key**: the Wikidata QID, with IMDb `tconst` and the TMDB id as required cross-IDs on every catalogue row. Under posture A the shipped data are Wikidata-derived, so the QID is the only key that is ours to publish; the other two are what the live enrichment sources need.
|
|
76
|
+
- **Two nested catalogue sets**: the collaborative-filtering artifact covers the 10k to 20k films that carry MovieLens signal (the 10k most-rated films carry about 97% of ratings); the content artifacts (facts, plot embeddings, vibe profiles) cover about 30k films. A film outside the CF set still gets semantic and content scores.
|
|
77
|
+
- **The user's example ratings file** (the addendum's CSV of 100+ personal ratings with MovieLens-style columns) goes into the repo as an example fixture for the importers and the smoke test, as the user asked. Real user ratings live behind seam 3 (a `MutableMapping` in Python, a zodal store provider in the browser, local storage by default). The synthesis notes that the `average_rating` column looks MovieLens-derived and that publishing a personal rating history has some re-identification exposure; both are the user's call and the file is theirs.
|
|
78
|
+
- **Charts**: chosen at build time following the `dataviz` skill; not a seam.
|
|
79
|
+
|
|
80
|
+
## Decisions the user has to make before the repo is created
|
|
81
|
+
|
|
82
|
+
1. **The name.** Shortlist from the brand report, all free on PyPI and npm today: cinepick, reelpick, flickpick, cinescout, cinesift (runners-up: watchnext, moodreel).
|
|
83
|
+
2. **Licensing posture.** (A) Open core as above, TMDB display-only via the user's key, no email needed; or (B) ask TMDB in writing for clearance to use TMDB content in the AI-assisted app, which would let the catalogue use TMDB overviews and keywords. The reports recommend A, with B pursued in parallel if the user wants TMDB text. Under A the deployed public instance must stay non-commercial.
|
|
84
|
+
3. **The data package's home and licence.** Hugging Face Hub dataset under the MovieLens conditions (non-commercial, share-alike on transformations) plus CC BY-SA attribution for Wikipedia text. Confirm this is acceptable for a public project.
|
|
85
|
+
|
|
86
|
+
## What v1 leaves out, by name
|
|
87
|
+
|
|
88
|
+
MCP (local and remote), the in-app assistant, shipped agent skills, sync, group/household recommendations, the LLM query parser. Each is an addition at seam 5, at the acture command layer, or at seam 1/3, and none needs the core to change.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Artifact format (v1)
|
|
2
|
+
|
|
3
|
+
*The contract between the Python build pipeline (writer) and the TypeScript core and Python scorer (readers). Versioned by `manifest.json`. Any change here is a new `format_version`.*
|
|
4
|
+
|
|
5
|
+
An **artifact set** is one directory (or one URL base) holding the files below. All binary files are little-endian, row-major, no header; their shapes are in the manifest. Item order is the same in every file: the row index `idx` is the key that joins them.
|
|
6
|
+
|
|
7
|
+
| File | Type | Content |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `manifest.json` | JSON | `format_version` (1), `built_at` (ISO), `name`, `n_items`, `sources` (list of `{name, version, licence, url}`), `cf` (`{method: "ease", topk, lambda, n_train_users, n_train_ratings, like_threshold}`), `embedding` (`{model, dim, dtype: "float32", query_prefix: "", text_field: "semantic_text"}` or `null` when absent), `files` (name → `{path, dtype, shape}`) |
|
|
10
|
+
| `catalog.json` | JSON array, one object per item, in `idx` order | `idx` (int), `imdb_id` (string, `tt0114369`; the canonical item id in v1), `tmdb_id` (int or null), `ml_id` (int or null, MovieLens movieId), `qid` (string or null, Wikidata), `title`, `year` (int or null), `genres` (string[]), `n_ratings` (int, population count), `mean_rating` (float 0-100, population mean on the canonical scale), `semantic_text` (string; v1: tags and genres joined; later: plot + vibe profile) |
|
|
11
|
+
| `cf_indptr.i32` | Int32[n_items+1] | CSR row pointers of the sparsified EASE item-item matrix B (rows = source item, columns = target item) |
|
|
12
|
+
| `cf_indices.i32` | Int32[nnz] | column indices, per row sorted by descending weight |
|
|
13
|
+
| `cf_values.f32` | Float32[nnz] | weights B[row, col]; the diagonal is zero and absent |
|
|
14
|
+
| `embeddings.f32` | Float32[n_items × dim] | L2-normalised sentence embeddings of `semantic_text`; absent when `manifest.embedding` is null |
|
|
15
|
+
|
|
16
|
+
**Ratings scale.** Everything the scorer sees is on the canonical 0-100 scale (the scale of the example file and of Criticker). Importers convert: Letterboxd and MovieLens stars ×20, IMDb ×10.
|
|
17
|
+
|
|
18
|
+
**Scoring an EASE artifact.** For a user with liked set L (items scored at or above the user's own median, or ≥ `like_threshold` when they have fewer than 5 ratings), the CF score of item j is `s_j = Σ_{i∈L} B[i, j]`, i.e. the sum of the rows of the liked items. Rated items are excluded from the output. The contributing items with the largest `B[i, j]` are the explanation ("because you liked …").
|
|
19
|
+
|
|
20
|
+
**Semantic score.** `cos(q, e_j)` with q the L2-normalised embedding of `query_prefix + query_text` from the model in `manifest.embedding`.
|
|
21
|
+
|
|
22
|
+
**Sizes (v1 defaults).** `topk = 100`, which puts a 10k-item artifact near 4 MB of CF files plus 15 MB of float32 embeddings at dim 384. int8 embeddings are the known next step and will be `format_version` 2.
|
|
23
|
+
|
|
24
|
+
**Licensing.** The artifact set is data, not code: its `sources` carry the licences (MovieLens: research, non-commercial, share-alike on transformations; Wikipedia: CC BY-SA 4.0), and the set must not be committed to this repository except as the small test fixture under `tests/fixtures/` built from MovieLens `ml-latest-small`.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Core contract (v1)
|
|
2
|
+
|
|
3
|
+
*What the TypeScript core (`js/`, npm `flickpick`) and the Python scorer (`flickpick/score.py`) both implement over the artifact format. Same inputs, same outputs, same defaults; parity tests run the two on the example ratings (`tests/fixtures/parity/`: `recommend` and `holdoutEvaluate`).*
|
|
4
|
+
|
|
5
|
+
## Types (Zod in TS, dataclasses in Python)
|
|
6
|
+
|
|
7
|
+
- `Rating { item_id: string (imdb_id), score: number (0-100), rated_at?: string (ISO), title?: string }`
|
|
8
|
+
- `Query { mood?: string, like_ids?: string[], include_genres?: string[], exclude_genres?: string[], year_min?: number, year_max?: number, min_ratings?: number, exclude_ids?: string[], k?: number (default 10), weights?: { cf: 1, semantic: 1, popularity: 0.1 } }`
|
|
9
|
+
- `Recommendation { item_id, idx, title, year, score, cf_score, semantic_score, popularity_score, reasons: string[], because_of: string[] (item_ids) }`
|
|
10
|
+
- `Artifacts { manifest, catalog, cf: { indptr, indices, values }, embeddings?: Float32Array }`
|
|
11
|
+
|
|
12
|
+
## Functions
|
|
13
|
+
|
|
14
|
+
- `loadArtifacts(source)` → `Artifacts`. `source` is a URL base or a directory path (seam 1). TS: `fetch` in the browser, `fs` in Node. Python: a directory path.
|
|
15
|
+
- `scoreCf(artifacts, ratings)` → `Float32Array[n_items]` (EASE row sum over liked items; rated items set to `-Infinity`).
|
|
16
|
+
- `scoreSemantic(artifacts, queryEmbedding)` → `Float32Array[n_items]` (cosine; brute force).
|
|
17
|
+
- `embedQuery(text, manifest.embedding)` → `Float32Array[dim]` (TS: transformers.js with the manifest's model; Python: the same model through `ef` or sentence-transformers). The only place a model runs at query time.
|
|
18
|
+
- `recommend(artifacts, ratings, query, { embedQuery })` → `Recommendation[]`: filters by the structured fields, percentile-ranks each available score over the candidate set, combines with `query.weights`, sorts, takes `k`, fills `reasons` and `because_of`.
|
|
19
|
+
- Importers (`importers/`): `parseLetterboxd(csvText)`, `parseImdb(csvText)`, `parseMovielens(csvText)`, `parseFlickpick(csvText)` → `Rating[]`; `detectFormat(csvText)` (rules under **Importers** below). The flickpick format is the example file's columns (`movie_id, imdb_id, tmdb_id, rating, average_rating, title`), with `imdb_id` numeric or `tt`-prefixed.
|
|
20
|
+
- Science (`science/`): `rankingMetrics(recommendedIdx, heldOutIdx, k)` → `{ hit_rate, recall, ndcg, precision }`; `holdoutEvaluate(artifacts, ratings, { folds: 5, k: 10, seed: 0 })` → `{ folds, k, n_liked, n_rated, per_fold, mean, ci95, note }` (algorithm under **Science** below); `agreement(ratings, catalog)` → `{ pearson, spearman, n, slope, intercept }` of the user's score against `mean_rating`; `crossValidatedFit(ratings, catalog, { folds })` → RMSE and MAE of predicting the user's score from `mean_rating` (linear) versus the user's mean (baseline).
|
|
21
|
+
|
|
22
|
+
## Importers
|
|
23
|
+
|
|
24
|
+
`detectFormat` reads the header row only. Each header cell is trimmed and compared case-insensitively. The formats are tried in this order and the first one whose required columns are all present wins (both languages hold this table as `SIGNATURES`):
|
|
25
|
+
|
|
26
|
+
| format | required columns | optional columns the parser reads |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| imdb | `Const`, `Your Rating` | `Date Rated`, `Title`, `Year` |
|
|
29
|
+
| flickpick | `movie_id`, `imdb_id`, `rating` | `tmdb_id`, `title`, `rated_at` |
|
|
30
|
+
| letterboxd | `Name`, `Year`, `Rating` | `Date`, `Letterboxd URI` (not required: `diary.csv` and hand-made files lack it) |
|
|
31
|
+
| movielens | `movieId`, `rating` | `userId`, `timestamp` (TS also `imdbId`, `title`) |
|
|
32
|
+
|
|
33
|
+
A MovieLens file whose `userId` column holds more than one user is refused unless the caller picks one (`user_id=` in Python, `{ userId }` in TS); merging users would score one person on everybody's ratings. A file without `userId` is one user. An unrecognised header is a `ValueError` in Python and `null` from TS `detectFormat` (and an error from `parseRatings`), naming the header.
|
|
34
|
+
|
|
35
|
+
## Science
|
|
36
|
+
|
|
37
|
+
`holdoutEvaluate` / `holdout_evaluate` is one algorithm, implemented identically in both languages; `tests/fixtures/parity/expected_evaluate.json` (written by `tests/fixtures/make_fixtures.py`) is the Python output and the TS test must match it to 1e-6.
|
|
38
|
+
|
|
39
|
+
Randomness is **mulberry32** with a 32-bit seed, chosen because it is one line in both languages. State `a = seed mod 2^32`; each call does, in unsigned 32-bit arithmetic (`imul` = low 32 bits of the product): `a += 0x6D2B79F5; t = a; t = imul(t ^ (t >> 15), t | 1); t ^= t + imul(t ^ (t >> 7), t | 61); return (t ^ (t >> 14)) / 2^32`. A **shuffle** is Fisher-Yates: for `i` from `n - 1` down to 1, swap positions `i` and `floor(random() * (i + 1))`.
|
|
40
|
+
|
|
41
|
+
a. **Restrict to the catalogue.** Resolve the ratings against the catalogue (by id, `ml:`/`tmdb:` id, or title and year); drop those not found; keep one rating per item (the later one wins); re-key each to the catalogue `imdb_id`; order by catalogue `idx`. Everything below uses only this set (its size is `n_rated`).
|
|
42
|
+
b. **Liked items** are those scoring at or above the like threshold computed on the restricted set (median, or 70 with fewer than 5 ratings). Fewer liked items than `folds` is an error.
|
|
43
|
+
c. **Folds.** Sort the liked `imdb_id`s lexicographically, shuffle them with `mulberry32(seed)`, and put the item at position `p` in fold `p mod folds`; within a fold, items keep their shuffled order (`test_ids`).
|
|
44
|
+
d. **Per fold**, the training ratings are the restricted set minus that fold's items (still in `idx` order). The scorer (default: the EASE row sum, `scoreCf`) scores the whole catalogue from them; every item outside the training ratings with a finite score is ranked, and `hit_rate@k` (1 if any held-out item is in the top k), `precision@k` (hits / k), `recall@k` (hits / fold size) and `ndcg@k` (gain 1 / log2(rank + 1), ideal DCG over `min(k, fold size)` hits) are computed against the fold's items. `mean` is the plain mean over folds.
|
|
45
|
+
e. **Tie-break** when ranking, in both scorers and in `recommend`: score descending, then `n_ratings` descending, then `idx` ascending.
|
|
46
|
+
f. **Output**: `{ folds, k, n_liked, n_rated, per_fold: [{ fold, n_test, test_ids, metrics: { hit_rate, recall, ndcg, precision } }], mean: {...}, ci95: { metric: [low, high] | null }, note: string | null }`.
|
|
47
|
+
|
|
48
|
+
**The 95% interval** resamples held-out items, not folds (five fold means made a degenerate interval, for example `[1, 1]` for hit rate). Each held-out item `i` in fold `f` contributes, for recall, `hit_i * n_liked / (folds * |f|)`; for precision, `hit_i * n_liked / (folds * k)`; for NDCG, `hit_i * discount_i * n_liked / (folds * idcg_f)`. These are scaled so that their mean over all held-out items equals the reported `mean`. The items are listed fold by fold in `test_ids` order. Draw `n_bootstrap` (1000) resamples of `n_liked` items with replacement, using one fresh `mulberry32(seed)`: resample `b` takes item `floor(u * n_liked)` for the next `n_liked` outputs `u`. The same draws serve all three metrics. The interval is the linear-interpolated 2.5% and 97.5% quantiles of the resample means. `hit_rate` is a per-fold quantity with no per-item form. With at least `min_folds_for_fold_ci` (10) folds its interval is the same bootstrap over the per-fold values. Below that it is `null`, and `note` says why.
|
|
49
|
+
|
|
50
|
+
## Defaults that are not seams
|
|
51
|
+
|
|
52
|
+
Like threshold: the user's median score (or 70 with fewer than 5 ratings). Fusion (`normalisation: percentile_rank`): each available component becomes percentile ranks over the candidate set (ranks 0..n-1 over the finite values, ties share their mean rank, divided by `max(n - 1, 1)`; `-Infinity` and other non-finite values are dropped before ranking and get 0), then a weighted sum; a missing component contributes 0. Ranks rather than z-scores because EASE scores are heavy-tailed, so their z-scores (up to ~15) swamped the cosine's (~3) and a mood barely reordered the list. Popularity: `log1p(n_ratings)`. Ties: by `n_ratings` descending, then `idx` ascending (the same order the evaluation ranks by).
|
|
53
|
+
|
|
54
|
+
## CLI (both)
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
npx flickpick recommend --artifacts <dir|url> --ratings <csv> [--mood "..."] [--exclude-genre Horror] [--k 10]
|
|
58
|
+
python -m flickpick recommend --artifacts <dir> --ratings <csv> [--mood "..."] ...
|
|
59
|
+
python -m flickpick build --out <dir> [--sample] [--topk 100] [--no-embeddings]
|
|
60
|
+
python -m flickpick evaluate --artifacts <dir> --ratings <csv> [--folds 5] [--k 10] [--seed 0]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Both CLIs accept `--include-genre`/`--exclude-genre` and the plural spellings.
|
|
64
|
+
|
|
65
|
+
Output of `recommend`: one line per item, `rank. title (year) score=… because: …`, or JSON with `--json`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Research
|
|
2
|
+
|
|
3
|
+
The research round of 2026-09-30 that preceded the first commit. Start with the synthesis, then the report a question points to. The architecture that came out of it is in [../architecture.md](../architecture.md).
|
|
4
|
+
|
|
5
|
+
| File | What it holds |
|
|
6
|
+
|---|---|
|
|
7
|
+
| [original-request.md](original-request.md) | The request that started the project, verbatim, plus its addendum |
|
|
8
|
+
| [movie-recommender-research-synthesis.md](movie-recommender-research-synthesis.md) | Cross-report synthesis: answers to each question, the licensing decision, conflicts and resolutions, what we use, numbers worth remembering, open questions |
|
|
9
|
+
| [inhouse-inventory.md](inhouse-inventory.md) | What already existed in the author's package ecosystem |
|
|
10
|
+
| [recsys-methods-libraries-evaluation.md](recsys-methods-libraries-evaluation.md) | Recommender methods and libraries, the single-user setting, personal evaluation, the statistics behind the "for scientists" page |
|
|
11
|
+
| [streaming-availability-and-ratings-apis.md](streaming-availability-and-ratings-apis.md) | Streaming availability feeds, population-rating sources, ID crosswalk, ratings import, terms matrix |
|
|
12
|
+
| [semantic-movie-data-and-filter-features.md](semantic-movie-data-and-filter-features.md) | Semantic text sources, existing embeddings, LLM vibe profiles and their cost, embedding models, filterable features |
|
|
13
|
+
| [movie-posters-and-media-sources.md](movie-posters-and-media-sources.md) | Poster, backdrop, logo and trailer sources; copyright posture; delivery |
|
|
14
|
+
| [browser-first-feasibility-licensing-agents-prior-art.md](browser-first-feasibility-licensing-agents-prior-art.md) | In-browser compute, hosting, licensing matrix, key handling, privacy, agent surfaces, prior art, terminology |
|
|
15
|
+
| [brand-name-report.md](brand-name-report.md) | The naming analysis that produced "flickpick" |
|
|
16
|
+
| [prompts/](prompts/) | The research prompts, one per theme, for rerunning or extending |
|
|
17
|
+
|
|
18
|
+
Verified from the primary source on 2026-09-30: TMDB's API Terms of Use (last updated 2023-10-20) prohibit using TMDB APIs or content "in connection with, including for training, a machine learning (ML) or artificial intelligence (AI) based Application", cap caching at 6 months, require TMDB-logo attribution, and require a written agreement for any revenue. That clause is why the shipped artifacts are built from Wikidata, Wikipedia and MovieLens only.
|