homesec 1.2.2__tar.gz → 1.2.3__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 (144) hide show
  1. homesec-1.2.3/.github/workflows/docker-publish.yml +58 -0
  2. {homesec-1.2.2 → homesec-1.2.3}/AGENTS.md +2 -1
  3. {homesec-1.2.2 → homesec-1.2.3}/CHANGELOG.md +18 -0
  4. {homesec-1.2.2 → homesec-1.2.3}/DESIGN.md +123 -137
  5. {homesec-1.2.2 → homesec-1.2.3}/Dockerfile +2 -2
  6. {homesec-1.2.2 → homesec-1.2.3}/PKG-INFO +7 -12
  7. {homesec-1.2.2 → homesec-1.2.3}/PLUGIN_DEVELOPMENT.md +12 -8
  8. {homesec-1.2.2 → homesec-1.2.3}/README.md +6 -11
  9. {homesec-1.2.2 → homesec-1.2.3}/config/example.yaml +17 -23
  10. {homesec-1.2.2 → homesec-1.2.3}/docker-compose.yml +2 -1
  11. {homesec-1.2.2 → homesec-1.2.3}/pyproject.toml +1 -1
  12. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/app.py +5 -14
  13. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/cli.py +5 -4
  14. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/config/__init__.py +8 -1
  15. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/config/loader.py +17 -2
  16. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/config/validation.py +99 -6
  17. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/interfaces.py +2 -2
  18. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/maintenance/cleanup_clips.py +17 -4
  19. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/__init__.py +3 -25
  20. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/clip.py +1 -1
  21. homesec-1.2.3/src/homesec/models/config.py +154 -0
  22. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/enums.py +8 -0
  23. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/events.py +1 -1
  24. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/filter.py +3 -21
  25. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/vlm.py +11 -20
  26. homesec-1.2.3/src/homesec/pipeline/__init__.py +5 -0
  27. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/pipeline/core.py +9 -10
  28. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/alert_policies/__init__.py +5 -5
  29. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/alert_policies/default.py +21 -2
  30. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/analyzers/__init__.py +1 -3
  31. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/analyzers/openai.py +20 -13
  32. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/filters/__init__.py +1 -2
  33. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/filters/yolo.py +25 -5
  34. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/notifiers/__init__.py +1 -6
  35. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/notifiers/mqtt.py +21 -1
  36. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/notifiers/sendgrid_email.py +52 -1
  37. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/registry.py +27 -0
  38. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/sources/__init__.py +4 -4
  39. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/sources/ftp.py +1 -1
  40. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/sources/local_folder.py +1 -1
  41. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/sources/rtsp.py +1 -1
  42. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/storage/__init__.py +1 -9
  43. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/storage/dropbox.py +13 -1
  44. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/storage/local.py +8 -1
  45. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/repository/clip_repository.py +1 -1
  46. homesec-1.2.3/src/homesec/sources/__init__.py +16 -0
  47. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/ftp.py +95 -2
  48. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/local_folder.py +27 -2
  49. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/core.py +162 -2
  50. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/conftest.py +1 -1
  51. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/rtsp/test_helpers.py +1 -2
  52. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/rtsp/test_runtime.py +1 -2
  53. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_alert_policy.py +2 -3
  54. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_app.py +14 -11
  55. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_cleanup_clips.py +5 -6
  56. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_cli.py +5 -7
  57. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_clip_repository.py +6 -6
  58. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_clip_sources.py +2 -2
  59. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_config.py +105 -54
  60. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_dropbox_storage.py +1 -2
  61. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_event_store.py +2 -2
  62. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_ftp_source.py +2 -3
  63. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_integration.py +8 -12
  64. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_local_storage.py +1 -2
  65. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_mqtt_notifier.py +1 -2
  66. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_notifiers.py +2 -3
  67. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_openai_vlm.py +6 -10
  68. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_pipeline.py +39 -43
  69. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_pipeline_events.py +14 -15
  70. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_sendgrid_notifier.py +1 -2
  71. {homesec-1.2.2 → homesec-1.2.3}/uv.lock +1 -1
  72. homesec-1.2.2/src/homesec/models/config.py +0 -405
  73. homesec-1.2.2/src/homesec/models/source/__init__.py +0 -3
  74. homesec-1.2.2/src/homesec/models/source/ftp.py +0 -97
  75. homesec-1.2.2/src/homesec/models/source/local_folder.py +0 -30
  76. homesec-1.2.2/src/homesec/models/source/rtsp.py +0 -165
  77. homesec-1.2.2/src/homesec/pipeline/__init__.py +0 -6
  78. homesec-1.2.2/src/homesec/pipeline/alert_policy.py +0 -5
  79. homesec-1.2.2/src/homesec/sources/__init__.py +0 -19
  80. {homesec-1.2.2 → homesec-1.2.3}/.dockerignore +0 -0
  81. {homesec-1.2.2 → homesec-1.2.3}/.env.example +0 -0
  82. {homesec-1.2.2 → homesec-1.2.3}/.github/workflows/ci.yml +0 -0
  83. {homesec-1.2.2 → homesec-1.2.3}/.github/workflows/release.yaml +0 -0
  84. {homesec-1.2.2 → homesec-1.2.3}/.github/workflows/validate-pr-title.yaml +0 -0
  85. {homesec-1.2.2 → homesec-1.2.3}/.gitignore +0 -0
  86. {homesec-1.2.2 → homesec-1.2.3}/LICENSE +0 -0
  87. {homesec-1.2.2 → homesec-1.2.3}/Makefile +0 -0
  88. {homesec-1.2.2 → homesec-1.2.3}/TESTING.md +0 -0
  89. {homesec-1.2.2 → homesec-1.2.3}/alembic/env.py +0 -0
  90. {homesec-1.2.2 → homesec-1.2.3}/alembic/script.py.mako +0 -0
  91. {homesec-1.2.2 → homesec-1.2.3}/alembic/versions/e6f25df0df90_initial.py +0 -0
  92. {homesec-1.2.2 → homesec-1.2.3}/alembic.ini +0 -0
  93. {homesec-1.2.2 → homesec-1.2.3}/docker-entrypoint.sh +0 -0
  94. {homesec-1.2.2 → homesec-1.2.3}/skills/local/homesec-db-logs/SKILL.md +0 -0
  95. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/__init__.py +0 -0
  96. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/errors.py +0 -0
  97. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/health/__init__.py +0 -0
  98. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/health/server.py +0 -0
  99. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/logging_setup.py +0 -0
  100. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/maintenance/__init__.py +0 -0
  101. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/alert.py +0 -0
  102. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/storage.py +0 -0
  103. {homesec-1.2.2/src/homesec/plugins → homesec-1.2.3/src/homesec}/notifiers/multiplex.py +0 -0
  104. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/__init__.py +0 -0
  105. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/alert_policies/noop.py +0 -0
  106. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/utils.py +0 -0
  107. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/py.typed +0 -0
  108. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/repository/__init__.py +0 -0
  109. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/base.py +0 -0
  110. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/__init__.py +0 -0
  111. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/clock.py +0 -0
  112. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/frame_pipeline.py +0 -0
  113. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/hardware.py +0 -0
  114. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/motion.py +0 -0
  115. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/recorder.py +0 -0
  116. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/utils.py +0 -0
  117. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/state/__init__.py +0 -0
  118. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/state/postgres.py +0 -0
  119. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/storage_paths.py +0 -0
  120. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/__init__.py +0 -0
  121. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/db/__init__.py +0 -0
  122. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/db/log_table.py +0 -0
  123. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/db_log_handler.py +0 -0
  124. {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/postgres_settings.py +0 -0
  125. {homesec-1.2.2 → homesec-1.2.3}/tests/__init__.py +0 -0
  126. {homesec-1.2.2 → homesec-1.2.3}/tests/conftest.py +0 -0
  127. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/__init__.py +0 -0
  128. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/__init__.py +0 -0
  129. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/event_store.py +0 -0
  130. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/filter.py +0 -0
  131. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/notifier.py +0 -0
  132. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/state_store.py +0 -0
  133. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/storage.py +0 -0
  134. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/vlm.py +0 -0
  135. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/rtsp/test_frame_pipeline.py +0 -0
  136. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/rtsp/test_hardware.py +0 -0
  137. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_enums.py +0 -0
  138. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_health.py +0 -0
  139. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_logging_setup.py +0 -0
  140. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_plugin_registration.py +0 -0
  141. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_plugin_utils.py +0 -0
  142. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_source_health.py +0 -0
  143. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_state_store.py +0 -0
  144. {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_yolo_filter.py +0 -0
@@ -0,0 +1,58 @@
1
+ name: Publish Docker Image
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ release:
7
+ types: [published]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ docker:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+
19
+ env:
20
+ IMAGE_NAME: ${{ vars.DOCKERHUB_IMAGE }}
21
+
22
+ steps:
23
+ - name: Checkout
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Set up Docker Buildx
27
+ uses: docker/setup-buildx-action@v3
28
+
29
+ - name: Log in to Docker Hub
30
+ uses: docker/login-action@v3
31
+ with:
32
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
33
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
34
+
35
+ - name: Resolve release version
36
+ if: github.event_name == 'release'
37
+ run: |
38
+ tag="${{ github.event.release.tag_name }}"
39
+ echo "RELEASE_VERSION=${tag#v}" >> "$GITHUB_ENV"
40
+
41
+ - name: Docker metadata
42
+ id: meta
43
+ uses: docker/metadata-action@v5
44
+ with:
45
+ images: ${{ env.IMAGE_NAME }}
46
+ tags: |
47
+ type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
48
+ type=raw,value=${{ env.RELEASE_VERSION }},enable=${{ github.event_name == 'release' }}
49
+
50
+ - name: Build and push
51
+ uses: docker/build-push-action@v5
52
+ with:
53
+ context: .
54
+ push: true
55
+ tags: ${{ steps.meta.outputs.tags }}
56
+ labels: ${{ steps.meta.outputs.labels }}
57
+ cache-from: type=gha
58
+ cache-to: type=gha,mode=max
@@ -9,6 +9,7 @@
9
9
 
10
10
  - **Strict type checking required**: Run `make typecheck` before committing. Error-as-value pattern requires explicit type narrowing via match/isinstance.
11
11
  - **Program to interfaces**: Use factory/registry helpers (e.g., `load_filter_plugin()`). Avoid direct instantiation of plugins.
12
+ - **Architecture constraints**: See `DESIGN.md` → “Architecture Constraints”. Boundary violations (core ↔ concrete plugins, backend-specific config in core) are bugs.
12
13
  - **Repository pattern**: Use `ClipRepository` for all state/event writes. Never touch `StateStore`/`EventStore` directly.
13
14
  - **Preserve stack traces**: Custom errors must set `self.__cause__ = cause` to preserve original exception.
14
15
  - **Tests must use Given/When/Then comments**: Every test must include these comments and follow behavioral testing principles (see `TESTING.md`).
@@ -42,7 +43,7 @@ async def _filter_stage(self, clip: Clip) -> FilterResult | FilterError:
42
43
  async with self._sem_filter:
43
44
  return await self._filter.detect(clip.local_path)
44
45
  except Exception as e:
45
- return FilterError(clip.clip_id, self._config.filter.plugin, cause=e)
46
+ return FilterError(clip.clip_id, self._config.filter.backend, cause=e)
46
47
 
47
48
  # Caller uses match for type narrowing
48
49
  filter_result = await self._filter_stage(clip)
@@ -2,6 +2,24 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.2.3 (2026-02-04)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Fix dockerfile ([#21](https://github.com/lan17/homesec/pull/21),
10
+ [`29e8ce4`](https://github.com/lan17/homesec/commit/29e8ce43a667d288b668417b7735c0c51620fca1))
11
+
12
+ ### Continuous Integration
13
+
14
+ - Add docker publish workflow ([#20](https://github.com/lan17/homesec/pull/20),
15
+ [`0ec59a0`](https://github.com/lan17/homesec/commit/0ec59a0509cf7a7a98900963d3d265bfdd2cb65c))
16
+
17
+ ### Refactoring
18
+
19
+ - Plugin config boundary cleanup ([#18](https://github.com/lan17/homesec/pull/18),
20
+ [`6746fa2`](https://github.com/lan17/homesec/commit/6746fa2f4285657df01d0dc1d25c7a1d1580407b))
21
+
22
+
5
23
  ## v1.2.2 (2026-01-27)
6
24
 
7
25
  ### Bug Fixes
@@ -10,6 +10,21 @@
10
10
  6. **Errors as values.** Stage methods return `Result | StageError` instead of raising exceptions. This enables partial failures (e.g., upload fails but filter succeeds → still send alert). Stack traces are preserved in error objects. **Strict type checking required** (mypy --strict or pyright) to prevent runtime errors from missing `isinstance()` checks.
11
11
  7. **Type-safe enums for domain values.** Use `StrEnum`/`IntEnum` from `models/enums.py` for type safety, IDE support, and maintainability. See [Type Safety & Enums](#type-safety--enums) below.
12
12
 
13
+ ## Architecture Constraints (Separation of Concerns & Abstraction Boundaries)
14
+
15
+ **Separation of concerns**: each component has a single responsibility. The core pipeline orchestrates *what* happens; plugins and backends implement *how* it happens.
16
+ **Abstraction boundaries**: the core depends only on stable contracts (interfaces, registries, repository APIs, and Pydantic models). Concrete implementations may depend on core abstractions, but the core must never depend on concrete implementations.
17
+
18
+ ### Constraints (Non‑Negotiable)
19
+
20
+ 1. **Dependency direction**: core modules (`pipeline`, `app`, `repository`, `models`) must not import concrete plugin/backends. Use interfaces + registry loaders only (e.g., `load_*_plugin()`).
21
+ 2. **Config boundary**: core sees only `backend` + opaque config payload. Plugin loaders validate/instantiate using plugin‑specific Pydantic config models. Core must not reference backend‑specific fields.
22
+ 3. **Persistence boundary**: all state/event writes go through `ClipRepository`; never touch `StateStore`/`EventStore` directly.
23
+ 4. **Plugin boundary**: plugins may import core interfaces/models; the core may not import plugin modules.
24
+ 5. **Boundary validation**: external inputs (config, DB JSONB, VLM outputs, MQTT payloads) must be validated at the boundary with Pydantic before entering core logic.
25
+
26
+ Violations are architecture bugs and should be treated as such during reviews.
27
+
13
28
  ## Type Safety & Enums
14
29
 
15
30
  Domain values that appear in multiple places use centralized enums in `models/enums.py`:
@@ -64,8 +79,9 @@ HomeSec employs a **Class-Based Plugin Architecture (V2)** designed for strict t
64
79
  In V1, plugins were factories accepting raw dictionaries and loose context objects. In V2, the **Configuration Model** (Pydantic) defines the entire contract for a plugin.
65
80
 
66
81
  - **Type Safety**: Every plugin defines a `config_cls` (Pydantic model). The registry validates raw JSON/YAML against this model *before* the plugin is instantiated.
67
- - **Dependency Injection**: Runtime dependencies (like `camera_name` for sources, or `trigger_classes` for analyzers) are injected into the configuration dictionary *before* validation.
68
- - *Benefit*: The plugin implementation doesn't need a separate `context` argument. It just declares `camera_name: str` in its config model, and the system ensures it's present.
82
+ - **Backend/Config Boundary**: Core config models expose only `backend` + opaque `config` payloads. Plugin‑specific fields live in the plugin’s config model (defined alongside the implementation).
83
+ - **Runtime Context (when needed)**: For plugin types that require runtime data (e.g., `camera_name`/timezone for sources, default alert policy `trigger_classes`), the loader injects those fields into the config dict *before* validation.
84
+ - *Benefit*: The plugin implementation doesn’t need a separate `context` argument; required runtime fields are validated alongside static config.
69
85
  - **Fail Fast**: Invalid configs cause a `ValidationError` at loading time, preventing partial start-ups.
70
86
 
71
87
  ### 2. The Unified Registry Pattern
@@ -203,16 +219,19 @@ Build a reliable, pluggable pipeline to:
203
219
 
204
220
  4. **Object Detection Filter (Pluggable)**
205
221
  - Uses pluggable object detection to decide "needs VLM".
206
- - Plugin interface: `ObjectFilter.detect(video_path, config) -> FilterResult`
222
+ - Plugin interface: `ObjectFilter.detect(video_path, overrides=None) -> FilterResult`
207
223
  - Reference implementation: YOLOv8 with configurable classes (default: person), frame sampling, early exit on detection.
208
224
  - Output: `FilterResult` (detected_classes, confidence, sampled_frames, model).
209
225
  - VLM trigger classes are configured globally (default: person only) based on `filter_result.detected_classes`.
210
226
 
211
227
  5. **VLM Analyzer (Pluggable)**
212
- - Consumes clips that match `vlm.trigger_classes` and produces structured analysis.
228
+ - Consumes clips (depending on `vlm.run_mode`) and produces structured analysis.
213
229
  - Plugin interface: `VLMAnalyzer.analyze(video_path, filter_result, config) -> AnalysisResult`
214
230
  - Reference implementation: OpenAI-compatible API (GPT-4o or similar).
215
- - Trigger rule: run VLM if `filter_result.detected_classes` intersects `vlm.trigger_classes`.
231
+ - Trigger rule:
232
+ - `run_mode: always` → run for every clip (after filter succeeds)
233
+ - `run_mode: trigger_only` → run if `filter_result.detected_classes` intersects `vlm.trigger_classes`
234
+ - `run_mode: never` → skip VLM
216
235
  - Output: `AnalysisResult` (risk_level, activity_type, summary, entities_timeline, requires_review, etc.).
217
236
  - Stores `analysis.json` in the same storage backend, next to the clip.
218
237
  - Prompting is configured globally (same for all cameras); per-camera alert policy decides which `activity_type` values trigger notifications.
@@ -222,12 +241,11 @@ Build a reliable, pluggable pipeline to:
222
241
  - MVP logic:
223
242
  - Default: notify if `risk_level > low` (or a configurable global threshold).
224
243
  - Additionally: allow per-camera `notify_on_activity_types` list that can trigger notifications even when `risk_level` is low.
225
- - Optional per-camera `notify_on_motion` to alert on any clip (bypasses VLM gating).
226
- - If VLM is skipped because no trigger classes are detected, default to `notify=false` unless `notify_on_motion` is enabled.
227
- - Notifications wait for upload so a `view_url` can be included.
228
- - Configuration:
229
- - `default`: `min_risk_level` + `notify_on_activity_types` list.
230
- - `per_camera`: overrides keyed by `camera_name` to tune alert thresholds and activity triggers.
244
+ - Optional per-camera `notify_on_motion` to alert on any clip (VLM may still be skipped).
245
+ - If VLM is skipped (run_mode=never or no trigger classes detected), default to `notify=false` unless `notify_on_motion` is enabled.
246
+ - Notifications wait for upload so a `view_url` can be included.
247
+ - Configuration:
248
+ - `config`: `min_risk_level` + `notify_on_activity_types` list, plus `overrides` keyed by `camera_name`.
231
249
  - Example use cases:
232
250
  - Front door: notify on `activity_type == "person_at_door"` or `"delivery"` even at low risk
233
251
  - Backyard: notify on `activity_type == "animal_running"` at medium+ risk
@@ -305,7 +323,7 @@ class Clip(BaseModel):
305
323
  start_ts: datetime
306
324
  end_ts: datetime
307
325
  duration_s: float
308
- source_type: str # "rtsp", "ftp", etc.
326
+ source_backend: str # "rtsp", "ftp", etc.
309
327
 
310
328
  class ClipSource(Protocol):
311
329
  """Produces finalized clips and notifies pipeline via callback."""
@@ -386,7 +404,7 @@ class ObjectFilter(Shutdownable, Protocol):
386
404
  - MUST be async (use asyncio.to_thread or run_in_executor for blocking code)
387
405
  - CPU/GPU-bound plugins should manage their own ProcessPoolExecutor internally
388
406
  - I/O-bound plugins can use async HTTP clients directly
389
- - Should respect the instance config max_workers if managing worker pool
407
+ - If managing a worker pool, use concurrency settings from the plugin's config model
390
408
  - Should support early exit on first detection for efficiency
391
409
  - overrides apply per-call (model path cannot be overridden)
392
410
 
@@ -414,7 +432,7 @@ class VLMAnalyzer(Shutdownable, Protocol):
414
432
  - MUST be async (use asyncio.to_thread or run_in_executor for blocking code)
415
433
  - Local models: manage ProcessPoolExecutor internally
416
434
  - API-based: use async HTTP clients (aiohttp, httpx)
417
- - Should respect config.max_workers if managing worker pool
435
+ - If managing a worker pool, use concurrency settings from the plugin's config model
418
436
  - Should use filter_result to focus analysis (e.g., detected person at timestamp X)
419
437
 
420
438
  Returns:
@@ -443,59 +461,42 @@ class VLMAnalyzer(Shutdownable, Protocol):
443
461
 
444
462
  Prefer a single YAML file for non-secret configuration (cameras, sources, policies, MQTT). Keep secrets in `.env` and reference them by env var name from YAML.
445
463
 
446
- ### Configuration Hierarchy & Merging
464
+ ### Configuration Hierarchy & Overrides
447
465
 
448
- **Use Pydantic models** for all config to handle per-camera alert overrides correctly. Do NOT attempt manual dict merging (error-prone).
466
+ Per-camera alert overrides live inside `alert_policy.config.overrides` and are merged by
467
+ the alert policy implementation (not the core config). The core remains backend-agnostic.
449
468
 
450
- **Pattern for per-camera alert overrides (filter/VLM config is global):**
451
- ```python
452
- class AlertPolicyConfig(BaseModel):
453
- min_risk_level: RiskLevel = "medium"
454
- notify_on_activity_types: list[str] = Field(default_factory=list)
455
- notify_on_motion: bool = False
456
-
457
- class AlertPolicyOverrides(BaseModel):
458
- """Per-camera alert policy overrides (only non-None fields override base)."""
459
- min_risk_level: RiskLevel | None = None
460
- notify_on_activity_types: list[str] | None = None
461
- notify_on_motion: bool | None = None
462
-
463
- class Config(BaseModel):
464
- """Main config (simplified example - full config includes storage, mqtt, etc.)."""
465
- filter: FilterConfig
466
- vlm: VLMConfig
467
- alert_policy: AlertPolicyConfig
468
- per_camera_alert: dict[str, AlertPolicyOverrides] = Field(default_factory=dict)
469
- # ... other fields (storage, mqtt, concurrency, etc.)
470
-
471
- def get_alert_policy(self, camera_name: str) -> AlertPolicyConfig:
472
- """Get merged alert policy for a specific camera."""
473
- if camera_name not in self.per_camera_alert:
474
- return self.alert_policy
475
- return self.alert_policy.model_copy(
476
- update=self.per_camera_alert[camera_name].model_dump(exclude_none=True)
477
- )
469
+ **Example (default alert policy):**
470
+ ```yaml
471
+ alert_policy:
472
+ backend: default
473
+ enabled: true
474
+ config:
475
+ min_risk_level: medium
476
+ notify_on_motion: false
477
+ overrides:
478
+ front_door:
479
+ min_risk_level: low
480
+ notify_on_activity_types: [delivery]
478
481
  ```
479
482
 
480
- **Benefits:**
481
- - Type-safe merging
482
- - Explicit override semantics (`None` means "use default")
483
- - No manual dict manipulation
484
-
485
483
  ### Config Structure
486
484
 
487
- - `cameras[]`: `{name, source: {type, config}}` (source-specific config is validated by each source implementation)
485
+ - `cameras[]`: `{name, source: {backend, config}}` (source-specific config is validated by each source implementation)
488
486
  - Default MQTT topic if not specified: `homecam/alerts/{name}`
489
- - `storage`: Dropbox folder root, upload path template, and view URL policy (no post-upload moves)
487
+ - `storage`: `{backend, config, paths}` (backend config validated by storage plugin)
490
488
  - `retention`: local disk limits and cleanup strategy
491
489
  - `mqtt`: broker host/port/credentials env var names
492
490
  - `filter`: plugin selection + defaults (global for all cameras)
493
- - `plugin`: name of object detection plugin (e.g., "yolo", "mock")
491
+ - `backend`: name of object detection plugin (e.g., "yolo", "mock")
494
492
  - `config`: plugin-specific config (classes to detect, model name, etc.)
495
493
  - `vlm`: plugin selection + defaults (global for all cameras)
496
- - `plugin`: name of VLM plugin (e.g., "openai", "anthropic", "mock")
494
+ - `backend`: name of VLM plugin (e.g., "openai", "anthropic", "mock")
495
+ - `run_mode`: `trigger_only | always | never`
496
+ - `trigger_classes`: object classes that gate VLM in `trigger_only` mode
497
497
  - `config`: plugin-specific config (model, prompts, activity_types, etc.)
498
- - `alert_policy`: defaults + per-camera overrides (supports "notify even if low risk" via `notify_on_activity_types`)
498
+ - `preprocessing`: frame extraction config
499
+ - `alert_policy`: backend selection + config (defaults + per-camera overrides live in `config`)
499
500
  - `concurrency`: per-stage parallel processing limits (upload, filter, vlm) + global limit
500
501
  - `retry`: max attempts, backoff delay, behavior on exhaustion
501
502
  - `health`: HTTP health check endpoint config
@@ -506,11 +507,15 @@ version: 1
506
507
 
507
508
  storage:
508
509
  backend: dropbox
509
- dropbox:
510
+ config:
510
511
  root: "/homecam"
511
512
  path_template: "{camera_name}/{filename}"
512
513
  token_env: "DROPBOX_TOKEN"
513
514
  web_url_prefix: "https://www.dropbox.com/home"
515
+ paths:
516
+ clips_dir: "clips"
517
+ backups_dir: "backups"
518
+ artifacts_dir: "artifacts"
514
519
 
515
520
  retention:
516
521
  max_local_size: "10GB"
@@ -526,7 +531,7 @@ mqtt:
526
531
  retain: false
527
532
 
528
533
  filter:
529
- plugin: "yolo" # Options: "yolo", "mock" (add more as needed)
534
+ backend: "yolo" # Options: "yolo", "mock" (add more as needed)
530
535
  config:
531
536
  model: "yolov8n"
532
537
  classes: ["person", "animal"]
@@ -536,38 +541,42 @@ filter:
536
541
  # per-camera overrides for filter are not supported in MVP
537
542
 
538
543
  vlm:
539
- plugin: "openai" # Options: "openai", "anthropic", "mock" (add more as needed)
544
+ backend: "openai" # Options: "openai", "anthropic", "mock" (add more as needed)
545
+ run_mode: "trigger_only"
546
+ trigger_classes: ["person"]
540
547
  config:
541
548
  model: "gpt-4o"
542
549
  api_key_env: "OPENAI_API_KEY"
543
550
  base_prompt: "Summarize activity and risk; be concise and structured."
544
551
  activity_types: ["delivery", "doorbell", "person_at_door", "unknown"]
545
- max_workers: 2
546
- trigger_classes: ["person"]
552
+ preprocessing:
553
+ max_frames: 10
554
+ max_size: 1024
555
+ quality: 85
547
556
  # per-camera overrides for VLM are not supported in MVP
548
557
 
549
558
  alert_policy:
550
- default:
559
+ backend: "default"
560
+ enabled: true
561
+ config:
551
562
  min_risk_level: "medium"
552
563
  notify_on_activity_types: []
553
564
  notify_on_motion: false
554
- mqtt_is_critical: false # If true, MQTT failure = unhealthy (not degraded)
555
- mqtt_retry_interval_s: 60 # Retry failed notifications
556
- per_camera:
557
- front_door:
558
- min_risk_level: "low"
559
- notify_on_activity_types: ["person_at_door", "delivery"]
560
- notify_on_motion: false
561
- backyard:
562
- min_risk_level: "medium"
563
- notify_on_activity_types: ["animal_running"]
564
- notify_on_motion: false
565
+ overrides:
566
+ front_door:
567
+ min_risk_level: "low"
568
+ notify_on_activity_types: ["person_at_door", "delivery"]
569
+ notify_on_motion: false
570
+ backyard:
571
+ min_risk_level: "medium"
572
+ notify_on_activity_types: ["animal_running"]
573
+ notify_on_motion: false
565
574
 
566
575
  concurrency:
567
576
  max_clips_in_flight: 10 # Global limit (prevents OOM when many cameras trigger)
568
- upload: 3 # Per-stage limits (within global limit)
569
- filter: 4
570
- vlm: 2
577
+ upload_workers: 3 # Per-stage limits (within global limit)
578
+ filter_workers: 4
579
+ vlm_workers: 2
571
580
 
572
581
  retry:
573
582
  max_attempts: 3
@@ -582,12 +591,12 @@ health:
582
591
  cameras:
583
592
  - name: "front_door"
584
593
  source:
585
- type: "rtsp"
594
+ backend: "rtsp"
586
595
  config:
587
596
  rtsp_url_env: "FRONT_DOOR_RTSP_URL"
588
597
  - name: "driveway"
589
598
  source:
590
- type: "ftp"
599
+ backend: "ftp"
591
600
  config:
592
601
  ftp_subdir: "driveway"
593
602
  ```
@@ -668,16 +677,16 @@ class Alert(BaseModel):
668
677
 
669
678
  class FilterConfig(BaseModel):
670
679
  """Base filter configuration (plugin-agnostic)."""
671
- plugin: str # e.g., "yolo", "mock"
672
- max_workers: int = 4
680
+ backend: str # e.g., "yolo", "mock"
673
681
  config: dict[str, Any] # Plugin-specific config (validated by plugin at load time)
674
682
 
675
683
  class VLMConfig(BaseModel):
676
684
  """Base VLM configuration (plugin-agnostic)."""
677
- plugin: str # e.g., "openai", "anthropic", "mock"
685
+ backend: str # e.g., "openai", "anthropic", "mock"
678
686
  trigger_classes: list[str] = Field(default_factory=lambda: ["person"])
679
- max_workers: int = 2
687
+ run_mode: Literal["trigger_only", "always", "never"] = "trigger_only"
680
688
  config: dict[str, Any] # Plugin-specific config (validated by plugin at load time)
689
+ preprocessing: VLMPreprocessConfig = Field(default_factory=VLMPreprocessConfig)
681
690
 
682
691
  # Note: Plugin-specific config validation:
683
692
  # - Each plugin receives the config dict and validates/transforms it during initialization
@@ -808,11 +817,11 @@ class ClipStateData(BaseModel):
808
817
  - CPU/GPU-bound plugins (e.g., YOLO) should use `ProcessPoolExecutor` internally to avoid blocking the event loop.
809
818
  - I/O-bound plugins (e.g., OpenAI API) should use async HTTP clients.
810
819
  - Plugins manage their own resources (process pools, GPU allocation, API rate limits).
811
- - VLM is the expensive step; only run it when `filter_result.detected_classes` intersects `vlm.trigger_classes`.
820
+ - VLM is the expensive step; in `run_mode: trigger_only`, only run it when `filter_result.detected_classes` intersects `vlm.trigger_classes`.
812
821
  - Prefer local processing when available to minimize Dropbox transfers, while still uploading clips ASAP.
813
822
 
814
823
  ### Dropbox URLs (No Share Links)
815
- - Use Dropbox web URLs derived from `storage_uri` and `web_url_prefix` (requires Dropbox login; not public).
824
+ - The Dropbox storage backend should derive `view_url` from its configured `web_url_prefix` and the uploaded path (requires Dropbox login; not public).
816
825
  - Example: `view_url = "{web_url_prefix}{dropbox_path}"` where `dropbox_path` is the path inside the Dropbox root.
817
826
  - Prefer using the `path_display` returned by the Dropbox upload response to build `view_url` (no extra API call).
818
827
  - URLs are stable as long as files are not moved; MVP assumes no post-upload moves.
@@ -860,43 +869,28 @@ async def recover_incomplete_clips(self):
860
869
  *Note: The following is PSEUDOCODE for illustration only. It demonstrates the design pattern but omits retry logic, comprehensive error handling, logging details, state management helper methods, and other production concerns. Actual implementation will include proper type annotations, error handling, logging, and resource cleanup as specified in AGENTS.md and throughout this document.*
861
870
 
862
871
  ```python
863
- # Plugin loading (simple registry pattern)
864
- FILTER_REGISTRY = {
865
- "yolo": YOLOv8Filter,
866
- "mock": MockFilter,
867
- }
872
+ # Plugin loading (registry pattern)
873
+ def load_filter(config: FilterConfig) -> ObjectFilter:
874
+ return load_plugin(PluginType.FILTER, config.backend, config.config)
868
875
 
869
- VLM_REGISTRY = {
870
- "openai": OpenAIVLM,
871
- "anthropic": AnthropicVLM,
872
- "mock": MockVLM,
873
- }
874
-
875
- def load_filter_plugin(config: FilterConfig) -> ObjectFilter:
876
- if config.plugin not in FILTER_REGISTRY:
877
- raise ValueError(f"Unknown filter plugin: {config.plugin}")
878
- return FILTER_REGISTRY[config.plugin](config)
879
-
880
- def load_vlm_plugin(config: VLMConfig) -> VLMAnalyzer:
881
- if config.plugin not in VLM_REGISTRY:
882
- raise ValueError(f"Unknown VLM plugin: {config.plugin}")
883
- return VLM_REGISTRY[config.plugin](config)
876
+ def load_analyzer(config: VLMConfig) -> VLMAnalyzer:
877
+ return load_plugin(PluginType.ANALYZER, config.backend, config.config)
884
878
 
885
879
  # Future: Support setuptools entry points for third-party plugins (see Open Questions)
886
880
 
887
881
  class ClipPipeline:
888
882
  def __init__(self, config: Config):
889
883
  # ... other init
890
- self.filter_plugin = load_filter_plugin(config.filter)
891
- self.vlm_plugin = load_vlm_plugin(config.vlm)
884
+ self.filter_plugin = load_filter(config.filter)
885
+ self.vlm_plugin = load_analyzer(config.vlm)
892
886
 
893
887
  # Global concurrency limit (prevents OOM)
894
888
  self.global_semaphore = asyncio.Semaphore(config.concurrency.max_clips_in_flight)
895
889
 
896
890
  # Per-stage limits (within global limit)
897
- self.upload_semaphore = asyncio.Semaphore(config.concurrency.upload)
898
- self.filter_semaphore = asyncio.Semaphore(config.concurrency.filter)
899
- self.vlm_semaphore = asyncio.Semaphore(config.concurrency.vlm)
891
+ self.upload_semaphore = asyncio.Semaphore(config.concurrency.upload_workers)
892
+ self.filter_semaphore = asyncio.Semaphore(config.concurrency.filter_workers)
893
+ self.vlm_semaphore = asyncio.Semaphore(config.concurrency.vlm_workers)
900
894
 
901
895
  def _on_new_clip(self, clip: Clip) -> None:
902
896
  """Callback from ClipSource when new clip is ready."""
@@ -908,21 +902,20 @@ class ClipPipeline:
908
902
  async with self.global_semaphore:
909
903
  await self._process_clip(clip)
910
904
 
911
- async def _upload_stage(self, clip: Clip) -> str | UploadError:
912
- """Upload clip. Returns storage_uri on success, UploadError on failure."""
905
+ async def _upload_stage(self, clip: Clip) -> StorageUploadResult | UploadError:
906
+ """Upload clip. Returns StorageUploadResult on success, UploadError on failure."""
913
907
  try:
914
- storage_uri = await self.storage.put(clip.local_path, f"{clip.camera_name}/{clip.clip_id}")
915
- return storage_uri
908
+ return await self.storage.put_file(clip.local_path, f"{clip.camera_name}/{clip.clip_id}")
916
909
  except Exception as e:
917
910
  return UploadError(clip.clip_id, storage_uri=None, cause=e)
918
911
 
919
912
  async def _filter_stage(self, clip: Clip) -> FilterResult | FilterError:
920
913
  """Run filter. Returns FilterResult on success, FilterError on failure."""
921
914
  try:
922
- result = await self.filter_plugin.detect(clip.local_path, self.config.filter)
915
+ result = await self.filter_plugin.detect(clip.local_path)
923
916
  return result
924
917
  except Exception as e:
925
- return FilterError(clip.clip_id, plugin_name=self.config.filter.plugin, cause=e)
918
+ return FilterError(clip.clip_id, plugin_name=self.config.filter.backend, cause=e)
926
919
 
927
920
  async def _process_clip(self, clip: Clip) -> None:
928
921
  # Stage 1 & 2: Upload and Filter in parallel
@@ -951,14 +944,14 @@ class ClipPipeline:
951
944
  self._update_state_stage(clip.clip_id, "upload", status="error", last_error=str(err))
952
945
  storage_uri = None
953
946
  view_url = None
954
- case str() as uri:
955
- storage_uri = uri
956
- view_url = self._compute_view_url(storage_uri)
947
+ case StorageUploadResult() as result:
948
+ storage_uri = result.storage_uri
949
+ view_url = result.view_url
957
950
  self._update_state_stage(clip.clip_id, "upload", status="ok")
958
951
 
959
952
  # Stage 3: VLM (conditional)
960
953
  analysis_result = None
961
- if self._should_run_vlm(clip.camera_name, filter_result):
954
+ if self._should_run_vlm(filter_result):
962
955
  vlm_res = await self._vlm_stage(clip, filter_result)
963
956
  match vlm_res:
964
957
  case VLMError() as err:
@@ -987,24 +980,17 @@ class ClipPipeline:
987
980
  self._update_state(clip.clip_id, status="done")
988
981
  self._cleanup_local_file(clip.local_path, alert_decision.notify)
989
982
 
990
- def _should_run_vlm(self, camera_name: str, filter_result: FilterResult) -> bool:
991
- """Check if VLM should run based on detected classes and config."""
992
- alert_config = self.config.get_alert_policy(camera_name)
993
-
994
- # If notify_on_motion enabled, always run VLM for richer context
995
- if alert_config.notify_on_motion:
996
- return True
997
-
998
- # Otherwise check if detected classes intersect trigger classes
999
- detected = set(filter_result.detected_classes)
1000
- trigger = set(self.config.vlm.trigger_classes)
1001
- return bool(detected & trigger)
1002
-
1003
- def _compute_view_url(self, storage_uri: str) -> str:
1004
- """Compute Dropbox web URL from storage_uri."""
1005
- # Example: dropbox:/front_door/clip.mp4 -> https://www.dropbox.com/home/homecam/front_door/clip.mp4
1006
- dropbox_path = storage_uri.removeprefix("dropbox:")
1007
- return f"{self.config.storage.dropbox.web_url_prefix}{dropbox_path}"
983
+ def _should_run_vlm(self, filter_result: FilterResult) -> bool:
984
+ """Check if VLM should run based on run_mode + detected classes."""
985
+ match self.config.vlm.run_mode:
986
+ case "never":
987
+ return False
988
+ case "always":
989
+ return True
990
+ case "trigger_only":
991
+ detected = set(filter_result.detected_classes)
992
+ trigger = set(self.config.vlm.trigger_classes)
993
+ return bool(detected & trigger)
1008
994
 
1009
995
  # Note: The following helper methods are omitted from pseudocode for brevity:
1010
996
  # - _vlm_stage(clip, filter_result) -> AnalysisResult | VLMError
@@ -1121,7 +1107,7 @@ async def main():
1121
1107
  **Non-critical checks (degraded if fail):**
1122
1108
  - `db`: Postgres unavailable (state tracking disabled, but processing continues)
1123
1109
  - `mqtt`: MQTT broker unreachable (notifications disabled, but processing continues)
1124
- - Configurable via `alert_policy.mqtt_is_critical`: if `true`, MQTT failure is treated as critical (unhealthy)
1110
+ - Configurable via `health.mqtt_is_critical`: if `true`, MQTT failure is treated as critical (unhealthy)
1125
1111
  - Failed notifications are retried at `alert_policy.mqtt_retry_interval_s` intervals
1126
1112
 
1127
1113
  **Activity monitoring:**
@@ -1149,7 +1135,7 @@ class HealthServer:
1149
1135
  status = "healthy"
1150
1136
  if not checks["sources"] or not checks["storage"]:
1151
1137
  status = "unhealthy" # Critical failure
1152
- elif not checks["mqtt"] and self.config.alert_policy.mqtt_is_critical:
1138
+ elif not checks["mqtt"] and self.config.health.mqtt_is_critical:
1153
1139
  status = "unhealthy" # MQTT failure treated as critical (configurable)
1154
1140
  elif not checks["db"] or not checks["mqtt"]:
1155
1141
  status = "degraded" # Non-critical failure
@@ -41,8 +41,8 @@ COPY src/ ./src/
41
41
  COPY alembic/ ./alembic/
42
42
  COPY alembic.ini ./
43
43
 
44
- # Install the project
45
- RUN uv sync --frozen --no-dev
44
+ # Install the project (ensure homesec is in site-packages)
45
+ RUN uv pip install --no-deps .
46
46
 
47
47
  # =============================================================================
48
48
  # Stage 2: Runtime
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: homesec
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Pluggable async home security camera pipeline with detection, VLM analysis, and alerts.
5
5
  Project-URL: Homepage, https://github.com/lan17/homesec
6
6
  Project-URL: Source, https://github.com/lan17/homesec
@@ -326,15 +326,10 @@ graph TD
326
326
 
327
327
  ## Quickstart
328
328
 
329
- ### 30-Second Start (Docker)
330
- The fastest way to see it in action. Includes a pre-configured Postgres and a dummy local source.
329
+ ### Docker
330
+ Use the included [docker-compose.yml](docker-compose.yml) (HomeSec + Postgres, pulls `leva/homesec:latest`).
331
331
 
332
- ```bash
333
- git clone https://github.com/lan17/homesec.git
334
- cd homesec
335
- make up
336
- ```
337
- *Modify `config/config.yaml` to add your real cameras, then restart.*
332
+ Configure your own config.yaml and .env files as described in Manual Setup.
338
333
 
339
334
  ### Manual Setup
340
335
  For standard production usage without Docker Compose:
@@ -400,7 +395,7 @@ Best for real-world setups with flaky cameras.
400
395
  cameras:
401
396
  - name: driveway
402
397
  source:
403
- type: rtsp
398
+ backend: rtsp
404
399
  config:
405
400
  rtsp_url_env: DRIVEWAY_RTSP_URL
406
401
  output_dir: "./recordings"
@@ -411,7 +406,7 @@ cameras:
411
406
  backoff_s: 5
412
407
 
413
408
  filter:
414
- plugin: yolo
409
+ backend: yolo
415
410
  config:
416
411
  classes: ["person", "car"]
417
412
  min_confidence: 0.6
@@ -428,7 +423,7 @@ Uploads to Cloud but keeps analysis local.
428
423
  ```yaml
429
424
  storage:
430
425
  backend: dropbox
431
- dropbox:
426
+ config:
432
427
  token_env: DROPBOX_TOKEN
433
428
  root: "/SecurityCam"
434
429