cinematch 0.1.0__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.
- cinematch-0.1.0/PKG-INFO +173 -0
- cinematch-0.1.0/README.md +143 -0
- cinematch-0.1.0/app/__init__.py +2 -0
- cinematch-0.1.0/app/cli.py +97 -0
- cinematch-0.1.0/app/config.py +38 -0
- cinematch-0.1.0/app/data/generate_starter.py +365 -0
- cinematch-0.1.0/app/data/starter_catalog.json +3621 -0
- cinematch-0.1.0/app/database.py +188 -0
- cinematch-0.1.0/app/main.py +1090 -0
- cinematch-0.1.0/app/models/schemas.py +124 -0
- cinematch-0.1.0/app/services/embeddings.py +115 -0
- cinematch-0.1.0/app/services/orchestrator.py +1433 -0
- cinematch-0.1.0/app/services/seeder.py +61 -0
- cinematch-0.1.0/app/services/taste_dossier.py +276 -0
- cinematch-0.1.0/app/services/tmdb.py +472 -0
- cinematch-0.1.0/app/static/css/cinematch.css +296 -0
- cinematch-0.1.0/app/static/images/placeholder_poster.svg +8 -0
- cinematch-0.1.0/app/templates/base.html +258 -0
- cinematch-0.1.0/app/templates/chat.html +282 -0
- cinematch-0.1.0/app/templates/components/chat_message.html +72 -0
- cinematch-0.1.0/app/templates/components/deck_card.html +258 -0
- cinematch-0.1.0/app/templates/components/for_you_results.html +78 -0
- cinematch-0.1.0/app/templates/components/search_results.html +43 -0
- cinematch-0.1.0/app/templates/deck.html +20 -0
- cinematch-0.1.0/app/templates/for_you.html +37 -0
- cinematch-0.1.0/app/templates/library.html +156 -0
- cinematch-0.1.0/app/templates/search.html +29 -0
- cinematch-0.1.0/cinematch.egg-info/PKG-INFO +173 -0
- cinematch-0.1.0/cinematch.egg-info/SOURCES.txt +40 -0
- cinematch-0.1.0/cinematch.egg-info/dependency_links.txt +1 -0
- cinematch-0.1.0/cinematch.egg-info/entry_points.txt +2 -0
- cinematch-0.1.0/cinematch.egg-info/requires.txt +17 -0
- cinematch-0.1.0/cinematch.egg-info/top_level.txt +1 -0
- cinematch-0.1.0/pyproject.toml +58 -0
- cinematch-0.1.0/setup.cfg +4 -0
- cinematch-0.1.0/tests/test_adaptive_drift.py +256 -0
- cinematch-0.1.0/tests/test_deck_refill.py +98 -0
- cinematch-0.1.0/tests/test_flow.py +73 -0
- cinematch-0.1.0/tests/test_for_you_cache.py +105 -0
- cinematch-0.1.0/tests/test_pipeline_regressions.py +307 -0
- cinematch-0.1.0/tests/test_search_perf.py +32 -0
- cinematch-0.1.0/tests/test_why_reasons.py +107 -0
cinematch-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cinematch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A personalized movie and series recommendation web app
|
|
5
|
+
Author-email: bkallash <bayankallash@gmail.com>
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Environment :: Web Environment
|
|
8
|
+
Classifier: Framework :: FastAPI
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: aiofiles>=24.1.0
|
|
15
|
+
Requires-Dist: fastapi>=0.115.0
|
|
16
|
+
Requires-Dist: httpx>=0.27.0
|
|
17
|
+
Requires-Dist: jinja2>=3.1.4
|
|
18
|
+
Requires-Dist: numpy>=1.26.0
|
|
19
|
+
Requires-Dist: openai>=1.40.0
|
|
20
|
+
Requires-Dist: pydantic>=2.8.0
|
|
21
|
+
Requires-Dist: pydantic-settings>=2.4.0
|
|
22
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
23
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
24
|
+
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=6.1.0; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# CineMatch — AI-Powered Movie & Series Suggestion App
|
|
32
|
+
|
|
33
|
+
A modern, cinephile-grade recommendation engine built in Python, featuring a living **Taste Dossier**, hybrid vector RAG retrieval, and an interactive dark-mode web UI.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Key Features
|
|
38
|
+
|
|
39
|
+
1. **💬 CineMatch Concierge (Assistant)**:
|
|
40
|
+
- Natural language vibe matching (e.g. *"Suggest a romantic movie like La La Land with bittersweet melancholia"*, *"Cozy 90s holiday comedy"*).
|
|
41
|
+
- Multi-turn conversational flow powered by OpenRouter **GPT-4o**.
|
|
42
|
+
- Dual-layer recommendation pipeline: fast in-process SQLite vector retrieval + LLM reranking against your living **Taste Dossier**.
|
|
43
|
+
- Surfaces personalized reasoning cards with "Why You'll Love It" callouts and highlights matching titles from your own Watchlist.
|
|
44
|
+
|
|
45
|
+
2. **🎴 Famous Titles Rating Deck**:
|
|
46
|
+
- Fast-fire card queue of widely recognized movies and TV series with posters, trailers, and synopses.
|
|
47
|
+
- Non-neutral **1 to 6 rating scale** (1–3 = Disliked/Flawed, 4–6 = Good/Masterpiece).
|
|
48
|
+
- Adaptive **Aspect Tags** (concise single-concept tags like `Visuals`, `Pacing`, `Ending`, `Soundtrack`, `Plot Holes`, `Cliche`).
|
|
49
|
+
- Quick **"Haven't Seen (Skip)"** and **"Want to Watch"** action buttons.
|
|
50
|
+
|
|
51
|
+
3. **🔍 Search & Rate**:
|
|
52
|
+
- Real-time debounced search bar connecting local SQLite storage with live TMDB (The Movie Database).
|
|
53
|
+
- Log 1–6 ratings and micro-notes on any title in cinema history.
|
|
54
|
+
|
|
55
|
+
4. **📚 My Library & Taste Dossier**:
|
|
56
|
+
- **Ratings**: Browse and filter your rated titles by score, media type, and aspect tags.
|
|
57
|
+
- **Watchlist**: Manage everything marked "Want to Watch".
|
|
58
|
+
- **Taste Dossier**: Transparently inspect what the AI has synthesized about your cinematic DNA (Core Loves, Deal-Breakers, Creator Affinities, and Narrative Tropes), with a one-click re-synthesis trigger.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quickstart Guide
|
|
63
|
+
|
|
64
|
+
### 1. Activate the Virtual Environment
|
|
65
|
+
|
|
66
|
+
```powershell
|
|
67
|
+
cd "D:\code Projects\SPOC\movie-suggestion"
|
|
68
|
+
.\venv\Scripts\activate
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Configure Your API Keys in `.env`
|
|
72
|
+
|
|
73
|
+
Copy `.env.example` to `.env` (or edit existing `.env`):
|
|
74
|
+
|
|
75
|
+
```env
|
|
76
|
+
# OpenRouter Configuration (for GPT-4o and Text Embeddings)
|
|
77
|
+
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
|
78
|
+
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
|
79
|
+
OPENROUTER_MODEL=openai/gpt-4o
|
|
80
|
+
|
|
81
|
+
# TMDB API Key (Free from https://www.themoviedb.org/settings/api)
|
|
82
|
+
TMDB_API_KEY=your_tmdb_api_key_here
|
|
83
|
+
|
|
84
|
+
# Embeddings
|
|
85
|
+
EMBEDDING_PROVIDER=openrouter
|
|
86
|
+
EMBEDDING_MODEL=openai/text-embedding-3-small
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> **Note**: Even before adding API keys, CineMatch runs fully with its pre-bundled catalog of famous titles and deterministic vector fallbacks.
|
|
90
|
+
|
|
91
|
+
Set `EMBEDDING_PROVIDER=local` for offline token-based text matching without a model download. Cloud embeddings provide richer semantic matching. Each stored vector tracks its provider, model, and dimension; startup rebuilds incompatible or older unversioned vectors in batches of 64. With OpenRouter configured, this rebuild makes billable embedding requests. Failed batches are retried at the next startup.
|
|
92
|
+
|
|
93
|
+
### 3. Run the Web Application
|
|
94
|
+
|
|
95
|
+
```powershell
|
|
96
|
+
.\venv\Scripts\python.exe -m uvicorn app.main:app --reload --port 8000
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Open your browser at: **[http://localhost:8000](http://localhost:8000)**
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Install from PyPI
|
|
104
|
+
|
|
105
|
+
Install and run CineMatch from any directory:
|
|
106
|
+
|
|
107
|
+
```powershell
|
|
108
|
+
python -m pip install cinematch
|
|
109
|
+
cinematch
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
On the first run, CineMatch prompts for:
|
|
113
|
+
|
|
114
|
+
1. Your OpenRouter API key. Create one at [OpenRouter Keys](https://openrouter.ai/settings/keys). The key is entered using a hidden password prompt.
|
|
115
|
+
2. An OpenRouter model identifier, such as `openai/gpt-4o`. Browse the available identifiers on the [OpenRouter Models](https://openrouter.ai/models) page.
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
CineMatch first-run setup
|
|
119
|
+
Create an API key at https://openrouter.ai/settings/keys
|
|
120
|
+
OpenRouter API key (input is hidden):
|
|
121
|
+
OpenRouter model [openai/gpt-4o]:
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The configuration is saved in CineMatch's operating-system user data directory and reused on later runs. The SQLite database is stored there as well. To change the API key or model later, run:
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
cinematch --configure
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
You can also create a `.env` file in the directory where you run CineMatch. This local configuration takes precedence:
|
|
131
|
+
|
|
132
|
+
```env
|
|
133
|
+
OPENROUTER_API_KEY=pypi_user_openrouter_key
|
|
134
|
+
OPENROUTER_MODEL=openai/gpt-4o
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Run `cinematch --help` to select a host, port, or custom data directory.
|
|
138
|
+
|
|
139
|
+
### Build and publish a release
|
|
140
|
+
|
|
141
|
+
```powershell
|
|
142
|
+
python -m pip install --upgrade build twine
|
|
143
|
+
python -m build
|
|
144
|
+
python -m twine check dist/*
|
|
145
|
+
python -m twine upload dist/*
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
PyPI accepts an API token as the username `__token__`; use the full token, including its `pypi-` prefix, as the password. Increase the version in `pyproject.toml` before every later release because PyPI release files cannot be replaced.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Architecture & Design Documents
|
|
153
|
+
|
|
154
|
+
All architectural choices and domain terminology are formally documented:
|
|
155
|
+
- [`CONTEXT.md`](./CONTEXT.md) — Ubiquitous language and domain glossary.
|
|
156
|
+
- [`docs/adr/0001-hybrid-rag-taste-dossier.md`](./docs/adr/0001-hybrid-rag-taste-dossier.md) — Dual-layer RAG & Taste Dossier architecture.
|
|
157
|
+
- [`docs/adr/0002-six-point-forced-choice-rating-scale.md`](./docs/adr/0002-six-point-forced-choice-rating-scale.md) — 1–6 discrete forced-choice rating scale.
|
|
158
|
+
- [`docs/adr/0003-hybrid-catalog-orchestrator.md`](./docs/adr/0003-hybrid-catalog-orchestrator.md) — Local vector search + dynamic TMDB discovery fallback.
|
|
159
|
+
- [`docs/adr/0004-in-process-sqlite-vectors.md`](./docs/adr/0004-in-process-sqlite-vectors.md) — Zero-dependency in-process SQLite BLOB vector storage.
|
|
160
|
+
- [`docs/adr/0005-dual-source-embedding-strategy.md`](./docs/adr/0005-dual-source-embedding-strategy.md) — OpenRouter with offline local embedding toggle.
|
|
161
|
+
- [`docs/adr/0006-bundled-curated-starter-catalog.md`](./docs/adr/0006-bundled-curated-starter-catalog.md) — Bundled curated starter dataset.
|
|
162
|
+
- [`docs/adr/0007-compatible-embeddings.md`](./docs/adr/0007-compatible-embeddings.md) — Embedding compatibility, batched rebuilds, and lightweight local matching.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Verification Tests
|
|
167
|
+
|
|
168
|
+
Install test dependencies and run the suite. Tests use a temporary database and disable external APIs, leaving your library untouched:
|
|
169
|
+
|
|
170
|
+
```powershell
|
|
171
|
+
.\venv\Scripts\python.exe -m pip install -r requirements-dev.txt
|
|
172
|
+
.\venv\Scripts\python.exe -m pytest -q
|
|
173
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# CineMatch — AI-Powered Movie & Series Suggestion App
|
|
2
|
+
|
|
3
|
+
A modern, cinephile-grade recommendation engine built in Python, featuring a living **Taste Dossier**, hybrid vector RAG retrieval, and an interactive dark-mode web UI.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
1. **💬 CineMatch Concierge (Assistant)**:
|
|
10
|
+
- Natural language vibe matching (e.g. *"Suggest a romantic movie like La La Land with bittersweet melancholia"*, *"Cozy 90s holiday comedy"*).
|
|
11
|
+
- Multi-turn conversational flow powered by OpenRouter **GPT-4o**.
|
|
12
|
+
- Dual-layer recommendation pipeline: fast in-process SQLite vector retrieval + LLM reranking against your living **Taste Dossier**.
|
|
13
|
+
- Surfaces personalized reasoning cards with "Why You'll Love It" callouts and highlights matching titles from your own Watchlist.
|
|
14
|
+
|
|
15
|
+
2. **🎴 Famous Titles Rating Deck**:
|
|
16
|
+
- Fast-fire card queue of widely recognized movies and TV series with posters, trailers, and synopses.
|
|
17
|
+
- Non-neutral **1 to 6 rating scale** (1–3 = Disliked/Flawed, 4–6 = Good/Masterpiece).
|
|
18
|
+
- Adaptive **Aspect Tags** (concise single-concept tags like `Visuals`, `Pacing`, `Ending`, `Soundtrack`, `Plot Holes`, `Cliche`).
|
|
19
|
+
- Quick **"Haven't Seen (Skip)"** and **"Want to Watch"** action buttons.
|
|
20
|
+
|
|
21
|
+
3. **🔍 Search & Rate**:
|
|
22
|
+
- Real-time debounced search bar connecting local SQLite storage with live TMDB (The Movie Database).
|
|
23
|
+
- Log 1–6 ratings and micro-notes on any title in cinema history.
|
|
24
|
+
|
|
25
|
+
4. **📚 My Library & Taste Dossier**:
|
|
26
|
+
- **Ratings**: Browse and filter your rated titles by score, media type, and aspect tags.
|
|
27
|
+
- **Watchlist**: Manage everything marked "Want to Watch".
|
|
28
|
+
- **Taste Dossier**: Transparently inspect what the AI has synthesized about your cinematic DNA (Core Loves, Deal-Breakers, Creator Affinities, and Narrative Tropes), with a one-click re-synthesis trigger.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quickstart Guide
|
|
33
|
+
|
|
34
|
+
### 1. Activate the Virtual Environment
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
cd "D:\code Projects\SPOC\movie-suggestion"
|
|
38
|
+
.\venv\Scripts\activate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Configure Your API Keys in `.env`
|
|
42
|
+
|
|
43
|
+
Copy `.env.example` to `.env` (or edit existing `.env`):
|
|
44
|
+
|
|
45
|
+
```env
|
|
46
|
+
# OpenRouter Configuration (for GPT-4o and Text Embeddings)
|
|
47
|
+
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
|
48
|
+
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
|
49
|
+
OPENROUTER_MODEL=openai/gpt-4o
|
|
50
|
+
|
|
51
|
+
# TMDB API Key (Free from https://www.themoviedb.org/settings/api)
|
|
52
|
+
TMDB_API_KEY=your_tmdb_api_key_here
|
|
53
|
+
|
|
54
|
+
# Embeddings
|
|
55
|
+
EMBEDDING_PROVIDER=openrouter
|
|
56
|
+
EMBEDDING_MODEL=openai/text-embedding-3-small
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> **Note**: Even before adding API keys, CineMatch runs fully with its pre-bundled catalog of famous titles and deterministic vector fallbacks.
|
|
60
|
+
|
|
61
|
+
Set `EMBEDDING_PROVIDER=local` for offline token-based text matching without a model download. Cloud embeddings provide richer semantic matching. Each stored vector tracks its provider, model, and dimension; startup rebuilds incompatible or older unversioned vectors in batches of 64. With OpenRouter configured, this rebuild makes billable embedding requests. Failed batches are retried at the next startup.
|
|
62
|
+
|
|
63
|
+
### 3. Run the Web Application
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
.\venv\Scripts\python.exe -m uvicorn app.main:app --reload --port 8000
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Open your browser at: **[http://localhost:8000](http://localhost:8000)**
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Install from PyPI
|
|
74
|
+
|
|
75
|
+
Install and run CineMatch from any directory:
|
|
76
|
+
|
|
77
|
+
```powershell
|
|
78
|
+
python -m pip install cinematch
|
|
79
|
+
cinematch
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
On the first run, CineMatch prompts for:
|
|
83
|
+
|
|
84
|
+
1. Your OpenRouter API key. Create one at [OpenRouter Keys](https://openrouter.ai/settings/keys). The key is entered using a hidden password prompt.
|
|
85
|
+
2. An OpenRouter model identifier, such as `openai/gpt-4o`. Browse the available identifiers on the [OpenRouter Models](https://openrouter.ai/models) page.
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
CineMatch first-run setup
|
|
89
|
+
Create an API key at https://openrouter.ai/settings/keys
|
|
90
|
+
OpenRouter API key (input is hidden):
|
|
91
|
+
OpenRouter model [openai/gpt-4o]:
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The configuration is saved in CineMatch's operating-system user data directory and reused on later runs. The SQLite database is stored there as well. To change the API key or model later, run:
|
|
95
|
+
|
|
96
|
+
```powershell
|
|
97
|
+
cinematch --configure
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
You can also create a `.env` file in the directory where you run CineMatch. This local configuration takes precedence:
|
|
101
|
+
|
|
102
|
+
```env
|
|
103
|
+
OPENROUTER_API_KEY=pypi_user_openrouter_key
|
|
104
|
+
OPENROUTER_MODEL=openai/gpt-4o
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run `cinematch --help` to select a host, port, or custom data directory.
|
|
108
|
+
|
|
109
|
+
### Build and publish a release
|
|
110
|
+
|
|
111
|
+
```powershell
|
|
112
|
+
python -m pip install --upgrade build twine
|
|
113
|
+
python -m build
|
|
114
|
+
python -m twine check dist/*
|
|
115
|
+
python -m twine upload dist/*
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
PyPI accepts an API token as the username `__token__`; use the full token, including its `pypi-` prefix, as the password. Increase the version in `pyproject.toml` before every later release because PyPI release files cannot be replaced.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Architecture & Design Documents
|
|
123
|
+
|
|
124
|
+
All architectural choices and domain terminology are formally documented:
|
|
125
|
+
- [`CONTEXT.md`](./CONTEXT.md) — Ubiquitous language and domain glossary.
|
|
126
|
+
- [`docs/adr/0001-hybrid-rag-taste-dossier.md`](./docs/adr/0001-hybrid-rag-taste-dossier.md) — Dual-layer RAG & Taste Dossier architecture.
|
|
127
|
+
- [`docs/adr/0002-six-point-forced-choice-rating-scale.md`](./docs/adr/0002-six-point-forced-choice-rating-scale.md) — 1–6 discrete forced-choice rating scale.
|
|
128
|
+
- [`docs/adr/0003-hybrid-catalog-orchestrator.md`](./docs/adr/0003-hybrid-catalog-orchestrator.md) — Local vector search + dynamic TMDB discovery fallback.
|
|
129
|
+
- [`docs/adr/0004-in-process-sqlite-vectors.md`](./docs/adr/0004-in-process-sqlite-vectors.md) — Zero-dependency in-process SQLite BLOB vector storage.
|
|
130
|
+
- [`docs/adr/0005-dual-source-embedding-strategy.md`](./docs/adr/0005-dual-source-embedding-strategy.md) — OpenRouter with offline local embedding toggle.
|
|
131
|
+
- [`docs/adr/0006-bundled-curated-starter-catalog.md`](./docs/adr/0006-bundled-curated-starter-catalog.md) — Bundled curated starter dataset.
|
|
132
|
+
- [`docs/adr/0007-compatible-embeddings.md`](./docs/adr/0007-compatible-embeddings.md) — Embedding compatibility, batched rebuilds, and lightweight local matching.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Verification Tests
|
|
137
|
+
|
|
138
|
+
Install test dependencies and run the suite. Tests use a temporary database and disable external APIs, leaving your library untouched:
|
|
139
|
+
|
|
140
|
+
```powershell
|
|
141
|
+
.\venv\Scripts\python.exe -m pip install -r requirements-dev.txt
|
|
142
|
+
.\venv\Scripts\python.exe -m pytest -q
|
|
143
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Command-line entry point for an installed CineMatch application."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import getpass
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from dotenv import load_dotenv, set_key
|
|
10
|
+
|
|
11
|
+
DEFAULT_OPENROUTER_MODEL = "openai/gpt-4o"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def default_data_dir() -> Path:
|
|
15
|
+
if sys.platform == "win32":
|
|
16
|
+
root = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
|
|
17
|
+
return Path(root) / "CineMatch" if root else Path.home() / "CineMatch"
|
|
18
|
+
if sys.platform == "darwin":
|
|
19
|
+
return Path.home() / "Library" / "Application Support" / "CineMatch"
|
|
20
|
+
return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "cinematch"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
parser = argparse.ArgumentParser(description="Run the CineMatch web application.")
|
|
25
|
+
parser.add_argument("--host", default="127.0.0.1", help="Address to listen on.")
|
|
26
|
+
parser.add_argument("--port", type=int, help="Port to listen on (default: APP_PORT or 8000).")
|
|
27
|
+
parser.add_argument("--reload", action="store_true", help="Reload when source files change.")
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--configure",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Prompt for the OpenRouter API key and model before starting.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--data-dir",
|
|
35
|
+
type=Path,
|
|
36
|
+
help="Directory for cinematch.db (default: the operating system's user data directory).",
|
|
37
|
+
)
|
|
38
|
+
return parser
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def openrouter_is_configured() -> bool:
|
|
42
|
+
api_key = os.environ.get("OPENROUTER_API_KEY", "").strip()
|
|
43
|
+
model = os.environ.get("OPENROUTER_MODEL", "").strip()
|
|
44
|
+
return bool(api_key and not api_key.startswith("your_") and model)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def configure_openrouter(env_path: Path) -> None:
|
|
48
|
+
if not sys.stdin.isatty():
|
|
49
|
+
raise SystemExit(
|
|
50
|
+
"OpenRouter is not configured. Run 'cinematch --configure' in an interactive "
|
|
51
|
+
"terminal, or set OPENROUTER_API_KEY and OPENROUTER_MODEL."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
print("\nCineMatch first-run setup")
|
|
55
|
+
print("Create an API key at https://openrouter.ai/settings/keys")
|
|
56
|
+
api_key = getpass.getpass("OpenRouter API key (input is hidden): ").strip()
|
|
57
|
+
if not api_key:
|
|
58
|
+
raise SystemExit("An OpenRouter API key is required.")
|
|
59
|
+
|
|
60
|
+
current_model = os.environ.get("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL)
|
|
61
|
+
model = input(f"OpenRouter model [{current_model}]: ").strip() or current_model
|
|
62
|
+
|
|
63
|
+
env_path.touch(exist_ok=True)
|
|
64
|
+
set_key(env_path, "OPENROUTER_API_KEY", api_key)
|
|
65
|
+
set_key(env_path, "OPENROUTER_MODEL", model)
|
|
66
|
+
os.environ["OPENROUTER_API_KEY"] = api_key
|
|
67
|
+
os.environ["OPENROUTER_MODEL"] = model
|
|
68
|
+
print(f"Configuration saved to {env_path}\n")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main() -> None:
|
|
72
|
+
args = build_parser().parse_args()
|
|
73
|
+
data_dir = args.data_dir or default_data_dir()
|
|
74
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
user_env_path = data_dir / ".env"
|
|
76
|
+
|
|
77
|
+
# A project-specific .env takes precedence over the saved user configuration.
|
|
78
|
+
load_dotenv(Path.cwd() / ".env")
|
|
79
|
+
load_dotenv(user_env_path)
|
|
80
|
+
if args.configure or not openrouter_is_configured():
|
|
81
|
+
configure_openrouter(user_env_path)
|
|
82
|
+
|
|
83
|
+
os.environ.setdefault("DATABASE_PATH", str(data_dir / "cinematch.db"))
|
|
84
|
+
|
|
85
|
+
from app.config import settings
|
|
86
|
+
import uvicorn
|
|
87
|
+
|
|
88
|
+
uvicorn.run(
|
|
89
|
+
"app.main:app",
|
|
90
|
+
host=args.host,
|
|
91
|
+
port=args.port or settings.APP_PORT,
|
|
92
|
+
reload=args.reload,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
8
|
+
|
|
9
|
+
class Settings(BaseSettings):
|
|
10
|
+
# OpenRouter
|
|
11
|
+
OPENROUTER_API_KEY: str = Field(default="", description="OpenRouter API Key")
|
|
12
|
+
OPENROUTER_BASE_URL: str = Field(default="https://openrouter.ai/api/v1", description="OpenRouter Base URL")
|
|
13
|
+
OPENROUTER_MODEL: str = Field(default="openai/gpt-4o", description="Model for reasoning and chat")
|
|
14
|
+
|
|
15
|
+
# Embeddings
|
|
16
|
+
EMBEDDING_PROVIDER: Literal["openrouter", "local"] = "openrouter"
|
|
17
|
+
EMBEDDING_MODEL: str = Field(default="openai/text-embedding-3-small", description="Model for text embeddings")
|
|
18
|
+
EMBEDDING_DIM: int = Field(default=1536, gt=0, description="Dimension of embedding vectors")
|
|
19
|
+
|
|
20
|
+
# TMDB API
|
|
21
|
+
TMDB_API_KEY: str = Field(default="", description="The Movie Database API Key")
|
|
22
|
+
TMDB_BASE_URL: str = Field(default="https://api.themoviedb.org/3", description="TMDB API Base URL")
|
|
23
|
+
TMDB_IMAGE_BASE_URL: str = Field(default="https://image.tmdb.org/t/p/w500", description="TMDB Poster CDN Base URL")
|
|
24
|
+
|
|
25
|
+
# Database
|
|
26
|
+
DATABASE_PATH: Path = Field(default=BASE_DIR / "cinematch.db", description="Path to SQLite database")
|
|
27
|
+
|
|
28
|
+
# App Settings
|
|
29
|
+
APP_PORT: int = Field(default=8000)
|
|
30
|
+
DEBUG: bool = Field(default=True)
|
|
31
|
+
|
|
32
|
+
model_config = SettingsConfigDict(
|
|
33
|
+
env_file=BASE_DIR / ".env",
|
|
34
|
+
env_file_encoding="utf-8",
|
|
35
|
+
extra="ignore"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
settings = Settings()
|