trailframe 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 (114) hide show
  1. trailframe-0.1.0/.gitignore +21 -0
  2. trailframe-0.1.0/AGENTS.md +45 -0
  3. trailframe-0.1.0/LICENSE +21 -0
  4. trailframe-0.1.0/PKG-INFO +130 -0
  5. trailframe-0.1.0/README.md +96 -0
  6. trailframe-0.1.0/docs/img/activities_garmin.png +0 -0
  7. trailframe-0.1.0/docs/img/gallery_fish_dog.jpg +0 -0
  8. trailframe-0.1.0/docs/img/slideshow_activity_1.png +0 -0
  9. trailframe-0.1.0/docs/img/slideshow_activity_2.jpg +0 -0
  10. trailframe-0.1.0/docs/img/tools_map.png +0 -0
  11. trailframe-0.1.0/frontend/.gitignore +24 -0
  12. trailframe-0.1.0/frontend/dist/assets/favicon-DuTzlsvV.png +0 -0
  13. trailframe-0.1.0/frontend/dist/assets/main-8Rix1cnZ.js +1 -0
  14. trailframe-0.1.0/frontend/dist/assets/main-vh-t_kPv.css +1 -0
  15. trailframe-0.1.0/frontend/dist/assets/slideshow-GzAXSY-n.js +1 -0
  16. trailframe-0.1.0/frontend/dist/assets/theme-CZT-v4sH.css +1 -0
  17. trailframe-0.1.0/frontend/dist/assets/theme-sVwLD9Y8.js +340 -0
  18. trailframe-0.1.0/frontend/dist/index.html +16 -0
  19. trailframe-0.1.0/frontend/dist/slideshow.html +15 -0
  20. trailframe-0.1.0/frontend/eslint.config.js +22 -0
  21. trailframe-0.1.0/frontend/favicon.png +0 -0
  22. trailframe-0.1.0/frontend/index.html +13 -0
  23. trailframe-0.1.0/frontend/package-lock.json +3622 -0
  24. trailframe-0.1.0/frontend/package.json +39 -0
  25. trailframe-0.1.0/frontend/slideshow.html +13 -0
  26. trailframe-0.1.0/frontend/src/App.tsx +120 -0
  27. trailframe-0.1.0/frontend/src/SlideshowApp.tsx +93 -0
  28. trailframe-0.1.0/frontend/src/api/client.ts +4 -0
  29. trailframe-0.1.0/frontend/src/api/generated/schema.ts +2146 -0
  30. trailframe-0.1.0/frontend/src/components/ActivityList.tsx +163 -0
  31. trailframe-0.1.0/frontend/src/components/CustomSlideshowMenu.tsx +253 -0
  32. trailframe-0.1.0/frontend/src/components/EventBar.tsx +115 -0
  33. trailframe-0.1.0/frontend/src/components/GarminActivity.tsx +190 -0
  34. trailframe-0.1.0/frontend/src/components/GpxActivity.tsx +174 -0
  35. trailframe-0.1.0/frontend/src/components/GroupedGallery.tsx +252 -0
  36. trailframe-0.1.0/frontend/src/components/ImageViewer.tsx +88 -0
  37. trailframe-0.1.0/frontend/src/components/PhotoMetadata.tsx +246 -0
  38. trailframe-0.1.0/frontend/src/components/SectionPage.tsx +54 -0
  39. trailframe-0.1.0/frontend/src/components/ThumbnailGallery.tsx +33 -0
  40. trailframe-0.1.0/frontend/src/components/ThumbnailItem.tsx +119 -0
  41. trailframe-0.1.0/frontend/src/components/UploadButton.tsx +74 -0
  42. trailframe-0.1.0/frontend/src/components/WireframeMap.tsx +65 -0
  43. trailframe-0.1.0/frontend/src/events/EventContext.ts +37 -0
  44. trailframe-0.1.0/frontend/src/events/EventProvider.tsx +98 -0
  45. trailframe-0.1.0/frontend/src/index.css +6 -0
  46. trailframe-0.1.0/frontend/src/lib/format.ts +16 -0
  47. trailframe-0.1.0/frontend/src/lib/groups.ts +7 -0
  48. trailframe-0.1.0/frontend/src/lib/projection.ts +21 -0
  49. trailframe-0.1.0/frontend/src/lib/slideshowSections.ts +42 -0
  50. trailframe-0.1.0/frontend/src/main.tsx +13 -0
  51. trailframe-0.1.0/frontend/src/pages/ActivitiesPage.tsx +41 -0
  52. trailframe-0.1.0/frontend/src/pages/ConfigurationPage.tsx +427 -0
  53. trailframe-0.1.0/frontend/src/pages/GalleryPage.tsx +342 -0
  54. trailframe-0.1.0/frontend/src/pages/SlideshowMenuPage.tsx +128 -0
  55. trailframe-0.1.0/frontend/src/pages/SlideshowPage.tsx +649 -0
  56. trailframe-0.1.0/frontend/src/pages/ToolsPage.tsx +420 -0
  57. trailframe-0.1.0/frontend/src/slideshow-main.tsx +13 -0
  58. trailframe-0.1.0/frontend/src/theme.tsx +27 -0
  59. trailframe-0.1.0/frontend/tsconfig.app.json +26 -0
  60. trailframe-0.1.0/frontend/tsconfig.json +7 -0
  61. trailframe-0.1.0/frontend/tsconfig.node.json +23 -0
  62. trailframe-0.1.0/frontend/vite.config.ts +21 -0
  63. trailframe-0.1.0/pyproject.toml +74 -0
  64. trailframe-0.1.0/supervisor.py +79 -0
  65. trailframe-0.1.0/trailframe/__init__.py +0 -0
  66. trailframe-0.1.0/trailframe/__main__.py +3 -0
  67. trailframe-0.1.0/trailframe/api/__init__.py +0 -0
  68. trailframe-0.1.0/trailframe/api/about.py +84 -0
  69. trailframe-0.1.0/trailframe/api/activities.py +205 -0
  70. trailframe-0.1.0/trailframe/api/configuration.py +19 -0
  71. trailframe-0.1.0/trailframe/api/events.py +45 -0
  72. trailframe-0.1.0/trailframe/api/map_data.py +63 -0
  73. trailframe-0.1.0/trailframe/api/models.py +52 -0
  74. trailframe-0.1.0/trailframe/api/photos.py +338 -0
  75. trailframe-0.1.0/trailframe/api/pipeline.py +37 -0
  76. trailframe-0.1.0/trailframe/api/statistics.py +43 -0
  77. trailframe-0.1.0/trailframe/api/tiles.py +16 -0
  78. trailframe-0.1.0/trailframe/main.py +132 -0
  79. trailframe-0.1.0/trailframe/models/__init__.py +0 -0
  80. trailframe-0.1.0/trailframe/models/activity.py +411 -0
  81. trailframe-0.1.0/trailframe/models/group.py +19 -0
  82. trailframe-0.1.0/trailframe/models/photo.py +110 -0
  83. trailframe-0.1.0/trailframe/models/scanner_stat.py +14 -0
  84. trailframe-0.1.0/trailframe/services/__init__.py +0 -0
  85. trailframe-0.1.0/trailframe/services/activity_service.py +224 -0
  86. trailframe-0.1.0/trailframe/services/configuration_service.py +168 -0
  87. trailframe-0.1.0/trailframe/services/database_service.py +137 -0
  88. trailframe-0.1.0/trailframe/services/folder_service.py +208 -0
  89. trailframe-0.1.0/trailframe/services/garmin_connect_service.py +201 -0
  90. trailframe-0.1.0/trailframe/services/gpx_service.py +92 -0
  91. trailframe-0.1.0/trailframe/services/location_service.py +435 -0
  92. trailframe-0.1.0/trailframe/services/map_service.py +230 -0
  93. trailframe-0.1.0/trailframe/services/pipeline_service.py +71 -0
  94. trailframe-0.1.0/trailframe/services/pipelines/__init__.py +0 -0
  95. trailframe-0.1.0/trailframe/services/pipelines/basic_pipeline.py +59 -0
  96. trailframe-0.1.0/trailframe/services/pipelines/creation_pipeline.py +54 -0
  97. trailframe-0.1.0/trailframe/services/pipelines/executor.py +15 -0
  98. trailframe-0.1.0/trailframe/services/pipelines/pipeline.py +174 -0
  99. trailframe-0.1.0/trailframe/services/scanners/__init__.py +0 -0
  100. trailframe-0.1.0/trailframe/services/scanners/activity_scanner.py +76 -0
  101. trailframe-0.1.0/trailframe/services/scanners/brisque_model_live.yml +9424 -0
  102. trailframe-0.1.0/trailframe/services/scanners/brisque_range_live.yml +25 -0
  103. trailframe-0.1.0/trailframe/services/scanners/brisque_scanner.py +28 -0
  104. trailframe-0.1.0/trailframe/services/scanners/exif_scanner.py +96 -0
  105. trailframe-0.1.0/trailframe/services/scanners/file_scanner.py +22 -0
  106. trailframe-0.1.0/trailframe/services/scanners/location_scanner.py +36 -0
  107. trailframe-0.1.0/trailframe/services/scanners/object_scanner.py +71 -0
  108. trailframe-0.1.0/trailframe/services/scanners/perceptual_hash_scanner.py +19 -0
  109. trailframe-0.1.0/trailframe/services/scanners/scanner.py +71 -0
  110. trailframe-0.1.0/trailframe/services/scanners/thumbnail_scanner.py +14 -0
  111. trailframe-0.1.0/trailframe/services/service.py +68 -0
  112. trailframe-0.1.0/trailframe/services/statistics_service.py +96 -0
  113. trailframe-0.1.0/trailframe/services/thumbnail_service.py +101 -0
  114. trailframe-0.1.0/trailframe/services/tile_service.py +168 -0
@@ -0,0 +1,21 @@
1
+ __pycache__
2
+ *.pyc
3
+ .ruff_cache
4
+
5
+ node_modules/
6
+ dist/
7
+
8
+ # Backend runtime artifacts (CWD when launched)
9
+ /config.yaml
10
+ /gallery.db
11
+ /photos/
12
+ /thumbnails/
13
+ /trash/
14
+ /maps/
15
+ /activities/
16
+ /tiles/
17
+ /models/
18
+
19
+ # Python build artifacts
20
+ /build/
21
+ *.egg-info
@@ -0,0 +1,45 @@
1
+ # AGENTS.md
2
+
3
+ Trailframe — photo gallery app: Python/FastAPI backend + React/TS/Vite frontend, packaged as the pip-installable app `trailframe` (wheels attached to GitHub releases). The backend serves the built frontend; in prod there is a single server.
4
+
5
+ ## Layout
6
+
7
+ - `trailframe/` (repo root) — FastAPI app (`trailframe` package).
8
+ - `frontend/` — Vite + React app.
9
+ - `pyproject.toml` (repo root) — package metadata, dependencies, ruff config, hatchling build (`packages = ["trailframe"]`, force-includes `frontend/dist` as `trailframe/frontend/dist` into the wheel); console script `trailframe` = `trailframe.main:main`.
10
+ - `supervisor.py` (repo root) — supervisor: starts `python -m trailframe.main` with CWD set to the repo root, restarts it when it exits, forwards SIGTERM/SIGBREAK.
11
+
12
+ ## Backend (FastAPI, Python 3.12)
13
+
14
+ - Dependencies are declared in the root `pyproject.toml`. Note: `torch` (pulled by `ultralytics`) can't extract into the Store Python's site-packages (Windows long-path limit, no admin to enable them); it's installed to `C:\pylibs` (short path) and wired in via a `.pth` file in the user site-packages. `pip install <pkg>` on this machine may fail on long paths — use `pip install --target C:/pylibs <pkg>` for packages with deep dirs.
15
+ - **Python style**: ruff configured in the root `pyproject.toml` (`ruff check`, `ruff format`).
16
+ - **Run from the repo root**: `python -m trailframe.main` (or the `trailframe` console script once pip-installed). Package imports are `trailframe.*`; `main.py` serves `frontend/dist` relative to CWD when present, otherwise the packaged copy inside the installed `trailframe` package (`trailframe/frontend/dist`).
17
+ - CLI flags (`main.py`): `--config` (default `config.yaml`), `--folder` (sets the `*_folder` subdirs), `--database`, `--port` (default 8000), `--openapi FILE` (write OpenAPI schema and exit).
18
+ - **Runtime artifacts are written to CWD**: `config.yaml` (auto-saved on startup), `gallery.db`, `photos/`, `thumbnails/`, `trash/`, `maps/`, `activities/`, `models/`, `tiles/`. All are gitignored at those repo-root-relative paths — don't commit them.
19
+ - **Wheels/releases**: build the web UI first (`npm run build` inside `frontend/`) — the hatchling force-include fails if `frontend/dist` is missing — then `python -m build` or `pip wheel .`; attach the wheel to a GitHub release (that's the distribution channel; no PyPI). The sdist carries `frontend/dist` at its original path (sdist `artifacts`) so the wheel built from the sdist (`python -m build` does this) can resolve its force-include.
20
+ - Services are class-based singletons (`Service` subclasses) wired by hand in the `main.py` lifespan. The `Service` base class exposes non-virtual classmethod templates `configure`/`start`/`stop` that call virtual `_configure`/`_start`/`_stop`, log via `_log` (`[Name] message`), and track a state flag (`ServiceState`: `not started`/`started`/`starting failure`, set to the last on a `_start` exception which is re-raised). `name` defaults to `cls.__name__` (`get_name()`), overridable via `_name`. Pipelines live in `trailframe/services/pipelines/`: `Pipeline` is the base class managing the async queue (`add`, `get_queue_size`, `accepts`, `start`, `stop`). `PipelineEmpty` is a sentinel event: all pipelines accept it (`is_pipeline_empty`) and `add` drops it without queuing. Start order matters: `DatabaseService` must start first (tables are created in its `_start`), then `PipelineService`, then `FolderService`.
21
+ - **Call hierarchy**: creating content or enriching a `Photo` goes `Pipeline -> Scanner -> Service` (when a service applies) — scanners orchestrate enrichment and delegate I/O/content work to services; API content fetching goes `API -> Service` and must **never** call a Scanner (e.g. wireframe/map data is read through `LocationService`/`MapService`, not the scanner).
22
+ - **Scanners are registered explicitly** in each pipeline's `_scanners` list (NOT auto-discovered despite the empty `scanners/` namespace package). Pipelines are managed by `PipelineService` (`trailframe/services/pipeline_service.py`): it explicitly registers `CreationPipeline`/`BasicPipeline`, configures/starts/stops them, and `next(item, pipeline=None)` routes the item to the first pipeline whose `accepts(item)` matches — `pipeline=None` starts the chain, passing the current pipeline class continues to the next accepting pipeline (no match → no-op). A pipeline hands off by calling `PipelineService.next(item, cls)` from `_process_item`. `CreationPipeline` (accepts `Path`/`str`) runs `FileScanner` (fills `filename`/`file_size`), skips files already in the DB by `filename`+`file_size`, then `ExifScanner` and `ThumbnailScanner`, then inserts the `Photo`; `BasicPipeline` (accepts `Photo`) runs its scanners, each gated by its `accept(photo)` (whether the photo still needs that scanner's data), then merges the `Photo`.
23
+ - **Scanner tracking**: scanners whose `accept(photo)` cannot be deduced from photo fields (e.g. `ObjectScanner` — YOLO object detection) set `needs_tracking = True`; after a successful scan the `Scanner` base appends the scanner name to `photo.scanners` (JSON column; NULL-safe — existing rows get `None` until re-scanned, so always read via `photo.scanners or []`). `ObjectScanner` runs ultralytics YOLO with the model named by `scanners.Object.model` (default `yolo26n.pt`; live config uses `yolo26m.pt`), stored in `general.models_folder` (`models/`) — not ultralytics' own cache; it stores detections in `photo.objects` (dedicated JSON column with `[{label, confidence, box}]`) and gates on `"Object" not in photo.scanners`. The models folder is also part of the storage statistics (`StorageService._folders`).
24
+ - DB models are SQLModel classes in `trailframe/models/` (with their converters as classmethods, e.g. `GarminActivity.to_activity`, `GarminActivity.from_data`); tables are auto-created at startup (`DatabaseService.start`). No migrations.
25
+ - **Thumbnails are multi-size**: `general.thumbnail_sizes` (sorted unique ints, default `[160, 400]`) drives `ThumbnailService` (`trailframe/services/thumbnail_service.py`). Files live at `{safe_filename[:2]}/{safe_filename}_{file_size}_{size}.webp` (fallback `{safe_filename}_{size}.webp` when `file_size` is missing). `generate_all` writes every size; `exists` = all sizes present; `select_size(preferred)` picks the lowest size ≥ preferred when ≤ 2×preferred, else the highest size below preferred (fallback smallest). The API serves `GET /api/photos/{photo_id}/thumbnail?size=<preferred>` and generates on demand. Legacy single-size files (`..._{file_size}.webp`, no size suffix) may still exist on disk but are unused.
26
+ - **Favorites**: `Photo.is_favorite` column (auto-added by `_add_missing_columns`); `PUT /api/photos/{photo_id}/favorite`, `GET /api/photos?favorites=true`, and lightweight `GET /api/photos/favorites` (ID list).
27
+ - **Maps/tiles**: `TileService` + `/api/tiles/{z}/{x}/{y}.png` proxy/cache map tiles locally (`general.tiles_folder`, TTL/size limits under `maps:` config). Activity/photo map data flows through `LocationService`/`MapService`/`api/map_data.py`.
28
+ - **Forced scans** (`Tools` page): `POST /api/pipeline/scan {"scanners": [...]}`; `GET /api/pipeline/scanners` lists scanners from BOTH pipelines. Both `CreationPipeline` and `BasicPipeline` honor `ForcedScan` markers — selected scanners run with `force=True` (bypassing `accept` gates) until an empty scan request clears the mode.
29
+ - **Photo deletion** (`DELETE /api/photos/{photo_id}` in `trailframe/api/photos.py`): moves the source file to the `trash/` folder (configurable via `general.trash_folder`, default `trash`) via `FolderService.delete()`, removes the thumbnail via `ThumbnailService`, and deletes the DB row.
30
+ - **Statistics**: everything statistics-related lives in `StatisticsService` (`trailframe/services/statistics_service.py`; configured with the `general.*_folder` paths). The `Scanner` base class times each successful scan (`_record`, per-run `count`/`total_ms`). `Pipeline._flush_stats` is invoked when a pipeline's queue drains with no item in-flight (a run has ended — low priority, so nothing is written mid-run) and persists one `ScannerStat` row per scanner via `StatisticsService.record_run`. `GET /api/statistics/scanners` (`trailframe/api/statistics.py`) returns throughput (`name`/`items`/`value` = items/s); `GET /api/statistics/storage` returns the whole-database file size and folder sizes (response models `FolderStat`/`StorageStats` are plain pydantic shapes in `api/statistics.py`, **not** DB tables — per-table DB sizes are intentionally not computed, they require serializing every row and are too slow). Both feed one merged "Name | Items | Value" table on the Configuration page's "Statistics" pane (`frontend/src/pages/ConfigurationPage.tsx`). For testing the statistics endpoints, insert mock `ScannerStat` rows directly into the DB rather than running real pipeline scans.
31
+
32
+ ## Frontend (Vite + React 19 + TS)
33
+
34
+ - Two Vite entries: `index.html` (main app) and `slideshow.html` (standalone kiosk slideshow, mini-app in `src/SlideshowApp.tsx`). Both sync state to URL: main app `/?page=<tab>&section=<activities|groups|custom>`, standalone `/slideshow.html?section=...&activity=<id>` or `?photos=<ids>&name=<label>&thumbs=1`. Param order is canonicalized by `src/lib/slideshowSections.ts`.
35
+ - `node`/`npm`/`npx` are on PATH (`/c/Program Files/nodejs`): lint = `npm run lint`, build = `npm run build`, dev server = `npm run dev`.
36
+ - `src/api/generated/schema.ts` is generated by `openapi-typescript` from the backend's `/openapi.json`. **Do not hand-edit it.** No script is committed and `openapi-typescript` is not in `package.json`; regenerate against `http://localhost:8000/openapi.json` (run backend first).
37
+ - API calls use relative same-origin URLs (no `baseUrl` in `client.ts`). In dev, `vite.config.ts` proxies `/api` → `http://localhost:8000`, so the backend must be running for the dev server to work; in prod the backend serves `dist` directly.
38
+
39
+ ## Verification
40
+
41
+ - No tests and no CI. Before testing, **check the frontend is available on port 8000**; if it is not, ask the user to run it. Perform all tests against `http://localhost:8000`.
42
+ - **NEVER TEST GARMIN CONNECT.**
43
+ - **Backend changes** (`trailframe/`): kill the process listening on port 8000 (e.g. `netstat -ano | findstr :8000`, then `taskkill -PID <pid> -F`). Wait automatic restart of the service.
44
+ - **Backend API changes**: restart as above, then regenerate the typed client from `frontend/`: `npx openapi-typescript http://localhost:8000/openapi.json -o src/api/generated/schema.ts`.
45
+ - **Frontend changes** (`frontend/`): build from `frontend/`: `npm run build`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thibault "Sildra" Fresnet
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.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.5
2
+ Name: trailframe
3
+ Version: 0.1.0
4
+ Summary: Photo gallery app with FastAPI backend and React frontend
5
+ Project-URL: Homepage, https://github.com/Sildra/Trailframe
6
+ Project-URL: Issues, https://github.com/Sildra/Trailframe/issues
7
+ Author-email: Thibaut Fresnet <sildra.fr@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Natural Language :: English
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.12
15
+ Requires-Dist: aiosqlite
16
+ Requires-Dist: fastapi
17
+ Requires-Dist: garminconnect
18
+ Requires-Dist: numpy
19
+ Requires-Dist: opencv-python
20
+ Requires-Dist: pillow
21
+ Requires-Dist: py-staticmaps
22
+ Requires-Dist: pyyaml
23
+ Requires-Dist: requests
24
+ Requires-Dist: s2sphere
25
+ Requires-Dist: shapely
26
+ Requires-Dist: sqlalchemy[asyncio]
27
+ Requires-Dist: sqlmodel
28
+ Requires-Dist: ultralytics
29
+ Requires-Dist: uvicorn
30
+ Provides-Extra: dev
31
+ Requires-Dist: openapi-typescript; extra == 'dev'
32
+ Requires-Dist: ruff; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Trailframe
36
+
37
+ [![GitHub Release](https://img.shields.io/github/release/sildra/trailframe.svg?style=for-the-badge)](https://github.com/sildra/trailframe/releases)
38
+ [![GitHub Activity](https://img.shields.io/github/commit-activity/y/sildra/trailframe.svg?style=for-the-badge)](https://github.com/sildra/trailframe/commits/main)
39
+ [![License](https://img.shields.io/github/license/sildra/trailframe.svg?style=for-the-badge)](LICENSE)
40
+
41
+
42
+ A self-hosted photo gallery for people who move: photos and GPS activities (Garmin Connect / GPX) live side by side, linked by time and place. One server hosts everything — a FastAPI backend that also serves its web UI.
43
+
44
+ ![Gallery view](docs/img/gallery_fish_dog.jpg)
45
+
46
+ ## Photo library
47
+
48
+ - Automatic ingestion of a photo folder, with EXIF capture (camera, date, GPS) on import.
49
+ - Multi-size thumbnails generated and cached per photo; sizes are configurable.
50
+ - **Favorites** — mark photos you love and find them instantly; favorite-only views everywhere.
51
+ - **Automatic groups** — related photos are clustered so bursts and series stay together.
52
+ - Filter and combine by date range, group, location, tags, and favorites.
53
+
54
+ ## Activities
55
+
56
+ - Import activities from Garmin Connect or GPX files.
57
+ - Every activity gets a trace map; photos taken during the activity appear alongside it.
58
+ - Interactive map with selectable traces and popups showing distance, duration, and time.
59
+
60
+ ![Garmin activities](docs/img/activities_garmin.png)
61
+
62
+ ## Slideshows
63
+
64
+ - Slideshow sources: an activity (photos + trace), a group, or a fully custom selection.
65
+ - Custom slideshows support date ranges, groups, locations, tags, randomization, and favorites-only.
66
+ - **Thumbnail mode** renders slides from cached thumbnails instead of full images — smooth playback even for large libraries, with the size chosen automatically from the available screen space.
67
+ - Standalone kiosk entry (`/slideshow.html`) with deep-linkable URLs for dedicated screens.
68
+ - ZIP export of an activity's photos.
69
+
70
+ ![Slideshow with activity trace](docs/img/slideshow_activity_1.png)
71
+ ![Slideshow photo view](docs/img/slideshow_activity_2.jpg)
72
+
73
+ ## Enrichment pipelines
74
+
75
+ Photos are processed by pluggable scanners:
76
+
77
+ | Scanner | What it does |
78
+ | --- | --- |
79
+ | File | Registers the file (name, size) |
80
+ | EXIF | Extracts camera metadata, dates, and GPS position |
81
+ | Thumbnail | Generates all configured thumbnail sizes |
82
+ | Activity | Links photos to imported activities |
83
+ | Brisque | Scores image quality |
84
+ | Location | Resolves GPS coordinates to places and countries |
85
+ | Object | Detects objects in photos (YOLO models, downloaded automatically) |
86
+ | PerceptualHash | Fingerprints images for duplicate/near-duplicate grouping |
87
+
88
+ Scanners can be force re-run individually from the Tools page — e.g. regenerate all thumbnails after changing their sizes.
89
+
90
+ ## Maps
91
+
92
+ - Wireframe maps of your photo coverage and per-photo location maps.
93
+ - Built-in tile cache/proxy: tiles are fetched once from the configured provider and served locally afterwards.
94
+
95
+ ![Tools map with activity traces](docs/img/tools_map.png)
96
+
97
+ ## Statistics
98
+
99
+ - Per-scanner throughput (items processed and speed).
100
+ - Storage overview: database size plus disk usage of every data folder.
101
+
102
+ ## Install
103
+
104
+ A wheel is attached to every [release](https://github.com/sildra/trailframe/releases/latest): download the latest `trailframe-…-py3-none-any.whl` and install it with pip (Python 3.12 or newer):
105
+
106
+ ```bash
107
+ pip install trailframe-…-py3-none-any.whl
108
+ ```
109
+
110
+ All Python dependencies are pulled in automatically; the web UI is bundled inside the wheel.
111
+
112
+ ## Running
113
+
114
+ Run the `trailframe` command from the folder where all data (database, thumbnails, caches, trash) should live:
115
+
116
+ ```bash
117
+ trailframe --config path/to/config.yaml
118
+ ```
119
+
120
+ (`python -m trailframe.main` is equivalent.) The server listens on port 8000 by default; open `http://localhost:8000`. On first start a default `config.yaml` is written to the current directory.
121
+
122
+ ### From source
123
+
124
+ Build the web UI once (`npm install && npm run build` inside `frontend/`), then start the server from the repository root:
125
+
126
+ ```bash
127
+ python supervisor.py --config path/to/config.yaml
128
+ ```
129
+
130
+ The supervisor restarts the server whenever it exits and forwards SIGTERM/SIGBREAK to it. To build a wheel yourself, build the web UI first, then run `pip wheel .`.
@@ -0,0 +1,96 @@
1
+ # Trailframe
2
+
3
+ [![GitHub Release](https://img.shields.io/github/release/sildra/trailframe.svg?style=for-the-badge)](https://github.com/sildra/trailframe/releases)
4
+ [![GitHub Activity](https://img.shields.io/github/commit-activity/y/sildra/trailframe.svg?style=for-the-badge)](https://github.com/sildra/trailframe/commits/main)
5
+ [![License](https://img.shields.io/github/license/sildra/trailframe.svg?style=for-the-badge)](LICENSE)
6
+
7
+
8
+ A self-hosted photo gallery for people who move: photos and GPS activities (Garmin Connect / GPX) live side by side, linked by time and place. One server hosts everything — a FastAPI backend that also serves its web UI.
9
+
10
+ ![Gallery view](docs/img/gallery_fish_dog.jpg)
11
+
12
+ ## Photo library
13
+
14
+ - Automatic ingestion of a photo folder, with EXIF capture (camera, date, GPS) on import.
15
+ - Multi-size thumbnails generated and cached per photo; sizes are configurable.
16
+ - **Favorites** — mark photos you love and find them instantly; favorite-only views everywhere.
17
+ - **Automatic groups** — related photos are clustered so bursts and series stay together.
18
+ - Filter and combine by date range, group, location, tags, and favorites.
19
+
20
+ ## Activities
21
+
22
+ - Import activities from Garmin Connect or GPX files.
23
+ - Every activity gets a trace map; photos taken during the activity appear alongside it.
24
+ - Interactive map with selectable traces and popups showing distance, duration, and time.
25
+
26
+ ![Garmin activities](docs/img/activities_garmin.png)
27
+
28
+ ## Slideshows
29
+
30
+ - Slideshow sources: an activity (photos + trace), a group, or a fully custom selection.
31
+ - Custom slideshows support date ranges, groups, locations, tags, randomization, and favorites-only.
32
+ - **Thumbnail mode** renders slides from cached thumbnails instead of full images — smooth playback even for large libraries, with the size chosen automatically from the available screen space.
33
+ - Standalone kiosk entry (`/slideshow.html`) with deep-linkable URLs for dedicated screens.
34
+ - ZIP export of an activity's photos.
35
+
36
+ ![Slideshow with activity trace](docs/img/slideshow_activity_1.png)
37
+ ![Slideshow photo view](docs/img/slideshow_activity_2.jpg)
38
+
39
+ ## Enrichment pipelines
40
+
41
+ Photos are processed by pluggable scanners:
42
+
43
+ | Scanner | What it does |
44
+ | --- | --- |
45
+ | File | Registers the file (name, size) |
46
+ | EXIF | Extracts camera metadata, dates, and GPS position |
47
+ | Thumbnail | Generates all configured thumbnail sizes |
48
+ | Activity | Links photos to imported activities |
49
+ | Brisque | Scores image quality |
50
+ | Location | Resolves GPS coordinates to places and countries |
51
+ | Object | Detects objects in photos (YOLO models, downloaded automatically) |
52
+ | PerceptualHash | Fingerprints images for duplicate/near-duplicate grouping |
53
+
54
+ Scanners can be force re-run individually from the Tools page — e.g. regenerate all thumbnails after changing their sizes.
55
+
56
+ ## Maps
57
+
58
+ - Wireframe maps of your photo coverage and per-photo location maps.
59
+ - Built-in tile cache/proxy: tiles are fetched once from the configured provider and served locally afterwards.
60
+
61
+ ![Tools map with activity traces](docs/img/tools_map.png)
62
+
63
+ ## Statistics
64
+
65
+ - Per-scanner throughput (items processed and speed).
66
+ - Storage overview: database size plus disk usage of every data folder.
67
+
68
+ ## Install
69
+
70
+ A wheel is attached to every [release](https://github.com/sildra/trailframe/releases/latest): download the latest `trailframe-…-py3-none-any.whl` and install it with pip (Python 3.12 or newer):
71
+
72
+ ```bash
73
+ pip install trailframe-…-py3-none-any.whl
74
+ ```
75
+
76
+ All Python dependencies are pulled in automatically; the web UI is bundled inside the wheel.
77
+
78
+ ## Running
79
+
80
+ Run the `trailframe` command from the folder where all data (database, thumbnails, caches, trash) should live:
81
+
82
+ ```bash
83
+ trailframe --config path/to/config.yaml
84
+ ```
85
+
86
+ (`python -m trailframe.main` is equivalent.) The server listens on port 8000 by default; open `http://localhost:8000`. On first start a default `config.yaml` is written to the current directory.
87
+
88
+ ### From source
89
+
90
+ Build the web UI once (`npm install && npm run build` inside `frontend/`), then start the server from the repository root:
91
+
92
+ ```bash
93
+ python supervisor.py --config path/to/config.yaml
94
+ ```
95
+
96
+ The supervisor restarts the server whenever it exits and forwards SIGTERM/SIGBREAK to it. To build a wheel yourself, build the web UI first, then run `pip wheel .`.
Binary file
@@ -0,0 +1,24 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?