rekindle 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.
Files changed (220) hide show
  1. rekindle-0.1.0/.github/workflows/ci.yml +46 -0
  2. rekindle-0.1.0/.gitignore +116 -0
  3. rekindle-0.1.0/CONTRIBUTING.md +183 -0
  4. rekindle-0.1.0/LICENSE +21 -0
  5. rekindle-0.1.0/PKG-INFO +622 -0
  6. rekindle-0.1.0/README.md +579 -0
  7. rekindle-0.1.0/SECURITY.md +62 -0
  8. rekindle-0.1.0/docs/audit-m3-semantic.md +953 -0
  9. rekindle-0.1.0/docs/connecting-google-photos.md +232 -0
  10. rekindle-0.1.0/docs/decision-log-memory-builder.md +936 -0
  11. rekindle-0.1.0/docs/decision-log-memory-engine.md +715 -0
  12. rekindle-0.1.0/docs/decision-log-prompt-memories.md +565 -0
  13. rekindle-0.1.0/docs/decision-log-takeout-enrichment.md +181 -0
  14. rekindle-0.1.0/docs/gallery.md +111 -0
  15. rekindle-0.1.0/docs/known-limitations.md +921 -0
  16. rekindle-0.1.0/docs/publishing.md +90 -0
  17. rekindle-0.1.0/docs/writing-recipes.md +213 -0
  18. rekindle-0.1.0/docs/writing-sources.md +172 -0
  19. rekindle-0.1.0/pyproject.toml +154 -0
  20. rekindle-0.1.0/src/rekindle/__init__.py +3 -0
  21. rekindle-0.1.0/src/rekindle/calibrate/__init__.py +23 -0
  22. rekindle-0.1.0/src/rekindle/calibrate/cli.py +529 -0
  23. rekindle-0.1.0/src/rekindle/calibrate/impact.py +293 -0
  24. rekindle-0.1.0/src/rekindle/calibrate/judge.py +271 -0
  25. rekindle-0.1.0/src/rekindle/calibrate/plan.py +310 -0
  26. rekindle-0.1.0/src/rekindle/calibrate/preview.py +226 -0
  27. rekindle-0.1.0/src/rekindle/calibrate/sampling.py +199 -0
  28. rekindle-0.1.0/src/rekindle/calibrate/session.py +404 -0
  29. rekindle-0.1.0/src/rekindle/calibrate/state.py +275 -0
  30. rekindle-0.1.0/src/rekindle/cli.py +777 -0
  31. rekindle-0.1.0/src/rekindle/config/__init__.py +48 -0
  32. rekindle-0.1.0/src/rekindle/config/cli.py +122 -0
  33. rekindle-0.1.0/src/rekindle/config/defaults.toml +383 -0
  34. rekindle-0.1.0/src/rekindle/config/settings.py +398 -0
  35. rekindle-0.1.0/src/rekindle/config/writer.py +103 -0
  36. rekindle-0.1.0/src/rekindle/db.py +1023 -0
  37. rekindle-0.1.0/src/rekindle/doctor.py +354 -0
  38. rekindle-0.1.0/src/rekindle/enrich/__init__.py +1 -0
  39. rekindle-0.1.0/src/rekindle/enrich/takeout.py +992 -0
  40. rekindle-0.1.0/src/rekindle/extras.py +120 -0
  41. rekindle-0.1.0/src/rekindle/fetch.py +271 -0
  42. rekindle-0.1.0/src/rekindle/identity.py +93 -0
  43. rekindle-0.1.0/src/rekindle/memory/__init__.py +0 -0
  44. rekindle-0.1.0/src/rekindle/memory/albums.py +63 -0
  45. rekindle-0.1.0/src/rekindle/memory/captioning.py +232 -0
  46. rekindle-0.1.0/src/rekindle/memory/captions.py +145 -0
  47. rekindle-0.1.0/src/rekindle/memory/cli.py +1505 -0
  48. rekindle-0.1.0/src/rekindle/memory/composition.py +557 -0
  49. rekindle-0.1.0/src/rekindle/memory/corpus/caption_vocab.toml +191 -0
  50. rekindle-0.1.0/src/rekindle/memory/corpus/festivals.toml +116 -0
  51. rekindle-0.1.0/src/rekindle/memory/corpus/prompt_tags.json +153 -0
  52. rekindle-0.1.0/src/rekindle/memory/corpus/scenery.toml +163 -0
  53. rekindle-0.1.0/src/rekindle/memory/dedup.py +308 -0
  54. rekindle-0.1.0/src/rekindle/memory/diversity.py +453 -0
  55. rekindle-0.1.0/src/rekindle/memory/engine.py +401 -0
  56. rekindle-0.1.0/src/rekindle/memory/festivals.py +118 -0
  57. rekindle-0.1.0/src/rekindle/memory/fingerprint.py +622 -0
  58. rekindle-0.1.0/src/rekindle/memory/history.py +251 -0
  59. rekindle-0.1.0/src/rekindle/memory/index.py +351 -0
  60. rekindle-0.1.0/src/rekindle/memory/llm.py +528 -0
  61. rekindle-0.1.0/src/rekindle/memory/policy.py +311 -0
  62. rekindle-0.1.0/src/rekindle/memory/prompt.py +562 -0
  63. rekindle-0.1.0/src/rekindle/memory/recipes/__init__.py +68 -0
  64. rekindle-0.1.0/src/rekindle/memory/recipes/base.py +109 -0
  65. rekindle-0.1.0/src/rekindle/memory/recipes/builtin.py +607 -0
  66. rekindle-0.1.0/src/rekindle/memory/recipes/registry.py +159 -0
  67. rekindle-0.1.0/src/rekindle/memory/recurring.py +346 -0
  68. rekindle-0.1.0/src/rekindle/memory/render/__init__.py +23 -0
  69. rekindle-0.1.0/src/rekindle/memory/render/frames.py +338 -0
  70. rekindle-0.1.0/src/rekindle/memory/render/gif.py +126 -0
  71. rekindle-0.1.0/src/rekindle/memory/render/motion.py +593 -0
  72. rekindle-0.1.0/src/rekindle/memory/render/mp4.py +277 -0
  73. rekindle-0.1.0/src/rekindle/memory/render/music.py +102 -0
  74. rekindle-0.1.0/src/rekindle/memory/render/onsets.py +260 -0
  75. rekindle-0.1.0/src/rekindle/memory/render/timeline.py +271 -0
  76. rekindle-0.1.0/src/rekindle/memory/scenery.py +351 -0
  77. rekindle-0.1.0/src/rekindle/memory/spec.py +283 -0
  78. rekindle-0.1.0/src/rekindle/memory/strata.py +263 -0
  79. rekindle-0.1.0/src/rekindle/memory/tags.py +378 -0
  80. rekindle-0.1.0/src/rekindle/memory/videoframe.py +422 -0
  81. rekindle-0.1.0/src/rekindle/memory/vocab.py +304 -0
  82. rekindle-0.1.0/src/rekindle/memory/watch.py +163 -0
  83. rekindle-0.1.0/src/rekindle/meta/__init__.py +0 -0
  84. rekindle-0.1.0/src/rekindle/meta/exif.py +235 -0
  85. rekindle-0.1.0/src/rekindle/meta/orientation.py +479 -0
  86. rekindle-0.1.0/src/rekindle/meta/timestamps.py +170 -0
  87. rekindle-0.1.0/src/rekindle/meta/xmp.py +122 -0
  88. rekindle-0.1.0/src/rekindle/models.py +283 -0
  89. rekindle-0.1.0/src/rekindle/py.typed +0 -0
  90. rekindle-0.1.0/src/rekindle/semantic/__init__.py +14 -0
  91. rekindle-0.1.0/src/rekindle/semantic/aesthetic.py +205 -0
  92. rekindle-0.1.0/src/rekindle/semantic/availability.py +97 -0
  93. rekindle-0.1.0/src/rekindle/semantic/cli.py +895 -0
  94. rekindle-0.1.0/src/rekindle/semantic/cluster.py +304 -0
  95. rekindle-0.1.0/src/rekindle/semantic/describe.py +110 -0
  96. rekindle-0.1.0/src/rekindle/semantic/diversity.py +276 -0
  97. rekindle-0.1.0/src/rekindle/semantic/embed.py +510 -0
  98. rekindle-0.1.0/src/rekindle/semantic/encoder.py +431 -0
  99. rekindle-0.1.0/src/rekindle/semantic/faces.py +587 -0
  100. rekindle-0.1.0/src/rekindle/semantic/model-locks.json +23 -0
  101. rekindle-0.1.0/src/rekindle/semantic/photos.py +345 -0
  102. rekindle-0.1.0/src/rekindle/semantic/registry.py +280 -0
  103. rekindle-0.1.0/src/rekindle/semantic/runtime.py +197 -0
  104. rekindle-0.1.0/src/rekindle/semantic/search.py +141 -0
  105. rekindle-0.1.0/src/rekindle/semantic/setup.py +290 -0
  106. rekindle-0.1.0/src/rekindle/semantic/store.py +545 -0
  107. rekindle-0.1.0/src/rekindle/semantic/torchfile.py +177 -0
  108. rekindle-0.1.0/src/rekindle/semantic/vectors.py +110 -0
  109. rekindle-0.1.0/src/rekindle/sidecars.py +60 -0
  110. rekindle-0.1.0/src/rekindle/sources/__init__.py +0 -0
  111. rekindle-0.1.0/src/rekindle/sources/base.py +24 -0
  112. rekindle-0.1.0/src/rekindle/sources/folder.py +401 -0
  113. rekindle-0.1.0/src/rekindle/web/__init__.py +32 -0
  114. rekindle-0.1.0/src/rekindle/web/api.py +976 -0
  115. rekindle-0.1.0/src/rekindle/web/assets/app.css +949 -0
  116. rekindle-0.1.0/src/rekindle/web/assets/app.js +1202 -0
  117. rekindle-0.1.0/src/rekindle/web/assets/index.html +292 -0
  118. rekindle-0.1.0/src/rekindle/web/calibrate_api.py +338 -0
  119. rekindle-0.1.0/src/rekindle/web/cli.py +216 -0
  120. rekindle-0.1.0/src/rekindle/web/draft.py +528 -0
  121. rekindle-0.1.0/src/rekindle/web/library.py +334 -0
  122. rekindle-0.1.0/src/rekindle/web/renderer.py +253 -0
  123. rekindle-0.1.0/src/rekindle/web/server.py +640 -0
  124. rekindle-0.1.0/src/rekindle/web/thumbs.py +110 -0
  125. rekindle-0.1.0/tests/__init__.py +0 -0
  126. rekindle-0.1.0/tests/fixtures/__init__.py +0 -0
  127. rekindle-0.1.0/tests/fixtures/gen.py +307 -0
  128. rekindle-0.1.0/tests/fixtures/oriented.py +122 -0
  129. rekindle-0.1.0/tests/fixtures/prompt_gate_concepts.json +113 -0
  130. rekindle-0.1.0/tests/fixtures/semantic.py +231 -0
  131. rekindle-0.1.0/tests/fixtures/takeout.py +257 -0
  132. rekindle-0.1.0/tests/fixtures/web.py +215 -0
  133. rekindle-0.1.0/tests/test_calibrate.py +927 -0
  134. rekindle-0.1.0/tests/test_calibrate_cli.py +329 -0
  135. rekindle-0.1.0/tests/test_caption_real.py +160 -0
  136. rekindle-0.1.0/tests/test_caption_vocab.py +403 -0
  137. rekindle-0.1.0/tests/test_captioning.py +408 -0
  138. rekindle-0.1.0/tests/test_cli.py +324 -0
  139. rekindle-0.1.0/tests/test_composition.py +544 -0
  140. rekindle-0.1.0/tests/test_config.py +836 -0
  141. rekindle-0.1.0/tests/test_db.py +582 -0
  142. rekindle-0.1.0/tests/test_db_v3.py +369 -0
  143. rekindle-0.1.0/tests/test_dedup.py +412 -0
  144. rekindle-0.1.0/tests/test_diversity.py +492 -0
  145. rekindle-0.1.0/tests/test_doctor.py +342 -0
  146. rekindle-0.1.0/tests/test_engine.py +1478 -0
  147. rekindle-0.1.0/tests/test_exif.py +170 -0
  148. rekindle-0.1.0/tests/test_fetch.py +500 -0
  149. rekindle-0.1.0/tests/test_fingerprint.py +553 -0
  150. rekindle-0.1.0/tests/test_fixtures.py +120 -0
  151. rekindle-0.1.0/tests/test_folder_source.py +543 -0
  152. rekindle-0.1.0/tests/test_history.py +297 -0
  153. rekindle-0.1.0/tests/test_identity.py +113 -0
  154. rekindle-0.1.0/tests/test_llm.py +501 -0
  155. rekindle-0.1.0/tests/test_memory_cli.py +1461 -0
  156. rekindle-0.1.0/tests/test_memory_conformance.py +399 -0
  157. rekindle-0.1.0/tests/test_memory_index.py +535 -0
  158. rekindle-0.1.0/tests/test_meta_orientation.py +676 -0
  159. rekindle-0.1.0/tests/test_models.py +290 -0
  160. rekindle-0.1.0/tests/test_motion.py +405 -0
  161. rekindle-0.1.0/tests/test_onsets.py +257 -0
  162. rekindle-0.1.0/tests/test_orientation.py +416 -0
  163. rekindle-0.1.0/tests/test_packaging.py +296 -0
  164. rekindle-0.1.0/tests/test_policy.py +365 -0
  165. rekindle-0.1.0/tests/test_prompt.py +538 -0
  166. rekindle-0.1.0/tests/test_prompt_real.py +431 -0
  167. rekindle-0.1.0/tests/test_prompt_tags.py +310 -0
  168. rekindle-0.1.0/tests/test_recipes.py +728 -0
  169. rekindle-0.1.0/tests/test_recurring.py +502 -0
  170. rekindle-0.1.0/tests/test_render.py +1104 -0
  171. rekindle-0.1.0/tests/test_scenery.py +371 -0
  172. rekindle-0.1.0/tests/test_scenery_real.py +161 -0
  173. rekindle-0.1.0/tests/test_semantic_aesthetic.py +216 -0
  174. rekindle-0.1.0/tests/test_semantic_availability.py +102 -0
  175. rekindle-0.1.0/tests/test_semantic_cli.py +197 -0
  176. rekindle-0.1.0/tests/test_semantic_cluster.py +180 -0
  177. rekindle-0.1.0/tests/test_semantic_diversity.py +314 -0
  178. rekindle-0.1.0/tests/test_semantic_embed.py +479 -0
  179. rekindle-0.1.0/tests/test_semantic_face_detector.py +380 -0
  180. rekindle-0.1.0/tests/test_semantic_faces.py +197 -0
  181. rekindle-0.1.0/tests/test_semantic_gate_driver.py +521 -0
  182. rekindle-0.1.0/tests/test_semantic_imports.py +188 -0
  183. rekindle-0.1.0/tests/test_semantic_mutants.py +289 -0
  184. rekindle-0.1.0/tests/test_semantic_onnx_preprocess.py +172 -0
  185. rekindle-0.1.0/tests/test_semantic_photos.py +199 -0
  186. rekindle-0.1.0/tests/test_semantic_pipeline.py +302 -0
  187. rekindle-0.1.0/tests/test_semantic_runtime.py +162 -0
  188. rekindle-0.1.0/tests/test_semantic_search.py +184 -0
  189. rekindle-0.1.0/tests/test_semantic_staleness_real.py +156 -0
  190. rekindle-0.1.0/tests/test_semantic_store.py +295 -0
  191. rekindle-0.1.0/tests/test_semantic_torch_encoder.py +274 -0
  192. rekindle-0.1.0/tests/test_sidecars.py +65 -0
  193. rekindle-0.1.0/tests/test_smoke.py +6 -0
  194. rekindle-0.1.0/tests/test_spec.py +280 -0
  195. rekindle-0.1.0/tests/test_strata.py +375 -0
  196. rekindle-0.1.0/tests/test_takeout_albums.py +189 -0
  197. rekindle-0.1.0/tests/test_takeout_apply.py +260 -0
  198. rekindle-0.1.0/tests/test_takeout_conformance.py +197 -0
  199. rekindle-0.1.0/tests/test_takeout_derivatives.py +190 -0
  200. rekindle-0.1.0/tests/test_takeout_enricher.py +858 -0
  201. rekindle-0.1.0/tests/test_takeout_fixtures.py +95 -0
  202. rekindle-0.1.0/tests/test_takeout_index.py +363 -0
  203. rekindle-0.1.0/tests/test_takeout_parse.py +253 -0
  204. rekindle-0.1.0/tests/test_timeline.py +182 -0
  205. rekindle-0.1.0/tests/test_timestamps.py +215 -0
  206. rekindle-0.1.0/tests/test_videoframe.py +636 -0
  207. rekindle-0.1.0/tests/test_watch.py +222 -0
  208. rekindle-0.1.0/tests/test_web_api.py +306 -0
  209. rekindle-0.1.0/tests/test_web_assets.py +152 -0
  210. rekindle-0.1.0/tests/test_web_calibrate.py +305 -0
  211. rekindle-0.1.0/tests/test_web_cli.py +204 -0
  212. rekindle-0.1.0/tests/test_web_draft.py +350 -0
  213. rekindle-0.1.0/tests/test_web_progress.py +202 -0
  214. rekindle-0.1.0/tests/test_web_render.py +257 -0
  215. rekindle-0.1.0/tests/test_web_semantic.py +164 -0
  216. rekindle-0.1.0/tests/test_web_server.py +550 -0
  217. rekindle-0.1.0/tests/test_web_suggestions.py +265 -0
  218. rekindle-0.1.0/tests/test_web_thumbs.py +106 -0
  219. rekindle-0.1.0/tests/test_xmp.py +125 -0
  220. rekindle-0.1.0/uv.lock +1424 -0
@@ -0,0 +1,46 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest, macos-latest, windows-latest]
15
+ python-version: ["3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v5
19
+ with:
20
+ enable-cache: true
21
+ # Without this the matrix axis is inert: setup-uv gets no version,
22
+ # there is no .python-version file, and all six legs run whatever
23
+ # interpreter the runner ships - so the 3.12 floor in pyproject.toml
24
+ # was never actually verified. The input sets UV_PYTHON, which
25
+ # `uv sync` and `uv run` both honour.
26
+ python-version: ${{ matrix.python-version }}
27
+ - run: uv sync --all-extras
28
+ # Proves the pin took effect. A silently-ignored python-version would
29
+ # otherwise leave this job green while testing the wrong interpreter.
30
+ - run: uv run python -c "import sys; assert sys.version.startswith('${{ matrix.python-version }}'), sys.version"
31
+ - run: uv run python -VV
32
+ - run: uv run ruff check .
33
+ - run: uv run ruff format --check .
34
+ - run: uv run pytest -v
35
+
36
+ # --all-extras above means the no-HEIC path is never exercised. It is a
37
+ # supported configuration, so it needs a job.
38
+ test-minimal:
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: astral-sh/setup-uv@v5
43
+ with:
44
+ python-version: "3.12" # the floor, so the minimal job guards it too
45
+ - run: uv sync
46
+ - run: uv run pytest -v
@@ -0,0 +1,116 @@
1
+ # Secrets — never commit
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ *.key
6
+ credentials*.json
7
+ client_secret*.json
8
+ token*.json
9
+
10
+ # Personal data — never commit
11
+ # Case variants are spelled out: git matches these patterns case-SENSITIVELY
12
+ # unless core.ignorecase is set (it is, on Windows and default macOS, which
13
+ # is exactly why a lowercase-only `takeout/` looked fine here). On Linux or a
14
+ # case-sensitive macOS volume a real `Takeout/` export is NOT ignored, and
15
+ # one `git add .` publishes tens of thousands of personal photos to a public
16
+ # repo. There is no undo for that.
17
+ [Dd]ata/
18
+ [Tt]akeout/
19
+ [Pp]hotos/
20
+ [Tt]humbnails/
21
+ index/
22
+ memories/
23
+
24
+ # Media extensions, at any depth, on any filesystem. The directory rules above
25
+ # only protect an export that sits in a directory NAMED Takeout/Data/Photos -
26
+ # extract one anywhere else (or into a subfolder of a working directory) and a
27
+ # contributor's `git add .` publishes it. This repo is PUBLIC and there is no
28
+ # undo for that, so this is defence in depth on the one irreversible mistake.
29
+ #
30
+ # Spelled out with the same [Jj][Pp][Gg] bracket style as the directories
31
+ # above, for the same reason: git matches patterns case-SENSITIVELY unless
32
+ # core.ignorecase is set (it is on Windows and default macOS - and NOT on
33
+ # Linux or a case-sensitive macOS volume, where a `.JPG` from a camera is the
34
+ # common case, not the exotic one).
35
+ #
36
+ # Verified with `git ls-files` before and after: the repo tracks no file with
37
+ # any of these extensions, so nothing already tracked becomes ignored. If you
38
+ # ever need to commit a real media test asset, force it with `git add -f` and
39
+ # add an explicit `!` negation here next to it.
40
+ *.[Jj][Pp][Gg]
41
+ *.[Jj][Pp][Ee][Gg]
42
+ *.[Pp][Nn][Gg]
43
+ *.[Gg][Ii][Ff]
44
+ *.[Bb][Mm][Pp]
45
+ *.[Ww][Ee][Bb][Pp]
46
+ *.[Tt][Ii][Ff]
47
+ *.[Tt][Ii][Ff][Ff]
48
+ *.[Hh][Ee][Ii][Cc]
49
+ *.[Hh][Ee][Ii][Ff]
50
+ *.[Dd][Nn][Gg]
51
+ *.[Cc][Rr]2
52
+ *.[Cc][Rr]3
53
+ *.[Nn][Ee][Ff]
54
+ *.[Aa][Rr][Ww]
55
+ *.[Oo][Rr][Ff]
56
+ *.[Rr][Ww]2
57
+ *.[Rr][Aa][Ff]
58
+ *.[Rr][Aa][Ww]
59
+ *.[Mm][Pp]4
60
+ *.[Mm][Oo][Vv]
61
+ *.[Mm]4[Vv]
62
+ *.[Aa][Vv][Ii]
63
+ *.[Mm][Kk][Vv]
64
+ *.[Ww][Mm][Vv]
65
+ *.[Ww][Ee][Bb][Mm]
66
+ *.3[Gg][Pp]
67
+ *.[Mm][Tt][Ss]
68
+ *.[Mm]2[Tt][Ss]
69
+ *.[Mm][Pp][Gg]
70
+ *.[Mm][Pp][Ee][Gg]
71
+ # Google's motion-photo video half, exported beside the still as `<name>.MP`.
72
+ *.[Mm][Pp]
73
+
74
+ # Python
75
+ __pycache__/
76
+ *.py[cod]
77
+ .venv/
78
+ venv/
79
+ *.egg-info/
80
+ dist/
81
+ build/
82
+ .pytest_cache/
83
+ .ruff_cache/
84
+ .mypy_cache/
85
+ .coverage
86
+ htmlcov/
87
+
88
+ # Models
89
+ models/
90
+ *.safetensors
91
+ *.onnx
92
+
93
+ # OS / editors
94
+ .DS_Store
95
+ Thumbs.db
96
+ .idea/
97
+ .vscode/
98
+
99
+ # Archives, except test fixtures
100
+ *.zip
101
+ *.tgz
102
+ !tests/fixtures/**/*.zip
103
+ !tests/fixtures/**/*.tgz
104
+
105
+ # SDD scratch workspace
106
+ .superpowers/
107
+
108
+ # Agent harness scratch. `.claude/worktrees/` holds git worktrees, which git
109
+ # records as GITLINKS - a `git add .` commits a reference to a commit that
110
+ # does not exist for anyone who clones the repo, producing a broken empty
111
+ # directory and submodule warnings. Ignored so it cannot happen again.
112
+ .claude/
113
+
114
+ # Music beds for memory soundtracks. Bring-your-own or fetched;
115
+ # never committed - this is a public repo.
116
+ [Mm]usic/
@@ -0,0 +1,183 @@
1
+ # Contributing to rekindle
2
+
3
+ Thanks for considering it. This project is meant to be extended.
4
+
5
+ ## The three extension points
6
+
7
+ Almost every contribution fits one of these, and none of them require touching
8
+ the core engine.
9
+
10
+ ### 1. A new memory type (best first contribution)
11
+
12
+ A `Recipe` decides which photos belong in a memory and how they are ordered.
13
+ One file, one protocol, one registry entry:
14
+
15
+ ```python
16
+ class Recipe(Protocol):
17
+ name: str
18
+ params_model: type[BaseModel]
19
+
20
+ def candidates(self, params, index) -> list[Photo]: ...
21
+ def order(self, photos, params) -> list[PhotoRef]: ...
22
+ def fact_sheet(self, ordered, params) -> FactSheet: ...
23
+ ```
24
+
25
+ Recipes choose photos and their order. They do **not** own timing — a separate
26
+ `Timeline` stage does, so beat-synced music doesn't break every recipe. That
27
+ stage now exists (`memory/render/timeline.py`) and nothing above it had to
28
+ change to get crossfades, Ken Burns or beat-snapped cuts. Full guide:
29
+ [writing-recipes.md](docs/writing-recipes.md).
30
+
31
+ A recipe also cannot reach the semantic layer, which is why **scenery memories
32
+ are not a recipe**: selection needs a retriever, `Recipe.select` has nowhere to
33
+ receive one, and CI has no embeddings at all. They hand the engine a
34
+ `Selection` directly, exactly as prompt memories do, and every guardrail below
35
+ `engine.build` still applies. See `memory/scenery.py`.
36
+
37
+ Ideas nobody has built yet: *Kids Growing Up*, *Every Sunset*, *This Café Over
38
+ The Years*, *Seasons In One Place*, *Everyone Who Came To Dinner*.
39
+
40
+ ### 2. A new photo source
41
+
42
+ `Source` normalises any library into `Photo` records. Apple Photos, Immich,
43
+ Nextcloud, Synology Photos and PhotoPrism are all wanted. One protocol, two
44
+ members. Full guide: [writing-sources.md](docs/writing-sources.md).
45
+
46
+ ```python
47
+ class Source(Protocol):
48
+ name: str
49
+
50
+ def scan(self, root: Path) -> tuple[list[Photo], SourceReport]: ...
51
+ ```
52
+
53
+ ### 3. A new model backend
54
+
55
+ `Embedder`, `Captioner` and `Narrator` are swappable. Ollama, llama.cpp,
56
+ Gemini and local VLMs all fit.
57
+
58
+ ### 4. A corpus entry (the smallest useful contribution)
59
+
60
+ Three checked-in data files decide what rekindle can find and what it may say,
61
+ and none of them needs a line of code:
62
+
63
+ - `corpus/festivals.toml` — a festival, and what a photograph of it looks like.
64
+ - `corpus/scenery.toml` — a scene that can be the subject of a memory.
65
+ - `corpus/caption_vocab.toml` — a thing a caption is allowed to mention.
66
+
67
+ **Grade what you add.** Every entry in the first two carries the count someone
68
+ got by looking at a contact sheet of what it returned, and the weak entries
69
+ say what is wrong with them rather than being quietly dropped. A description
70
+ that "seems right" is how nine of twenty-four shots of a Kali Puja memory
71
+ ended up on a Durga Puja day. The caption vocabulary is stricter still: five
72
+ rules about what may never be in it, all enforced by
73
+ `tests/test_caption_vocab.py`, which fails the build rather than trusting a
74
+ comment.
75
+
76
+ ## Ground rules
77
+
78
+ **Never commit personal data.** No photos, no Takeout exports, no `.env`. The
79
+ `.gitignore` is deliberately aggressive — please keep it that way. It ignores
80
+ the usual export folders (`Takeout/`, `Data/`, `Photos/`, `Thumbnails/`) **and**
81
+ every common photo and video extension at any depth — `.jpg`, `.JPG`, `.heic`,
82
+ `.dng`, `.CR2`, `.mp4`, `.MOV`, `.MP` and friends — each spelled with
83
+ `[Jj][Pp][Gg]`-style brackets so it still holds on a case-sensitive filesystem,
84
+ where an export extracted outside those folders would otherwise have no
85
+ protection at all. This repo is public and that mistake cannot be undone. If you
86
+ genuinely need to commit a media test asset, `git add -f` it and add an explicit
87
+ `!` negation beside the rule.
88
+
89
+ **Tests must run without photos, without a GPU, and without API keys.** CI has
90
+ none of those. Use the synthetic fixture generator in `tests/fixtures/` and the
91
+ `Fake*` providers. If your change needs a real model to be tested, it needs a
92
+ fake too.
93
+
94
+ **Cross-platform.** Windows, macOS and Linux are all supported. Use `pathlib`,
95
+ never hardcode separators or drive letters, and don't assume ffmpeg's location.
96
+
97
+ **Found a new Takeout quirk?** That's a genuinely valuable bug report. Please
98
+ include the filename shape and what `rekindle doctor` said — and if you can,
99
+ add a fixture case reproducing it.
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ uv sync
105
+ uv run pytest
106
+ uv run ruff check .
107
+ uv run ruff format .
108
+ ```
109
+
110
+ ### Packaging
111
+
112
+ ```bash
113
+ uv build # wheel + sdist into dist/
114
+ uv run pytest tests/test_packaging.py
115
+ ```
116
+
117
+ `tests/test_packaging.py` checks the wheel's contents only when `dist/` holds
118
+ one, so build first or the interesting half silently skips.
119
+
120
+ **Publishing is not automated and no agent does it.** See
121
+ [docs/publishing.md](https://github.com/abhikatoldtrafford/rekindle/blob/main/docs/publishing.md)
122
+ for the single command, what it irreversibly does, and the checklist before it.
123
+
124
+ ### The semantic extras
125
+
126
+ `rekindle semantic ...` (embeddings, search, scene clusters, aesthetic
127
+ ranking, the face gate) lives behind two optional extras, and **CI installs
128
+ neither**:
129
+
130
+ ```bash
131
+ uv sync --extra semantic # numpy + onnxruntime, CPU, ~120 MB
132
+ uv sync --extra semantic-gpu # adds torch + transformers, CUDA, ~2.5 GB
133
+ ```
134
+
135
+ You do not need either to work on rekindle, or to run the tests. The semantic
136
+ tests use `tests/fixtures/semantic.py`, whose `ToyEncoder` is a real (tiny)
137
+ joint image/text embedding function over a colour grid — not a mock. If you
138
+ add a test there, assert the *answer*, not that the encoder was called: a test
139
+ that mocks the model and checks the mock was called proves nothing.
140
+
141
+ Three rules for anything under `src/rekindle/semantic/`:
142
+
143
+ - **No heavy import at module level.** `rekindle --help` must not import
144
+ numpy, torch, onnxruntime or transformers.
145
+ `tests/test_semantic_imports.py` checks this in a clean subprocess.
146
+ - **Every feature degrades to a sentence**, never an ImportError, and exits 3.
147
+ - **Model weights are pinned to a commit and checksummed.** Adding a model
148
+ means adding a `RepoPin` with a 40-hex revision, a licence with a URL, and
149
+ an entry in `model-locks.json` (`rekindle semantic setup --write-lock`).
150
+
151
+ ### Testing against a real Takeout export
152
+
153
+ Two of this project's three worst bugs were invisible to a green test suite and
154
+ obvious within one run against real data. If you have a Google Takeout export:
155
+
156
+ ```bash
157
+ REKINDLE_TAKEOUT_DIR="/path/to/Takeout/Google Photos" uv run pytest \
158
+ tests/test_takeout_conformance.py -v
159
+ ```
160
+
161
+ Those tests skip without the variable, so CI never needs your photos. Never
162
+ commit an export, or any file from one.
163
+
164
+ Every genuine bug in the Takeout enrichment milestone was found this way and
165
+ none by reading. The memory engine repeated the pattern: two of its bugs were
166
+ invisible to 700 passing tests and obvious within one run against real data.
167
+
168
+ If you have an index, the memory engine has its own conformance suite:
169
+
170
+ ```bash
171
+ REKINDLE_MEMORY_DB=data/rekindle.sqlite uv run pytest \
172
+ tests/test_memory_conformance.py -v
173
+ ```
174
+
175
+ Both decision logs record the defects, the judgement calls and the testing
176
+ discipline they led to:
177
+ [Takeout enrichment](docs/decision-log-takeout-enrichment.md) ·
178
+ [the memory engine](docs/decision-log-memory-engine.md).
179
+
180
+ ## Pull requests
181
+
182
+ Keep them focused, explain the why, and add tests. If you're planning something
183
+ large, open an issue first so we can agree on the shape before you build it.
rekindle-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abhik Maiti
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.