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.
- homesec-1.2.3/.github/workflows/docker-publish.yml +58 -0
- {homesec-1.2.2 → homesec-1.2.3}/AGENTS.md +2 -1
- {homesec-1.2.2 → homesec-1.2.3}/CHANGELOG.md +18 -0
- {homesec-1.2.2 → homesec-1.2.3}/DESIGN.md +123 -137
- {homesec-1.2.2 → homesec-1.2.3}/Dockerfile +2 -2
- {homesec-1.2.2 → homesec-1.2.3}/PKG-INFO +7 -12
- {homesec-1.2.2 → homesec-1.2.3}/PLUGIN_DEVELOPMENT.md +12 -8
- {homesec-1.2.2 → homesec-1.2.3}/README.md +6 -11
- {homesec-1.2.2 → homesec-1.2.3}/config/example.yaml +17 -23
- {homesec-1.2.2 → homesec-1.2.3}/docker-compose.yml +2 -1
- {homesec-1.2.2 → homesec-1.2.3}/pyproject.toml +1 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/app.py +5 -14
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/cli.py +5 -4
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/config/__init__.py +8 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/config/loader.py +17 -2
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/config/validation.py +99 -6
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/interfaces.py +2 -2
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/maintenance/cleanup_clips.py +17 -4
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/__init__.py +3 -25
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/clip.py +1 -1
- homesec-1.2.3/src/homesec/models/config.py +154 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/enums.py +8 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/events.py +1 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/filter.py +3 -21
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/vlm.py +11 -20
- homesec-1.2.3/src/homesec/pipeline/__init__.py +5 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/pipeline/core.py +9 -10
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/alert_policies/__init__.py +5 -5
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/alert_policies/default.py +21 -2
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/analyzers/__init__.py +1 -3
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/analyzers/openai.py +20 -13
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/filters/__init__.py +1 -2
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/filters/yolo.py +25 -5
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/notifiers/__init__.py +1 -6
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/notifiers/mqtt.py +21 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/notifiers/sendgrid_email.py +52 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/registry.py +27 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/sources/__init__.py +4 -4
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/sources/ftp.py +1 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/sources/local_folder.py +1 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/sources/rtsp.py +1 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/storage/__init__.py +1 -9
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/storage/dropbox.py +13 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/storage/local.py +8 -1
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/repository/clip_repository.py +1 -1
- homesec-1.2.3/src/homesec/sources/__init__.py +16 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/ftp.py +95 -2
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/local_folder.py +27 -2
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/core.py +162 -2
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/conftest.py +1 -1
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/rtsp/test_helpers.py +1 -2
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/rtsp/test_runtime.py +1 -2
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_alert_policy.py +2 -3
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_app.py +14 -11
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_cleanup_clips.py +5 -6
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_cli.py +5 -7
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_clip_repository.py +6 -6
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_clip_sources.py +2 -2
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_config.py +105 -54
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_dropbox_storage.py +1 -2
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_event_store.py +2 -2
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_ftp_source.py +2 -3
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_integration.py +8 -12
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_local_storage.py +1 -2
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_mqtt_notifier.py +1 -2
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_notifiers.py +2 -3
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_openai_vlm.py +6 -10
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_pipeline.py +39 -43
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_pipeline_events.py +14 -15
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_sendgrid_notifier.py +1 -2
- {homesec-1.2.2 → homesec-1.2.3}/uv.lock +1 -1
- homesec-1.2.2/src/homesec/models/config.py +0 -405
- homesec-1.2.2/src/homesec/models/source/__init__.py +0 -3
- homesec-1.2.2/src/homesec/models/source/ftp.py +0 -97
- homesec-1.2.2/src/homesec/models/source/local_folder.py +0 -30
- homesec-1.2.2/src/homesec/models/source/rtsp.py +0 -165
- homesec-1.2.2/src/homesec/pipeline/__init__.py +0 -6
- homesec-1.2.2/src/homesec/pipeline/alert_policy.py +0 -5
- homesec-1.2.2/src/homesec/sources/__init__.py +0 -19
- {homesec-1.2.2 → homesec-1.2.3}/.dockerignore +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/.env.example +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/.github/workflows/ci.yml +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/.github/workflows/release.yaml +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/.github/workflows/validate-pr-title.yaml +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/.gitignore +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/LICENSE +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/Makefile +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/TESTING.md +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/alembic/env.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/alembic/script.py.mako +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/alembic/versions/e6f25df0df90_initial.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/alembic.ini +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/docker-entrypoint.sh +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/skills/local/homesec-db-logs/SKILL.md +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/errors.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/health/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/health/server.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/logging_setup.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/maintenance/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/alert.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/models/storage.py +0 -0
- {homesec-1.2.2/src/homesec/plugins → homesec-1.2.3/src/homesec}/notifiers/multiplex.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/alert_policies/noop.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/plugins/utils.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/py.typed +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/repository/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/base.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/clock.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/frame_pipeline.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/hardware.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/motion.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/recorder.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/sources/rtsp/utils.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/state/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/state/postgres.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/storage_paths.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/db/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/db/log_table.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/db_log_handler.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/src/homesec/telemetry/postgres_settings.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/conftest.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/__init__.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/event_store.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/filter.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/notifier.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/state_store.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/storage.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/mocks/vlm.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/rtsp/test_frame_pipeline.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/rtsp/test_hardware.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_enums.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_health.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_logging_setup.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_plugin_registration.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_plugin_utils.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_source_health.py +0 -0
- {homesec-1.2.2 → homesec-1.2.3}/tests/homesec/test_state_store.py +0 -0
- {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.
|
|
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
|
-
- **
|
|
68
|
-
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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 &
|
|
464
|
+
### Configuration Hierarchy & Overrides
|
|
447
465
|
|
|
448
|
-
|
|
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
|
-
**
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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: {
|
|
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`:
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
546
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
594
|
+
backend: "rtsp"
|
|
586
595
|
config:
|
|
587
596
|
rtsp_url_env: "FRONT_DOOR_RTSP_URL"
|
|
588
597
|
- name: "driveway"
|
|
589
598
|
source:
|
|
590
|
-
|
|
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
|
-
|
|
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
|
-
|
|
685
|
+
backend: str # e.g., "openai", "anthropic", "mock"
|
|
678
686
|
trigger_classes: list[str] = Field(default_factory=lambda: ["person"])
|
|
679
|
-
|
|
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
|
-
-
|
|
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 (
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
|
|
870
|
-
|
|
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 =
|
|
891
|
-
self.vlm_plugin =
|
|
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.
|
|
898
|
-
self.filter_semaphore = asyncio.Semaphore(config.concurrency.
|
|
899
|
-
self.vlm_semaphore = asyncio.Semaphore(config.concurrency.
|
|
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) ->
|
|
912
|
-
"""Upload clip. Returns
|
|
905
|
+
async def _upload_stage(self, clip: Clip) -> StorageUploadResult | UploadError:
|
|
906
|
+
"""Upload clip. Returns StorageUploadResult on success, UploadError on failure."""
|
|
913
907
|
try:
|
|
914
|
-
|
|
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
|
|
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.
|
|
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
|
|
955
|
-
storage_uri =
|
|
956
|
-
view_url =
|
|
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(
|
|
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,
|
|
991
|
-
"""Check if VLM should run based on detected classes
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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 `
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
###
|
|
330
|
-
|
|
329
|
+
### Docker
|
|
330
|
+
Use the included [docker-compose.yml](docker-compose.yml) (HomeSec + Postgres, pulls `leva/homesec:latest`).
|
|
331
331
|
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
+
config:
|
|
432
427
|
token_env: DROPBOX_TOKEN
|
|
433
428
|
root: "/SecurityCam"
|
|
434
429
|
|