14 Commits

Author SHA1 Message Date
Roman Meshko 4c11bd5b9e Added int8 cache to test compatibility
ci/woodpecker/push/02-build-push Pipeline was canceled
2026-06-07 10:44:18 +00:00
Roman Meshko cd1a89c495 Changed to update image version
ci/woodpecker/push/02-build-push Pipeline was successful
2026-05-31 16:34:04 +03:00
Roman Meshko 5d1b00c8b6 Changed to update image version
ci/woodpecker/push/02-build-push Pipeline failed
2026-05-31 16:25:23 +03:00
Roman Meshko a637683731 Changed to use SoftMax layers
ci/woodpecker/push/02-build-push Pipeline was successful
2026-05-23 22:20:18 +03:00
Roman Meshko 1250542800 Changed to use additional calibration parameters
ci/woodpecker/push/02-build-push Pipeline was successful
2026-05-23 22:11:57 +03:00
Roman Meshko bd9bc2e05d Changed to use prepare for TensorRT
ci/woodpecker/push/02-build-push Pipeline was successful
2026-05-23 22:07:01 +03:00
Oleksandr Bezdieniezhnykh a85c238c92 Merge branch 'dev' of https://github.com/azaion/detections into dev
ci/woodpecker/push/02-build-push Pipeline was successful
2026-05-17 13:19:25 +03:00
Oleksandr Bezdieniezhnykh 3a5c7df2c9 [no-ticket] Sync .cursor with suite root
Bring this repo's .cursor/ in line with the suite monorepo root .cursor/
so rules, skills, and autodev artifacts stay consistent across
submodules and sibling repos.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 13:11:01 +03:00
Roman Meshko 080f257423 Fix e2e tests
ci/woodpecker/push/02-build-push Pipeline was canceled
ci/woodpecker/manual/01-test Pipeline was successful
ci/woodpecker/manual/02-build-push Pipeline was successful
ci/woodpecker/manual/e2e-smoke-jetson Pipeline was successful
2026-05-15 13:15:42 +03:00
Roman Meshko a0ec2cd563 Merge branch 'dev' of https://github.com/azaion/detections into dev
ci/woodpecker/push/02-build-push Pipeline was canceled
ci/woodpecker/manual/01-test Pipeline failed
ci/woodpecker/manual/02-build-push Pipeline was successful
2026-05-15 12:49:37 +03:00
Roman Meshko 255ec36f8a Fix e2e tests 2026-05-15 12:45:18 +03:00
Roman Meshko c9aeed3dd9 Added camera config
ci/woodpecker/push/02-build-push Pipeline was successful
ci/woodpecker/manual/02-build-push Pipeline was successful
ci/woodpecker/manual/01-test Pipeline failed
2026-05-15 09:31:23 +03:00
Roman Meshko 2eb5b5d8ad Added video file for e2e smoke tests
ci/woodpecker/push/02-build-push Pipeline was canceled
ci/woodpecker/manual/e2e-smoke-jetson Pipeline was successful
2026-05-12 18:47:30 +00:00
Roman Meshko f0f20177a0 Changed to have video in smoke-test 2026-05-12 21:43:46 +03:00
37 changed files with 1169 additions and 106 deletions
+1
View File
@@ -39,6 +39,7 @@ alwaysApply: true
- When you think you are done with changes, run the full test suite. Every failure in tests that cover code you modified or that depend on code you modified is a **blocking gate**. For pre-existing failures in unrelated areas, report them to the user but do not block on them. Never silently ignore or skip a failure without reporting it. On any blocking failure, stop and ask the user to choose one of: - When you think you are done with changes, run the full test suite. Every failure in tests that cover code you modified or that depend on code you modified is a **blocking gate**. For pre-existing failures in unrelated areas, report them to the user but do not block on them. Never silently ignore or skip a failure without reporting it. On any blocking failure, stop and ask the user to choose one of:
- **Investigate and fix** the failing test or source code - **Investigate and fix** the failing test or source code
- **Remove the test** if it is obsolete or no longer relevant - **Remove the test** if it is obsolete or no longer relevant
- **Iterative-skill exception**: when an iterative loop skill is active (e.g. autodev / `implement/SKILL.md` batch loop, `refactor/SKILL.md` batch loop), the skill governs full-suite cadence — typically focused tests per task/batch and a single full-suite gate at the very end of the implementation phase, NOT after each batch. "Done with changes" means done with the entire implementation phase the skill is running, not done with one batch. Do not run the full suite per batch unless the skill explicitly says to.
- Do not rename any databases or tables or table columns without confirmation. Avoid such renaming if possible. - Do not rename any databases or tables or table columns without confirmation. Avoid such renaming if possible.
- Make sure we don't commit binaries, create and keep .gitignore up to date and delete binaries after you are done with the task - Make sure we don't commit binaries, create and keep .gitignore up to date and delete binaries after you are done with the task
+41
View File
@@ -0,0 +1,41 @@
---
description: "Use chunked writes (Write + StrReplace marker pattern) for large generated files, especially after a monolithic Write fails"
alwaysApply: true
---
# Large File Writes — Chunk on Failure
When a `Write` call to a single file fails (timeout, payload limit, "Invalid arguments", or any tool error) and the intended content is large (>~500 lines or >~50 KB), do NOT retry the same monolithic Write. Switch to chunked writes:
1. **First Write** — create the file with header + table of contents (if applicable) + an explicit append marker, e.g.
```
<!-- INSERTION_POINT do-not-remove-until-final-chunk -->
```
2. **Each subsequent chunk** — use `StrReplace` to replace the marker with `<new content>\n<marker>` so the marker stays at the end. This is idempotent: if a chunk fails, retry it without losing earlier chunks.
3. **Final chunk** — `StrReplace` removes the marker.
## Why
- Tool argument size limits and transient failures hit large monolithic writes hardest. Retrying the same large payload typically fails for the same reason.
- Chunked writes are recoverable per chunk. The earlier chunks are durable on disk.
- A unique marker is greppable, visible in diffs, and stops accidental insertion in the wrong place.
## Triggers
- Generated documentation that aggregates per-component content (epics, design docs, multi-section architecture summaries, traceability dumps).
- Large fixture or test-data files written from a template.
- Any single-file artifact you can pre-estimate at >~500 lines.
## Do NOT chunk
- Files under ~200 lines — a single `Write` is faster, clearer, and easier to review.
- Source code files where appending breaks module structure (functions, classes, imports). Split into multiple files instead.
- Files where ordering of sections is computed late and inserting in the middle is required — use a single `Write` once the full content is known.
## Anti-patterns
- Retrying the same failed monolithic `Write` more than once. Twice is the limit; on the second failure, switch strategies.
- Using `Shell` with heredoc (`cat <<EOF`) or `echo >>` to append — these bypass the editor diff view and break the StrReplace contract for the next chunk.
- Embedding the marker so deep inside structured content that a chunk's `StrReplace` becomes ambiguous. Place the marker on its own line at the very end of the file.
+6 -3
View File
@@ -14,11 +14,14 @@ alwaysApply: true
- Issue types: Epic, Story, Task, Bug, Subtask - Issue types: Epic, Story, Task, Bug, Subtask
## Tracker Availability Gate ## Tracker Availability Gate
- If Jira MCP returns **Unauthorized**, **errored**, **connection refused**, or any non-success response: **STOP** tracker operations and notify the user via the Choose A/B/C/D format documented in `.cursor/skills/autodev/protocols.md`. - If Jira MCP returns **Unauthorized**, **errored**, **connection refused**, **timeout**, a non-2xx status code, an empty body, or any response shape that does not clearly confirm the requested change: **STOP IMMEDIATELY** — no automatic retry, no silent continuation. Surface the full raw error/response to the user verbatim and notify via the Choose A/B/C/D format documented in `.cursor/skills/autodev/protocols.md`.
- A minimal `{"success": true}` body with no echoed issue state is NOT a confirmed transition. When a transition's success matters (status moves, ticket creation, blocking link), follow it with a read-back call (`getJiraIssue` or equivalent) and confirm the new state matches what you asked for. If the read-back disagrees → STOP and ASK.
- Do NOT loop "retry up to N times before asking". One call, one verification. On failure, the user decides whether to retry.
- The user may choose to: - The user may choose to:
- **Retry authentication** — preferred; the tracker remains the source of truth. - **Retry the same operation** — once, after the user authorizes it. If it fails again, surface both responses.
- **Retry authentication** — preferred when the failure looks like an auth/credentials problem; the tracker remains the source of truth.
- **Continue in `tracker: local` mode** — only when the user explicitly accepts this option. In that mode all tasks keep numeric prefixes and a `Tracker: pending` marker is written into each task header. The state file records `tracker: local`. The mode is NOT silent — the user has been asked and has acknowledged the trade-off. - **Continue in `tracker: local` mode** — only when the user explicitly accepts this option. In that mode all tasks keep numeric prefixes and a `Tracker: pending` marker is written into each task header. The state file records `tracker: local`. The mode is NOT silent — the user has been asked and has acknowledged the trade-off.
- Do NOT auto-fall-back to `tracker: local` without a user decision. Do not pretend a write succeeded. If the user is unreachable (e.g., non-interactive run), stop and wait. - Do NOT auto-fall-back to `tracker: local` without a user decision. Do not pretend a write succeeded. Do not paper over an opaque response by moving on. If the user is unreachable (e.g., non-interactive run), stop and wait.
- When the tracker becomes available again, any `Tracker: pending` tasks should be synced — this is done at the start of the next `/autodev` invocation via the Leftovers Mechanism below. - When the tracker becomes available again, any `Tracker: pending` tasks should be synced — this is done at the start of the next `/autodev` invocation via the Leftovers Mechanism below.
## Leftovers Mechanism (non-user-input blockers only) ## Leftovers Mechanism (non-user-input blockers only)
+3 -2
View File
@@ -67,8 +67,9 @@ B3. Read state — `_docs/_autodev_state.md` (if it exists).
B4. Read File Index — `state.md`, `protocols.md`, and the active flow file. B4. Read File Index — `state.md`, `protocols.md`, and the active flow file.
### Resolve (once per invocation, after Bootstrap) ### Resolve (once per invocation, after Bootstrap)
R1. Reconcile state — verify state file against `_docs/` contents; on disagreement, trust the folders R1. Reconcile state — verify state file against `_docs/` contents; probe `<workspace-root>/../docs`
and update the state file (rules: `state.md` → "State File Rules" #4). (parent suite `docs/` — see `state.md` → "State File Rules" #4); on disagreement,
trust the folders and update the state file (rules: `state.md` → "State File Rules" #4).
After this step, `state.step` / `state.status` are authoritative. After this step, `state.step` / `state.status` are authoritative.
R2. Resolve flow — see §Flow Resolution above. R2. Resolve flow — see §Flow Resolution above.
R3. Resolve current step — when a state file exists, `state.step` drives detection. R3. Resolve current step — when a state file exists, `state.step` drives detection.
+158 -13
View File
@@ -5,7 +5,8 @@ Workflow for **meta-repositories** — repos that aggregate multiple components
This flow differs fundamentally from `greenfield` and `existing-code`: This flow differs fundamentally from `greenfield` and `existing-code`:
- **No problem/research/plan phases** — meta-repos don't build features, they coordinate existing ones - **No problem/research/plan phases** — meta-repos don't build features, they coordinate existing ones
- **No test spec / implement / run tests** — the meta-repo has no code to test - **No test spec / run tests** — the meta-repo has no code to test
- **`implement` is scoped to suite-level work only** — cross-repo concerns, repo/folder renames, suite-root infra additions (e.g., `.gitmodules`, `_infra/`, suite `e2e/`). Per-component implementation lives in each component's own workspace `/autodev` cycle. The meta-repo's implement step (Step 3.5) executes only when `_docs/tasks/todo/` is non-empty AND the user explicitly opts in; placement is **before** the sync skills so subsequent Doc/E2E/CICD sync propagates the post-implementation state.
- **No `_docs/00_problem/` artifacts** — documentation target is `_docs/*.md` unified docs, not per-feature `_docs/NN_feature/` folders - **No `_docs/00_problem/` artifacts** — documentation target is `_docs/*.md` unified docs, not per-feature `_docs/NN_feature/` folders
- **Primary artifact is `_docs/_repo-config.yaml`** — generated by `monorepo-discover`, read by every other step - **Primary artifact is `_docs/_repo-config.yaml`** — generated by `monorepo-discover`, read by every other step
@@ -17,6 +18,7 @@ This flow differs fundamentally from `greenfield` and `existing-code`:
| 2 | Config Review | (human checkpoint, no sub-skill) | — | | 2 | Config Review | (human checkpoint, no sub-skill) | — |
| 2.5 | Glossary & Architecture Vision | (inline, no sub-skill) | Steps 15 | | 2.5 | Glossary & Architecture Vision | (inline, no sub-skill) | Steps 15 |
| 3 | Status | monorepo-status/SKILL.md | Sections 15 | | 3 | Status | monorepo-status/SKILL.md | Sections 15 |
| 3.5 | Suite Implement | implement/SKILL.md (suite-level invocation context) | Steps 114 + 16 (Step 14.5 + Step 15 skipped); conditional on `_docs/tasks/todo/` non-empty AND user opt-in |
| 4 | Document Sync | monorepo-document/SKILL.md | Phase 17 (conditional on doc drift) | | 4 | Document Sync | monorepo-document/SKILL.md | Phase 17 (conditional on doc drift) |
| 4.5 | Integration Test Sync | monorepo-e2e/SKILL.md | Phase 16 (conditional on suite-e2e drift; skipped if `suite_e2e:` block absent in config) | | 4.5 | Integration Test Sync | monorepo-e2e/SKILL.md | Phase 16 (conditional on suite-e2e drift; skipped if `suite_e2e:` block absent in config) |
| 5 | CICD Sync | monorepo-cicd/SKILL.md | Phase 17 (conditional on CI drift) | | 5 | CICD Sync | monorepo-cicd/SKILL.md | Phase 17 (conditional on CI drift) |
@@ -184,11 +186,16 @@ The status report identifies:
- Registry/config mismatches - Registry/config mismatches
- Unresolved questions - Unresolved questions
Based on the report, auto-chain branches: Based on the report, auto-chain branches in this evaluation order (first match wins):
- If **doc drift** found → auto-chain to **Step 4 (Document Sync)** 1. **Registry mismatch** (new components not in config, or config component not in registry) → present the Choose format below FIRST. After the user resolves it (A: refresh discover, B: onboard, C: continue with mismatch acknowledged), proceed to the next rule. This rule has priority because a stale config would mislead Step 3.5's ownership-envelope synthesis and any sync skill's component scope.
- Else if **CI drift** (only) found → auto-chain to **Step 5 (CICD Sync)** 2. **Pre-routing gate (Step 3.5 detection)** — check `_docs/tasks/todo/` for suite-level task files (`*.md` excluding files starting with `_`). If ≥1 task is present, auto-chain to **Step 3.5 (Suite Implement)**. After Step 3.5 returns (regardless of A/B outcome), the post-implement re-status applies rules 36 below to the post-implementation state.
- Else if **registry mismatch** found (new components not in config) → present Choose format: 3. If **doc drift** found → auto-chain to **Step 4 (Document Sync)**
4. Else if **CI drift** (only) found → auto-chain to **Step 5 (CICD Sync)**
5. Else if **suite-e2e drift** (only) found → auto-chain to **Step 4.5 (Integration Test Sync)** (only when `suite_e2e:` block exists in config)
6. Else → **workflow done for this cycle**.
**Registry mismatch Choose format** (rule 1):
``` ```
══════════════════════════════════════ ══════════════════════════════════════
@@ -205,7 +212,134 @@ Based on the report, auto-chain branches:
══════════════════════════════════════ ══════════════════════════════════════
``` ```
- Else → **workflow done for this cycle**. Report "No drift. Meta-repo is in sync." Loop waits for next invocation. When rule 6 fires (no drift, no todo tasks), report "No drift. Meta-repo is in sync." and end the cycle. Loop waits for next invocation.
---
**Step 3.5 — Suite Implement**
Condition (folder fallback): `_docs/tasks/todo/` exists AND contains ≥1 file matching `*.md` excluding files starting with `_` (e.g., `_dependencies_table.md` is excluded by convention).
State-driven: reached by auto-chain from Step 3 when the pre-routing gate detected todo tasks. Inserted **before** the sync skills (Step 4 / 4.5 / 5) by deliberate design: implementing renames + cross-repo edits first means the subsequent sync skills propagate the actual landed state rather than the pre-change state, avoiding a second cycle to fix downstream drift.
**Skip condition**: `_docs/tasks/todo/` is empty, missing, or contains only `_*` files. In that case Step 3.5 is skipped entirely and the cycle proceeds with Step 3's existing drift-based routing.
**Goal**: Execute suite-level implementation tasks — cross-repo concerns (e.g., `autopilot` + `ui` + suite `e2e/` cutover in a coordinated change-set), folder renames (e.g., `git mv flights missions` + `.gitmodules` edit + `_infra/` path refs), and suite-root infrastructure additions (e.g., `_infra/dev/docker-compose.dev.yml`). Per-component implementation work stays in each component's own workspace `/autodev` cycle.
**Why this exists**: the meta-repo's existing sync skills (`monorepo-document`, `monorepo-cicd`, `monorepo-e2e`) only **propagate** changes that already landed. They cannot **execute** a task spec. Without Step 3.5, suite-level tickets like AZ-543 (B4 repo rename) or AZ-506 (new dev compose) have no flow path forward — they require operator action outside autodev.
**Inputs**:
- `_docs/tasks/todo/*.md` (excluding `_*`) — task specs in the existing format (`Task` / `Component` / `Dependencies` / `Acceptance criteria` headers)
- `_docs/_repo-config.yaml` — `components[].path` list, used to compute the suite-level OWNED envelope (workspace root EXCLUDING any path under a component's folder)
- `_docs/tasks/_dependencies_table.md` — synthesized by this step if missing (see Procedure)
- `_docs/tasks/_suite_module_layout.md` — synthesized by this step if missing (see Procedure)
**Procedure**:
1. **Detection (already done by Step 3 pre-routing gate)**. List task files in `_docs/tasks/todo/` (excluding `_*`). If 0 → skip Step 3.5. If ≥1 → continue.
2. **Present Choose**:
```
══════════════════════════════════════
DECISION REQUIRED: <N> suite-level task(s) in _docs/tasks/todo/
══════════════════════════════════════
Task(s) detected:
- AZ-XXX: <title> (deps: <list or "—">)
- AZ-YYY: <title> (deps: <list or "—">)
...
A) Run implement skill on these task(s) now (then continue to Doc / E2E / CICD sync)
B) Skip implement this cycle — continue to Doc / E2E / CICD sync without executing tasks
C) Pause — review the tasks before deciding (end session, no state changes)
══════════════════════════════════════
Recommendation: A — running implement BEFORE syncs means subsequent
sync skills propagate the post-implementation state.
B is appropriate when tasks are blocked on user input
or external coordination. C when the tasks themselves
need owner clarification before execution.
══════════════════════════════════════
```
3. **On user A — Pre-flight**:
a. **Working tree clean check**. Run `git status --porcelain`. If non-empty, surface to the user with a Choose A/B/C identical to the implement skill's prerequisite gate (commit/stash manually; agent commits as `chore: WIP pre-implement`; abort).
b. **Synthesize `_docs/tasks/_dependencies_table.md`** if missing. Parse each in-scope task's `Dependencies:` field. Write a minimal table of the form:
```markdown
# Suite-Level Task Dependencies
| Task ID | Depends on | Notes |
|---------|------------|-------|
| AZ-XXX | (none) | — |
| AZ-YYY | AZ-XXX | — |
```
If a task lists a dependency that is neither in `todo/` nor `done/`, log a warning in the synthesized file but do not block — implement skill's Step 1 (Parse) will surface the issue if it actually blocks execution.
c. **Synthesize `_docs/tasks/_suite_module_layout.md`** if missing. Default content:
```markdown
# Suite-Level Module Layout (synthetic)
Generated by autodev meta-repo Step 3.5. The suite root has no per-feature decomposition; ownership is defined at the component-boundary level only.
## Per-Component Mapping
| Component | Owns | Imports from |
|-----------|----------------------------------|--------------|
| suite | (workspace root) excluding any path listed under `_repo-config.yaml.components[].path` | (read-only) every component's primary doc + `_docs/*.md` |
Suite-level tasks operate on: `.gitmodules`, `_infra/**`, `_docs/**` (excluding `_docs/tasks/_*` regenerated files), root `README.md`, `e2e/**` (suite e2e harness only).
Forbidden paths for suite-level tasks: `<component>/**` for every component listed in `_repo-config.yaml.components[].path` — those edits live in the component's own workspace `/autodev` cycle.
```
d. **Prepare invocation context**:
```
suite_level: true
TASKS_DIR: _docs/tasks/
module_layout_path: _docs/tasks/_suite_module_layout.md
```
4. **Invoke implement skill**. Read and execute `.cursor/skills/implement/SKILL.md` with the prepared context. The skill's "Suite-level invocation context" subsection (added in tandem with this flow change) honors the three flags above and skips:
- Step 14.5 (cumulative code review) — no `architecture_compliance_baseline.md` exists at the suite level; cross-task drift is captured by the next `monorepo-status` cycle instead.
- Step 15 (Product Implementation Completeness Gate) — the gate's inputs (`_docs/02_document/architecture.md`, `system-flows.md`, `components/*/description.md`) do not exist in the meta-repo artifact layout. Suite tasks are infrastructure / coordination work, not feature implementation.
All other implement skill steps (114, 16) execute unchanged. Tracker integration (Step 5: In Progress, Step 12: In Testing) runs normally.
5. **Post-implement re-status**. After the implement skill completes (last batch committed, all originally-todo tasks moved to `_docs/tasks/done/`), silently re-run Step 3's drift detection logic — do NOT re-render the full Status report; just re-evaluate the drift signals against the post-implementation tree. Then auto-chain per the post-implementation drift findings:
- Doc drift → Step 4 (Document Sync)
- Suite-e2e drift only → Step 4.5
- CI drift only → Step 5
- No drift → cycle complete
Note: the post-implement re-status is exactly why Step 3.5 is placed before sync. A repo rename will typically introduce doc + CI drift; the next invocation of Step 4 / Step 5 catches it on the same cycle.
6. **On user B (skip)** → mark Step 3.5 `skipped` in state file. Apply Step 3's original drift-based routing (compute from the pre-Step-3.5 Status report).
7. **On user C (pause)** → end session. Update state to `step: 3.5, status: in_progress, sub_step: {phase: 0, name: awaiting-task-review, detail: "<N> tasks pending review"}`. Tell the user to invoke `/autodev` again after deciding. **Do NOT modify any files** — pre-flight has not run yet.
**Self-verification** (executed before invoking implement):
- [ ] Working tree is clean (or user explicitly chose B in the WIP-stash sub-Choose)
- [ ] `_docs/tasks/_dependencies_table.md` exists (synthesized if it didn't)
- [ ] `_docs/tasks/_suite_module_layout.md` exists (synthesized if it didn't)
- [ ] All in-scope task files have a `Component:` field (skip + report any that don't — don't guess ownership)
- [ ] Tracker availability gate satisfied per `protocols.md` (or `tracker: local` previously chosen)
**Failure handling**:
- If implement returns FAILED → standard Failure Handling (`protocols.md`): retry up to 3 times, then escalate.
- If implement is interrupted mid-batch → next invocation re-detects via the implement skill's resumability protocol (read latest `_docs/03_implementation/suite_batch_*.md`). Step 3.5 itself is reentrant: on re-entry, if `todo/` still has tasks, it presents the Choose again with the remaining set.
- **Half-applied state risk** (acknowledged): if implement is interrupted between commits, the working tree is clean at the last commit boundary but the in-flight batch is lost. The user is responsible for inspecting and re-invoking. This is intentional — automated rollback of suite-level renames + `.gitmodules` edits is more dangerous than a human-driven recovery.
**Idempotency**: if `_docs/tasks/todo/` becomes empty after this step (all tasks moved to `done/`), the next `/autodev` invocation skips Step 3.5 entirely and proceeds with normal Status → sync flow.
--- ---
@@ -287,11 +421,16 @@ After onboarding completes, the config is updated. Auto-chain back to **Step 3 (
| Config Review (2, user picked A, confirmed_by_user: true) | Auto-chain → Glossary & Architecture Vision (2.5) | | Config Review (2, user picked A, confirmed_by_user: true) | Auto-chain → Glossary & Architecture Vision (2.5) |
| Config Review (2, user picked B) | **Session boundary** — end session, await re-invocation | | Config Review (2, user picked B) | **Session boundary** — end session, await re-invocation |
| Glossary & Architecture Vision (2.5) | Auto-chain → Status (3) | | Glossary & Architecture Vision (2.5) | Auto-chain → Status (3) |
| Status (3, doc drift) | Auto-chain → Document Sync (4) | | Status (3, todo tasks present) | Auto-chain → Suite Implement (3.5) — pre-routing gate fires before drift-based routing |
| Status (3, suite-e2e drift only) | Auto-chain → Integration Test Sync (4.5) | | Status (3, no todo tasks, doc drift) | Auto-chain → Document Sync (4) |
| Status (3, CI drift only) | Auto-chain → CICD Sync (5) | | Status (3, no todo tasks, suite-e2e drift only) | Auto-chain → Integration Test Sync (4.5) |
| Status (3, no drift) | **Cycle complete** — end session, await re-invocation | | Status (3, no todo tasks, CI drift only) | Auto-chain → CICD Sync (5) |
| Status (3, no todo tasks, no drift) | **Cycle complete** — end session, await re-invocation |
| Status (3, registry mismatch) | Ask user (A: discover, B: onboard, C: continue) | | Status (3, registry mismatch) | Ask user (A: discover, B: onboard, C: continue) |
| Suite Implement (3.5, user picked A, success) | Silent re-status; auto-chain per post-implementation drift (Step 4 / 4.5 / 5 / cycle complete) |
| Suite Implement (3.5, user picked B) | Mark `skipped`; auto-chain per Step 3's original drift findings |
| Suite Implement (3.5, user picked C) | **Session boundary** — end session, await re-invocation |
| Suite Implement (3.5, FAILED ×3) | Standard Failure Handling escalation (`protocols.md`) |
| Document Sync (4) + suite-e2e drift pending | Auto-chain → Integration Test Sync (4.5) | | Document Sync (4) + suite-e2e drift pending | Auto-chain → Integration Test Sync (4.5) |
| Document Sync (4) + CI drift only pending | Auto-chain → CICD Sync (5) | | Document Sync (4) + CI drift only pending | Auto-chain → CICD Sync (5) |
| Document Sync (4) + no further drift | **Cycle complete** | | Document Sync (4) + no further drift | **Cycle complete** |
@@ -317,11 +456,12 @@ Flow-specific slot values:
| 2 | Config Review | `IN PROGRESS (awaiting human)` | | 2 | Config Review | `IN PROGRESS (awaiting human)` |
| 2.5 | Glossary & Architecture Vision | `SKIPPED (already captured)` | | 2.5 | Glossary & Architecture Vision | `SKIPPED (already captured)` |
| 3 | Status | `DONE (no drift)`, `DONE (N drifts)` | | 3 | Status | `DONE (no drift)`, `DONE (N drifts)` |
| 3.5 | Suite Implement | `DONE (N tasks)`, `SKIPPED (no todo tasks)`, `SKIPPED (user picked B)`, `IN PROGRESS (batch M of ~N)`, `IN PROGRESS (awaiting-task-review)` |
| 4 | Document Sync | `DONE (N docs)`, `SKIPPED (no doc drift)` | | 4 | Document Sync | `DONE (N docs)`, `SKIPPED (no doc drift)` |
| 4.5 | Integration Test Sync | `DONE (N files)`, `SKIPPED (no suite-e2e drift)`, `SKIPPED (no suite_e2e config block)` | | 4.5 | Integration Test Sync | `DONE (N files)`, `SKIPPED (no suite-e2e drift)`, `SKIPPED (no suite_e2e config block)` |
| 5 | CICD Sync | `DONE (N files)`, `SKIPPED (no CI drift)` | | 5 | CICD Sync | `DONE (N files)`, `SKIPPED (no CI drift)` |
All rows accept the shared state tokens (`DONE`, `IN PROGRESS`, `NOT STARTED`, `FAILED (retry N/3)`); rows 2.5, 4, 4.5, and 5 additionally accept `SKIPPED`. All rows accept the shared state tokens (`DONE`, `IN PROGRESS`, `NOT STARTED`, `FAILED (retry N/3)`); rows 2.5, 3.5, 4, 4.5, and 5 additionally accept `SKIPPED`.
Row rendering format: Row rendering format:
@@ -330,6 +470,7 @@ Row rendering format:
Step 2 Config Review [<state token>] Step 2 Config Review [<state token>]
Step 2.5 Glossary & Architecture Vision [<state token>] Step 2.5 Glossary & Architecture Vision [<state token>]
Step 3 Status [<state token>] Step 3 Status [<state token>]
Step 3.5 Suite Implement [<state token>]
Step 4 Document Sync [<state token>] Step 4 Document Sync [<state token>]
Step 4.5 Integration Test Sync [<state token>] Step 4.5 Integration Test Sync [<state token>]
Step 5 CICD Sync [<state token>] Step 5 CICD Sync [<state token>]
@@ -337,8 +478,12 @@ Row rendering format:
## Notes for the meta-repo flow ## Notes for the meta-repo flow
- **No session boundary except Step 2 and Step 2.5**: unlike existing-code flow (which has boundaries around decompose), meta-repo flow only pauses at config review and the one-shot glossary/vision capture. Once both are confirmed, syncing is fast enough to complete in one session and Step 2.5 idempotently no-ops on every subsequent invocation. - **Session boundaries**: Step 2 (Config Review pending), Step 2.5 (one-shot glossary/vision review), and Step 3.5 (when user picks C "Pause"). Step 3.5's A/B picks do NOT cross a session boundary — they auto-chain to syncs in the same session.
- **Cyclical, not terminal**: no "done forever" state. Each invocation completes a drift cycle; next invocation starts fresh. - **Cyclical, not terminal**: no "done forever" state. Each invocation completes a drift cycle; next invocation starts fresh.
- **No tracker integration**: this flow does NOT create Jira/ADO tickets. Maintenance is not a feature — if a feature-level ticket spans the meta-repo's concerns, it lives in the per-component workspace. - **Tracker integration scope**: this flow does NOT create Jira/ADO tickets in its sync skills (Status / Document Sync / E2E / CICD). Step 3.5 (Suite Implement) IS tracker-integrated — it transitions existing tickets In Progress → In Testing per the implement skill's standard tracker handling. Suite-level tickets are authored manually by the operator (typically as children of an Epic that spans multiple components, like AZ-539); the flow doesn't auto-create them.
- **Per-component vs. suite-level work**:
- Tickets that touch component source code (`<component>/src/**`) belong in that component's own workspace `/autodev` cycle. The meta-repo flow does NOT execute them.
- Tickets that touch suite-root paths only (`.gitmodules`, `_infra/**`, suite `e2e/**`, root `README.md`, suite `_docs/**` outside `tasks/_*`) are eligible for Step 3.5.
- Tickets that span both (e.g., AZ-550 B11 consumer cutover, which touches `autopilot/`, `ui/`, AND suite `e2e/`) are NOT executable from a single workspace by design — split the ticket so the suite-level slice can run in Step 3.5 and the component slices run in their owning workspaces.
- **Onboarding is opt-in**: never auto-onboarded. User must explicitly request. - **Onboarding is opt-in**: never auto-onboarded. User must explicitly request.
- **Failure handling**: uses the same retry/escalation protocol as other flows (see `protocols.md`). - **Failure handling**: uses the same retry/escalation protocol as other flows (see `protocols.md`).
+2 -1
View File
@@ -114,6 +114,7 @@ Before entering a step from this table for the first time in a session, verify t
| greenfield | Decompose Tests | Step 1t + Step 3 — All test tasks | Create ticket per task, link to epic | | greenfield | Decompose Tests | Step 1t + Step 3 — All test tasks | Create ticket per task, link to epic |
| existing-code | Decompose Tests | Step 1t + Step 3 — All test tasks | Create ticket per task, link to epic | | existing-code | Decompose Tests | Step 1t + Step 3 — All test tasks | Create ticket per task, link to epic |
| existing-code | New Task | Step 7 — Ticket | Create ticket per task, link to epic | | existing-code | New Task | Step 7 — Ticket | Create ticket per task, link to epic |
| meta-repo | Suite Implement | Step 3.5 — implement skill Step 5 / Step 12 | Transition existing tickets In Progress → In Testing per implement skill (does NOT create new tickets — operator authors them) |
### State File Marker ### State File Marker
@@ -388,7 +389,7 @@ The banner shell is defined here once. Each flow file contributes only its step-
where `<state token>` comes from the state-token set defined per row in the flow's step-list table. where `<state token>` comes from the state-token set defined per row in the flow's step-list table.
- `<current-suffix>` — optional, flow-specific. The existing-code flow appends ` (cycle <N>)` when `state.cycle > 1`; other flows leave it empty. - `<current-suffix>` — optional, flow-specific. The existing-code flow appends ` (cycle <N>)` when `state.cycle > 1`; other flows leave it empty.
- `Retry:` row — omit entirely when `retry_count` is 0. Include it with `<N>/3` otherwise. - `Retry:` row — omit entirely when `retry_count` is 0. Include it with `<N>/3` otherwise.
- `<footer-extras>` — optional, flow-specific. The meta-repo flow adds a `Config:` line with `_docs/_repo-config.yaml` state; other flows leave it empty. - `<footer-extras>` — optional, flow-specific. The meta-repo flow adds a `Config:` line with `_docs/_repo-config.yaml` state; other flows leave it empty unless **parent suite docs** apply: if `<workspace-root>/../docs` exists and is a directory, append `Suite docs (parent): <absolute path>` on its own line (or `Suite docs (parent): absent` is **not** required — omit when missing). This line is orthogonal to flow-specific footer lines; both may appear.
### State token set (shared) ### State token set (shared)
+15 -2
View File
@@ -13,7 +13,7 @@ The autodev persists its position to `_docs/_autodev_state.md`. This is a lightw
## Current Step ## Current Step
flow: [greenfield | existing-code | meta-repo] flow: [greenfield | existing-code | meta-repo]
step: [1-17 for greenfield, 1-17 for existing-code, 1-6 for meta-repo, or "done"] step: [1-17 for greenfield, 1-17 for existing-code, 1-6 for meta-repo (incl. fractional 2.5 and 3.5), or "done"]
name: [step name from the active flow's Step Reference Table] name: [step name from the active flow's Step Reference Table]
status: [not_started / in_progress / completed / skipped / failed] status: [not_started / in_progress / completed / skipped / failed]
sub_step: sub_step:
@@ -82,6 +82,19 @@ retry_count: 0
cycle: 1 cycle: 1
``` ```
```
flow: meta-repo
step: 3.5
name: Suite Implement
status: in_progress
sub_step:
phase: 7
name: batch-loop
detail: "AZ-543 batch 1 of 1; suite-level"
retry_count: 0
cycle: 1
```
``` ```
flow: existing-code flow: existing-code
step: 10 step: 10
@@ -100,7 +113,7 @@ cycle: 3
1. **Create** on the first autodev invocation (after state detection determines Step 1) 1. **Create** on the first autodev invocation (after state detection determines Step 1)
2. **Update** after every change — this includes: batch completion, sub-step progress, step completion, session boundary, failed retry, or any meaningful state transition. The state file must always reflect the current reality. 2. **Update** after every change — this includes: batch completion, sub-step progress, step completion, session boundary, failed retry, or any meaningful state transition. The state file must always reflect the current reality.
3. **Read** as the first action on every invocation — before folder scanning 3. **Read** as the first action on every invocation — before folder scanning
4. **Cross-check**: verify against actual `_docs/` folder contents. If they disagree, trust the folder structure and update the state file 4. **Cross-check**: verify against actual `_docs/` folder contents. If they disagree, trust the folder structure and update the state file. **Parent suite `docs/`**: on every invocation, also probe `<workspace-root>/../docs` (the parent directorys `docs` folder — typical suite-level shared documentation next to a component repo). If it exists, mention it in the Status Summary footer per `protocols.md`; use it only as supplemental reading context unless a flow step explicitly ties detection to it. It never replaces workspace `_docs/` for step detection by default.
5. **Never delete** the state file 5. **Never delete** the state file
6. **Retry tracking**: increment `retry_count` on each failed auto-retry; reset to `0` on success. If `retry_count` reaches 3, set `status: failed` 6. **Retry tracking**: increment `retry_count` on each failed auto-retry; reset to `0` on success. If `retry_count` reaches 3, set `status: failed`
7. **Failed state on re-entry**: if `status: failed` with `retry_count: 3`, do NOT auto-retry — present the issue to the user first 7. **Failed state on re-entry**: if `status: failed` with `retry_count: 3`, do NOT auto-retry — present the issue to the user first
+27 -3
View File
@@ -64,6 +64,27 @@ TASKS_DIR/
└── done/ ← completed tasks (moved here after implementation) └── done/ ← completed tasks (moved here after implementation)
``` ```
### Suite-level invocation context (meta-repo flow)
When invoked from `.cursor/skills/autodev/flows/meta-repo.md` Step 3.5 (or any caller that supplies the same context envelope), the skill receives:
```
suite_level: true
TASKS_DIR: <override> # e.g., _docs/tasks/ (vs. default _docs/02_tasks/)
module_layout_path: <override> # e.g., _docs/tasks/_suite_module_layout.md
```
When `suite_level: true` is present, the following gate adjustments apply — and ONLY these. All other steps (114, 16) execute unchanged:
1. **TASKS_DIR override** is honored throughout the skill (Step 1 Parse, Step 13 Archive, Step 15 input paths if it ran). Default `_docs/02_tasks/` is replaced by the supplied path.
2. **module_layout_path override** is read instead of the hardcoded `_docs/02_document/module-layout.md` in Step 4 (Assign File Ownership). The supplied file uses the same `Per-Component Mapping` schema. If both the override and the hardcoded path are missing, behavior is unchanged from default mode (STOP and instruct).
3. **Step 14.5 (Cumulative Code Review) — SKIPPED**. The meta-repo has no `_docs/02_document/architecture_compliance_baseline.md`; cross-task drift is captured by the next `monorepo-status` cycle instead.
4. **Step 15 (Product Implementation Completeness Gate) — SKIPPED**. The gate's hard inputs (`_docs/02_document/architecture.md`, `system-flows.md`, `components/*/description.md`) do not exist in the meta-repo artifact layout. Suite-level tasks are infrastructure / coordination work (renames, cross-repo edits, suite-root infra additions), not feature implementation; the equivalent completeness signal is the next `monorepo-status` drift report (which the meta-repo flow re-runs immediately after Step 3.5 returns).
5. **Final report filename**: `_docs/03_implementation/suite_implementation_report_{run_name}.md` (in addition to the existing feature/test/refactor variants). Batch reports follow `_docs/03_implementation/suite_batch_{NN}_report.md`.
6. **Tracker integration** (Step 5: In Progress, Step 12: In Testing) runs unchanged — suite-level tickets follow the same tracker rules as any other.
Without `suite_level: true`, none of these adjustments apply and the skill runs exactly as documented in default mode.
## Prerequisite Checks (BLOCKING) ## Prerequisite Checks (BLOCKING)
1. `TASKS_DIR/todo/` exists and contains at least one task file for the selected context — **STOP if missing** 1. `TASKS_DIR/todo/` exists and contains at least one task file for the selected context — **STOP if missing**
@@ -103,7 +124,7 @@ TASKS_DIR/
### 4. Assign File Ownership ### 4. Assign File Ownership
The authoritative file-ownership map is `_docs/02_document/module-layout.md` (produced by the decompose skill's Step 1.5). Task specs are purely behavioral — they do NOT carry file paths. Derive ownership from the layout, not from the task spec's prose. The authoritative file-ownership map is `_docs/02_document/module-layout.md` (produced by the decompose skill's Step 1.5), unless `suite_level: true` was supplied in the invocation context — in which case the `module_layout_path` override is read instead (see "Suite-level invocation context" above). Task specs are purely behavioral — they do NOT carry file paths. Derive ownership from the layout, not from the task spec's prose.
For each task in the batch: For each task in the batch:
- Read the task spec's **Component** field. - Read the task spec's **Component** field.
@@ -222,6 +243,8 @@ For product implementation, this archive means "batch implementation accepted."
### 14.5. Cumulative Code Review (every K batches) ### 14.5. Cumulative Code Review (every K batches)
**Skipped entirely when `suite_level: true`** (see "Suite-level invocation context" above) — the meta-repo has no `architecture_compliance_baseline.md` to evaluate against; cross-task drift is captured by the next `monorepo-status` cycle.
- **Trigger**: every K completed batches (default `K = 3`; configurable per run via a `cumulative_review_interval` knob in the invocation context) - **Trigger**: every K completed batches (default `K = 3`; configurable per run via a `cumulative_review_interval` knob in the invocation context)
- **Purpose**: per-batch review (Step 9) catches batch-local issues; cumulative review catches issues that only appear when tasks are combined — architecture drift, cross-task inconsistency, duplicate symbols introduced across different batches, contracts that drifted across producer/consumer batches - **Purpose**: per-batch review (Step 9) catches batch-local issues; cumulative review catches issues that only appear when tasks are combined — architecture drift, cross-task inconsistency, duplicate symbols introduced across different batches, contracts that drifted across producer/consumer batches
- **Scope**: the union of files changed since the **last** cumulative review (or since the start of the run if this is the first) - **Scope**: the union of files changed since the **last** cumulative review (or since the start of the run if this is the first)
@@ -239,7 +262,7 @@ For product implementation, this archive means "batch implementation accepted."
### 15. Product Implementation Completeness Gate ### 15. Product Implementation Completeness Gate
Run this gate after all **product implementation** tasks are complete and before writing any final product implementation report or allowing autodev to proceed to testability/test decomposition. Skip this gate only when the remaining context is explicitly test implementation or refactoring, as determined by the task files and report filename rules. Run this gate after all **product implementation** tasks are complete and before writing any final product implementation report or allowing autodev to proceed to testability/test decomposition. Skip this gate when (a) the remaining context is explicitly test implementation or refactoring (as determined by the task files and report filename rules), OR (b) `suite_level: true` was supplied in the invocation context (the gate's inputs do not exist in the meta-repo artifact layout — see "Suite-level invocation context" above).
**Goal**: catch the failure mode where narrow tests validate scaffold behavior while the task's actual outcome, included scope, architecture promise, or named integration remains unimplemented. **Goal**: catch the failure mode where narrow tests validate scaffold behavior while the task's actual outcome, included scope, architecture promise, or named integration remains unimplemented.
@@ -309,8 +332,9 @@ After each batch completes, save the batch report to `_docs/03_implementation/ba
- **Test implementation** (tasks from test decomposition): `_docs/03_implementation/implementation_report_tests.md` - **Test implementation** (tasks from test decomposition): `_docs/03_implementation/implementation_report_tests.md`
- **Feature implementation**: `_docs/03_implementation/implementation_report_{feature_slug}_cycle{N}.md` where `{feature_slug}` is derived from the batch task names (e.g., `implementation_report_core_api_cycle2.md`) and `{N}` is the current `state.cycle` from `_docs/_autodev_state.md`. If `state.cycle` is absent (pre-migration), default to `cycle1`. - **Feature implementation**: `_docs/03_implementation/implementation_report_{feature_slug}_cycle{N}.md` where `{feature_slug}` is derived from the batch task names (e.g., `implementation_report_core_api_cycle2.md`) and `{N}` is the current `state.cycle` from `_docs/_autodev_state.md`. If `state.cycle` is absent (pre-migration), default to `cycle1`.
- **Refactoring**: `_docs/03_implementation/implementation_report_refactor_{run_name}.md` - **Refactoring**: `_docs/03_implementation/implementation_report_refactor_{run_name}.md`
- **Suite-level** (when `suite_level: true` was supplied — see "Suite-level invocation context" above): `_docs/03_implementation/suite_implementation_report_{run_name}.md`. Batch reports use `_docs/03_implementation/suite_batch_{NN}_report.md`. `{run_name}` is derived from the batch task IDs (e.g., `suite_implementation_report_az543_az549_az550.md`).
Determine the context from the task files being implemented: if all tasks have test-related names or belong to a test epic, use the tests filename; otherwise derive the feature slug from the component names and append the cycle suffix. Determine the context from the task files being implemented: if all tasks have test-related names or belong to a test epic, use the tests filename; if `suite_level: true` was supplied, use the suite filename; otherwise derive the feature slug from the component names and append the cycle suffix.
Batch report filenames must also include the cycle counter when running feature implementation: `_docs/03_implementation/batch_{NN}_cycle{N}_report.md` (test and refactor runs may use the plain `batch_{NN}_report.md` form since they are not cycle-scoped). Batch report filenames must also include the cycle counter when running feature implementation: `_docs/03_implementation/batch_{NN}_cycle{N}_report.md` (test and refactor runs may use the plain `batch_{NN}_report.md` form since they are not cycle-scoped).
+9 -2
View File
@@ -30,9 +30,16 @@ steps:
echo "ERROR: fixtures/image_small.jpg is missing; cannot warm up Jetson engine" echo "ERROR: fixtures/image_small.jpg is missing; cannot warm up Jetson engine"
exit 1 exit 1
fi fi
ls -lh fixtures/image_small.jpg if [ ! -f fixtures/video_test01.mp4 ]; then
echo "ERROR: fixtures/video_test01.mp4 is missing; cannot run Jetson video smoke test"
exit 1
fi
ls -lh fixtures/image_small.jpg fixtures/video_test01.mp4
bash scripts/pull_jetson_engine.sh bash scripts/pull_jetson_engine.sh
E2E_PROFILE=jetson bash run_test.sh tests/test_health_engine.py::TestHealthEngineStep03Warmed -rs E2E_PROFILE=jetson bash run_test.sh \
tests/test_health_engine.py::TestHealthEngineStep03Warmed \
tests/test_video.py::test_ft_p_10_frame_sampling_ac1 \
-rs
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
+1 -1
View File
@@ -1,4 +1,4 @@
FROM nvcr.io/nvidia/l4t-jetpack:r36.2.0 FROM nvcr.io/nvidia/l4t-jetpack:r36.4.0
ARG CI_COMMIT_SHA=unknown ARG CI_COMMIT_SHA=unknown
ENV AZAION_REVISION=$CI_COMMIT_SHA ENV AZAION_REVISION=$CI_COMMIT_SHA
+1 -1
View File
@@ -17,7 +17,7 @@
- Images ≤ 1.5× model dimensions (1280×1280): processed as single frame. - Images ≤ 1.5× model dimensions (1280×1280): processed as single frame.
- Larger images: tiled based on ground sampling distance. Tile physical size: 25 meters (METERS_IN_TILE). Tile overlap: `big_image_tile_overlap_percent` (default: 20%). - Larger images: tiled based on ground sampling distance. Tile physical size: 25 meters (METERS_IN_TILE). Tile overlap: `big_image_tile_overlap_percent` (default: 20%).
- GSD calculation: `sensor_width * altitude / (focal_length * image_width)` when `altitude` is provided. - GSD calculation: `sensor_width * current_height / (focal_length * current_zoom * image_width * sin(current_angle))` when `camera_config.current_height` and valid camera parameters are provided. `current_angle` is in degrees and defaults to 90.
## API ## API
+11 -1
View File
@@ -36,9 +36,19 @@ Media path is resolved from the Annotations service via `GET /api/media/{media_i
| tracking_intersection_threshold | float | 0.6 | Overlap ratio for NMS deduplication | | tracking_intersection_threshold | float | 0.6 | Overlap ratio for NMS deduplication |
| model_batch_size | int | 8 | Inference batch size | | model_batch_size | int | 8 | Inference batch size |
| big_image_tile_overlap_percent | int | 20 | Tile overlap for large images (0-100%) | | big_image_tile_overlap_percent | int | 20 | Tile overlap for large images (0-100%) |
| altitude | float | optional | Camera altitude in meters. When omitted, GSD-based size filtering and image tiling are skipped. | | camera_config | object | null | Camera parameters for GSD. When omitted or missing height, GSD-based size filtering and image tiling are skipped. |
### camera_config
| Field | Type | Default | Range/Meaning |
|-------|------|---------|---------------|
| focal_length | float | 24 | Camera focal length in mm | | focal_length | float | 24 | Camera focal length in mm |
| sensor_width | float | 23.5 | Camera sensor width in mm | | sensor_width | float | 23.5 | Camera sensor width in mm |
| current_zoom | float | 1 | Optical zoom multiplier; effective focal length is `focal_length * current_zoom` |
| current_angle | float | 90 | Camera angle in degrees; 90 is nadir/downward |
| current_height | float | optional | Camera height in meters |
Legacy flat `altitude`, `focal_length`, and `sensor_width` keys are still accepted for backward compatibility, but new clients should send `camera_config`.
`paths` field was removed in AZ-174 — media paths are now resolved via the Annotations service. `paths` field was removed in AZ-174 — media paths are now resolved via the Annotations service.
+1 -1
View File
@@ -32,7 +32,7 @@ graph LR
| Cython inference pipeline | Python 3, Cython 3.1.3, OpenCV 4.10 | Near-C performance for tight detection loops while retaining Python ecosystem | Build complexity, limited IDE/debug support | Compilation step via setup.py | N/A | Low (open-source) | High — critical for postprocessing throughput | | Cython inference pipeline | Python 3, Cython 3.1.3, OpenCV 4.10 | Near-C performance for tight detection loops while retaining Python ecosystem | Build complexity, limited IDE/debug support | Compilation step via setup.py | N/A | Low (open-source) | High — critical for postprocessing throughput |
| Dual engine strategy (TensorRT + ONNX) | TensorRT 10.11, ONNX Runtime 1.22 | Maximum GPU speed with CPU fallback; auto-conversion and caching | Two code paths; GPU-specific engine files not portable | NVIDIA GPU (CC ≥ 6.1) for TensorRT | N/A | TensorRT free for NVIDIA GPUs | High — balances performance and portability | | Dual engine strategy (TensorRT + ONNX) | TensorRT 10.11, ONNX Runtime 1.22 | Maximum GPU speed with CPU fallback; auto-conversion and caching | Two code paths; GPU-specific engine files not portable | NVIDIA GPU (CC ≥ 6.1) for TensorRT | N/A | TensorRT free for NVIDIA GPUs | High — balances performance and portability |
| FastAPI HTTP service | FastAPI, Uvicorn, Pydantic | Async SSE, auto-generated docs, fast development | Sync inference offloaded to ThreadPoolExecutor (2 workers) | Python 3.8+ | Bearer token pass-through | Low (open-source) | High — fits async streaming + sync inference pattern | | FastAPI HTTP service | FastAPI, Uvicorn, Pydantic | Async SSE, auto-generated docs, fast development | Sync inference offloaded to ThreadPoolExecutor (2 workers) | Python 3.8+ | Bearer token pass-through | Low (open-source) | High — fits async streaming + sync inference pattern |
| GSD-based image tiling | OpenCV, NumPy | Preserves small object detail in large aerial images | Complex tile dedup logic; overlap increases compute | Camera metadata (altitude, focal length, sensor width) | N/A | Compute cost scales with image size | High — essential for aerial imagery use case | | GSD-based image tiling | OpenCV, NumPy | Preserves small object detail in large aerial images | Complex tile dedup logic; overlap increases compute | Camera metadata (`camera_config`: height, angle, zoom, focal length, sensor width) | N/A | Compute cost scales with image size | High — essential for aerial imagery use case |
| Lazy engine initialization | pynvml, threading | Fast API startup; background model conversion | First request has high latency; engine may be unavailable | None | N/A | N/A | High — prevents blocking startup on slow model download/conversion | | Lazy engine initialization | pynvml, threading | Fast API startup; background model conversion | First request has high latency; engine may be unavailable | None | N/A | N/A | High — prevents blocking startup on slow model download/conversion |
## 3. Testing Strategy ## 3. Testing Strategy
@@ -109,7 +109,7 @@ None — internal component, consumed by API layer.
### Large Image Tiling ### Large Image Tiling
- Ground Sampling Distance: `sensor_width * altitude / (focal_length * image_width)` - Ground Sampling Distance: `sensor_width * current_height / (focal_length * current_zoom * image_width * sin(current_angle))`
- Tile size: `METERS_IN_TILE / GSD` pixels - Tile size: `METERS_IN_TILE / GSD` pixels
- Overlap: configurable percentage - Overlap: configurable percentage
- Tile deduplication: absolute-coordinate Detection equality across adjacent tiles - Tile deduplication: absolute-coordinate Detection equality across adjacent tiles
+8 -4
View File
@@ -37,9 +37,13 @@ erDiagram
double tracking_intersection_threshold double tracking_intersection_threshold
int big_image_tile_overlap_percent int big_image_tile_overlap_percent
int model_batch_size int model_batch_size
double altitude bool has_camera_config
double current_height
double current_zoom
double current_angle
double focal_length double focal_length
double sensor_width double sensor_width
double altitude
} }
AIAvailabilityStatus { AIAvailabilityStatus {
@@ -107,7 +111,7 @@ Groups detections for a single frame or image tile.
### AIRecognitionConfig ### AIRecognitionConfig
Runtime configuration for inference behavior. Created from dict (API) or msgpack (internal). Runtime configuration for inference behavior. Created from dict (API). Camera values are grouped under `camera_config` at the API boundary and expanded into `current_height`, `current_zoom`, `current_angle`, `focal_length`, and `sensor_width` internally. `altitude` remains as a legacy alias for `current_height`.
### AIAvailabilityStatus ### AIAvailabilityStatus
@@ -125,7 +129,7 @@ SSE event payload. Status values: AIProcessing, AIProcessed, Error.
### AIConfigDto ### AIConfigDto
API input configuration. Same fields as AIRecognitionConfig with defaults. API input configuration. Same inference fields as `AIRecognitionConfig` with defaults, plus nested `camera_config` for GSD and physical-size filtering.
### HealthResponse ### HealthResponse
@@ -144,7 +148,7 @@ Annotation names encode media source and processing context:
| Entity | Format | Usage | | Entity | Format | Usage |
|--------|--------|-------| |--------|--------|-------|
| Detection/Annotation | msgpack (compact keys) | `annotation.serialize()` | | Detection/Annotation | msgpack (compact keys) | `annotation.serialize()` |
| AIRecognitionConfig | msgpack (compact keys) | `from_msgpack()` | | AIRecognitionConfig | Python dict | `AIRecognitionConfig.from_dict()` |
| AIAvailabilityStatus | msgpack | `serialize()` | | AIAvailabilityStatus | msgpack | `serialize()` |
| DetectionDto/Event | JSON (Pydantic) | HTTP API responses, SSE | | DetectionDto/Event | JSON (Pydantic) | HTTP API responses, SSE |
+7 -3
View File
@@ -20,9 +20,13 @@ Data class holding all AI recognition configuration parameters, with factory met
| `tracking_intersection_threshold` | double | 0.6 | IoU threshold for overlapping detection removal | | `tracking_intersection_threshold` | double | 0.6 | IoU threshold for overlapping detection removal |
| `model_batch_size` | int | 1 | Batch size for inference | | `model_batch_size` | int | 1 | Batch size for inference |
| `big_image_tile_overlap_percent` | int | 20 | Tile overlap percentage for large image splitting | | `big_image_tile_overlap_percent` | int | 20 | Tile overlap percentage for large image splitting |
| `altitude` | double? | optional | Camera altitude in meters. When missing, GSD-based filtering is disabled | | `has_camera_config` | bool | false | Whether camera parameters were supplied |
| `current_height` | double | 0.0 | Camera height in meters, from `camera_config.current_height` |
| `current_zoom` | double | 1.0 | Camera zoom multiplier |
| `current_angle` | double | 90.0 | Camera angle in degrees; 90 is nadir/downward |
| `focal_length` | double | 24 | Camera focal length in mm | | `focal_length` | double | 24 | Camera focal length in mm |
| `sensor_width` | double | 23.5 | Camera sensor width in mm | | `sensor_width` | double | 23.5 | Camera sensor width in mm |
| `altitude` / `has_altitude` | double / bool | legacy | Backward-compatible aliases for older flat camera config |
#### Methods #### Methods
@@ -32,7 +36,7 @@ Data class holding all AI recognition configuration parameters, with factory met
## Internal Logic ## Internal Logic
`from_dict` applies defaults for missing keys using full descriptive key names. `from_dict` applies defaults for missing keys using full descriptive key names. Camera parameters are read from nested `camera_config` first; legacy flat `altitude`, `focal_length`, and `sensor_width` keys remain supported for older clients.
**Removed**: `paths` field and `file_data` field were removed as part of the distributed architecture shift (AZ-174). Media paths are now resolved via the Annotations service API, not passed in config. `from_msgpack()` was also removed as it was unused. **Removed**: `paths` field and `file_data` field were removed as part of the distributed architecture shift (AZ-174). Media paths are now resolved via the Annotations service API, not passed in config. `from_msgpack()` was also removed as it was unused.
@@ -51,7 +55,7 @@ Data class holding all AI recognition configuration parameters, with factory met
## Configuration ## Configuration
Camera/altitude parameters (`altitude`, `focal_length`, `sensor_width`) are used for ground sampling distance calculation in aerial image processing. If `altitude` is missing, the service skips GSD-based size filtering and does not tile large images by physical size. Camera parameters (`camera_config.focal_length`, `camera_config.sensor_width`, `camera_config.current_zoom`, `camera_config.current_angle`, `camera_config.current_height`) are used for ground sampling distance calculation in aerial image processing. If `camera_config` is missing or height/optics are invalid, the service skips GSD-based size filtering and does not tile large images by physical size.
## External Integrations ## External Integrations
+1 -1
View File
@@ -90,7 +90,7 @@ Both `run_detect_image` and `run_detect_video` accept raw bytes instead of file
### Ground Sampling Distance (GSD) ### Ground Sampling Distance (GSD)
`GSD = sensor_width * altitude / (focal_length * image_width)` — meters per pixel, used for physical size filtering of aerial detections. `GSD = sensor_width * current_height / (focal_length * current_zoom * image_width * sin(current_angle))` — meters per pixel, used for physical size filtering of aerial detections. `current_angle` is configured in degrees and defaults to 90.
## Dependencies ## Dependencies
+3 -2
View File
@@ -23,7 +23,8 @@ FastAPI application entry point — exposes HTTP API for object detection on ima
| `DetectionDto` | centerX, centerY, width, height, classNum, label, confidence | Single detection result | | `DetectionDto` | centerX, centerY, width, height, classNum, label, confidence | Single detection result |
| `DetectionEvent` | annotations (list[DetectionDto]), mediaId, mediaStatus, mediaPercent | SSE event payload | | `DetectionEvent` | annotations (list[DetectionDto]), mediaId, mediaStatus, mediaPercent | SSE event payload |
| `HealthResponse` | status, aiAvailability, engineType, errorMessage | Health check response | | `HealthResponse` | status, aiAvailability, engineType, errorMessage | Health check response |
| `AIConfigDto` | frame_period_recognition, frame_recognition_seconds, probability_threshold, tracking_*, model_batch_size, big_image_tile_overlap_percent, altitude, focal_length, sensor_width | Configuration input (no `paths` field — removed in AZ-174) | | `CameraConfigDto` | focal_length, sensor_width, current_zoom, current_angle, current_height | Camera input used for GSD and physical-size filtering |
| `AIConfigDto` | frame_period_recognition, frame_recognition_seconds, probability_threshold, tracking_*, model_batch_size, big_image_tile_overlap_percent, camera_config | Configuration input (no `paths` field — removed in AZ-174) |
### Class: TokenManager ### Class: TokenManager
@@ -37,7 +38,7 @@ FastAPI application entry point — exposes HTTP API for object detection on ima
| Function | Signature | Description | | Function | Signature | Description |
|----------|-----------|-------------| |----------|-----------|-------------|
| `_merged_annotation_settings_payload` | `(raw: object) -> dict` | Merges nested AI settings from Annotations service response (handles `aiRecognitionSettings`, `cameraSettings` sub-objects and PascalCase/camelCase/snake_case aliases) | | `_merged_annotation_settings_payload` | `(raw: object) -> dict` | Merges nested AI settings from Annotations service response (handles `aiRecognitionSettings`, `camera_config`/`cameraSettings` sub-objects and PascalCase/camelCase/snake_case aliases) |
| `_resolve_media_for_detect` | `(media_id, token_mgr, override) -> tuple[dict, str]` | Fetches user AI settings + media path from Annotations service, merges with client overrides | | `_resolve_media_for_detect` | `(media_id, token_mgr, override) -> tuple[dict, str]` | Fetches user AI settings + media path from Annotations service, merges with client overrides |
| `_detect_upload_kind` | `(filename, data) -> tuple[str, str]` | Determines if upload is image or video by extension, falls back to content probing (cv2/PyAV) | | `_detect_upload_kind` | `(filename, data) -> tuple[str, str]` | Determines if upload is image or video by extension, falls back to content probing (cv2/PyAV) |
| `_post_media_record` | `(payload, bearer) -> bool` | Creates media record via `POST /api/media` on Annotations service | | `_post_media_record` | `(payload, bearer) -> bool` | Creates media record via `POST /api/media` on Annotations service |
+3 -3
View File
@@ -83,7 +83,7 @@
**Preconditions**: **Preconditions**:
- Engine is initialized - Engine is initialized
- Config includes altitude, focal_length, sensor_width for GSD calculation - Config includes `camera_config` with `current_height`, `focal_length`, `sensor_width`, `current_zoom`, and `current_angle` for GSD calculation
**Input data**: large-image (4000×3000) **Input data**: large-image (4000×3000)
@@ -91,7 +91,7 @@
| Step | Consumer Action | Expected System Response | | Step | Consumer Action | Expected System Response |
|------|----------------|------------------------| |------|----------------|------------------------|
| 1 | `POST /detect` with large-image and config `{"altitude": 400, "focal_length": 24, "sensor_width": 23.5}` | 200 OK | | 1 | `POST /detect` with large-image and config `{"camera_config":{"current_height":400,"focal_length":24,"sensor_width":23.5,"current_zoom":1,"current_angle":90}}` | 200 OK |
| 2 | Parse response JSON | Array of detections | | 2 | Parse response JSON | Array of detections |
| 3 | Verify detection coordinates | Bounding box coordinates are in 0.01.0 range relative to the full original image | | 3 | Verify detection coordinates | Bounding box coordinates are in 0.01.0 range relative to the full original image |
@@ -167,7 +167,7 @@
| Step | Consumer Action | Expected System Response | | Step | Consumer Action | Expected System Response |
|------|----------------|------------------------| |------|----------------|------------------------|
| 1 | `POST /detect` with small-image and config `{"altitude": 400, "focal_length": 24, "sensor_width": 23.5}` | 200 OK | | 1 | `POST /detect` with small-image and config `{"camera_config":{"current_height":400,"focal_length":24,"sensor_width":23.5,"current_zoom":1,"current_angle":90}}` | 200 OK |
| 2 | For each detection, compute physical size from bounding box + GSD | No detection's physical size exceeds the MaxSizeM defined for its class in classes.json | | 2 | For each detection, compute physical size from bounding box + GSD | No detection's physical size exceeds the MaxSizeM defined for its class in classes.json |
**Expected outcome**: All returned detections have plausible physical dimensions for their class. **Expected outcome**: All returned detections have plausible physical dimensions for their class.
@@ -2,7 +2,7 @@
**Task**: AZ-180_jetson_orin_nano_support **Task**: AZ-180_jetson_orin_nano_support
**Name**: Jetson Orin Nano Support **Name**: Jetson Orin Nano Support
**Description**: Run the detection service on NVIDIA Jetson Orin Nano with a JetPack 6.x container image, INT8 engine conversion using a pre-generated calibration cache, and docker-compose configuration. **Description**: Run the detection service on NVIDIA Jetson Orin Nano with a JetPack 6.2.x-compatible container image, INT8 engine conversion using a pre-generated calibration cache, and docker-compose configuration.
**Complexity**: 5 points **Complexity**: 5 points
**Dependencies**: None **Dependencies**: None
**Component**: Deployment + Inference Engine **Component**: Deployment + Inference Engine
@@ -18,7 +18,7 @@ The detection service cannot run on NVIDIA Jetson Orin Nano for two reasons:
## Outcome ## Outcome
- A `Dockerfile.jetson` that builds and runs on Jetson Orin Nano (aarch64, JetPack 6.x) - A `Dockerfile.jetson` that builds and runs on Jetson Orin Nano (aarch64, JetPack 6.x)
- A `requirements-jetson.txt` that installs Python dependencies without pip-installing tensorrt or pycuda - A `requirements-jetson.txt` that installs Python dependencies without pip-installing tensorrt
- A `docker-compose.jetson.yml` with NVIDIA Container Runtime configuration - A `docker-compose.jetson.yml` with NVIDIA Container Runtime configuration
- `convert_from_source()` in `tensorrt_engine.pyx` extended to accept an optional INT8 calibration cache path — if the cache is present, INT8 is used; otherwise FP16 fallback - `convert_from_source()` in `tensorrt_engine.pyx` extended to accept an optional INT8 calibration cache path — if the cache is present, INT8 is used; otherwise FP16 fallback
- `init_ai()` in `inference.pyx` extended to try downloading the calibration cache from the Loader service before starting the conversion thread - `init_ai()` in `inference.pyx` extended to try downloading the calibration cache from the Loader service before starting the conversion thread
@@ -28,7 +28,7 @@ The detection service cannot run on NVIDIA Jetson Orin Nano for two reasons:
### Included ### Included
- `Dockerfile.jetson` using a JetPack 6.x L4T base image with pre-installed TensorRT and PyCUDA - `Dockerfile.jetson` using a JetPack 6.x L4T base image with pre-installed TensorRT and PyCUDA
- `requirements-jetson.txt` derived from `requirements.txt`, excluding tensorrt and pycuda - `requirements-jetson.txt` derived from `requirements.txt`, excluding tensorrt and installing PyCUDA via pip where the JetPack apt package is unavailable
- `docker-compose.jetson.yml` with `runtime: nvidia` - `docker-compose.jetson.yml` with `runtime: nvidia`
- `tensorrt_engine.pyx`: extend `convert_from_source(bytes onnx_model, str calib_cache_path=None)` — set `INT8` flag and load cache when path is provided; fall back to FP16 when not - `tensorrt_engine.pyx`: extend `convert_from_source(bytes onnx_model, str calib_cache_path=None)` — set `INT8` flag and load cache when path is provided; fall back to FP16 when not
- `inference.pyx`: extend `init_ai()` to attempt download of `azaion.int8_calib.cache` from Loader before spawning the conversion thread; pass the local path to `convert_from_source()` - `inference.pyx`: extend `init_ai()` to attempt download of `azaion.int8_calib.cache` from Loader before spawning the conversion thread; pass the local path to `convert_from_source()`
@@ -75,7 +75,7 @@ Then the detections service is reachable on port 8080
## Non-Functional Requirements ## Non-Functional Requirements
**Compatibility** **Compatibility**
- JetPack 6.x (CUDA 12.2, TensorRT 10.x) - JetPack 6.2.x-compatible container (`nvcr.io/nvidia/l4t-jetpack:r36.4.0`, CUDA 12.6 / TensorRT 10.3 compute stack)
- Jetson Orin Nano (aarch64, SM 8.7) - Jetson Orin Nano (aarch64, SM 8.7)
**Reliability** **Reliability**
@@ -101,7 +101,7 @@ Note: AC-2, AC-5, AC-6 require physical Jetson hardware and cannot run in standa
## Constraints ## Constraints
- TensorRT and PyCUDA must NOT be pip-installed — provided by JetPack in the base image - TensorRT must NOT be pip-installed — provided by JetPack in the base image. PyCUDA may be pip-installed on `l4t-jetpack:r36.4.0` because `python3-pycuda` is unavailable in the apt repositories.
- Base image must be a JetPack 6.x L4T image — not a generic CUDA image - Base image must be a JetPack 6.x L4T image — not a generic CUDA image
- Calibration cache download failure must be non-fatal — log a warning and fall back to FP16 - Calibration cache download failure must be non-fatal — log a warning and fall back to FP16
- INT8 conversion and FP16 conversion produce different engine files (different filenames) so cached engines are not confused - INT8 conversion and FP16 conversion produce different engine files (different filenames) so cached engines are not confused
@@ -114,7 +114,7 @@ Note: AC-2, AC-5, AC-6 require physical Jetson hardware and cannot run in standa
**Risk 2: PyCUDA availability in base image** **Risk 2: PyCUDA availability in base image**
- *Risk*: Some L4T images do not include pycuda - *Risk*: Some L4T images do not include pycuda
- *Mitigation*: Fall back to `apt-get install python3-pycuda` or source build with `CUDA_ROOT` set - *Mitigation*: Fall back to pip source build with `CUDA_ROOT` set when no `python3-pycuda` apt package is available
**Risk 3: INT8 accuracy degradation** **Risk 3: INT8 accuracy degradation**
- *Risk*: Without a well-representative calibration dataset, mAP may drop >1 point - *Risk*: Without a well-representative calibration dataset, mAP may drop >1 point
+3 -3
View File
@@ -119,9 +119,9 @@ Already exists: `e2e/docker-compose.test.yml`. No changes needed — supports bo
| Aspect | Specification | | Aspect | Specification |
|--------|--------------| |--------|--------------|
| Base image | `nvcr.io/nvidia/l4t-base:r36.3.0` (JetPack 6.x, aarch64) | | Base image | `nvcr.io/nvidia/l4t-jetpack:r36.4.0` (JetPack 6.2.x-compatible, aarch64) |
| TensorRT | Pre-installed via JetPack — `python3-libnvinfer` apt package (NOT pip) | | TensorRT | Pre-installed via JetPack — `python3-libnvinfer` apt package (NOT pip) |
| PyCUDA | Pre-installed via JetPack — `python3-pycuda` apt package (NOT pip) | | PyCUDA | Installed via pip in `requirements-jetson.txt` because `python3-pycuda` is not available in the `l4t-jetpack:r36.4.0` apt repositories |
| Build stages | Single stage (Cython compile requires gcc) | | Build stages | Single stage (Cython compile requires gcc) |
| Non-root user | `adduser --disabled-password --gecos '' appuser` + `USER appuser` | | Non-root user | `adduser --disabled-password --gecos '' appuser` + `USER appuser` |
| Exposed ports | 8080 | | Exposed ports | 8080 |
@@ -129,7 +129,7 @@ Already exists: `e2e/docker-compose.test.yml`. No changes needed — supports bo
| Runtime | Requires NVIDIA Container Runtime (`runtime: nvidia` in docker-compose) | | Runtime | Requires NVIDIA Container Runtime (`runtime: nvidia` in docker-compose) |
**Jetson-specific behaviour**: **Jetson-specific behaviour**:
- `requirements-jetson.txt` derives from `requirements.txt``tensorrt` and `pycuda` are excluded from pip and provided by JetPack - `requirements-jetson.txt` derives from `requirements.txt``tensorrt` is excluded from pip and installed from the JetPack/L4T apt packages in `Dockerfile.jetson`; PyCUDA is installed via pip on this image line because the apt package is unavailable
- Engine filename auto-encodes CC+SM (e.g. `azaion.cc_8.7_sm_16.engine` for Orin Nano), ensuring the Jetson engine is distinct from any x86-cached engine - Engine filename auto-encodes CC+SM (e.g. `azaion.cc_8.7_sm_16.engine` for Orin Nano), ensuring the Jetson engine is distinct from any x86-cached engine
- INT8 is used when `azaion.int8_calib.cache` is available on the Loader service; precision suffix appended to engine filename (`*.int8.engine`); FP16 fallback when cache is absent - INT8 is used when `azaion.int8_calib.cache` is available on the Loader service; precision suffix appended to engine filename (`*.int8.engine`); FP16 fallback when cache is absent
- `docker-compose.jetson.yml` uses `runtime: nvidia` for the NVIDIA Container Runtime - `docker-compose.jetson.yml` uses `runtime: nvidia` for the NVIDIA Container Runtime
+4 -3
View File
@@ -249,7 +249,7 @@ def _health(http_client):
def _health_ai_active(data: dict) -> bool: def _health_ai_active(data: dict) -> bool:
return data.get("aiAvailability") not in ("None", "Downloading", "Error") return data.get("aiAvailability") == "Enabled"
def _wait_for_ai_active(http_client, timeout: float = 30) -> dict | None: def _wait_for_ai_active(http_client, timeout: float = 30) -> dict | None:
@@ -324,7 +324,8 @@ def corrupt_image():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def warm_engine(http_client, image_small, auth_headers): def warm_engine(http_client, image_small, auth_headers):
deadline = time.time() + 120 timeout = int(os.environ.get("E2E_ENGINE_WAIT_TIMEOUT", "900"))
deadline = time.time() + timeout
last_status = None last_status = None
consecutive_errors = 0 consecutive_errors = 0
@@ -403,4 +404,4 @@ def warm_engine(http_client, image_small, auth_headers):
th.join(timeout=1) th.join(timeout=1)
time.sleep(2) time.sleep(2)
pytest.fail(f"engine warm-up timed out after 120s (last status: {last_status})") pytest.fail(f"engine warm-up timed out after {timeout}s (last status: {last_status})")
+524
View File
@@ -0,0 +1,524 @@
TRT-100400-EntropyCalibration2
images: 3c010a14
/model/model.0/conv/Conv_output_0: 3e1f8ae8
/model/model.0/act/Sigmoid_output_0: 3c010a14
/model/model.0/act/Mul_output_0: 3d90aeb4
/model/model.1/conv/Conv_output_0: 3e576a8c
/model/model.1/act/Sigmoid_output_0: 3c010a14
/model/model.1/act/Mul_output_0: 3d966075
/model/model.2/cv1/conv/Conv_output_0: 3e4b863d
/model/model.2/cv1/act/Sigmoid_output_0: 3c010a14
/model/model.2/cv1/act/Mul_output_0: 3d73e151
/model/model.2/Split_output_0: 3d6edf08
/model/model.2/Split_output_1: 3d73e151
/model/model.2/m.0/cv1/conv/Conv_output_0: 3dcab8c2
/model/model.2/m.0/cv2/conv/Conv_output_0: 3e169cf0
/model/model.2/m.0/cv1/act/Sigmoid_output_0: 3c0315c8
/model/model.2/m.0/cv2/act/Sigmoid_output_0: 3c010a14
/model/model.2/m.0/cv1/act/Mul_output_0: 3d462366
/model/model.2/m.0/cv2/act/Mul_output_0: 3d1d9bf9
/model/model.2/m.0/m/m.0/cv1/conv/Conv_output_0: 3e24ca61
/model/model.2/m.0/m/m.0/cv1/act/Sigmoid_output_0: 3c010a14
/model/model.2/m.0/m/m.0/cv1/act/Mul_output_0: 3d91e144
/model/model.2/m.0/m/m.0/cv2/conv/Conv_output_0: 3e292a8b
/model/model.2/m.0/m/m.0/cv2/act/Sigmoid_output_0: 3c010a14
/model/model.2/m.0/m/m.0/cv2/act/Mul_output_0: 3d92e80d
/model/model.2/m.0/m/m.0/Add_output_0: 3ddff1c4
/model/model.2/m.0/m/m.1/cv1/conv/Conv_output_0: 3dde776e
/model/model.2/m.0/m/m.1/cv1/act/Sigmoid_output_0: 3c0110bd
/model/model.2/m.0/m/m.1/cv1/act/Mul_output_0: 3ceb9f4b
/model/model.2/m.0/m/m.1/cv2/conv/Conv_output_0: 3de42a65
/model/model.2/m.0/m/m.1/cv2/act/Sigmoid_output_0: 3c010a14
/model/model.2/m.0/m/m.1/cv2/act/Mul_output_0: 3d134882
/model/model.2/m.0/m/m.1/Add_output_0: 3def94e5
/model/model.2/m.0/Concat_output_0: 3d88d903
/model/model.2/m.0/cv3/conv/Conv_output_0: 3e05527e
/model/model.2/m.0/cv3/act/Sigmoid_output_0: 3c010a14
/model/model.2/m.0/cv3/act/Mul_output_0: 3d6ed4b2
/model/model.2/Concat_output_0: 3d73e151
/model/model.2/cv2/conv/Conv_output_0: 3dfb04b8
/model/model.2/cv2/act/Sigmoid_output_0: 3c01121f
/model/model.2/cv2/act/Mul_output_0: 3cd1849d
/model/model.3/conv/Conv_output_0: 3daf0f26
/model/model.3/act/Sigmoid_output_0: 3c011202
/model/model.3/act/Mul_output_0: 3cc02203
/model/model.4/cv1/conv/Conv_output_0: 3d97c00c
/model/model.4/cv1/act/Sigmoid_output_0: 3c00f136
/model/model.4/cv1/act/Mul_output_0: 3d27ab2c
/model/model.4/Split_output_0: 3d025b5d
/model/model.4/Split_output_1: 3ceac523
/model/model.4/m.0/cv1/conv/Conv_output_0: 3d482f4e
/model/model.4/m.0/cv2/conv/Conv_output_0: 3d75efdd
/model/model.4/m.0/cv1/act/Sigmoid_output_0: 3c010688
/model/model.4/m.0/cv2/act/Sigmoid_output_0: 3c010f49
/model/model.4/m.0/cv1/act/Mul_output_0: 3d12c9be
/model/model.4/m.0/cv2/act/Mul_output_0: 3cd47803
/model/model.4/m.0/m/m.0/cv1/conv/Conv_output_0: 3d998103
/model/model.4/m.0/m/m.0/cv1/act/Sigmoid_output_0: 3c00d97b
/model/model.4/m.0/m/m.0/cv1/act/Mul_output_0: 3cb15d2a
/model/model.4/m.0/m/m.0/cv2/conv/Conv_output_0: 3d8ac915
/model/model.4/m.0/m/m.0/cv2/act/Sigmoid_output_0: 3c00b8b7
/model/model.4/m.0/m/m.0/cv2/act/Mul_output_0: 3ce9200f
/model/model.4/m.0/m/m.0/Add_output_0: 3d4f3893
/model/model.4/m.0/m/m.1/cv1/conv/Conv_output_0: 3d7c5d9d
/model/model.4/m.0/m/m.1/cv1/act/Sigmoid_output_0: 3c00bdcd
/model/model.4/m.0/m/m.1/cv1/act/Mul_output_0: 3c72a678
/model/model.4/m.0/m/m.1/cv2/conv/Conv_output_0: 3d9d361c
/model/model.4/m.0/m/m.1/cv2/act/Sigmoid_output_0: 3c010a06
/model/model.4/m.0/m/m.1/cv2/act/Mul_output_0: 3cff9796
/model/model.4/m.0/m/m.1/Add_output_0: 3db0a17c
/model/model.4/m.0/Concat_output_0: 3d439aad
/model/model.4/m.0/cv3/conv/Conv_output_0: 3d8fa6e7
/model/model.4/m.0/cv3/act/Sigmoid_output_0: 3c010a0a
/model/model.4/m.0/cv3/act/Mul_output_0: 3cd13d78
/model/model.4/Concat_output_0: 3cffeda3
/model/model.4/cv2/conv/Conv_output_0: 3d7b7215
/model/model.4/cv2/act/Sigmoid_output_0: 3c00f3c2
/model/model.4/cv2/act/Mul_output_0: 3c80b352
/model/model.5/conv/Conv_output_0: 3d7b4080
/model/model.5/act/Sigmoid_output_0: 3c00f9ba
/model/model.5/act/Mul_output_0: 3cb11f1f
/model/model.6/cv1/conv/Conv_output_0: 3d73ef03
/model/model.6/cv1/act/Sigmoid_output_0: 3c00e9c9
/model/model.6/cv1/act/Mul_output_0: 3cd4f9ea
/model/model.6/Split_output_0: 3cd3b02b
/model/model.6/Split_output_1: 3cd4f9ea
/model/model.6/m.0/cv1/conv/Conv_output_0: 3d1a2aec
/model/model.6/m.0/cv2/conv/Conv_output_0: 3d54c4f6
/model/model.6/m.0/cv1/act/Sigmoid_output_0: 3c03c14c
/model/model.6/m.0/cv2/act/Sigmoid_output_0: 3c010985
/model/model.6/m.0/cv1/act/Mul_output_0: 3cc06408
/model/model.6/m.0/cv2/act/Mul_output_0: 3ca4399f
/model/model.6/m.0/m/m.0/cv1/conv/Conv_output_0: 3d5c460e
/model/model.6/m.0/m/m.0/cv1/act/Sigmoid_output_0: 3c00b175
/model/model.6/m.0/m/m.0/cv1/act/Mul_output_0: 3c901a66
/model/model.6/m.0/m/m.0/cv2/conv/Conv_output_0: 3d6a8c7b
/model/model.6/m.0/m/m.0/cv2/act/Sigmoid_output_0: 3c0140d7
/model/model.6/m.0/m/m.0/cv2/act/Mul_output_0: 3cb17b0b
/model/model.6/m.0/m/m.0/Add_output_0: 3d3cc7b4
/model/model.6/m.0/m/m.1/cv1/conv/Conv_output_0: 3d501102
/model/model.6/m.0/m/m.1/cv1/act/Sigmoid_output_0: 3c0369e8
/model/model.6/m.0/m/m.1/cv1/act/Mul_output_0: 3c907699
/model/model.6/m.0/m/m.1/cv2/conv/Conv_output_0: 3d9ff611
/model/model.6/m.0/m/m.1/cv2/act/Sigmoid_output_0: 3c030a71
/model/model.6/m.0/m/m.1/cv2/act/Mul_output_0: 3ce605a6
/model/model.6/m.0/m/m.1/Add_output_0: 3d8ee5d9
/model/model.6/m.0/Concat_output_0: 3d24399f
/model/model.6/m.0/cv3/conv/Conv_output_0: 3d89743b
/model/model.6/m.0/cv3/act/Sigmoid_output_0: 3c00e89e
/model/model.6/m.0/cv3/act/Mul_output_0: 3cd24615
/model/model.6/Concat_output_0: 3cd4f9ea
/model/model.6/cv2/conv/Conv_output_0: 3d7763db
/model/model.6/cv2/act/Sigmoid_output_0: 3c008f22
/model/model.6/cv2/act/Mul_output_0: 3c9aee92
/model/model.7/conv/Conv_output_0: 3d89d8e3
/model/model.7/act/Sigmoid_output_0: 3c00fd32
/model/model.7/act/Mul_output_0: 3ce7105b
/model/model.8/cv1/conv/Conv_output_0: 3d74814b
/model/model.8/cv1/act/Sigmoid_output_0: 3c010666
/model/model.8/cv1/act/Mul_output_0: 3ce64a67
/model/model.8/Split_output_0: 3ce64a67
/model/model.8/Split_output_1: 3cb37746
/model/model.8/m.0/cv1/conv/Conv_output_0: 3d3bd8c1
/model/model.8/m.0/cv2/conv/Conv_output_0: 3d452297
/model/model.8/m.0/cv1/act/Sigmoid_output_0: 3c0313a0
/model/model.8/m.0/cv2/act/Sigmoid_output_0: 3c0b5b52
/model/model.8/m.0/cv1/act/Mul_output_0: 3c875eac
/model/model.8/m.0/cv2/act/Mul_output_0: 3cff5adf
/model/model.8/m.0/m/m.0/cv1/conv/Conv_output_0: 3d8ec83d
/model/model.8/m.0/m/m.0/cv1/act/Sigmoid_output_0: 3c0c2827
/model/model.8/m.0/m/m.0/cv1/act/Mul_output_0: 3cd2a4d6
/model/model.8/m.0/m/m.0/cv2/conv/Conv_output_0: 3d8fa417
/model/model.8/m.0/m/m.0/cv2/act/Sigmoid_output_0: 3c04a4d9
/model/model.8/m.0/m/m.0/cv2/act/Mul_output_0: 3cb1ff35
/model/model.8/m.0/m/m.0/Add_output_0: 3d66c7ed
/model/model.8/m.0/m/m.1/cv1/conv/Conv_output_0: 3d8034ad
/model/model.8/m.0/m/m.1/cv1/act/Sigmoid_output_0: 3c0119a7
/model/model.8/m.0/m/m.1/cv1/act/Mul_output_0: 3ce92630
/model/model.8/m.0/m/m.1/cv2/conv/Conv_output_0: 3da3d49f
/model/model.8/m.0/m/m.1/cv2/act/Sigmoid_output_0: 3c0b71d8
/model/model.8/m.0/m/m.1/cv2/act/Mul_output_0: 3d11c999
/model/model.8/m.0/m/m.1/Add_output_0: 3d97f033
/model/model.8/m.0/Concat_output_0: 3d8ee06e
/model/model.8/m.0/cv3/conv/Conv_output_0: 3d7d7ffb
/model/model.8/m.0/cv3/act/Sigmoid_output_0: 3c00c712
/model/model.8/m.0/cv3/act/Mul_output_0: 3ce94830
/model/model.8/Concat_output_0: 3ce94830
/model/model.8/cv2/conv/Conv_output_0: 3d769a89
/model/model.8/cv2/act/Sigmoid_output_0: 3c00da11
/model/model.8/cv2/act/Mul_output_0: 3ce97c64
/model/model.9/cv1/conv/Conv_output_0: 3d8837ee
/model/model.9/cv1/act/Sigmoid_output_0: 3c00d9d5
/model/model.9/cv1/act/Mul_output_0: 3d252968
/model/model.9/m/MaxPool_output_0: 3d252968
/model/model.9/m_1/MaxPool_output_0: 3d252968
/model/model.9/m_2/MaxPool_output_0: 3d252968
/model/model.9/Concat_output_0: 3d68e8a3
/model/model.9/cv2/conv/Conv_output_0: 3d865c32
/model/model.9/cv2/act/Sigmoid_output_0: 3c02b2e2
/model/model.9/cv2/act/Mul_output_0: 3cc051e5
/model/model.10/cv1/conv/Conv_output_0: 3dadc239
/model/model.10/cv1/act/Sigmoid_output_0: 3c0307dd
/model/model.10/cv1/act/Mul_output_0: 3d0172dc
/model/model.10/Split_output_0: 3d0172dc
/model/model.10/Split_output_1: 3d104521
/model/model.23/Reshape_17_output_0: 3e9f4871
/model/model.10/m/m.0/attn/qkv/conv/Conv_output_0: 3d565c50
/model/model.23/Unsqueeze_13_output_0: 3e9f4871
/ConstantOfShape_output_0: 0
/model/model.23/Expand_5_output_0: 3e9f4871
/model/model.23/Add_7_output_0: 3fa1a91c
/model/model.23/Concat_23_output_0: 3fa0cb8f
/model/model.23/Transpose_1_output_0: 3e810a14
/model/model.23/Reshape_5_output_0: 3e172ed8
ONNXTRT_Broadcast_388_output: 41214c99
/NonMaxSuppression_output: 3be038d4
(Unnamed Layer* 904) [Constant]_output: 3bb4a7b5
/Mul_2_output_0: 3a010a14
/model/model.23/Concat_7_output_0: 3e35f778
/model/model.23/dfl/Transpose_output_0: 3db903fa
/model/model.23/dfl/conv/Conv_output_0: 3df78149
/model/model.10/m/m.0/attn/Reshape_output_0: 3d565c50
/model/model.10/m/m.0/attn/Split_output_0: 3d3a32c6
/model/model.10/m/m.0/attn/Split_output_1: 3d1f87dd
/model/model.10/m/m.0/attn/Split_output_2: 3d565c50
/model/model.10/m/m.0/attn/Transpose_output_0: 3d3a32c6
/model/model.10/m/m.0/attn/Reshape_2_output_0: 3d565c50
/model/model.10/m/m.0/attn/MatMul_output_0: 3f265d4b
/model/model.10/m/m.0/attn/pe/conv/Conv_output_0: 3d1b7dbf
/model/model.10/m/m.0/attn/Constant_7_output_0_output: 3ab67d3d
ONNXTRT_Broadcast_output: 3ab67d3d
/model/model.10/m/m.0/attn/Mul_1_output_0: 3deb465b
/model/model.10/m/m.0/attn/Softmax_output: 3a0444a2
/model/model.10/m/m.0/attn/Softmax_output_0: 3a0444a2
/model/model.10/m/m.0/attn/Transpose_1_output_0: 3a0444a2
/model/model.10/m/m.0/attn/MatMul_1_output_0: 3d36730f
/model/model.10/m/m.0/attn/Reshape_1_output_0: 3d36730f
/model/model.10/m/m.0/attn/Add_output_0: 3d0dc681
/model/model.10/m/m.0/attn/proj/conv/Conv_output_0: 3cf84311
/model/model.10/m/m.0/Add_output_0: 3d732d8f
/model/model.10/m/m.0/ffn/ffn.0/conv/Conv_output_0: 3d6f70d0
/model/model.10/m/m.0/ffn/ffn.0/act/Sigmoid_output_0: 3c009cab
/model/model.10/m/m.0/ffn/ffn.0/act/Mul_output_0: 3ce5faa1
/model/model.10/m/m.0/ffn/ffn.1/conv/Conv_output_0: 3d2b25a7
/model/model.10/m/m.0/Add_1_output_0: 3d6b45e5
/model/model.10/Concat_output_0: 3d112d53
/model/model.10/cv2/conv/Conv_output_0: 3d7992e9
/model/model.10/cv2/act/Sigmoid_output_0: 3bffd143
/model/model.10/cv2/act/Mul_output_0: 3cb327ec
/model/model.11/Resize_output_0: 3cb327ec
/model/model.12/Concat_output_0: 3c9aee92
/model/model.13/cv1/conv/Conv_output_0: 3d7c4511
/model/model.13/cv1/act/Sigmoid_output_0: 3bfe2c99
/model/model.13/cv1/act/Mul_output_0: 3c40bb69
/model/model.13/Split_output_0: 3c736557
/model/model.13/Split_output_1: 3c40bb69
/model/model.13/m.0/cv1/conv/Conv_output_0: 3d402e87
/model/model.13/m.0/cv2/conv/Conv_output_0: 3d8473a6
/model/model.13/m.0/cv1/act/Sigmoid_output_0: 3c02c0f3
/model/model.13/m.0/cv2/act/Sigmoid_output_0: 3c00df44
/model/model.13/m.0/cv1/act/Mul_output_0: 3cd5941b
/model/model.13/m.0/cv2/act/Mul_output_0: 3d128ff9
/model/model.13/m.0/m/m.0/cv1/conv/Conv_output_0: 3d892125
/model/model.13/m.0/m/m.0/cv1/act/Sigmoid_output_0: 3c01731b
/model/model.13/m.0/m/m.0/cv1/act/Mul_output_0: 3c9a8a15
/model/model.13/m.0/m/m.0/cv2/conv/Conv_output_0: 3d46b1a8
/model/model.13/m.0/m/m.0/cv2/act/Sigmoid_output_0: 3c02b95b
/model/model.13/m.0/m/m.0/cv2/act/Mul_output_0: 3cb2733d
/model/model.13/m.0/m/m.0/Add_output_0: 3d502097
/model/model.13/m.0/m/m.1/cv1/conv/Conv_output_0: 3d3dc870
/model/model.13/m.0/m/m.1/cv1/act/Sigmoid_output_0: 3c00e20e
/model/model.13/m.0/m/m.1/cv1/act/Mul_output_0: 3cb43074
/model/model.13/m.0/m/m.1/cv2/conv/Conv_output_0: 3d88462f
/model/model.13/m.0/m/m.1/cv2/act/Sigmoid_output_0: 3c01f913
/model/model.13/m.0/m/m.1/cv2/act/Mul_output_0: 3d27cad6
/model/model.13/m.0/m/m.1/Add_output_0: 3d84350f
/model/model.13/m.0/Concat_output_0: 3d12160a
/model/model.13/m.0/cv3/conv/Conv_output_0: 3d384a74
/model/model.13/m.0/cv3/act/Sigmoid_output_0: 3bfb0570
/model/model.13/m.0/cv3/act/Mul_output_0: 3c31c584
/model/model.13/Concat_output_0: 3c40bb69
/model/model.13/cv2/conv/Conv_output_0: 3d7cc99e
/model/model.13/cv2/act/Sigmoid_output_0: 3c00e49f
/model/model.13/cv2/act/Mul_output_0: 3cb373dc
/model/model.14/Resize_output_0: 3cb373dc
/model/model.15/Concat_output_0: 3c80b352
/model/model.16/cv1/conv/Conv_output_0: 3d76166b
/model/model.16/cv1/act/Sigmoid_output_0: 3c00f39f
/model/model.16/cv1/act/Mul_output_0: 3c99aa62
/model/model.16/Split_output_0: 3c99aa62
/model/model.16/Split_output_1: 3c9c0426
/model/model.16/m.0/cv1/conv/Conv_output_0: 3d19f950
/model/model.16/m.0/cv2/conv/Conv_output_0: 3d92e2b5
/model/model.16/m.0/cv1/act/Sigmoid_output_0: 3bf6f7c1
/model/model.16/m.0/cv2/act/Sigmoid_output_0: 3c01121f
/model/model.16/m.0/cv1/act/Mul_output_0: 3cd29b35
/model/model.16/m.0/cv2/act/Mul_output_0: 3cd34c09
/model/model.16/m.0/m/m.0/cv1/conv/Conv_output_0: 3d9270a0
/model/model.16/m.0/m/m.0/cv1/act/Sigmoid_output_0: 3c00d126
/model/model.16/m.0/m/m.0/cv1/act/Mul_output_0: 3c9ae848
/model/model.16/m.0/m/m.0/cv2/conv/Conv_output_0: 3d8324bc
/model/model.16/m.0/m/m.0/cv2/act/Sigmoid_output_0: 3c030c5f
/model/model.16/m.0/m/m.0/cv2/act/Mul_output_0: 3ceb0b76
/model/model.16/m.0/m/m.0/Add_output_0: 3d66a28d
/model/model.16/m.0/m/m.1/cv1/conv/Conv_output_0: 3d790c93
/model/model.16/m.0/m/m.1/cv1/act/Sigmoid_output_0: 3c010f57
/model/model.16/m.0/m/m.1/cv1/act/Mul_output_0: 3cd4ee49
/model/model.16/m.0/m/m.1/cv2/conv/Conv_output_0: 3dea8a96
/model/model.16/m.0/m/m.1/cv2/act/Sigmoid_output_0: 3c053a74
/model/model.16/m.0/m/m.1/cv2/act/Mul_output_0: 3d65ec44
/model/model.16/m.0/m/m.1/Add_output_0: 3def9bf8
/model/model.16/m.0/Concat_output_0: 3d6dd599
/model/model.16/m.0/cv3/conv/Conv_output_0: 3d787a23
/model/model.16/m.0/cv3/act/Sigmoid_output_0: 3c02069e
/model/model.16/m.0/cv3/act/Mul_output_0: 3ce8c62f
/model/model.16/Concat_output_0: 3cb3307d
/model/model.16/cv2/conv/Conv_output_0: 3d848b81
/model/model.16/cv2/act/Sigmoid_output_0: 3c020ff8
/model/model.16/cv2/act/Mul_output_0: 3cd2dfa3
/model/model.17/conv/Conv_output_0: 3d4e817b
/model/model.23/Transpose_output_0: 3fa0cb8f
/model/model.23/cv2.0/cv2.0.0/conv/Conv_output_0: 3d7adcfc
/model/model.23/cv3.0/cv3.0.0/cv3.0.0.0/conv/Conv_output_0: 3d67e3d5
/model/model.17/act/Sigmoid_output_0: 3c0066fb
/Cast_2_output_0: 3be038d4
/model/model.23/cv2.0/cv2.0.0/act/Sigmoid_output_0: 3c0508fc
/model/model.23/cv3.0/cv3.0.0/cv3.0.0.0/act/Sigmoid_output_0: 3c010a14
/GatherND_1_output_0: 3be038d4
/model/model.23/cv3.2/cv3.2.1/cv3.2.1.1/conv/Conv_output_0: 3d9fa03e
/model/model.17/act/Mul_output_0: 3c9989bc
/model/model.23/cv2.0/cv2.0.0/act/Mul_output_0: 3cb3d08e
/model/model.23/cv3.0/cv3.0.0/cv3.0.0.0/act/Mul_output_0: 3d6971cd
/model/model.23/cv3.2/cv3.2.0/cv3.2.0.1/act/Mul_output_0: 3d00da36
/model/model.23/Sub_output_0: 3fa05c98
/model/model.23/Mul_5_output_0: 4121deb6
/model/model.18/Concat_output_0: 3cb373dc
/model/model.23/cv2.0/cv2.0.1/conv/Conv_output_0: 3e06e9ea
/Slice_3_output_0: 38ccff45
/model/model.23/Slice_output_0: 3df7d8d4
/model/model.23/cv3.0/cv3.0.0/cv3.0.0.1/conv/Conv_output_0: 3d83ebbb
/Gather_6_output_0: 3be038d4
/model/model.23/Constant_15_output_0_output: 0
/model/model.23/Constant_16_output_0_output: 3c010a14
/Gather_13_output_0: 0
/model/model.23/dfl/Reshape_output_0: 3db903fa
/model/model.23/ConstantOfShape_2_output_0: 3e810a14
/model/model.23/cv3.2/cv3.2.1/cv3.2.1.0/act/Sigmoid_output_0: 3c0067ee
/model/model.23/Range_output_0: 3fa04a85
ONNXTRT_ShapeShuffle_38_output: 3c010a14
/Mul_output_0: 38ccff45
/model/model.23/Reshape_18_output_0: 3e9f4871
/model/model.23/Reshape_2_output_0: 3d90adf3
/Div_output_0: 3c001f8b
/model/model.23/Range_1_output_0: 3fa04a85
ONNXTRT_ShapeShuffle_48_output: 3c010a14
/Transpose_output_0: 4121deb6
/model/model.19/cv1/conv/Conv_output_0: 3d6cadc5
/model/model.23/cv2.0/cv2.0.1/act/Sigmoid_output_0: 3c010a14
/model/model.23/cv3.0/cv3.0.0/cv3.0.0.1/act/Sigmoid_output_0: 3c02115c
/model/model.23/Constant_17_output_0_output: 3b810a14
ONNXTRT_Broadcast_51_output: 3b810a14
/model/model.23/Reshape_7_output_0: 3fa0cb8f
ONNXTRT_Broadcast_53_output: 3b810a14
/model/model.23/Reshape_6_output_0: 3fa0cb8f
ONNXTRT_ShapeShuffle_170_output: 3e810a14
/model/model.19/cv1/act/Sigmoid_output_0: 3c010a14
/model/model.23/cv2.0/cv2.0.1/act/Mul_output_0: 3d916b73
/model/model.23/cv3.0/cv3.0.0/cv3.0.0.1/act/Mul_output_0: 3d12c421
(Unnamed Layer* 339) [Constant]_output: 3d810a14
ONNXTRT_ShapeShuffle_56_output: 3d810a14
/model/model.23/ConstantOfShape_output_0: 3d810a14
ONNXTRT_Broadcast_165_output: 3b810a14
/model/model.23/cv2.2/cv2.2.1/act/Mul_output_0: 3d90122e
/model/model.23/Reshape_8_output_0: 3fa0cb8f
/model/model.23/Reshape_9_output_0: 3fa0cb8f
/model/model.19/cv1/act/Mul_output_0: 3d137ed8
/model/model.23/cv2.0/cv2.0.2/Conv_output_0: 3db51eb3
/model/model.23/cv3.0/cv3.0.1/cv3.0.1.0/conv/Conv_output_0: 3d5dd023
/model/model.23/Unsqueeze_12_output_0: 3e9f4871
/model/model.19/Split_output_0: 3d141e80
/model/model.19/Split_output_1: 3cc336b6
/model/model.23/Reshape_output_0: 3db51eb3
/model/model.23/cv3.0/cv3.0.1/cv3.0.1.0/act/Sigmoid_output_0: 3c035690
/model/model.23/Sigmoid_output_0: 3be038d4
/model/model.23/Concat_24_output_0: 3e810a14
/Slice_output_0: 4121deb6
/model/model.23/Reshape_16_output_0: 3e9f4871
/model/model.23/Expand_output_0: 3fa0cb8f
ONNXTRT_Broadcast_167_output: 3b810a14
/model/model.23/dfl/Softmax_output: 3bb85f93
output0: 41273e0c
/ArgMax_output: 3be038d4
/model/model.23/Concat_3_output_0: 3db903fa
/model/model.23/Expand_1_output_0: 3fa0cb8f
/model/model.19/m.0/cv1/conv/Conv_output_0: 3d207cfb
/model/model.19/m.0/cv2/conv/Conv_output_0: 3d54882a
/model/model.23/cv3.0/cv3.0.1/cv3.0.1.0/act/Mul_output_0: 3d6031fb
/model/model.23/Unsqueeze_6_output_0: 3fa0cb8f
/model/model.23/Unsqueeze_7_output_0: 3fa0cb8f
/model/model.19/m.0/cv1/act/Sigmoid_output_0: 3c00af23
/model/model.19/m.0/cv2/act/Sigmoid_output_0: 3c012670
/model/model.23/cv3.0/cv3.0.1/cv3.0.1.1/conv/Conv_output_0: 3dcaafff
/model/model.23/Concat_11_output_0: 3fa0cb8f
/model/model.19/m.0/cv1/act/Mul_output_0: 3c913aca
/model/model.19/m.0/cv2/act/Mul_output_0: 3cb066bc
/model/model.23/cv3.0/cv3.0.1/cv3.0.1.1/act/Sigmoid_output_0: 3c010a14
/model/model.23/Reshape_10_output_0: 3fa0cb8f
/model/model.19/m.0/m/m.0/cv1/conv/Conv_output_0: 3d71695c
/model/model.23/cv3.0/cv3.0.1/cv3.0.1.1/act/Mul_output_0: 3d838dd5
/model/model.19/m.0/m/m.0/cv1/act/Sigmoid_output_0: 3c02e5dc
/model/model.23/cv3.0/cv3.0.2/Conv_output_0: 3e3404db
/model/model.19/m.0/m/m.0/cv1/act/Mul_output_0: 3ce9656f
/model/model.23/Reshape_3_output_0: 3e3404db
/model/model.19/m.0/m/m.0/cv2/conv/Conv_output_0: 3d8e8f13
/model/model.19/m.0/m/m.0/cv2/act/Sigmoid_output_0: 3c0414db
/model/model.19/m.0/m/m.0/cv2/act/Mul_output_0: 3d44be6a
/model/model.19/m.0/m/m.0/Add_output_0: 3d5bbb81
/model/model.19/m.0/m/m.1/cv1/conv/Conv_output_0: 3d8521b8
/model/model.19/m.0/m/m.1/cv1/act/Sigmoid_output_0: 3c0312c4
/model/model.19/m.0/m/m.1/cv1/act/Mul_output_0: 3cd2aee8
/model/model.19/m.0/m/m.1/cv2/conv/Conv_output_0: 3db63419
/model/model.19/m.0/m/m.1/cv2/act/Sigmoid_output_0: 3c0880f4
/model/model.19/m.0/m/m.1/cv2/act/Mul_output_0: 3d454159
/model/model.19/m.0/m/m.1/Add_output_0: 3d9bdece
/model/model.19/m.0/Concat_output_0: 3d931541
/model/model.19/m.0/cv3/conv/Conv_output_0: 3d7f2ed4
/model/model.19/m.0/cv3/act/Sigmoid_output_0: 3c0111cf
/model/model.19/m.0/cv3/act/Mul_output_0: 3d000afe
/model/model.19/Concat_output_0: 3d141e80
/model/model.19/cv2/conv/Conv_output_0: 3d8ba44e
/model/model.19/cv2/act/Sigmoid_output_0: 3c0111f3
/model/model.19/cv2/act/Mul_output_0: 3d018b26
/model/model.20/conv/Conv_output_0: 3d27c70e
/model/model.23/cv2.1/cv2.1.0/conv/Conv_output_0: 3d6e66fc
/model/model.23/cv3.1/cv3.1.0/cv3.1.0.0/conv/Conv_output_0: 3d54c606
/model/model.23/cv2.2/cv2.2.2/Conv_output_0: 3d90adf3
/model/model.20/act/Sigmoid_output_0: 3c007673
/model/model.23/cv2.1/cv2.1.0/act/Sigmoid_output_0: 3c00ce4e
/model/model.23/cv3.1/cv3.1.0/cv3.1.0.0/act/Sigmoid_output_0: 3c011221
/GatherND_output_0: 4120276d
/model/model.23/dfl/Softmax_output_0: 3bb85f93
/model/model.20/act/Mul_output_0: 3d02afbd
/model/model.23/cv2.1/cv2.1.0/act/Mul_output_0: 3d0fe579
/model/model.23/cv3.1/cv3.1.0/cv3.1.0.0/act/Mul_output_0: 3d2a13f8
/Slice_2_output_0: 0
/Slice_4_output_0: 0
/model/model.21/Concat_output_0: 3cd57183
/model/model.23/cv2.1/cv2.1.1/conv/Conv_output_0: 3df7a216
/model/model.23/cv3.1/cv3.1.0/cv3.1.0.1/conv/Conv_output_0: 3d7557d6
/model/model.23/Concat_25_output_0: 3fa29d64
(Unnamed Layer* 569) [Constant]_output: 3e810a14
/Constant_15_output_0_output: 38ce7687
/model/model.23/cv3.2/cv3.2.1/cv3.2.1.1/act/Sigmoid_output_0: 3c010a14
/model/model.23/Expand_4_output_0: 3e9f4871
/Constant_14_output_0_output: 41214c99
/model/model.23/Range_2_output_0: 3f1f4871
ONNXTRT_ShapeShuffle_95_output: 3c010a14
/Slice_1_output_0: 3be038d4
ONNXTRT_Broadcast_269_output: 3e810a14
/model/model.23/Reshape_19_output_0: 3e9f4871
/model/model.23/Concat_26_output_0: 4121deb6
/model/model.23/Reshape_20_output_0: 3e9f4871
/Reshape_6_output_0: 41273e0c
/model/model.23/Range_3_output_0: 3f1f4871
ONNXTRT_ShapeShuffle_105_output: 3c010a14
/Gather_10_output_0: 41273e0c
/model/model.22/cv1/conv/Conv_output_0: 3d54ed5d
/model/model.23/cv2.1/cv2.1.1/act/Sigmoid_output_0: 3c010a14
/model/model.23/cv3.1/cv3.1.0/cv3.1.0.1/act/Sigmoid_output_0: 3c021329
ONNXTRT_Broadcast_108_output: 3b810a14
/model/model.23/Reshape_12_output_0: 3f204a85
ONNXTRT_Broadcast_110_output: 3b810a14
/model/model.23/Reshape_11_output_0: 3f204a85
/model/model.22/cv1/act/Sigmoid_output_0: 3c0213bd
/model/model.23/cv2.1/cv2.1.1/act/Mul_output_0: 3d91002d
/model/model.23/cv3.1/cv3.1.0/cv3.1.0.1/act/Mul_output_0: 3d118ec2
(Unnamed Layer* 457) [Constant]_output: 3e010a14
ONNXTRT_ShapeShuffle_113_output: 3e010a14
/model/model.23/ConstantOfShape_1_output_0: 3e010a14
/model/model.23/cv3.2/cv3.2.2/Conv_output_0: 3e172ed8
/Gather_11_output_0: 3be2fe01
/Cast_3_output_0: 3d214c99
/model/model.23/Reshape_13_output_0: 3f204a85
/model/model.23/Reshape_14_output_0: 3f204a85
/model/model.22/cv1/act/Mul_output_0: 3d037465
/model/model.23/cv2.1/cv2.1.2/Conv_output_0: 3db881cc
/model/model.23/cv3.1/cv3.1.1/cv3.1.1.0/conv/Conv_output_0: 3d798e51
/model/model.22/Split_output_0: 3d037465
/model/model.22/Split_output_1: 3cb0b046
/model/model.23/Reshape_1_output_0: 3db881cc
/model/model.23/cv3.1/cv3.1.1/cv3.1.1.0/act/Sigmoid_output_0: 3c010a14
ONNXTRT_ShapeShuffle_382_output: 0
/model/model.23/dfl/Reshape_1_output_0: 3df78149
/model/model.23/cv3.2/cv3.2.1/cv3.2.1.0/conv/Conv_output_0: 3d3888f4
/model/model.23/Expand_2_output_0: 3f204a85
/model/model.23/Slice_1_output_0: 3dcb4fbd
/model/model.23/cv3.2/cv3.2.1/cv3.2.1.1/act/Mul_output_0: 3d0fbe3e
/Cast_output_0: 3d214c99
ONNXTRT_Broadcast_479_output: 38ce7687
/model/model.23/Expand_3_output_0: 3f204a85
/model/model.22/m.0/cv1/conv/Conv_output_0: 3d223bd1
/model/model.22/m.0/cv2/conv/Conv_output_0: 3d08f193
/model/model.23/cv3.1/cv3.1.1/cv3.1.1.0/act/Mul_output_0: 3d01f178
/model/model.23/Unsqueeze_9_output_0: 3f204a85
/model/model.23/Unsqueeze_10_output_0: 3f204a85
/model/model.22/m.0/cv1/act/Sigmoid_output_0: 3c007dc7
/model/model.22/m.0/cv2/act/Sigmoid_output_0: 3c005708
/model/model.23/cv3.1/cv3.1.1/cv3.1.1.1/conv/Conv_output_0: 3dae2c0f
/model/model.23/Concat_16_output_0: 3f204a85
/model/model.22/m.0/cv1/act/Mul_output_0: 3cd210d6
/model/model.22/m.0/cv2/act/Mul_output_0: 3ce842ef
/model/model.23/cv3.1/cv3.1.1/cv3.1.1.1/act/Sigmoid_output_0: 3c010a14
/model/model.23/Reshape_15_output_0: 3f204a85
/model/model.22/m.0/m/m.0/cv1/conv/Conv_output_0: 3d8565b0
/model/model.23/cv3.1/cv3.1.1/cv3.1.1.1/act/Mul_output_0: 3d65059a
/model/model.22/m.0/m/m.0/cv1/act/Sigmoid_output_0: 3c010594
/model/model.23/cv3.1/cv3.1.2/Conv_output_0: 3e2c307a
/model/model.22/m.0/m/m.0/cv1/act/Mul_output_0: 3cd3fbee
/model/model.23/Reshape_4_output_0: 3e2c307a
/model/model.22/m.0/m/m.0/cv2/conv/Conv_output_0: 3d732edb
/model/model.22/m.0/m/m.0/cv2/act/Sigmoid_output_0: 3c030ffb
/model/model.22/m.0/m/m.0/cv2/act/Mul_output_0: 3d27e8fc
/model/model.22/m.0/m/m.0/Add_output_0: 3d4f8df7
/model/model.22/m.0/m/m.1/cv1/conv/Conv_output_0: 3d4c6b2d
/model/model.22/m.0/m/m.1/cv1/act/Sigmoid_output_0: 3c037544
/model/model.22/m.0/m/m.1/cv1/act/Mul_output_0: 3cc02a92
/model/model.22/m.0/m/m.1/cv2/conv/Conv_output_0: 3d73bad0
/model/model.22/m.0/m/m.1/cv2/act/Sigmoid_output_0: 3c00ef04
/model/model.22/m.0/m/m.1/cv2/act/Mul_output_0: 3d11a2d6
/model/model.22/m.0/m/m.1/Add_output_0: 3d870dbb
/model/model.22/m.0/Concat_output_0: 3d6a0660
/model/model.22/m.0/cv3/conv/Conv_output_0: 3d2f1e4a
/model/model.22/m.0/cv3/act/Sigmoid_output_0: 3c015ce6
/model/model.22/m.0/cv3/act/Mul_output_0: 3ce6eeb4
/model/model.22/Concat_output_0: 3d037465
/model/model.22/cv2/conv/Conv_output_0: 3d6474ca
/model/model.22/cv2/act/Sigmoid_output_0: 3c021395
/model/model.22/cv2/act/Mul_output_0: 3cff379f
/model/model.23/cv2.2/cv2.2.0/conv/Conv_output_0: 3d95d6c2
/model/model.23/cv3.2/cv3.2.0/cv3.2.0.0/conv/Conv_output_0: 3ce9369e
/Cast_1_output_0: 3a51155e
/model/model.23/Concat_21_output_0: 3e9f4871
/model/model.23/cv2.2/cv2.2.0/act/Sigmoid_output_0: 3c010890
/model/model.23/cv3.2/cv3.2.0/cv3.2.0.0/act/Sigmoid_output_0: 3bff8e87
/model/model.23/cv2.2/cv2.2.0/act/Mul_output_0: 3d26a32f
/model/model.23/cv3.2/cv3.2.0/cv3.2.0.0/act/Mul_output_0: 3cb25688
/model/model.23/cv3.2/cv3.2.0/cv3.2.0.1/act/Sigmoid_output_0: 3c00c67f
/Add_3_output_0: 3a51155e
/model/model.23/cv2.2/cv2.2.1/conv/Conv_output_0: 3e12fc33
/model/model.23/cv3.2/cv3.2.0/cv3.2.0.1/conv/Conv_output_0: 3d47791b
/Reshape_2_output_0: 3be2fe01
/ReduceMax_output_0: 3be038d4
/model/model.23/Unsqueeze_15_output_0: 3fa0cb8f
ONNXTRT_Broadcast_397_output: 38ce7687
/model/model.23/Range_4_output_0: 3e9d4449
ONNXTRT_ShapeShuffle_152_output: 3c010a14
/ScatterND_output_0: 41273e0c
/Concat_3_output_0: 3a51155e
/Concat_4_output_0: 41273e0c
/model/model.23/cv3.2/cv3.2.1/cv3.2.1.0/act/Mul_output_0: 3d11d75e
/Expand_1_output_0: 41273e0c
(Unnamed Layer* 793) [Constant]_output: 0
/model/model.23/Range_5_output_0: 3e9d4449
ONNXTRT_ShapeShuffle_162_output: 3c010a14
/model/model.23/cv2.2/cv2.2.1/act/Sigmoid_output_0: 3c010a14
Binary file not shown.
+7 -3
View File
@@ -41,9 +41,13 @@ def user_ai_settings(user_id):
"tracking_intersection_threshold": 0.6, "tracking_intersection_threshold": 0.6,
"model_batch_size": 8, "model_batch_size": 8,
"big_image_tile_overlap_percent": 20, "big_image_tile_overlap_percent": 20,
"altitude": 400, "camera_config": {
"focal_length": 24, "focal_length": 24,
"sensor_width": 23.5, "sensor_width": 23.5,
"current_zoom": 1,
"current_angle": 90,
"current_height": 400,
},
} }
+11 -1
View File
@@ -46,7 +46,17 @@ def test_nft_perf_03_tiling_overhead_large_image(
_, small_ms = image_detect(image_small, "small.jpg", timeout=20) _, small_ms = image_detect(image_small, "small.jpg", timeout=20)
_, large_ms = image_detect( _, large_ms = image_detect(
image_large, "large.jpg", image_large, "large.jpg",
config=json.dumps({"altitude": 400, "focal_length": 24, "sensor_width": 23.5}), config=json.dumps(
{
"camera_config": {
"focal_length": 24,
"sensor_width": 23.5,
"current_zoom": 1,
"current_angle": 90,
"current_height": 400,
}
}
),
timeout=20, timeout=20,
) )
assert large_ms < 30_000.0 assert large_ms < 30_000.0
+7 -3
View File
@@ -149,9 +149,13 @@ def test_ft_p_07_physical_size_filtering_ac4(image_detect, image_small, warm_eng
gsd = (sensor_width * altitude) / (focal_length * image_width_px) gsd = (sensor_width * altitude) / (focal_length * image_width_px)
cfg = json.dumps( cfg = json.dumps(
{ {
"altitude": altitude, "camera_config": {
"focal_length": focal_length, "focal_length": focal_length,
"sensor_width": sensor_width, "sensor_width": sensor_width,
"current_zoom": 1,
"current_angle": 90,
"current_height": altitude,
},
} }
) )
body, _ = image_detect(image_small, "img.jpg", config=cfg, timeout=_DETECT_SLOW_TIMEOUT) body, _ = image_detect(image_small, "img.jpg", config=cfg, timeout=_DETECT_SLOW_TIMEOUT)
+43 -15
View File
@@ -1,9 +1,9 @@
""" """
AZ-178: True streaming video detection e2e tests. AZ-178: True streaming video detection e2e tests.
Both tests upload video_test01.mp4 (12 MB), wait for the first SSE event, Both tests upload video_test01.mp4 (12 MB), wait for the first SSE event
then stop. The goal is to prove the service starts and produces detections, to prove streaming starts early, then keep draining SSE until the terminal
not to process the whole file. event so later tests do not overlap with background video inference.
Run with: pytest e2e/tests/test_streaming_video_upload.py -s -v Run with: pytest e2e/tests/test_streaming_video_upload.py -s -v
""" """
@@ -18,7 +18,8 @@ import sseclient
FIXTURES_DIR = Path(__file__).resolve().parent.parent / "fixtures" FIXTURES_DIR = Path(__file__).resolve().parent.parent / "fixtures"
_TIMEOUT = 5.0 _TIMEOUT = 5.0
_STOP_AFTER = 5 _DRAIN_TIMEOUT = 45.0
_VIDEO_CONFIG = json.dumps({"model_batch_size": 1, "frame_period_recognition": 100})
def _fixture_path(name: str) -> str: def _fixture_path(name: str) -> str:
@@ -39,10 +40,19 @@ def _chunked_reader(path: str, chunk_size: int = 64 * 1024):
def _start_sse_listener( def _start_sse_listener(
http_client, channel_id: str, auth_headers: dict http_client, channel_id: str, auth_headers: dict
) -> tuple[list[dict], list[BaseException], threading.Event]: ) -> tuple[
list[dict],
list[BaseException],
threading.Event,
threading.Event,
threading.Event,
threading.Thread,
]:
events: list[dict] = [] events: list[dict] = []
errors: list[BaseException] = [] errors: list[BaseException] = []
first_event = threading.Event() first_event = threading.Event()
terminal_event = threading.Event()
listener_done = threading.Event()
connected = threading.Event() connected = threading.Event()
def _listen(): def _listen():
@@ -50,7 +60,7 @@ def _start_sse_listener(
with http_client.get( with http_client.get(
f"/detect/events/{channel_id}", f"/detect/events/{channel_id}",
stream=True, stream=True,
timeout=_TIMEOUT + 2, timeout=_DRAIN_TIMEOUT + 5,
headers=auth_headers, headers=auth_headers,
) as resp: ) as resp:
resp.raise_for_status() resp.raise_for_status()
@@ -58,8 +68,11 @@ def _start_sse_listener(
for event in sseclient.SSEClient(resp).events(): for event in sseclient.SSEClient(resp).events():
if not event.data or not str(event.data).strip(): if not event.data or not str(event.data).strip():
continue continue
events.append(json.loads(event.data)) data = json.loads(event.data)
if len(events) >= _STOP_AFTER: events.append(data)
first_event.set()
if data.get("mediaStatus") in ("AIProcessed", "Error"):
terminal_event.set()
first_event.set() first_event.set()
break break
except BaseException as exc: except BaseException as exc:
@@ -67,21 +80,24 @@ def _start_sse_listener(
finally: finally:
connected.set() connected.set()
first_event.set() first_event.set()
listener_done.set()
th = threading.Thread(target=_listen, daemon=True) th = threading.Thread(target=_listen, daemon=True)
th.start() th.start()
connected.wait(timeout=3) connected.wait(timeout=3)
return events, errors, first_event return events, errors, first_event, terminal_event, listener_done, th
@pytest.mark.timeout(10) @pytest.mark.timeout(60)
def test_streaming_video_detections_appear_during_upload( def test_streaming_video_detections_appear_during_upload(
warm_engine, http_client, auth_headers warm_engine, http_client, auth_headers
): ):
# Arrange # Arrange
video_path = _fixture_path("video_test01.mp4") video_path = _fixture_path("video_test01.mp4")
channel_id = str(uuid.uuid4()) channel_id = str(uuid.uuid4())
events, errors, first_event = _start_sse_listener(http_client, channel_id, auth_headers) events, errors, first_event, terminal_event, listener_done, th = _start_sse_listener(
http_client, channel_id, auth_headers
)
# Act # Act
r = http_client.post( r = http_client.post(
@@ -91,27 +107,34 @@ def test_streaming_video_detections_appear_during_upload(
**auth_headers, **auth_headers,
"X-Channel-Id": channel_id, "X-Channel-Id": channel_id,
"X-Filename": "video_test01.mp4", "X-Filename": "video_test01.mp4",
"X-Config": _VIDEO_CONFIG,
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
}, },
timeout=8, timeout=8,
) )
assert r.status_code == 202 assert r.status_code == 202
first_event.wait(timeout=_TIMEOUT) assert first_event.wait(timeout=_TIMEOUT)
assert terminal_event.wait(timeout=_DRAIN_TIMEOUT)
assert listener_done.wait(timeout=2)
th.join(timeout=2)
# Assert # Assert
assert not errors, f"SSE thread error: {errors}" assert not errors, f"SSE thread error: {errors}"
assert len(events) >= 1, "Expected at least one SSE event within 5s" assert len(events) >= 1, "Expected at least one SSE event within 5s"
assert events[-1].get("mediaStatus") == "AIProcessed"
print(f"\n First {len(events)} SSE events:") print(f"\n First {len(events)} SSE events:")
for e in events: for e in events:
print(f" {e}") print(f" {e}")
@pytest.mark.timeout(10) @pytest.mark.timeout(60)
def test_non_faststart_video_still_works(warm_engine, http_client, auth_headers): def test_non_faststart_video_still_works(warm_engine, http_client, auth_headers):
# Arrange # Arrange
video_path = _fixture_path("video_test01.mp4") video_path = _fixture_path("video_test01.mp4")
channel_id = str(uuid.uuid4()) channel_id = str(uuid.uuid4())
events, errors, first_event = _start_sse_listener(http_client, channel_id, auth_headers) events, errors, first_event, terminal_event, listener_done, th = _start_sse_listener(
http_client, channel_id, auth_headers
)
# Act # Act
r = http_client.post( r = http_client.post(
@@ -121,16 +144,21 @@ def test_non_faststart_video_still_works(warm_engine, http_client, auth_headers)
**auth_headers, **auth_headers,
"X-Channel-Id": channel_id, "X-Channel-Id": channel_id,
"X-Filename": "video_test01_plain.mp4", "X-Filename": "video_test01_plain.mp4",
"X-Config": _VIDEO_CONFIG,
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
}, },
timeout=8, timeout=8,
) )
assert r.status_code == 202 assert r.status_code == 202
first_event.wait(timeout=_TIMEOUT) assert first_event.wait(timeout=_TIMEOUT)
assert terminal_event.wait(timeout=_DRAIN_TIMEOUT)
assert listener_done.wait(timeout=2)
th.join(timeout=2)
# Assert # Assert
assert not errors, f"SSE thread error: {errors}" assert not errors, f"SSE thread error: {errors}"
assert len(events) >= 1, "Expected at least one SSE event within 5s" assert len(events) >= 1, "Expected at least one SSE event within 5s"
assert events[-1].get("mediaStatus") == "AIProcessed"
print(f"\n First {len(events)} SSE events:") print(f"\n First {len(events)} SSE events:")
for e in events: for e in events:
print(f" {e}") print(f" {e}")
+9 -1
View File
@@ -3,7 +3,15 @@ import json
import pytest import pytest
_TILING_TIMEOUT = 120 _TILING_TIMEOUT = 120
_GSD = {"altitude": 400, "focal_length": 24, "sensor_width": 23.5} _GSD = {
"camera_config": {
"focal_length": 24,
"sensor_width": 23.5,
"current_zoom": 1,
"current_angle": 90,
"current_height": 400,
}
}
_DUP_THRESHOLD = 0.01 _DUP_THRESHOLD = 0.01
+3
View File
@@ -72,6 +72,9 @@ def video_events(warm_engine, http_client, auth_headers):
**auth_headers, **auth_headers,
"X-Channel-Id": channel_id, "X-Channel-Id": channel_id,
"X-Filename": "video_test01.mp4", "X-Filename": "video_test01.mp4",
"X-Config": json.dumps(
{"model_batch_size": 1, "frame_period_recognition": 100}
),
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
}, },
timeout=15, timeout=15,
+37 -4
View File
@@ -30,6 +30,13 @@ def parse_args():
parser.add_argument("--output", default="azaion.int8_calib.cache") parser.add_argument("--output", default="azaion.int8_calib.cache")
parser.add_argument("--input-size", type=int, default=1280, help="Model input H=W (default 1280)") parser.add_argument("--input-size", type=int, default=1280, help="Model input H=W (default 1280)")
parser.add_argument("--num-samples", type=int, default=500) parser.add_argument("--num-samples", type=int, default=500)
parser.add_argument("--workspace-gb", type=float, default=4.0)
parser.add_argument("--no-fp16", action="store_true", help="Do not enable FP16 fallback during INT8 calibration")
parser.add_argument(
"--softmax-fp32",
action="store_true",
help="Force TensorRT SoftMax layers to FP32 as a workaround for Jetson INT8 calibration failures",
)
return parser.parse_args() return parser.parse_args()
@@ -66,7 +73,7 @@ def main():
if not images: if not images:
print(f"No images found in {args.images_dir}", file=sys.stderr) print(f"No images found in {args.images_dir}", file=sys.stderr)
sys.exit(1) sys.exit(1)
print(f"Using {len(images)} calibration images") print(f"Using {len(images)} calibration images", flush=True)
H = W = args.input_size H = W = args.input_size
@@ -98,6 +105,13 @@ def main():
print(f"Cache written → {args.output}") print(f"Cache written → {args.output}")
onnx_data = Path(args.onnx).read_bytes() onnx_data = Path(args.onnx).read_bytes()
try:
from engines.onnx_tensorrt_compat import prepare_for_tensorrt
onnx_data = prepare_for_tensorrt(onnx_data)
print("Prepared ONNX model for TensorRT static Jetson build", flush=True)
except Exception as e:
print(f"WARNING: ONNX TensorRT compatibility preparation failed: {e}", file=sys.stderr)
logger = trt.Logger(trt.Logger.INFO) logger = trt.Logger(trt.Logger.INFO)
explicit_batch = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) explicit_batch = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
@@ -107,9 +121,11 @@ def main():
trt.OnnxParser(network, logger) as parser, trt.OnnxParser(network, logger) as parser,
builder.create_builder_config() as config, builder.create_builder_config() as config,
): ):
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 4 * 1024 ** 3) config.set_memory_pool_limit(
trt.MemoryPoolType.WORKSPACE, int(args.workspace_gb * 1024 ** 3)
)
config.set_flag(trt.BuilderFlag.INT8) config.set_flag(trt.BuilderFlag.INT8)
if builder.platform_has_fast_fp16: if not args.no_fp16 and builder.platform_has_fast_fp16:
config.set_flag(trt.BuilderFlag.FP16) config.set_flag(trt.BuilderFlag.FP16)
calibrator = _ImageCalibrator() calibrator = _ImageCalibrator()
@@ -120,6 +136,23 @@ def main():
print(parser.get_error(i), file=sys.stderr) print(parser.get_error(i), file=sys.stderr)
sys.exit(1) sys.exit(1)
if args.softmax_fp32:
constrained = 0
for i in range(network.num_layers):
layer = network.get_layer(i)
if layer.type == trt.LayerType.SOFTMAX:
layer.precision = trt.float32
for j in range(layer.num_outputs):
layer.set_output_type(j, trt.float32)
constrained += 1
if constrained:
for flag_name in ("PREFER_PRECISION_CONSTRAINTS", "OBEY_PRECISION_CONSTRAINTS"):
flag = getattr(trt.BuilderFlag, flag_name, None)
if flag is not None:
config.set_flag(flag)
break
print(f"Forced {constrained} SoftMax layers to FP32", flush=True)
inp = network.get_input(0) inp = network.get_input(0)
shape = inp.shape shape = inp.shape
C = shape[1] C = shape[1]
@@ -128,7 +161,7 @@ def main():
profile.set_shape(inp.name, (1, C, H, W), (1, C, H, W), (1, C, H, W)) profile.set_shape(inp.name, (1, C, H, W), (1, C, H, W), (1, C, H, W))
config.add_optimization_profile(profile) config.add_optimization_profile(profile)
print("Building TensorRT engine with INT8 calibration (several minutes on Jetson)") print("Building TensorRT engine with INT8 calibration (several minutes on Jetson)...", flush=True)
plan = builder.build_serialized_network(network, config) plan = builder.build_serialized_network(network, config)
if plan is None: if plan is None:
print("Engine build failed", file=sys.stderr) print("Engine build failed", file=sys.stderr)
+5
View File
@@ -12,6 +12,11 @@ cdef class AIRecognitionConfig:
cdef public int model_batch_size cdef public int model_batch_size
cdef public bint has_camera_config
cdef public double current_height
cdef public double current_zoom
cdef public double current_angle
cdef public bint has_altitude cdef public bint has_altitude
cdef public double altitude cdef public double altitude
cdef public double focal_length cdef public double focal_length
+64 -9
View File
@@ -9,9 +9,12 @@ cdef class AIRecognitionConfig:
tracking_intersection_threshold, tracking_intersection_threshold,
model_batch_size, model_batch_size,
big_image_tile_overlap_percent, big_image_tile_overlap_percent,
camera_config,
altitude, altitude,
focal_length, focal_length,
sensor_width sensor_width,
current_zoom,
current_angle
): ):
self.frame_period_recognition = frame_period_recognition self.frame_period_recognition = frame_period_recognition
self.frame_recognition_seconds = frame_recognition_seconds self.frame_recognition_seconds = frame_recognition_seconds
@@ -25,10 +28,15 @@ cdef class AIRecognitionConfig:
self.big_image_tile_overlap_percent = big_image_tile_overlap_percent self.big_image_tile_overlap_percent = big_image_tile_overlap_percent
self.has_altitude = altitude is not None self.has_camera_config = camera_config is not None or altitude is not None
self.altitude = 0.0 if altitude is None else float(altitude) self.current_height = 0.0 if altitude is None else float(altitude)
self.focal_length = focal_length self.current_zoom = float(current_zoom)
self.sensor_width = sensor_width self.current_angle = float(current_angle)
self.has_altitude = self.has_camera_config
self.altitude = self.current_height
self.focal_length = float(focal_length)
self.sensor_width = float(sensor_width)
def __str__(self): def __str__(self):
return (f'frame_seconds : {self.frame_recognition_seconds}, distance_confidence : {self.tracking_distance_confidence}, ' return (f'frame_seconds : {self.frame_recognition_seconds}, distance_confidence : {self.tracking_distance_confidence}, '
@@ -37,13 +45,57 @@ cdef class AIRecognitionConfig:
f'frame_period_recognition : {self.frame_period_recognition}, ' f'frame_period_recognition : {self.frame_period_recognition}, '
f'big_image_tile_overlap_percent: {self.big_image_tile_overlap_percent}, ' f'big_image_tile_overlap_percent: {self.big_image_tile_overlap_percent}, '
f'model_batch_size: {self.model_batch_size}, ' f'model_batch_size: {self.model_batch_size}, '
f'altitude: {self.altitude if self.has_altitude else None}, ' f'camera_config: {self.has_camera_config}, '
f'current_height: {self.current_height if self.has_camera_config else None}, '
f'current_zoom: {self.current_zoom}, '
f'current_angle: {self.current_angle}, '
f'focal_length: {self.focal_length}, ' f'focal_length: {self.focal_length}, '
f'sensor_width: {self.sensor_width}' f'sensor_width: {self.sensor_width}'
) )
@staticmethod @staticmethod
cdef AIRecognitionConfig from_dict(dict data): cdef AIRecognitionConfig from_dict(dict data):
cdef object camera_config = data.get("camera_config", data.get("cameraConfig", None))
if camera_config is not None and not isinstance(camera_config, dict):
camera_config = None
cdef object altitude = data.get("altitude", None)
cdef object focal_length = data.get("focal_length", data.get("focalLength", 24))
cdef object sensor_width = data.get("sensor_width", data.get("sensorWidth", 23.5))
cdef object current_zoom = data.get("current_zoom", data.get("currentZoom", 1))
cdef object current_angle = data.get("current_angle", data.get("currentAngle", 90))
if camera_config is not None:
altitude = camera_config.get(
"current_height",
camera_config.get("currentHeight", camera_config.get("altitude", altitude)),
)
focal_length = camera_config.get(
"focal_length",
camera_config.get("focalLength", focal_length),
)
sensor_width = camera_config.get(
"sensor_width",
camera_config.get("sensorWidth", sensor_width),
)
current_zoom = camera_config.get(
"current_zoom",
camera_config.get("currentZoom", current_zoom),
)
current_angle = camera_config.get(
"current_angle",
camera_config.get("currentAngle", current_angle),
)
if focal_length is None:
focal_length = 24
if sensor_width is None:
sensor_width = 23.5
if current_zoom is None:
current_zoom = 1
if current_angle is None:
current_angle = 90
return AIRecognitionConfig( return AIRecognitionConfig(
data.get("frame_period_recognition", 4), data.get("frame_period_recognition", 4),
data.get("frame_recognition_seconds", 2), data.get("frame_recognition_seconds", 2),
@@ -57,7 +109,10 @@ cdef class AIRecognitionConfig:
data.get("big_image_tile_overlap_percent", 20), data.get("big_image_tile_overlap_percent", 20),
data.get("altitude", None), camera_config,
data.get("focal_length", 24), altitude,
data.get("sensor_width", 23.5) focal_length,
sensor_width,
current_zoom,
current_angle
) )
+24 -6
View File
@@ -5,6 +5,7 @@ import av
import cv2 import cv2
import numpy as np import numpy as np
cimport constants_inf cimport constants_inf
from libc.math cimport sin
from ai_availability_status cimport AIAvailabilityEnum, AIAvailabilityStatus from ai_availability_status cimport AIAvailabilityEnum, AIAvailabilityStatus
from annotation cimport Detection, Annotation from annotation cimport Detection, Annotation
@@ -309,25 +310,42 @@ cdef class Inference:
cdef _append_image_frame_entries(self, AIRecognitionConfig ai_config, list all_frame_data, frame, str original_media_name): cdef _append_image_frame_entries(self, AIRecognitionConfig ai_config, list all_frame_data, frame, str original_media_name):
cdef double ground_sampling_distance cdef double ground_sampling_distance
cdef double angle_radians
cdef double angle_scale
cdef double effective_focal_length
cdef int model_h, model_w cdef int model_h, model_w
cdef int img_h, img_w cdef int img_h, img_w
cdef bint has_gsd cdef bint has_gsd
model_h, model_w = self.engine.get_input_shape() model_h, model_w = self.engine.get_input_shape()
img_h, img_w, _ = frame.shape img_h, img_w, _ = frame.shape
has_gsd = ai_config.has_altitude and ai_config.focal_length > 0 and ai_config.sensor_width > 0 and img_w > 0 angle_radians = ai_config.current_angle * 3.141592653589793 / 180.0
angle_scale = sin(angle_radians)
effective_focal_length = ai_config.focal_length * ai_config.current_zoom
has_gsd = (
ai_config.has_camera_config
and ai_config.current_height > 0
and effective_focal_length > 0
and ai_config.sensor_width > 0
and angle_scale > 0
and img_w > 0
)
ground_sampling_distance = 0.0 ground_sampling_distance = 0.0
if has_gsd: if has_gsd:
ground_sampling_distance = ai_config.sensor_width * ai_config.altitude / (ai_config.focal_length * img_w) ground_sampling_distance = (
ai_config.sensor_width
* ai_config.current_height
/ (effective_focal_length * img_w * angle_scale)
)
constants_inf.log(<str>f'ground sampling distance: {ground_sampling_distance}') constants_inf.log(<str>f'ground sampling distance: {ground_sampling_distance}')
else: else:
constants_inf.log(<str>'ground sampling distance: skipped (altitude unavailable)') constants_inf.log(<str>'ground sampling distance: skipped (camera_config unavailable)')
if img_h <= 1.5 * model_h and img_w <= 1.5 * model_w: if img_h <= 1.5 * model_h and img_w <= 1.5 * model_w:
all_frame_data.append((frame, original_media_name, f'{original_media_name}_000000', ground_sampling_distance)) all_frame_data.append((frame, original_media_name, f'{original_media_name}_000000', ground_sampling_distance))
else: else:
if not has_gsd: if not has_gsd:
all_frame_data.append((frame, original_media_name, f'{original_media_name}_000000', ground_sampling_distance)) all_frame_data.append((frame, original_media_name, f'{original_media_name}_000000', ground_sampling_distance))
return return
tile_size = int(constants_inf.METERS_IN_TILE / ground_sampling_distance) tile_size = max(1, int(constants_inf.METERS_IN_TILE / ground_sampling_distance))
constants_inf.log(<str> f'calc tile size: {tile_size}') constants_inf.log(<str> f'calc tile size: {tile_size}')
res = self.split_to_tiles(frame, original_media_name, tile_size, ai_config.big_image_tile_overlap_percent) res = self.split_to_tiles(frame, original_media_name, tile_size, ai_config.big_image_tile_overlap_percent)
for tile_frame, omn, tile_name in res: for tile_frame, omn, tile_name in res:
@@ -362,8 +380,8 @@ cdef class Inference:
cdef split_to_tiles(self, frame, str media_stem, tile_size, overlap_percent): cdef split_to_tiles(self, frame, str media_stem, tile_size, overlap_percent):
constants_inf.log(<str>f'splitting image {media_stem} to tiles...') constants_inf.log(<str>f'splitting image {media_stem} to tiles...')
img_h, img_w, _ = frame.shape img_h, img_w, _ = frame.shape
stride_w = int(tile_size * (1 - overlap_percent / 100)) stride_w = max(1, int(tile_size * (1 - overlap_percent / 100)))
stride_h = int(tile_size * (1 - overlap_percent / 100)) stride_h = max(1, int(tile_size * (1 - overlap_percent / 100)))
results = [] results = []
original_media_name = media_stem original_media_name = media_stem
+48 -3
View File
@@ -155,6 +155,14 @@ class HealthResponse(BaseModel):
errorMessage: Optional[str] = None errorMessage: Optional[str] = None
class CameraConfigDto(BaseModel):
focal_length: float = 24
sensor_width: float = 23.5
current_zoom: float = 1
current_angle: float = 90
current_height: Optional[float] = None
class AIConfigDto(BaseModel): class AIConfigDto(BaseModel):
frame_period_recognition: int = 4 frame_period_recognition: int = 4
frame_recognition_seconds: int = 2 frame_recognition_seconds: int = 2
@@ -164,6 +172,7 @@ class AIConfigDto(BaseModel):
tracking_intersection_threshold: float = 0.6 tracking_intersection_threshold: float = 0.6
model_batch_size: int = 8 model_batch_size: int = 8
big_image_tile_overlap_percent: int = 20 big_image_tile_overlap_percent: int = 20
camera_config: Optional[CameraConfigDto] = None
altitude: Optional[float] = None altitude: Optional[float] = None
focal_length: float = 24 focal_length: float = 24
sensor_width: float = 23.5 sensor_width: float = 23.5
@@ -218,9 +227,12 @@ _AI_SETTINGS_FIELD_KEYS = (
"BigImageTileOverlapPercent", "BigImageTileOverlapPercent",
), ),
), ),
)
_CAMERA_SETTINGS_FIELD_KEYS = (
( (
"altitude", "current_height",
("altitude", "Altitude"), ("current_height", "currentHeight", "CurrentHeight", "altitude", "Altitude"),
), ),
( (
"focal_length", "focal_length",
@@ -230,6 +242,14 @@ _AI_SETTINGS_FIELD_KEYS = (
"sensor_width", "sensor_width",
("sensor_width", "sensorWidth", "SensorWidth"), ("sensor_width", "sensorWidth", "SensorWidth"),
), ),
(
"current_zoom",
("current_zoom", "currentZoom", "CurrentZoom"),
),
(
"current_angle",
("current_angle", "currentAngle", "CurrentAngle"),
),
) )
@@ -249,6 +269,21 @@ def _merged_annotation_settings_payload(raw: object) -> dict:
if key in merged and merged[key] is not None: if key in merged and merged[key] is not None:
out[snake] = merged[key] out[snake] = merged[key]
break break
camera_source = {}
for key in ("camera_config", "cameraConfig", "cameraSettings"):
value = raw.get(key)
if isinstance(value, dict):
camera_source.update(value)
camera_merged = dict(merged)
camera_merged.update(camera_source)
camera_config = {}
for snake, aliases in _CAMERA_SETTINGS_FIELD_KEYS:
for key in aliases:
if key in camera_merged and camera_merged[key] is not None:
camera_config[snake] = camera_merged[key]
break
if camera_config:
out["camera_config"] = camera_config
return out return out
@@ -306,7 +341,13 @@ def _resolve_media_for_detect(
cfg.update(_merged_annotation_settings_payload(raw)) cfg.update(_merged_annotation_settings_payload(raw))
if override is not None: if override is not None:
for k, v in override.model_dump(exclude_defaults=True).items(): for k, v in override.model_dump(exclude_defaults=True).items():
cfg[k] = v if k == "camera_config" and isinstance(v, dict):
existing = cfg.get("camera_config")
camera_cfg = dict(existing) if isinstance(existing, dict) else {}
camera_cfg.update(v)
cfg[k] = camera_cfg
else:
cfg[k] = v
media_path = annotations_client.fetch_media_path(media_id, bearer) media_path = annotations_client.fetch_media_path(media_id, bearer)
if not media_path: if not media_path:
raise HTTPException( raise HTTPException(
@@ -515,6 +556,8 @@ async def detect_image(
_post_annotation_to_service(token_mgr, content_hash, annotation, dtos) _post_annotation_to_service(token_mgr, content_hash, annotation, dtos)
def run_sync(): def run_sync():
if not inf.is_engine_ready:
raise RuntimeError("Detection service unavailable")
inf.run_detect_image(image_bytes, ai_cfg, media_name, on_annotation) inf.run_detect_image(image_bytes, ai_cfg, media_name, on_annotation)
try: try:
@@ -609,6 +652,8 @@ async def detect_video_upload(
_post_annotation_to_service(token_mgr, mid, annotation, dtos) _post_annotation_to_service(token_mgr, mid, annotation, dtos)
def run_inference(): def run_inference():
if not inf.is_engine_ready:
raise RuntimeError("Detection service unavailable")
inf.run_detect_video_stream(buffer, ai_cfg, media_name, on_annotation, lambda *_: None) inf.run_detect_video_stream(buffer, ai_cfg, media_name, on_annotation, lambda *_: None)
inference_future = loop.run_in_executor(executor, run_inference) inference_future = loop.run_in_executor(executor, run_inference)
+37
View File
@@ -1,24 +1,61 @@
def test_ai_config_from_dict_defaults(): def test_ai_config_from_dict_defaults():
# Arrange
from inference import ai_config_from_dict from inference import ai_config_from_dict
# Act
cfg = ai_config_from_dict({}) cfg = ai_config_from_dict({})
# Assert
assert cfg.model_batch_size == 8 assert cfg.model_batch_size == 8
assert cfg.frame_period_recognition == 4 assert cfg.frame_period_recognition == 4
assert cfg.frame_recognition_seconds == 2 assert cfg.frame_recognition_seconds == 2
assert cfg.has_camera_config is False
assert cfg.has_altitude is False assert cfg.has_altitude is False
def test_ai_config_from_dict_altitude_override_sets_flag(): def test_ai_config_from_dict_altitude_override_sets_flag():
# Arrange
from inference import ai_config_from_dict from inference import ai_config_from_dict
# Act
cfg = ai_config_from_dict({"altitude": 400}) cfg = ai_config_from_dict({"altitude": 400})
# Assert
assert cfg.has_camera_config is True
assert cfg.has_altitude is True assert cfg.has_altitude is True
assert cfg.altitude == 400 assert cfg.altitude == 400
assert cfg.current_height == 400
def test_ai_config_from_dict_overrides(): def test_ai_config_from_dict_overrides():
# Arrange
from inference import ai_config_from_dict from inference import ai_config_from_dict
# Act
cfg = ai_config_from_dict({"model_batch_size": 4, "probability_threshold": 0.5}) cfg = ai_config_from_dict({"model_batch_size": 4, "probability_threshold": 0.5})
# Assert
assert cfg.model_batch_size == 4 assert cfg.model_batch_size == 4
assert cfg.probability_threshold == 0.5 assert cfg.probability_threshold == 0.5
def test_ai_config_from_dict_camera_config_sets_physical_filter_fields():
# Arrange
from inference import ai_config_from_dict
# Act
cfg = ai_config_from_dict(
{
"camera_config": {
"focal_length": 35,
"sensor_width": 36,
"current_zoom": 2,
"current_angle": 80,
"current_height": 300,
}
}
)
# Assert
assert cfg.has_camera_config is True
assert cfg.current_height == 300
assert cfg.focal_length == 35
assert cfg.sensor_width == 36
assert cfg.current_zoom == 2
assert cfg.current_angle == 80
+38 -5
View File
@@ -61,7 +61,9 @@ def test_merged_annotation_settings_pascal_case():
# Assert # Assert
assert out["frame_period_recognition"] == 5 assert out["frame_period_recognition"] == 5
assert out["probability_threshold"] == 0.4 assert out["probability_threshold"] == 0.4
assert out["altitude"] == 300 assert out["camera_config"]["current_height"] == 300
assert out["camera_config"]["focal_length"] == 35
assert out["camera_config"]["sensor_width"] == 36
def test_merged_annotation_nested_sections(): def test_merged_annotation_nested_sections():
@@ -76,7 +78,7 @@ def test_merged_annotation_nested_sections():
out = _merged_annotation_settings_payload(raw) out = _merged_annotation_settings_payload(raw)
# Assert # Assert
assert out["model_batch_size"] == 4 assert out["model_batch_size"] == 4
assert out["altitude"] == 100 assert out["camera_config"]["current_height"] == 100
def test_resolve_media_for_detect_uses_api_path_and_defaults_when_api_empty(): def test_resolve_media_for_detect_uses_api_path_and_defaults_when_api_empty():
@@ -105,7 +107,7 @@ def test_resolve_media_for_detect_override_wins():
mock_ann = MagicMock() mock_ann = MagicMock()
mock_ann.fetch_user_ai_settings.return_value = { mock_ann.fetch_user_ai_settings.return_value = {
"probabilityThreshold": 0.2, "probabilityThreshold": 0.2,
"altitude": 500, "camera_config": {"current_height": 500},
} }
mock_ann.fetch_media_path.return_value = "/m/v.mp4" mock_ann.fetch_media_path.return_value = "/m/v.mp4"
with patch("main.annotations_client", mock_ann): with patch("main.annotations_client", mock_ann):
@@ -113,11 +115,42 @@ def test_resolve_media_for_detect_override_wins():
cfg, path = main._resolve_media_for_detect("vid-1", tm, override) cfg, path = main._resolve_media_for_detect("vid-1", tm, override)
# Assert # Assert
assert cfg["probability_threshold"] == 0.99 assert cfg["probability_threshold"] == 0.99
assert cfg["altitude"] == 500 assert cfg["camera_config"]["current_height"] == 500
assert path == "/m/v.mp4" assert path == "/m/v.mp4"
assert "paths" not in cfg assert "paths" not in cfg
def test_resolve_media_for_detect_merges_camera_config_override():
# Arrange
import main
tm = main.TokenManager(_access_jwt(), "")
override = main.AIConfigDto(
camera_config=main.CameraConfigDto(current_height=500)
)
mock_ann = MagicMock()
mock_ann.fetch_user_ai_settings.return_value = {
"camera_config": {
"focal_length": 35,
"sensor_width": 36,
"current_zoom": 2,
"current_angle": 80,
"current_height": 300,
}
}
mock_ann.fetch_media_path.return_value = "/m/v.mp4"
with patch("main.annotations_client", mock_ann):
# Act
cfg, path = main._resolve_media_for_detect("vid-1", tm, override)
# Assert
assert cfg["camera_config"]["current_height"] == 500
assert cfg["camera_config"]["focal_length"] == 35
assert cfg["camera_config"]["sensor_width"] == 36
assert cfg["camera_config"]["current_zoom"] == 2
assert cfg["camera_config"]["current_angle"] == 80
assert path == "/m/v.mp4"
def test_resolve_media_for_detect_omits_altitude_when_not_provided(): def test_resolve_media_for_detect_omits_altitude_when_not_provided():
# Arrange # Arrange
import main import main
@@ -130,7 +163,7 @@ def test_resolve_media_for_detect_omits_altitude_when_not_provided():
# Act # Act
cfg, path = main._resolve_media_for_detect("vid-2", tm, None) cfg, path = main._resolve_media_for_detect("vid-2", tm, None)
# Assert # Assert
assert "altitude" not in cfg assert "camera_config" not in cfg
assert cfg["probability_threshold"] == 0.2 assert cfg["probability_threshold"] == 0.2
assert path == "/m/v.mp4" assert path == "/m/v.mp4"