Compare commits
12
Commits
31476d2fa7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb9be7a402 | ||
|
|
9ea6820a37 | ||
|
|
e5a4658ae0 | ||
|
|
90038da298 | ||
|
|
ea370b0ffd | ||
|
|
32921ea3f3 | ||
|
|
1d1199fa04 | ||
|
|
139cff7474 | ||
|
|
501ffa88a4 | ||
|
|
30515086d3 | ||
|
|
ef17a9105b | ||
|
|
aca5683487 |
No files matched your search
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
name: backprop
|
||||||
|
description: |
|
||||||
|
Bug → spec protocol. When a bug is found or a test fails, trace the cause,
|
||||||
|
decide whether a new §V invariant would catch recurrence, append to §B.
|
||||||
|
This is the one non-obvious thing SDD does that plan-then-execute doesn't.
|
||||||
|
Triggers on test failure, bug report, post-mortem, or explicit user ask.
|
||||||
|
---
|
||||||
|
|
||||||
|
# backprop — bug → spec
|
||||||
|
|
||||||
|
Plan-then-execute fixes the code & forgets.
|
||||||
|
SDD fixes the code AND edits spec so recurrence is impossible.
|
||||||
|
That edit is backprop.
|
||||||
|
|
||||||
|
## WHEN TO BACKPROP
|
||||||
|
|
||||||
|
- Test failed at `/build` verification.
|
||||||
|
- User reports bug.
|
||||||
|
- Post-mortem after production incident.
|
||||||
|
- `/check` flags VIOLATE with root cause found.
|
||||||
|
|
||||||
|
## SIX STEPS
|
||||||
|
|
||||||
|
### 1. TRACE
|
||||||
|
Read failure output / bug report.
|
||||||
|
Find exact file:line of wrong behavior.
|
||||||
|
Name root cause in one caveman sentence.
|
||||||
|
|
||||||
|
### 2. ANALYZE
|
||||||
|
Ask three questions:
|
||||||
|
- Would a new §V invariant catch this class of bug? (most common: yes)
|
||||||
|
- Is §I wrong — did spec claim shape the code cannot deliver? (sometimes)
|
||||||
|
- Is §T wrong — did we build the wrong thing? (rare but real)
|
||||||
|
|
||||||
|
### 3. PROPOSE
|
||||||
|
Draft the spec change. Never skip §B; §V/§I/§T are case-by-case.
|
||||||
|
|
||||||
|
Template:
|
||||||
|
```
|
||||||
|
§B row: B<next>|<date>|<root cause>|V<N>
|
||||||
|
§V line: V<next>: <testable rule that would have caught it>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
§B row: B3|2026-04-20|refund job ran twice on retry|V7
|
||||||
|
§V line: V7: ∀ refund → idempotency key check before charge reversal
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. GENERATE TEST
|
||||||
|
New invariant without test = lie. Add failing test first.
|
||||||
|
Name test so it cites the invariant: `TestV7_RefundIdempotent`.
|
||||||
|
|
||||||
|
### 5. VERIFY
|
||||||
|
Fix code. Run test. Must pass. Run full suite. Must not regress.
|
||||||
|
|
||||||
|
### 6. LOG
|
||||||
|
Commit spec edit + test + code fix together.
|
||||||
|
Commit msg: `backprop §B.<n> + §V.<N>: <one-line cause>`.
|
||||||
|
|
||||||
|
## WHAT MAKES A GOOD INVARIANT
|
||||||
|
|
||||||
|
- Testable in code (grep-able or assert-able).
|
||||||
|
- Scoped to a behavior, not a file.
|
||||||
|
- Stated positively when possible (`! hold` over `⊥ forbid`).
|
||||||
|
- References §I surface where it applies.
|
||||||
|
|
||||||
|
**Bad**: V8: code should be correct.
|
||||||
|
**Good**: V8: ∀ pg_query ! params interpolated via driver, ⊥ string concat.
|
||||||
|
|
||||||
|
## WHEN NOT TO ADD §V
|
||||||
|
|
||||||
|
- Bug was purely mechanical typo with no class (`i++` vs `i--` in throwaway).
|
||||||
|
- Fix is a one-time migration.
|
||||||
|
- Root cause is external dep (upgrade deps instead, note in §C).
|
||||||
|
|
||||||
|
Still append §B entry — record that this failure mode was considered. Future bug with same smell → §B search shows precedent.
|
||||||
|
|
||||||
|
## OUTPUT SHAPE
|
||||||
|
|
||||||
|
Every backprop run produces:
|
||||||
|
1. §B entry (always).
|
||||||
|
2. §V entry (usually).
|
||||||
|
3. Test file (when §V added).
|
||||||
|
4. Code fix.
|
||||||
|
5. One commit.
|
||||||
|
|
||||||
|
No dashboards. No log files. SPEC.md + git is the full history.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
name: build
|
||||||
|
description: |
|
||||||
|
Plan-then-execute implementation against SPEC.md. Native single-thread
|
||||||
|
loop, no sub-agents. On test or build failure, auto-invokes the backprop
|
||||||
|
skill before retrying — a failed verification always considers whether
|
||||||
|
a new §V invariant would prevent recurrence. Triggers when the user asks
|
||||||
|
to build, implement, execute the spec, or tackle a specific §T task
|
||||||
|
(`build §T.3`, `build --next`, `implement next task`, `run the build`).
|
||||||
|
Expects SPEC.md to exist; if not, defers to the spec skill.
|
||||||
|
---
|
||||||
|
|
||||||
|
# build — implement spec
|
||||||
|
|
||||||
|
Single-thread native plan→execute. You are main Claude. No swarm.
|
||||||
|
|
||||||
|
## LOAD
|
||||||
|
|
||||||
|
1. Read `SPEC.md`. If missing → tell user to invoke the spec skill first. Stop.
|
||||||
|
2. Read `FORMAT.md` once if not loaded.
|
||||||
|
3. Read §R if present — external facts the build must honor, ⊥ re-derive or contradict.
|
||||||
|
4. Parse invocation args:
|
||||||
|
- `§T.n` → that task only
|
||||||
|
- `--next` → lowest-numbered row with status `.` or `~`
|
||||||
|
- `--all` or empty → every `.` row in §T order
|
||||||
|
|
||||||
|
High blast radius (shared module, auth, data, money, public §I)? Run `/review` first. Trivial & reversible? Skip planning ceremony, just do step EXECUTE.
|
||||||
|
|
||||||
|
## PLAN
|
||||||
|
|
||||||
|
Native plan mode — you delegate to it, you do not reinvent task breakdown. For chosen task(s):
|
||||||
|
|
||||||
|
1. Cite every §V invariant that applies. Plan must respect all.
|
||||||
|
2. Cite every §I interface touched. Plan must preserve shape.
|
||||||
|
3. List files to create / edit.
|
||||||
|
4. **Verification contract** — name the EXACT test(s) / acceptance criteria that
|
||||||
|
prove each §V touched. Which test, not "add tests". "Do TDD" alone backfires;
|
||||||
|
the spec says *what to check*. Each §V touched → a named test that fails first.
|
||||||
|
5. Name verification command (test, build, lint) — this is the external oracle. Green = done; ⊥ "looks done".
|
||||||
|
|
||||||
|
Show plan. Wait for user OK unless auto mode.
|
||||||
|
|
||||||
|
## EXECUTE
|
||||||
|
|
||||||
|
Per task in order:
|
||||||
|
|
||||||
|
1. Flip §T.n status cell `.` → `~`. Just write to SPEC.md.
|
||||||
|
2. Edit code per plan.
|
||||||
|
3. Run verification command.
|
||||||
|
4. **Pass** → flip `~` → `x`. Next task.
|
||||||
|
5. **Fail** → invoke backprop skill. Do NOT retry blindly.
|
||||||
|
|
||||||
|
## FAIL → BACKPROP
|
||||||
|
|
||||||
|
On test/build failure:
|
||||||
|
|
||||||
|
1. Read failure output.
|
||||||
|
2. Ask: is failure (a) my code bug, (b) spec wrong, or (c) unspecified edge case?
|
||||||
|
3. If (a) → fix code, re-run. No spec change.
|
||||||
|
4. If (b) or (c) → invoke spec skill with `bug: <cause>` first, let it update §V and §B, then resume build against updated spec.
|
||||||
|
|
||||||
|
Rule: never silently fix root-cause without considering backprop. §B is the memory that stops recurrence.
|
||||||
|
|
||||||
|
## WRITE POLICY
|
||||||
|
|
||||||
|
- Only flip §T status. No other SPEC.md edits from build.
|
||||||
|
- Other spec edits → invoke spec skill.
|
||||||
|
- Commit after each §T completes. Message: `T<n>: <goal line>` + §V cites.
|
||||||
|
|
||||||
|
## VERIFICATION
|
||||||
|
|
||||||
|
Task `x` only if:
|
||||||
|
- Verification command (the oracle) exits 0.
|
||||||
|
- Every §V touched has its named test from the verification contract, and it passes.
|
||||||
|
- No §V invariant regressed (run full test suite at end).
|
||||||
|
|
||||||
|
## NON-GOALS
|
||||||
|
|
||||||
|
- No sub-agents. No parallel workers. Main thread only.
|
||||||
|
- No progress dashboards. `cat SPEC.md | grep §T` is the dashboard.
|
||||||
|
- No speculative work beyond chosen task scope.
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
name: caveman
|
||||||
|
description: |
|
||||||
|
Caveman encoding for SPEC.md and spec-adjacent writes. Loaded by /spec, /build,
|
||||||
|
/check. Cuts tokens ~75% vs prose while staying precise. Triggers on any write
|
||||||
|
to SPEC.md or when user says "caveman", "compress this", "be brief".
|
||||||
|
---
|
||||||
|
|
||||||
|
# caveman — spec encoding
|
||||||
|
|
||||||
|
Applies to SPEC.md writes, spec-referencing prose, backprop entries.
|
||||||
|
Does NOT apply to code, error strings, commit messages, PR descriptions.
|
||||||
|
|
||||||
|
## GRAMMAR
|
||||||
|
|
||||||
|
- Drop articles (a, an, the).
|
||||||
|
- Drop filler (just, really, basically, simply, actually).
|
||||||
|
- Drop aux verbs where fragment works (is, are, was, were, being).
|
||||||
|
- Drop pleasantries.
|
||||||
|
- No hedging (skip "might", "perhaps", "could be worth").
|
||||||
|
- Fragments fine.
|
||||||
|
- Short synonyms: fix > implement, big > extensive, run > execute.
|
||||||
|
|
||||||
|
## SYMBOLS
|
||||||
|
|
||||||
|
Prefer over words:
|
||||||
|
|
||||||
|
```
|
||||||
|
→ leads to / becomes / on <x>
|
||||||
|
∴ therefore / fix
|
||||||
|
∀ for all / every
|
||||||
|
∃ exists / some
|
||||||
|
! must / required
|
||||||
|
? may / optional / unknown
|
||||||
|
⊥ never / forbidden / nil
|
||||||
|
≠ not equal
|
||||||
|
∈ in
|
||||||
|
∉ not in
|
||||||
|
≤ at most
|
||||||
|
≥ at least
|
||||||
|
& and
|
||||||
|
| or
|
||||||
|
§ section reference
|
||||||
|
```
|
||||||
|
|
||||||
|
## PRESERVE VERBATIM
|
||||||
|
|
||||||
|
Never compress:
|
||||||
|
|
||||||
|
- Code blocks, snippets, one-liners with backticks.
|
||||||
|
- Paths: `src/auth/mw.go`.
|
||||||
|
- URLs.
|
||||||
|
- Identifiers: function names, variable names, env vars.
|
||||||
|
- Numbers and versions.
|
||||||
|
- Error message strings.
|
||||||
|
- SQL, regex, JSON, YAML.
|
||||||
|
- Quoted strings.
|
||||||
|
|
||||||
|
## SHAPES
|
||||||
|
|
||||||
|
**Invariant**:
|
||||||
|
```
|
||||||
|
V<n>: <subject> <relation> <condition>
|
||||||
|
V1: ∀ req → auth check before handler
|
||||||
|
V2: token expiry ≤ current_time → reject
|
||||||
|
```
|
||||||
|
|
||||||
|
**Bug row** (pipe table under §B):
|
||||||
|
```
|
||||||
|
id|date|cause|fix
|
||||||
|
B1|2026-04-20|token `<` not `≤`|V2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Task row** (pipe table under §T):
|
||||||
|
```
|
||||||
|
id|status|task|cites
|
||||||
|
T3|x|add auth mw|V1,I.api
|
||||||
|
```
|
||||||
|
Status: `x` done, `~` wip, `.` todo. Escape literal `|` as `\|`.
|
||||||
|
|
||||||
|
**Interface**:
|
||||||
|
```
|
||||||
|
<kind>: <name> → <shape>
|
||||||
|
api: POST /x → 200 {id:string}
|
||||||
|
cmd: `foo bar <arg>` → stdout JSON
|
||||||
|
env: FOO_KEY ! set
|
||||||
|
```
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
**Bad**:
|
||||||
|
> The system should ensure that every incoming request is properly authenticated before being forwarded to its corresponding handler function.
|
||||||
|
|
||||||
|
**Good**:
|
||||||
|
> V1: ∀ req → auth check before handler
|
||||||
|
|
||||||
|
**Bad**:
|
||||||
|
> We discovered that the token expiration check in the middleware was using a strict less-than comparison operator, which meant tokens were being rejected at the exact moment of their expiry.
|
||||||
|
|
||||||
|
**Good**:
|
||||||
|
> B1: token `<` not `≤` → reject @ expiry boundary.
|
||||||
|
|
||||||
|
**Bad**:
|
||||||
|
> The POST endpoint at /x accepts a JSON body and returns a 200 response with an object containing the created id.
|
||||||
|
|
||||||
|
**Good**:
|
||||||
|
> api: POST /x → 200 {id}
|
||||||
|
|
||||||
|
## BOUNDARIES
|
||||||
|
|
||||||
|
- User asks for prose explanation → switch to normal English.
|
||||||
|
- Spec documents for external review (RFC, pitch) → normal English.
|
||||||
|
- Commit message → normal English (git readers expect it).
|
||||||
|
- Diff comment in code → normal English.
|
||||||
|
|
||||||
|
## WHEN UNSURE
|
||||||
|
|
||||||
|
If cutting a word loses a fact, keep it. Caveman is compression, not amputation.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
name: check
|
||||||
|
description: |
|
||||||
|
Read-only drift detector. Diffs SPEC.md against current code and reports
|
||||||
|
violations grouped by severity. Writes nothing — suggests remedies via
|
||||||
|
the spec or build skills but never invokes them. Triggers when the user
|
||||||
|
asks to check drift, audit the spec, verify invariants, or ask whether
|
||||||
|
code still matches the spec. Phrasings: "check drift", "audit the spec",
|
||||||
|
"does the code still match §V", "check invariants", "spec vs code".
|
||||||
|
---
|
||||||
|
|
||||||
|
# check — drift report
|
||||||
|
|
||||||
|
Pure diagnostic. Reports violations. Writes nothing. User decides remedy.
|
||||||
|
|
||||||
|
Spec drifting silently from code is the #1 SDD failure mode. check is the
|
||||||
|
detector. Run it after each `/build` and before each ship — drift caught here is
|
||||||
|
a diff; drift caught in prod is a §B.
|
||||||
|
|
||||||
|
## LOAD
|
||||||
|
|
||||||
|
1. Read `SPEC.md`. If missing → "no spec, nothing to check." Stop.
|
||||||
|
2. Parse invocation args:
|
||||||
|
- `§V` → check invariants only (default)
|
||||||
|
- `§I` → check interfaces
|
||||||
|
- `§T` → audit task status vs code
|
||||||
|
- `--all` → all three
|
||||||
|
|
||||||
|
## CHECK §V — invariants
|
||||||
|
|
||||||
|
For each V<n>:
|
||||||
|
|
||||||
|
1. Translate invariant into verifiable claim about code.
|
||||||
|
2. Grep / read relevant files.
|
||||||
|
3. Classify: **HOLD** / **VIOLATE** / **UNVERIFIABLE**.
|
||||||
|
4. Record address + file:line evidence.
|
||||||
|
|
||||||
|
## CHECK §I — interfaces
|
||||||
|
|
||||||
|
For each I item:
|
||||||
|
|
||||||
|
1. Locate implementation.
|
||||||
|
2. Classify:
|
||||||
|
- **MATCH** — shape in code = shape in spec.
|
||||||
|
- **DRIFT** — impl exists, shape differs.
|
||||||
|
- **MISSING** — impl absent.
|
||||||
|
- **EXTRA** — code exposes surface not in §I.
|
||||||
|
|
||||||
|
## CHECK §T — tasks
|
||||||
|
|
||||||
|
For each T<n>:
|
||||||
|
|
||||||
|
1. If `x`: verify claimed work present.
|
||||||
|
2. If `~`: note as in-progress.
|
||||||
|
3. If `.`: note as pending.
|
||||||
|
4. Flag `x` rows with no evidence as **STALE**.
|
||||||
|
|
||||||
|
## REPORT
|
||||||
|
|
||||||
|
Caveman. Grouped by severity.
|
||||||
|
|
||||||
|
```
|
||||||
|
## §V drift
|
||||||
|
V2 VIOLATE: auth/mw.go:47 uses `<` not `≤`. see §B.1.
|
||||||
|
V5 UNVERIFIABLE: no test covers ∀ req path.
|
||||||
|
|
||||||
|
## §I drift
|
||||||
|
I.api DRIFT: POST /x returns `{result}` not `{id}`. route.go:112.
|
||||||
|
I.cmd MISSING: `foo bar` absent from cli/*.go.
|
||||||
|
|
||||||
|
## §T drift
|
||||||
|
T3 STALE: status `x`, no middleware file exists.
|
||||||
|
|
||||||
|
## summary
|
||||||
|
2 violate. 1 missing. 1 stale. 1 unverifiable.
|
||||||
|
next: spec skill with `bug:` or fix code at cited lines.
|
||||||
|
```
|
||||||
|
|
||||||
|
## REMEDY HINTS (not actions)
|
||||||
|
|
||||||
|
End report with one-line hint per class:
|
||||||
|
- VIOLATE / DRIFT → invoke spec skill `bug: <V.n>` or fix code.
|
||||||
|
- MISSING → invoke build skill on `§T.n` if task exists; else spec skill `amend §T`.
|
||||||
|
- STALE → spec skill `amend §T` to uncheck.
|
||||||
|
- EXTRA → spec skill `amend §I` to document, or delete code.
|
||||||
|
|
||||||
|
Never invoke fixes. Report only.
|
||||||
|
|
||||||
|
## NON-GOALS
|
||||||
|
|
||||||
|
- Zero writes. No SPEC.md edits. No code edits.
|
||||||
|
- No sub-agents. Main thread reads.
|
||||||
|
- No scores, no grades. Binary per item: holds or drifts.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
name: deepen
|
||||||
|
description: |
|
||||||
|
Optional design-improvement pass for when you have spare usage to drain. Finds
|
||||||
|
the shallowest modules in the code the spec touches, researches a deeper
|
||||||
|
design, and proposes refactors that shrink interfaces and hide decisions —
|
||||||
|
behavior held constant, tests green before and after. Proposes §I/§V/§T edits,
|
||||||
|
never silent rewrites. Triggers when the user says "deepen this", "improve the
|
||||||
|
design", "this module feels shallow", "pull complexity down", "use spare
|
||||||
|
budget on the codebase", or invokes /ck:deepen. Leans on the codebase-design
|
||||||
|
skill's deep-module vocabulary when present.
|
||||||
|
---
|
||||||
|
|
||||||
|
# deepen — make modules deep
|
||||||
|
|
||||||
|
**Behavior is sacred: tests green before AND after. Every change shrinks an interface or hides a decision — deepen, don't churn.**
|
||||||
|
|
||||||
|
A **deep module** hides a lot behind a small interface; a **shallow** one's
|
||||||
|
interface costs as much to use as writing the code yourself. Complexity =
|
||||||
|
dependencies + obscurity, and it compounds. Deepen spends spare usage paying
|
||||||
|
that down *before* it becomes a §B. Run it when the build is green & you have
|
||||||
|
budget to drain — not under deadline.
|
||||||
|
|
||||||
|
## WHEN TO DEEPEN
|
||||||
|
|
||||||
|
- Build is green, tests pass, & you have token budget spare.
|
||||||
|
- A module's interface feels as complex as its implementation (shallow smell).
|
||||||
|
- The same change keeps touching many files (change amplification).
|
||||||
|
- User explicitly asks to improve design quality.
|
||||||
|
|
||||||
|
⊥ run mid-feature or under pressure. Deepen is the deliberate pass, not the reflex.
|
||||||
|
|
||||||
|
## FIVE STEPS
|
||||||
|
|
||||||
|
### 1. PICK THE SHALLOW
|
||||||
|
Scan the modules the spec touches. Rank by shallowness — interface surface vs
|
||||||
|
work done. Pick the **one** worst offender. Tells:
|
||||||
|
- Pass-through method that only forwards to one other (shallow layer).
|
||||||
|
- Caller must set 5 flags right to use it (config leakage).
|
||||||
|
- Same abstraction repeated at two layers (no information hiding).
|
||||||
|
- A `?` or §B that traces back to a confusing interface.
|
||||||
|
|
||||||
|
One module per pass. Deepening is surgical, ⊥ a codebase sweep.
|
||||||
|
|
||||||
|
### 2. DIAGNOSE
|
||||||
|
Name the design defect in caveman, citing file:line:
|
||||||
|
> src/auth/token.go: 6-arg ctor leaks rotation policy to every caller. shallow.
|
||||||
|
Complexity is real only if it shows: change amplification, high cognitive load,
|
||||||
|
or an unknown-unknown (caller must know a hidden fact to call it right).
|
||||||
|
|
||||||
|
### 3. RESEARCH THE DEEPENING
|
||||||
|
What does a deeper version look like? Pull a known pattern (hand to **research**
|
||||||
|
for the external case → §R) or derive from the codebase's own better modules.
|
||||||
|
Moves that deepen:
|
||||||
|
- **Pull complexity down** — hide the hard part inside, give callers the simple path.
|
||||||
|
- **Define errors out of existence** — design the interface so the edge can't occur.
|
||||||
|
- **Information hiding** — one decision, one module; callers don't learn it.
|
||||||
|
- **General-purpose interface** over a pile of special-case methods.
|
||||||
|
|
||||||
|
### 4. PROPOSE
|
||||||
|
Draft the change as spec edits, not a silent rewrite:
|
||||||
|
- New/simpler §I shape for the module.
|
||||||
|
- §V that locks the deepened invariant so a future build can't re-shallow it.
|
||||||
|
- §T refactor row(s), each citing the §V/§I it serves.
|
||||||
|
Hand to **spec** to write. Show the before/after interface so the user sees the shrink.
|
||||||
|
|
||||||
|
### 5. VERIFY BEHAVIOR HELD
|
||||||
|
Refactor ≠ rewrite. Full suite green before you start AND after. A deepening that
|
||||||
|
changes behavior is a feature in disguise — stop, route through `/spec` + `/build`.
|
||||||
|
New interface gets a test proving the old callers still work.
|
||||||
|
|
||||||
|
## WHEN TO STOP
|
||||||
|
|
||||||
|
Done when the chosen module's interface is strictly smaller, its hidden decision
|
||||||
|
no longer leaks, tests are green, and §I/§V record the new shape. One module
|
||||||
|
deepened beats five churned. Budget left → pick the next shallowest, fresh pass.
|
||||||
|
|
||||||
|
## BOUNDARIES
|
||||||
|
|
||||||
|
- ⊥ change behavior. Green before, green after. Pure structure.
|
||||||
|
- ⊥ write SPEC.md. Propose §I/§V/§T; spec writes.
|
||||||
|
- ⊥ deepen more than one module per pass.
|
||||||
|
- ⊥ run under deadline or mid-feature. This is the spare-budget pass.
|
||||||
|
- ⊥ add abstraction for single-use code. A deep module earns its hiding; a speculative one is just more surface.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: grill
|
||||||
|
description: |
|
||||||
|
Calibrated interrogation of a fuzzy idea before it becomes a spec. Asks one
|
||||||
|
question at a time, recommends an answer, and lands each answer in §G (goal)
|
||||||
|
or §C (constraints) — unknowns parked as `?` items, never guessed. The
|
||||||
|
cheapest place to kill a bad idea is before §T exists. Triggers when the user
|
||||||
|
has a vague idea, says "grill me", "stress-test this", "challenge my plan",
|
||||||
|
"interview me before I spec", or invokes /ck:grill. Defers the actual write to
|
||||||
|
the spec skill.
|
||||||
|
---
|
||||||
|
|
||||||
|
# grill — sharpen idea before spec
|
||||||
|
|
||||||
|
**One question at a time. Every answer lands in a § or gets parked `?`. Never guess a constraint into existence.**
|
||||||
|
|
||||||
|
Plan-then-execute guesses the fuzzy parts & builds the wrong thing.
|
||||||
|
Grill drags the fuzz into §G/§C *before* a single §T row exists.
|
||||||
|
A bad assumption caught here costs one question. Caught in §B it costs a bug.
|
||||||
|
|
||||||
|
## WHEN TO GRILL
|
||||||
|
|
||||||
|
- Idea is one sentence & you can feel the holes.
|
||||||
|
- Multiple readings of the goal exist & you are about to pick one silently.
|
||||||
|
- Before `/spec new` on anything non-trivial.
|
||||||
|
- User asks to be challenged / stress-tested.
|
||||||
|
|
||||||
|
Skip for a typo or a one-line fix. Grill scales to uncertainty, ⊥ to ego.
|
||||||
|
|
||||||
|
## CALIBRATE FIRST
|
||||||
|
|
||||||
|
One opening read, not a quiz:
|
||||||
|
1. How well does user know this domain? (sets question depth)
|
||||||
|
2. How locked is the idea? (exploring vs committed)
|
||||||
|
3. Pressure wanted: light / normal / brutal.
|
||||||
|
|
||||||
|
Match it. Brutal grilling on a half-formed idea just demoralizes. Light
|
||||||
|
grilling on a committed plan misses the load-bearing flaw.
|
||||||
|
|
||||||
|
## QUESTION LADDER
|
||||||
|
|
||||||
|
Climb in order. Each rung, ask **one** question, **recommend** an answer, wait.
|
||||||
|
|
||||||
|
1. **Goal** — what must the code *do*, in one line? (→ §G)
|
||||||
|
2. **Done** — how do we know it works? name the observable. (→ §C / future §V)
|
||||||
|
3. **Boundary** — what is explicitly out of scope? (→ §C)
|
||||||
|
4. **Lock** — what tech/lib/pattern is non-negotiable? what is forbidden? (→ §C)
|
||||||
|
5. **Surface** — what does the outside world touch — cmd, api, file, env? (→ §I)
|
||||||
|
6. **Edge** — the one input that breaks the happy path? (→ future §V)
|
||||||
|
7. **Unknown** — what do we *not* know yet? (→ park as `?` §C bullet)
|
||||||
|
|
||||||
|
Stop climbing the moment the spec would be unambiguous. Do not ask all seven by reflex.
|
||||||
|
|
||||||
|
## ANSWER FORMAT
|
||||||
|
|
||||||
|
Each question carries a recommended answer so the user can grunt "yes" & move:
|
||||||
|
|
||||||
|
> Q: auth — session cookie or JWT?
|
||||||
|
> rec: JWT — stateless, you named horizontal scaling as a §C.
|
||||||
|
> (a) JWT (b) cookie (c) something else?
|
||||||
|
|
||||||
|
## HANDOFF
|
||||||
|
|
||||||
|
When done, emit a compact block — goal line, constraint bullets, surfaced
|
||||||
|
unknowns as `?` — and hand to the **spec** skill to write §G/§C. Grill proposes;
|
||||||
|
spec is the sole mutator. Never write SPEC.md directly.
|
||||||
|
|
||||||
|
## WHEN TO STOP
|
||||||
|
|
||||||
|
Done when ALL hold:
|
||||||
|
- §G is one line, one reading, zero "or maybe".
|
||||||
|
- §C covers every non-negotiable the user stated or implied.
|
||||||
|
- Every blocking unknown is either answered or parked as an explicit `?`.
|
||||||
|
|
||||||
|
Unresolved blocking unknown that needs the outside world → recommend `/research`, not a guess.
|
||||||
|
|
||||||
|
## BOUNDARIES
|
||||||
|
|
||||||
|
- ⊥ make product decisions for the user. Recommend, never decide.
|
||||||
|
- ⊥ write SPEC.md. Hand structured answers to spec.
|
||||||
|
- ⊥ ask in bulk. One question, one recommendation, wait.
|
||||||
|
- ⊥ grill a trivial change. Right-size or skip.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
name: research
|
||||||
|
description: |
|
||||||
|
Gather external knowledge the spec needs and distill it into §R — the durable
|
||||||
|
research log — so build grounds in facts instead of hallucinating library
|
||||||
|
behavior. Each finding cites a source; unsourced claims are flagged, never
|
||||||
|
written as fact. Triggers when a spec decision hinges on a library/API/best
|
||||||
|
practice the agent is unsure of, when the user says "research this", "what's
|
||||||
|
the best lib for…", "check current best practice", or invokes /ck:research.
|
||||||
|
Defers the §R write to the spec skill.
|
||||||
|
---
|
||||||
|
|
||||||
|
# research — external knowledge → §R
|
||||||
|
|
||||||
|
**Every finding cites a source. No source → flag it `?`, never write a guess as fact.**
|
||||||
|
|
||||||
|
"Process without library context gives you well-organized hallucinations."
|
||||||
|
Build invents a plausible-but-wrong API & §B fills with avoidable bugs.
|
||||||
|
Research is the external oracle: pull the real fact once, log it caveman, never re-derive.
|
||||||
|
|
||||||
|
## WHEN TO RESEARCH
|
||||||
|
|
||||||
|
- A §C/§I/§V decision hinges on a lib, API, version, or pattern you are unsure of.
|
||||||
|
- You are about to assume how an external dependency behaves.
|
||||||
|
- The idea touches a domain with real prior art (auth, payments, crypto, rate-limit).
|
||||||
|
- `/grill` parked a `?` that the outside world must answer.
|
||||||
|
|
||||||
|
Skip when the build touches only code you already wrote. Research scales to the unknown, ⊥ to habit.
|
||||||
|
|
||||||
|
## FOUR STEPS
|
||||||
|
|
||||||
|
### 1. SCOPE
|
||||||
|
Turn the unknown into 1-3 concrete questions. Vague "research auth" → "JWT lib
|
||||||
|
for Node ESM, maintained?" + "refresh-token rotation: current best practice?".
|
||||||
|
A scoped question gets a citable answer; a vague one gets an essay.
|
||||||
|
|
||||||
|
### 2. GATHER
|
||||||
|
Use web search / docs tools. Prefer primary sources: official docs, the repo,
|
||||||
|
the RFC, the paper. Two independent sources beat one confident blog. For a big
|
||||||
|
sweep, spawn a sub-agent so the raw pages never touch this context — it returns
|
||||||
|
only the distilled finding + source.
|
||||||
|
|
||||||
|
### 3. DISTILL
|
||||||
|
Crush each answer to one caveman line + its source. Drop the prose. The §R row
|
||||||
|
is the memory; the tab you read is not.
|
||||||
|
|
||||||
|
> R3|refresh token|rotate on use, revoke family on reuse-detect|datatracker.ietf.org/doc/html/rfc6819#section-5.2.2.3
|
||||||
|
|
||||||
|
### 4. HAND OFF
|
||||||
|
Emit the §R rows & hand to the **spec** skill to append. If a finding changes a
|
||||||
|
constraint or interface, note the §C/§I edit for spec too. Research proposes;
|
||||||
|
spec writes.
|
||||||
|
|
||||||
|
## SOURCE DISCIPLINE
|
||||||
|
|
||||||
|
- Cite a URL, repo, RFC, or paper per row. Verbatim identifiers/versions.
|
||||||
|
- Could not verify → write the row but flag `?` in the finding & say so. An
|
||||||
|
unverified claim labeled honestly is fine; one disguised as fact is a future §B.
|
||||||
|
- Conflicting sources → log both, let the user pick. ⊥ silently average them.
|
||||||
|
|
||||||
|
## WHEN TO STOP
|
||||||
|
|
||||||
|
Done when every scoped question has a sourced §R row (or an honest `?`), and no
|
||||||
|
build decision still rests on an unchecked assumption. ⊥ research past the
|
||||||
|
questions you scoped — that is just burning the attention budget.
|
||||||
|
|
||||||
|
## BOUNDARIES
|
||||||
|
|
||||||
|
- ⊥ write SPEC.md. Hand §R rows to spec.
|
||||||
|
- ⊥ write a finding as fact without a source.
|
||||||
|
- ⊥ dump raw pages into context or §R. Distill or it does not land.
|
||||||
|
- ⊥ research what you can read in the repo. Local truth > web guess.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
name: review
|
||||||
|
description: |
|
||||||
|
Adversarial senior review of the spec before any code is written. Constructs a
|
||||||
|
skeptical reviewer whose authority comes from the codebase, §R research, and
|
||||||
|
live best-practice — then tries to REFUTE the spec, not rubber-stamp it. Every
|
||||||
|
finding cites evidence (file:line or source); unverifiable ones are flagged.
|
||||||
|
Survivors harden §V; the run ends in an explicit go / no-go gate. Triggers
|
||||||
|
before building anything high-blast-radius, when the user says "review the
|
||||||
|
spec", "red-team this", "is this plan sound", "senior review", or invokes
|
||||||
|
/ck:review.
|
||||||
|
---
|
||||||
|
|
||||||
|
# review — refute the spec before build
|
||||||
|
|
||||||
|
**Every finding cites evidence — file:line or a source. No evidence → flag `[unverified]`. Default to refuted: a flaw you cannot prove is a flaw you note, not one you wave through.**
|
||||||
|
|
||||||
|
An LLM cannot self-correct on its own judgment — left alone it drifts or
|
||||||
|
degrades. Review fixes that the only way that works: a *separate* skeptic
|
||||||
|
anchored to an *external oracle* — the code, §R, the test suite, the docs.
|
||||||
|
"Looks good" is not a review. A refutation attempt is.
|
||||||
|
|
||||||
|
## WHEN TO REVIEW
|
||||||
|
|
||||||
|
- Before `/build` on a high-blast-radius change (shared module, auth, data, money, public API).
|
||||||
|
- Spec touched §I or §V that other code depends on.
|
||||||
|
- Right-sizing says the cost of a wrong build > the cost of one review pass.
|
||||||
|
|
||||||
|
Skip for a trivial, reversible, well-understood change. Adversarial review on a
|
||||||
|
typo hallucinates flaws & wastes the budget — the self-critique paradox is real.
|
||||||
|
|
||||||
|
## PHASE 0 — CAPTURE
|
||||||
|
|
||||||
|
Read the spec: §G §C §I §R §V §T. Hold the whole thing. You review the *spec*,
|
||||||
|
not your memory of the conversation.
|
||||||
|
|
||||||
|
## PHASE 1 — CONSTRUCT THE SENIOR
|
||||||
|
|
||||||
|
Build a reviewer with real authority, not a generic critic:
|
||||||
|
- **Codebase** — grep/read the modules this spec touches. What patterns, what invariants already hold?
|
||||||
|
- **§R** — what did research establish? A spec decision that contradicts §R is a finding.
|
||||||
|
- **Live** — for any best-practice claim you are unsure of, fetch it. An out-of-date assumption is a flaw.
|
||||||
|
|
||||||
|
A reviewer with no evidence is just an opinion. Earn the authority first.
|
||||||
|
|
||||||
|
## PHASE 2 — REFUTE
|
||||||
|
|
||||||
|
Attack the spec on these axes. For each, try to find the case where it breaks:
|
||||||
|
- **Goal vs reality** — does §G solve the actual problem, or a proxy?
|
||||||
|
- **Missing invariant** — what can go wrong that no §V catches? (most findings live here)
|
||||||
|
- **Interface drift** — does §I match what callers already expect? (cite the caller, file:line)
|
||||||
|
- **Constraint conflict** — do two §C bullets contradict? does one fight §R?
|
||||||
|
- **Unowned edge** — the input, ordering, failure, or concurrency case no §T covers.
|
||||||
|
- **Altitude** — §T too vague to act on, or so granular it is just typing?
|
||||||
|
|
||||||
|
## PHASE 3 — CLASSIFY
|
||||||
|
|
||||||
|
Each finding: `evidence → claim → severity`.
|
||||||
|
- **BLOCK** — build on this spec ships a real defect. Must fix first.
|
||||||
|
- **HARDEN** — add/sharpen a §V so the build cannot regress it.
|
||||||
|
- **NOTE** — worth knowing, not blocking.
|
||||||
|
|
||||||
|
No evidence? Down-rank to NOTE & tag `[unverified]`. ⊥ inflate a hunch to BLOCK.
|
||||||
|
|
||||||
|
## PHASE 4 — HARDEN §V & GATE
|
||||||
|
|
||||||
|
- Each HARDEN finding → a draft §V line (testable, cites the §I/behavior it guards). Hand to **spec** to write.
|
||||||
|
- End on an explicit gate:
|
||||||
|
|
||||||
|
```
|
||||||
|
## review verdict
|
||||||
|
BLOCK: 1 — §I.api shape ≠ caller src/client.ts:40. fix §I before build.
|
||||||
|
HARDEN: 2 — drafted V8 (idempotent refund), V9 (tx around dual write).
|
||||||
|
NOTE: 1 — §T4 vague, split before /build.
|
||||||
|
gate: NO-GO until BLOCK cleared. then /build §T after spec writes V8,V9.
|
||||||
|
```
|
||||||
|
|
||||||
|
GO or NO-GO, never a shrug. Review is the checkpoint that stops a confident wrong build.
|
||||||
|
|
||||||
|
## BOUNDARIES
|
||||||
|
|
||||||
|
- ⊥ write SPEC.md. Draft §V & hand to spec.
|
||||||
|
- ⊥ pass a finding with no evidence as fact. Flag `[unverified]`.
|
||||||
|
- ⊥ review trivia. Right-size or skip.
|
||||||
|
- ⊥ rewrite the user's intent. You harden the spec, you do not replace its goal.
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
name: spec
|
||||||
|
description: |
|
||||||
|
Create, amend, or backprop bugs into SPEC.md at repo root. Sole mutator
|
||||||
|
of the project spec. Triggers when the user asks to write a spec, start
|
||||||
|
a new spec, distill a spec from existing code, add invariants, amend
|
||||||
|
sections (§G, §C, §I, §V, §T, §B), or record a bug via backprop.
|
||||||
|
Common phrasings: "write the spec for...", "new spec", "bug: ...",
|
||||||
|
"amend §V.3", "distill spec from code", "spec this idea". Reads and
|
||||||
|
follows FORMAT.md for the caveman encoding rules and pipe-table shape
|
||||||
|
of §T and §B.
|
||||||
|
---
|
||||||
|
|
||||||
|
# spec — spec mutator
|
||||||
|
|
||||||
|
Read `FORMAT.md` at repo root if not already loaded. Caveman skill applies to all writes here.
|
||||||
|
|
||||||
|
## DISPATCH
|
||||||
|
|
||||||
|
Inspect user request and project state:
|
||||||
|
|
||||||
|
1. No `SPEC.md` at repo root AND args describe idea → **NEW**
|
||||||
|
2. No `SPEC.md` AND `from-code` in args → **DISTILL**
|
||||||
|
3. `SPEC.md` exists AND args start `bug:` → **BACKPROP**
|
||||||
|
4. `SPEC.md` exists AND args start `amend` → **AMEND**
|
||||||
|
5. `SPEC.md` exists, no args → ask user which mode
|
||||||
|
|
||||||
|
## INPUTS — spec is the sole mutator
|
||||||
|
|
||||||
|
The other verbs produce material; spec writes it. Ingest their handoff blocks
|
||||||
|
into the right section, show a diff, write on OK:
|
||||||
|
|
||||||
|
- **grill** → sharpened §G + §C
|
||||||
|
- **research** → §R rows (add the §R section if absent)
|
||||||
|
- **review** → drafted §V lines + the risk verdict
|
||||||
|
- **deepen** → §I/§V/§T amendments
|
||||||
|
|
||||||
|
⊥ rewrite a section the handoff did not name. Sectioned ownership (see FORMAT.md).
|
||||||
|
|
||||||
|
## NEW — idea → spec
|
||||||
|
|
||||||
|
Input: user idea. If it arrived fuzzy, prefer running **grill** first.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Extract goal (1 line, caveman). → §G.
|
||||||
|
2. List constraints user stated or implied. → §C.
|
||||||
|
3. List external surfaces user named. → §I.
|
||||||
|
4. §R only if **research** ran — else omit the section (right-size).
|
||||||
|
5. Propose initial invariants. → §V (numbered V1…).
|
||||||
|
6. Break goal into ordered tasks. → §T pipe table, all status `.`, ids T1…
|
||||||
|
7. §B section with header row only (`id|date|cause|fix`).
|
||||||
|
|
||||||
|
Write to `SPEC.md`. Show user full file. Ask: "spec OK? `/review` if high-blast-radius, else `/build`."
|
||||||
|
|
||||||
|
## DISTILL — code → spec
|
||||||
|
|
||||||
|
Walk repo. Produce §G (infer from README/package.json/main entry), §C (infer from stack), §I (enumerate public APIs/CLIs/configs), §V (derive from tests and assertions), §T (one task per known TODO or missing test), §B (empty).
|
||||||
|
|
||||||
|
Caveman everywhere. Flag uncertain items with `?` in text so user can confirm.
|
||||||
|
|
||||||
|
## BACKPROP — bug → §B + §V
|
||||||
|
|
||||||
|
Input: `bug: <description>`.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Parse bug description.
|
||||||
|
2. Find root cause (read relevant code).
|
||||||
|
3. Decide: would a new invariant catch recurrence? If yes → draft `V<next>`.
|
||||||
|
4. Append §B row: `B<next>|<date>|<cause>|V<N>`.
|
||||||
|
5. Append new invariant to §V.
|
||||||
|
6. If fix also changes behavior → add/update §T rows.
|
||||||
|
7. Show diff. Apply only on user OK.
|
||||||
|
|
||||||
|
Rule: every bug gets a §B entry. Invariant optional but preferred.
|
||||||
|
|
||||||
|
## AMEND — targeted edit
|
||||||
|
|
||||||
|
Input: `amend §V.3` or `amend §T` etc.
|
||||||
|
|
||||||
|
Read that section. Show current. Ask user what changes. Write. Show diff.
|
||||||
|
|
||||||
|
Never silently rewrite sections user did not name.
|
||||||
|
|
||||||
|
## OUTPUT RULES
|
||||||
|
|
||||||
|
- Caveman format per `FORMAT.md`.
|
||||||
|
- Preserve identifiers, paths, code verbatim.
|
||||||
|
- Numbering monotonic — never reuse §V.N or §B.N.
|
||||||
|
- §T row `cites` column ! list §V/§I deps: `T5|.|impl auth mw|V2,I.api`.
|
||||||
|
|
||||||
|
## NON-GOALS
|
||||||
|
|
||||||
|
- No sub-agents. Main thread writes.
|
||||||
|
- No dashboards, no logs, no state files beyond SPEC.md itself.
|
||||||
|
- No auto-build after spec. User invokes build explicitly.
|
||||||
+39245
-9
File diff suppressed because it is too large.
Load diff
@@ -45,6 +45,21 @@ Place a `.reset` file in `SHM_DIR` (default `/dev/shm/bytetrack-counter`) to tri
|
|||||||
touch /dev/shm/bytetrack-counter/.reset
|
touch /dev/shm/bytetrack-counter/.reset
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## STATE_FILE lifecycle (file sources)
|
||||||
|
|
||||||
|
For local file input, STATE_FILE persists the recount result so the dashboard's
|
||||||
|
`/api/current-batch` still shows the final batch after the process exits:
|
||||||
|
|
||||||
|
- **Start**: STATE_FILE is deleted unless a live instance is running — detected by
|
||||||
|
a `/proc/*/cmdline` scan for another `bytetrack-counter` whose source is
|
||||||
|
`rtsp://`/`http://` (argv or `SOURCE=` in its config). If live is active, the
|
||||||
|
file is kept and a `[INFO] Live instance active — keeping STATE_FILE` line is
|
||||||
|
logged. Stop the live service before recounting so stale recount state is cleared.
|
||||||
|
- **Finish**: on graceful shutdown (`end_batch("shutdown")` — EOF or SIGTERM) the
|
||||||
|
final batch snapshot is written to STATE_FILE (in both the persist and the
|
||||||
|
discard-below-minimum paths) instead of being deleted.
|
||||||
|
- Live sources are unchanged: resume at start, delete on finish.
|
||||||
|
|
||||||
## Shared memory
|
## Shared memory
|
||||||
|
|
||||||
`SHM_DIR` (`/dev/shm/bytetrack-counter` by default) stores:
|
`SHM_DIR` (`/dev/shm/bytetrack-counter` by default) stores:
|
||||||
@@ -56,9 +71,13 @@ Directory auto-created at startup.
|
|||||||
|
|
||||||
## Video recordings
|
## Video recordings
|
||||||
|
|
||||||
- Recordings are written to `SHM_DIR` as `batch_pending_*.mp4`, confirmed when ayam detected, and moved via `fork()` (non-blocking) to `OUTPUT_DIR/<batch counting_date>/batch_N_HHMMSS.mp4`.
|
- Recordings are written to `SHM_DIR` as `batch_pending_*.mp4`, confirmed when ayam detected, and moved via `fork()` (non-blocking) to `OUTPUT_DIR/<batch counting_date>/batch_<num>_<YYYYMMDD>_<HHMMSS>.mp4`.
|
||||||
|
- **Live sources**: recording speculates (SPECULATING) and is discarded after `RECORD_DELAY_SECONDS` if no ayam confirms; only CONFIRMED/STOPPING recordings are saved.
|
||||||
|
- **File sources** (`file_source_` flag): recording opens at frame 0, is **never discarded** by the SPECULATING timer, and any active recording is saved on batch close and on shutdown — so the output's first frame matches the source mp4's frame 0 (no `RECORD_DELAY` offset).
|
||||||
|
- `feed_frame` is called **every frame** (outside the detections block), so recordings are continuous with no gaps on detection-less frames.
|
||||||
- The day folder uses the **batch counting date** (today before `DAILY_CUTOFF_TIME`, tomorrow after), forwarded from `BatchStore` through the batch-closed callback — matching the SQLite `counting_date`.
|
- The day folder uses the **batch counting date** (today before `DAILY_CUTOFF_TIME`, tomorrow after), forwarded from `BatchStore` through the batch-closed callback — matching the SQLite `counting_date`.
|
||||||
- Retention cleanup compares day folders with `std::localtime` to match folder naming.
|
- Retention cleanup compares day folders with `std::localtime` to match folder naming.
|
||||||
|
- H.264 encode quality is set via `RECORD_CRF` (lower = better, default unset) and `RECORD_PRESET` — `start_recording()` writes `OPENCV_FFMPEG_WRITE_OPTIONS` (`crf;<n>|preset;<p>`) before `writer_.open()`.
|
||||||
|
|
||||||
## Source override
|
## Source override
|
||||||
|
|
||||||
@@ -67,3 +86,22 @@ bytetrack-counter <config.env> --source /tmp/test.mp4 # local file (single
|
|||||||
bytetrack-counter <config.env> --source rtsp://cam:554/x # live stream
|
bytetrack-counter <config.env> --source rtsp://cam:554/x # live stream
|
||||||
bytetrack-counter <config.env> -s path # or --source=path
|
bytetrack-counter <config.env> -s path # or --source=path
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Capture setup (`open_capture` in `src/main.cpp`):
|
||||||
|
- **Live sources**: `CAP_PROP_BUFFERSIZE=1` is set (reconnect-agnostic).
|
||||||
|
- **File sources**: `CAP_PROP_POS_FRAMES=0` is set — the decoder is forced to frame
|
||||||
|
0. Never set `CAP_PROP_BUFFERSIZE` on a file: it makes OpenCV's FFMPEG build
|
||||||
|
buffer land mid-file and the first decoded frame ≠ source frame 0 (e.g. the
|
||||||
|
pause-on-first-frame view showed a ~11s-later camera clock).
|
||||||
|
- First successful read logs one INFO line with `pos_frames`, `pos_msec`,
|
||||||
|
`frame_count`, `fps` — `pos_msec=0` ⇒ frame 0 is the true first frame.
|
||||||
|
|
||||||
|
## Pause on first frame (file sources)
|
||||||
|
|
||||||
|
For local file input, hold the first frame as the live stream view until signalled:
|
||||||
|
|
||||||
|
- Enable via `--pause-on-first-frame` (CLI, overrides env) or `PAUSE_ON_FIRST_FRAME=true`.
|
||||||
|
- Frame 0 is fully processed (inference, tracking, HUD) then the loop idles: the annotated
|
||||||
|
frame is re-written to `LIVE_STREAM_FRAME_PATH` while polling for `SHM_DIR/.continue`.
|
||||||
|
- Resume by `touch /dev/shm/bytetrack-counter/.continue` (file is deleted on consume).
|
||||||
|
- Ignored for live (rtsp/http) sources with a warning. A stale `.continue` is cleared at startup.
|
||||||
@@ -22,14 +22,15 @@ Talenan crossing → close batch. Persist to SQLite + JSON.
|
|||||||
env: .env KEY=VALUE file — all config per config.env.example
|
env: .env KEY=VALUE file — all config per config.env.example
|
||||||
env: SHM_DIR → shared memory directory (/dev/shm/bytetrack-counter)
|
env: SHM_DIR → shared memory directory (/dev/shm/bytetrack-counter)
|
||||||
env: LIVE_STREAM_FRAME_PATH → JPEG snapshot path
|
env: LIVE_STREAM_FRAME_PATH → JPEG snapshot path
|
||||||
env: RECORD_VIDEO, RECORD_DELAY_SECONDS, RECORD_END_DELAY, RECORD_RETENTION_DAYS
|
env: RECORD_VIDEO, RECORD_DELAY_SECONDS, RECORD_END_DELAY, RECORD_RETENTION_DAYS, RECORD_CRF, RECORD_PRESET
|
||||||
cmd: bytetrack-counter <config.env> [--source <rtsp://|http://|/path/to/file.mp4>]
|
cmd: bytetrack-counter <config.env> [--source <rtsp://|http://|/path/to/file.mp4>] [--pause-on-first-frame]
|
||||||
db: SQLite batches(id,counting_date,batch_number,camera_name,object_label,count,start_time,end_time)
|
db: SQLite batches(id,counting_date,batch_number,camera_name,object_label,count,start_time,end_time)
|
||||||
db: SQLite daily_summaries(id,counting_date,camera_name,object_label,total_count,total_batches)
|
db: SQLite daily_summaries(id,counting_date,camera_name,object_label,total_count,total_batches)
|
||||||
file: JSON state — batch_number,count,start_time,last_detection_time,counted_event_ids[]
|
file: JSON state — batch_number,count,start_time,last_detection_time,counted_event_ids[]
|
||||||
file: CSV crossing log — batch,frame,timestamp,chicken_id
|
file: CSV crossing log — batch,frame,timestamp,chicken_id
|
||||||
file: SHM_DIR/.reset → trigger full state reset
|
file: SHM_DIR/.reset → trigger full state reset
|
||||||
file: SHM_DIR/batch_N_HHMMSS.mp4 → per-batch video recording (moved to OUTPUT_DIR/<batch counting_date>/)
|
file: SHM_DIR/.continue → resume processing after pause-on-first-frame
|
||||||
|
file: SHM_DIR/batch_pending_*.mp4 → confirmed when ayam detected, moved to OUTPUT_DIR/<batch counting_date>/batch_<num>_<YYYYMMDD>_<HHMMSS>.mp4 (num=batch, YYYYMMDD=counting_date, HHMMSS=move time)
|
||||||
sig: SIGTERM|SIGINT → shutdown_requested → graceful exit
|
sig: SIGTERM|SIGINT → shutdown_requested → graceful exit
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -48,8 +49,13 @@ V9: live_stream_enabled → parent dir of LIVE_STREAM_FRAME_PATH created @ star
|
|||||||
V10: stale track (> TRACKED_PRUNE_SEC unseen) → prune from external tracking maps
|
V10: stale track (> TRACKED_PRUNE_SEC unseen) → prune from external tracking maps
|
||||||
V11: all dirs (output_dir, shm_dir, live_stream parent) created @ startup
|
V11: all dirs (output_dir, shm_dir, live_stream parent) created @ startup
|
||||||
V12: SHM_DIR/.reset → BatchStore::reset() — end active batch, clear memory, delete state_file, write blank live JPEG
|
V12: SHM_DIR/.reset → BatchStore::reset() — end active batch, clear memory, delete state_file, write blank live JPEG
|
||||||
V13: video recording: motion → SPECULATING (≤RECORD_DELAY_SECONDS), ayam → CONFIRMED → talenan/batch-end → STOPPING (+RECORD_END_DELAY) → move to OUTPUT_DIR/<batch counting_date>/ via fork
|
V13: video recording: motion → SPECULATING (≤RECORD_DELAY_SECONDS, live only; file sources never discard) → CONFIRMED → talenan/batch-end → STOPPING (+RECORD_END_DELAY) → move to OUTPUT_DIR/<batch counting_date>/ via fork. feed_frame called every frame (no detection gaps)
|
||||||
V14: video retention: day-dirs > RECORD_RETENTION_DAYS old → removed @ startup & @ each CUTOFF_TIME (localtime cutoff matches folder names)
|
V14: video retention: day-dirs > RECORD_RETENTION_DAYS old → removed @ startup & @ each CUTOFF_TIME (localtime cutoff matches folder names)
|
||||||
|
V15: RECORD_CRF (lower = better, -1 unset) & RECORD_PRESET → OPENCV_FFMPEG_WRITE_OPTIONS before writer_.open() (crf;<n>|preset;<p>)
|
||||||
|
V16: pause_on_first_frame (env/--pause-on-first-frame, file source only; live ignored w/ warn): process frame 0 → idle-loop, re-write annotated frame to LIVE_STREAM_FRAME_PATH, wait for SHM_DIR/.continue → delete → resume
|
||||||
|
V17: file sources (non-live) record from frame 0: SPECULATING never discarded, any active recording saved on batch close and shutdown → output first frame = source frame 0 (no RECORD_DELAY offset)
|
||||||
|
V18: file sources: cap opened w/ CAP_PROP_POS_FRAMES=0 (frame-0 seek); CAP_PROP_BUFFERSIZE=1 live-only; ∀ live|file → first decoded frame == source frame 0 (no build-buffer offset into file)
|
||||||
|
V19: file sources: STATE_FILE deleted @ startup IF no live instance active (bytetrack-counter w/ rtsp|http src in argv|config → /proc scan); preserved @ graceful shutdown (end_batch "shutdown" → final snapshot written, ⊥ deleted). Live: resume @ start, delete @ finish (unchanged)
|
||||||
```
|
```
|
||||||
|
|
||||||
## §T — Tasks
|
## §T — Tasks
|
||||||
@@ -71,15 +77,18 @@ T12|x|CsvLogger for crossing events|I.file
|
|||||||
T13|x|Live JPEG snapshot to shared memory|V9
|
T13|x|Live JPEG snapshot to shared memory|V9
|
||||||
T14|x|systemd service with ProtectSystem + ReadWritePaths|I.cmd
|
T14|x|systemd service with ProtectSystem + ReadWritePaths|I.cmd
|
||||||
T15|x|Graceful shutdown: SIGTERM → close batch → save state|I.sig
|
T15|x|Graceful shutdown: SIGTERM → close batch → save state|I.sig
|
||||||
T16|x|Video per-batch recording: motion-triggered → speculating in shm, confirmed when ayam detected ≤ RECORD_DELAY_SECONDS, stopped RECORD_END_DELAY after batch close, moved to OUTPUT_DIR/<batch counting_date>/ via fork, retention cleanup at startup & cutoff. Unconfirmed recordings deleted.|V13,V14
|
T16|x|Video per-batch recording: motion-triggered → speculating in shm (live only, discarded after RECORD_DELAY_SECONDS unconfirmed), confirmed when ayam detected, stopped RECORD_END_DELAY after batch close, moved to OUTPUT_DIR/<batch counting_date>/ via fork, retention cleanup at startup & cutoff, H.264 quality via RECORD_CRF/RECORD_PRESET (OPENCV_FFMPEG_WRITE_OPTIONS). Every frame fed (no detection gaps). File sources: record from frame 0, never discard, save on close & shutdown.|V13,V14,V15,V17
|
||||||
T17|~?|Python DEBUG_TRACKING equivalent (per-frame score/track dump)|-
|
T17|~?|Python DEBUG_TRACKING equivalent (per-frame score/track dump)|-
|
||||||
T18|x|CLI --source/-s/--source= override of SOURCE (rtsp|http|local mp4; file = single pass)|I.cmd
|
T18|x|CLI --source/-s/--source= override of SOURCE (rtsp|http|local mp4; file = single pass)|I.cmd
|
||||||
|
T19|x|Pause-on-first-frame for file source: process frame 0, hold annotated frame as live view, wait for SHM_DIR/.continue, resume. Env PAUSE_ON_FIRST_FRAME + --pause-on-first-frame; stale .continue cleared @ startup; live source ignored w/ warning.|V16,V18
|
||||||
|
T20|x|File-source capture: POS_FRAMES=0 seek @ open (frame 0 !), BUFFERSIZE live-only; log pos_frames/pos_msec/frame_count/fps on first decoded frame|V18
|
||||||
|
T21|x|File-source STATE_FILE lifecycle: startup delete (guard: live-instance /proc scan → skip delete if active), preserve final snapshot @ graceful shutdown (end_batch "shutdown")|V19
|
||||||
```
|
```
|
||||||
|
|
||||||
## §B — Bugs
|
## §B — Bugs
|
||||||
|
|
||||||
```
|
```
|
||||||
id|date|cause|fix
|
id|date|cause|fix
|
||||||
|
B1|2026-08-10|file source CAP_PROP_BUFFERSIZE=1 @ open → OpenCV FFMPEG build buffer offset → first decoded frame ≠ source frame 0 (paused view showed ~11s-later clock 21:07:39 vs mp4 frame0 21:07:28)|V18
|
||||||
|
B2|2026-08-12|file input graceful finish deleted STATE_FILE (save_state w/ null current_state_) → recount result invisible to /api/current-batch after exit; stale file resumed next run|V19
|
||||||
```
|
```
|
||||||
|
|
||||||
(base empty — distilled from code, no bug history)
|
|
||||||
Binary file not shown.
+3
-3
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
namespace version {
|
namespace version {
|
||||||
|
|
||||||
const std::string GIT_COMMIT = "37f98f24";
|
const std::string GIT_COMMIT = "ea370b0f";
|
||||||
const std::string GIT_BRANCH = "main";
|
const std::string GIT_BRANCH = "main";
|
||||||
const std::string GIT_TAG = "";
|
const std::string GIT_TAG = "";
|
||||||
const std::string BUILD_DATE = "2026-08-05";
|
const std::string BUILD_DATE = "2026-08-10";
|
||||||
const std::string BUILD_TIME = "00:25:19";
|
const std::string BUILD_TIME = "11:03:50";
|
||||||
const int BUILD_YEAR = 2026;
|
const int BUILD_YEAR = 2026;
|
||||||
const bool BUILD_RKNN = 1;
|
const bool BUILD_RKNN = 1;
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ MOTION_THRESHOLD=5.0
|
|||||||
RATE_WINDOW_SEC=60
|
RATE_WINDOW_SEC=60
|
||||||
# Number of frames to discard at startup to let the stream buffer stabilise
|
# Number of frames to discard at startup to let the stream buffer stabilise
|
||||||
WARMUP_FRAMES=30
|
WARMUP_FRAMES=30
|
||||||
|
# Pause after the first frame of a FILE source, holding it as the live stream view,
|
||||||
|
# until a '.continue' file is placed in SHM_DIR (true/false, default: false).
|
||||||
|
# Ignored for live (rtsp/http) sources. CLI: --pause-on-first-frame (overrides env).
|
||||||
|
PAUSE_ON_FIRST_FRAME=false
|
||||||
# Delay in seconds between stream reconnection attempts
|
# Delay in seconds between stream reconnection attempts
|
||||||
RECONNECT_DELAY_SEC=3
|
RECONNECT_DELAY_SEC=3
|
||||||
# Maximum reconnection attempts (0 = infinite)
|
# Maximum reconnection attempts (0 = infinite)
|
||||||
@@ -142,6 +146,11 @@ RECORD_DELAY_SECONDS=10
|
|||||||
RECORD_END_DELAY=1
|
RECORD_END_DELAY=1
|
||||||
# Days to keep recorded video folders (folders older than this deleted on startup)
|
# Days to keep recorded video folders (folders older than this deleted on startup)
|
||||||
RECORD_RETENTION_DAYS=3
|
RECORD_RETENTION_DAYS=3
|
||||||
|
# H.264 quality: lower CRF = higher quality (17-18 ~ visually lossless, 23 is ffmpeg default).
|
||||||
|
# Leave empty to use ffmpeg default.
|
||||||
|
RECORD_CRF=18
|
||||||
|
# x264 preset: slow = better compression/quality at higher CPU cost. Empty = default (medium).
|
||||||
|
RECORD_PRESET=slow
|
||||||
# Shared memory directory — used for video recording, live snapshots, and .reset trigger
|
# Shared memory directory — used for video recording, live snapshots, and .reset trigger
|
||||||
# RECORD_SHM_DIR is an alias; SHM_DIR takes priority if both set
|
# RECORD_SHM_DIR is an alias; SHM_DIR takes priority if both set
|
||||||
SHM_DIR=/dev/shm/bytetrack-counter
|
SHM_DIR=/dev/shm/bytetrack-counter
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"skills": {
|
||||||
|
"backprop": {
|
||||||
|
"source": "JuliusBrussee/cavekit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/backprop/SKILL.md",
|
||||||
|
"computedHash": "7d23e415bf412ff01fa8c3d920c269f6ac7a9ca0c337eafda861d412ba8f946c"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"source": "JuliusBrussee/cavekit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/build/SKILL.md",
|
||||||
|
"computedHash": "6cf3f7fc11d4c9892bd9b71002715ab9b5915aabef15d2da30d955d117f1052d"
|
||||||
|
},
|
||||||
|
"caveman": {
|
||||||
|
"source": "JuliusBrussee/cavekit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/caveman/SKILL.md",
|
||||||
|
"computedHash": "b7296730e7d074253e6806b9e71dab4a6e1cbfeb643cafc000f9ce7bec0a825c"
|
||||||
|
},
|
||||||
|
"check": {
|
||||||
|
"source": "JuliusBrussee/cavekit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/check/SKILL.md",
|
||||||
|
"computedHash": "28e8c45a027c7c1c9832417100aff4d78b9981d45d3672c0797eb606e020c2d9"
|
||||||
|
},
|
||||||
|
"deepen": {
|
||||||
|
"source": "JuliusBrussee/cavekit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/deepen/SKILL.md",
|
||||||
|
"computedHash": "5e0fe183377f9b12811ce6a05173d72a8f9194055a519d4a0b26000f0c776392"
|
||||||
|
},
|
||||||
|
"grill": {
|
||||||
|
"source": "JuliusBrussee/cavekit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/grill/SKILL.md",
|
||||||
|
"computedHash": "2bad4607d6fb2b0538d6f3e57f151064d219133bc4cd08a941c0850a0967d6ca"
|
||||||
|
},
|
||||||
|
"research": {
|
||||||
|
"source": "JuliusBrussee/cavekit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/research/SKILL.md",
|
||||||
|
"computedHash": "91566d23ee13a46fe8d1094c9e5b25dc2a622e4b5ea2ecc97179a58f4f901b8d"
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"source": "JuliusBrussee/cavekit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/review/SKILL.md",
|
||||||
|
"computedHash": "eb74a6bf82fff552294e7c3ce0a281d6e668f6b61ce93b1f8ffa2a10bf99a7d3"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"source": "JuliusBrussee/cavekit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/spec/SKILL.md",
|
||||||
|
"computedHash": "80ec98e14d6e5bb009d2f36d57cfc073f3cd136cf83ddf2176efb39a581ef072"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-2
@@ -332,7 +332,12 @@ bool BatchStore::end_batch_locked(const std::string& closed_by) {
|
|||||||
|
|
||||||
if (count < min_object_per_batch_ || duration_sec < min_duration_per_batch_) {
|
if (count < min_object_per_batch_ || duration_sec < min_duration_per_batch_) {
|
||||||
current_state_ = nullptr;
|
current_state_ = nullptr;
|
||||||
save_state();
|
if (preserve_state_file_ && closed_by == "shutdown") {
|
||||||
|
std::ofstream f(state_file_);
|
||||||
|
f << state.dump(2);
|
||||||
|
} else {
|
||||||
|
save_state();
|
||||||
|
}
|
||||||
batch_timer_running_ = false;
|
batch_timer_running_ = false;
|
||||||
if (batch_closed_cb_) batch_closed_cb_(batch_number, counting_date);
|
if (batch_closed_cb_) batch_closed_cb_(batch_number, counting_date);
|
||||||
return false;
|
return false;
|
||||||
@@ -347,7 +352,12 @@ bool BatchStore::end_batch_locked(const std::string& closed_by) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
current_state_ = nullptr;
|
current_state_ = nullptr;
|
||||||
save_state();
|
if (preserve_state_file_ && closed_by == "shutdown") {
|
||||||
|
std::ofstream f(state_file_);
|
||||||
|
f << state.dump(2);
|
||||||
|
} else {
|
||||||
|
save_state();
|
||||||
|
}
|
||||||
batch_timer_running_ = false;
|
batch_timer_running_ = false;
|
||||||
if (batch_closed_cb_) batch_closed_cb_(batch_number, counting_date);
|
if (batch_closed_cb_) batch_closed_cb_(batch_number, counting_date);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public:
|
|||||||
int display_total();
|
int display_total();
|
||||||
|
|
||||||
void set_batch_closed_callback(std::function<void(int, const std::string&)> cb);
|
void set_batch_closed_callback(std::function<void(int, const std::string&)> cb);
|
||||||
|
void set_preserve_state_file(bool v) { preserve_state_file_ = v; }
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
bool check_cutoff_reset();
|
bool check_cutoff_reset();
|
||||||
@@ -85,4 +86,5 @@ private:
|
|||||||
|
|
||||||
json current_state_;
|
json current_state_;
|
||||||
json previous_state_;
|
json previous_state_;
|
||||||
|
bool preserve_state_file_ = false;
|
||||||
};
|
};
|
||||||
@@ -124,6 +124,9 @@ void AppConfig::load_from_env(const char* env_path) {
|
|||||||
auto mt_s = get_env("MOTION_THRESHOLD");
|
auto mt_s = get_env("MOTION_THRESHOLD");
|
||||||
if (!mt_s.empty()) motion_threshold = std::stof(mt_s);
|
if (!mt_s.empty()) motion_threshold = std::stof(mt_s);
|
||||||
|
|
||||||
|
auto poff_s = get_env("PAUSE_ON_FIRST_FRAME");
|
||||||
|
pause_on_first_frame = (poff_s == "true");
|
||||||
|
|
||||||
auto wf_s = get_env("WARMUP_FRAMES");
|
auto wf_s = get_env("WARMUP_FRAMES");
|
||||||
if (!wf_s.empty()) warmup_frames = std::stoi(wf_s);
|
if (!wf_s.empty()) warmup_frames = std::stoi(wf_s);
|
||||||
auto rd_s = get_env("RECONNECT_DELAY_SEC");
|
auto rd_s = get_env("RECONNECT_DELAY_SEC");
|
||||||
@@ -146,6 +149,9 @@ void AppConfig::load_from_env(const char* env_path) {
|
|||||||
if (!reds_s.empty()) record_end_delay_seconds = std::stof(reds_s);
|
if (!reds_s.empty()) record_end_delay_seconds = std::stof(reds_s);
|
||||||
auto rret_s = get_env("RECORD_RETENTION_DAYS");
|
auto rret_s = get_env("RECORD_RETENTION_DAYS");
|
||||||
if (!rret_s.empty()) record_retention_days = std::stoi(rret_s);
|
if (!rret_s.empty()) record_retention_days = std::stoi(rret_s);
|
||||||
|
auto rcrf_s = get_env("RECORD_CRF");
|
||||||
|
if (!rcrf_s.empty()) record_crf = std::stoi(rcrf_s);
|
||||||
|
record_preset = get_env("RECORD_PRESET", record_preset);
|
||||||
shm_dir = get_env("RECORD_SHM_DIR", shm_dir);
|
shm_dir = get_env("RECORD_SHM_DIR", shm_dir);
|
||||||
shm_dir = get_env("SHM_DIR", shm_dir);
|
shm_dir = get_env("SHM_DIR", shm_dir);
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ struct AppConfig {
|
|||||||
|
|
||||||
bool motion_detection_enabled = false;
|
bool motion_detection_enabled = false;
|
||||||
float motion_threshold = 5.0f;
|
float motion_threshold = 5.0f;
|
||||||
|
bool pause_on_first_frame = false;
|
||||||
int warmup_frames = 30;
|
int warmup_frames = 30;
|
||||||
int reconnect_delay_sec = 3;
|
int reconnect_delay_sec = 3;
|
||||||
int max_reconnect_attempts = 0;
|
int max_reconnect_attempts = 0;
|
||||||
@@ -64,6 +65,8 @@ struct AppConfig {
|
|||||||
float record_delay_seconds = 10.0f;
|
float record_delay_seconds = 10.0f;
|
||||||
float record_end_delay_seconds = 1.0f;
|
float record_end_delay_seconds = 1.0f;
|
||||||
int record_retention_days = 3;
|
int record_retention_days = 3;
|
||||||
|
int record_crf = -1;
|
||||||
|
std::string record_preset;
|
||||||
std::string shm_dir = "/dev/shm/bytetrack-counter";
|
std::string shm_dir = "/dev/shm/bytetrack-counter";
|
||||||
|
|
||||||
bool live_stream_enabled = false;
|
bool live_stream_enabled = false;
|
||||||
|
|||||||
+142
-22
@@ -14,11 +14,16 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <cstdlib>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
static std::atomic<bool> shutdown_requested{false};
|
static std::atomic<bool> shutdown_requested{false};
|
||||||
|
|
||||||
@@ -35,13 +40,64 @@ static std::string now_str() {
|
|||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool live_instance_running() {
|
||||||
|
pid_t self_pid = getpid();
|
||||||
|
DIR* dir = opendir("/proc");
|
||||||
|
if (!dir) return false;
|
||||||
|
struct dirent* ent;
|
||||||
|
while ((ent = readdir(dir)) != nullptr) {
|
||||||
|
if (ent->d_name[0] < '0' || ent->d_name[0] > '9') continue;
|
||||||
|
if (std::atoi(ent->d_name) == static_cast<int>(self_pid)) continue;
|
||||||
|
std::string cmdline_path = std::string("/proc/") + ent->d_name + "/cmdline";
|
||||||
|
std::ifstream f(cmdline_path);
|
||||||
|
if (!f) continue;
|
||||||
|
std::vector<std::string> args;
|
||||||
|
std::string tok;
|
||||||
|
while (std::getline(f, tok, '\0')) {
|
||||||
|
if (!tok.empty()) args.push_back(tok);
|
||||||
|
}
|
||||||
|
if (args.empty()) continue;
|
||||||
|
std::string base = args[0];
|
||||||
|
auto slash = base.find_last_of("/\\");
|
||||||
|
if (slash != std::string::npos) base = base.substr(slash + 1);
|
||||||
|
if (base != "bytetrack-counter") continue;
|
||||||
|
for (const auto& a : args) {
|
||||||
|
if (a.rfind("rtsp://", 0) == 0 || a.rfind("http://", 0) == 0) {
|
||||||
|
closedir(dir);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string cfg = args.size() > 1 ? args[1] : "";
|
||||||
|
if (!cfg.empty() && cfg[0] != '-' && cfg.find("rtsp://") == std::string::npos &&
|
||||||
|
cfg.find("http://") == std::string::npos) {
|
||||||
|
std::ifstream cf(cfg);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(cf, line)) {
|
||||||
|
if (line.rfind("SOURCE=", 0) == 0) {
|
||||||
|
std::string v = line.substr(7);
|
||||||
|
if (v.rfind("rtsp://", 0) == 0 || v.rfind("http://", 0) == 0) {
|
||||||
|
closedir(dir);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static cv::VideoCapture open_capture(const std::string& source) {
|
static cv::VideoCapture open_capture(const std::string& source) {
|
||||||
bool is_live = source.substr(0, 7) == "rtsp://" || source.substr(0, 7) == "http://";
|
bool is_live = source.substr(0, 7) == "rtsp://" || source.substr(0, 7) == "http://";
|
||||||
if (is_live) {
|
if (is_live) {
|
||||||
setenv("OPENCV_FFMPEG_CAPTURE_OPTIONS", g_config.rtsp_ffmpeg_options.c_str(), 1);
|
setenv("OPENCV_FFMPEG_CAPTURE_OPTIONS", g_config.rtsp_ffmpeg_options.c_str(), 1);
|
||||||
}
|
}
|
||||||
cv::VideoCapture cap(source, cv::CAP_FFMPEG);
|
cv::VideoCapture cap(source, cv::CAP_FFMPEG);
|
||||||
cap.set(cv::CAP_PROP_BUFFERSIZE, 1);
|
if (is_live) {
|
||||||
|
cap.set(cv::CAP_PROP_BUFFERSIZE, 1);
|
||||||
|
} else {
|
||||||
|
cap.set(cv::CAP_PROP_POS_FRAMES, 0);
|
||||||
|
}
|
||||||
return cap;
|
return cap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +157,7 @@ int main(int argc, char* argv[]) {
|
|||||||
signal(SIGTERM, signal_handler);
|
signal(SIGTERM, signal_handler);
|
||||||
|
|
||||||
std::string cli_source;
|
std::string cli_source;
|
||||||
|
bool cli_pause = false;
|
||||||
for (int i = 2; i < argc; ++i) {
|
for (int i = 2; i < argc; ++i) {
|
||||||
std::string a = argv[i];
|
std::string a = argv[i];
|
||||||
if (a == "--source" || a == "-s") {
|
if (a == "--source" || a == "-s") {
|
||||||
@@ -112,6 +169,8 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
} else if (a.rfind("--source=", 0) == 0) {
|
} else if (a.rfind("--source=", 0) == 0) {
|
||||||
cli_source = a.substr(9);
|
cli_source = a.substr(9);
|
||||||
|
} else if (a == "--pause-on-first-frame") {
|
||||||
|
cli_pause = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,19 +183,33 @@ int main(int argc, char* argv[]) {
|
|||||||
if (!version::GIT_TAG.empty())
|
if (!version::GIT_TAG.empty())
|
||||||
std::cout << " | tag: " << version::GIT_TAG;
|
std::cout << " | tag: " << version::GIT_TAG;
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
std::cout << "Usage: bytetrack-counter <config.env> [--source <rtsp://|http://|/path/to/file.mp4>]" << std::endl;
|
std::cout << "Usage: bytetrack-counter <config.env> [--source <rtsp://|http://|/path/to/file.mp4>] [--pause-on-first-frame]" << std::endl;
|
||||||
std::cout << " config.env path to environment configuration file (required)" << std::endl;
|
std::cout << " config.env path to environment configuration file (required)" << std::endl;
|
||||||
std::cout << " --source, -s override video source (rtsp://, http://, or local file)" << std::endl;
|
std::cout << " --source, -s override video source (rtsp://, http://, or local file)" << std::endl;
|
||||||
|
std::cout << " --pause-on-first-frame for file input: show first frame, pause until .continue in SHM_DIR" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_config.load_from_env(argv[1]);
|
g_config.load_from_env(argv[1]);
|
||||||
|
|
||||||
|
if (cli_pause) {
|
||||||
|
g_config.pause_on_first_frame = true;
|
||||||
|
std::cout << "Pause-on-first-frame enabled by CLI" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
if (!cli_source.empty()) {
|
if (!cli_source.empty()) {
|
||||||
g_config.source = cli_source;
|
g_config.source = cli_source;
|
||||||
std::cout << "Source overridden by CLI: " << g_config.source << std::endl;
|
std::cout << "Source overridden by CLI: " << g_config.source << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool is_live = g_config.source.substr(0, 7) == "rtsp://" ||
|
||||||
|
g_config.source.substr(0, 7) == "http://";
|
||||||
|
|
||||||
|
if (g_config.pause_on_first_frame && is_live) {
|
||||||
|
std::cerr << "Warning: --pause-on-first-frame ignored (source is live, not a file)" << std::endl;
|
||||||
|
g_config.pause_on_first_frame = false;
|
||||||
|
}
|
||||||
|
|
||||||
std::cout << "Starting ByteTrack Counter (C++)" << std::endl;
|
std::cout << "Starting ByteTrack Counter (C++)" << std::endl;
|
||||||
std::cout << "Version: " << version::GIT_COMMIT
|
std::cout << "Version: " << version::GIT_COMMIT
|
||||||
<< " | branch: " << version::GIT_BRANCH
|
<< " | branch: " << version::GIT_BRANCH
|
||||||
@@ -181,6 +254,18 @@ int main(int argc, char* argv[]) {
|
|||||||
std::filesystem::remove(g_config.live_stream_frame_path, ec);
|
std::filesystem::remove(g_config.live_stream_frame_path, ec);
|
||||||
std::filesystem::remove(reset_file, ec);
|
std::filesystem::remove(reset_file, ec);
|
||||||
}
|
}
|
||||||
|
std::error_code ce;
|
||||||
|
std::filesystem::remove(g_config.shm_dir + "/.continue", ce);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_live && !live_instance_running()) {
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove(g_config.state_file, ec);
|
||||||
|
std::cout << "[INFO] File source — cleared STATE_FILE "
|
||||||
|
<< g_config.state_file << std::endl;
|
||||||
|
} else if (!is_live) {
|
||||||
|
std::cout << "[INFO] Live instance active — keeping STATE_FILE "
|
||||||
|
<< g_config.state_file << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
int class_ayam = g_config.ayam_class_id;
|
int class_ayam = g_config.ayam_class_id;
|
||||||
@@ -198,6 +283,7 @@ int main(int argc, char* argv[]) {
|
|||||||
g_config.min_object_per_batch,
|
g_config.min_object_per_batch,
|
||||||
g_config.min_duration_per_batch,
|
g_config.min_duration_per_batch,
|
||||||
logger);
|
logger);
|
||||||
|
store.set_preserve_state_file(!is_live);
|
||||||
store.start_cutoff_watcher();
|
store.start_cutoff_watcher();
|
||||||
if (startup_reset) store.reset();
|
if (startup_reset) store.reset();
|
||||||
|
|
||||||
@@ -248,9 +334,6 @@ int main(int argc, char* argv[]) {
|
|||||||
store.shutdown();
|
store.shutdown();
|
||||||
return shutdown_requested ? 0 : 1;
|
return shutdown_requested ? 0 : 1;
|
||||||
}
|
}
|
||||||
bool is_live = g_config.source.substr(0, 7) == "rtsp://" ||
|
|
||||||
g_config.source.substr(0, 7) == "http://";
|
|
||||||
|
|
||||||
int line_x = resolve_line_x(w, g_config.line_x, g_config.line_x_frac);
|
int line_x = resolve_line_x(w, g_config.line_x, g_config.line_x_frac);
|
||||||
|
|
||||||
std::cout << "RKNN+ByteTrack counter | " << w << "x" << h << " @ "
|
std::cout << "RKNN+ByteTrack counter | " << w << "x" << h << " @ "
|
||||||
@@ -264,13 +347,30 @@ int main(int argc, char* argv[]) {
|
|||||||
w, h, static_cast<int>(fps_prop),
|
w, h, static_cast<int>(fps_prop),
|
||||||
g_config.record_delay_seconds,
|
g_config.record_delay_seconds,
|
||||||
g_config.record_end_delay_seconds,
|
g_config.record_end_delay_seconds,
|
||||||
g_config.record_retention_days);
|
g_config.record_retention_days,
|
||||||
|
g_config.record_crf,
|
||||||
|
g_config.record_preset,
|
||||||
|
!is_live);
|
||||||
auto* vw = video_writer.get();
|
auto* vw = video_writer.get();
|
||||||
store.set_batch_closed_callback([vw](int bn, const std::string& date) { vw->on_batch_closed(bn, date); });
|
store.set_batch_closed_callback([vw](int bn, const std::string& date) { vw->on_batch_closed(bn, date); });
|
||||||
}
|
}
|
||||||
|
|
||||||
int reconnect_count = 0;
|
int reconnect_count = 0;
|
||||||
bool blank_live_frame = false;
|
bool blank_live_frame = false;
|
||||||
|
bool paused = false;
|
||||||
|
bool first_read_logged = false;
|
||||||
|
|
||||||
|
auto write_live_jpeg = [](const cv::Mat& out_frame, int quality) {
|
||||||
|
try {
|
||||||
|
std::filesystem::path p(g_config.live_stream_frame_path);
|
||||||
|
if (p.has_parent_path())
|
||||||
|
std::filesystem::create_directories(p.parent_path());
|
||||||
|
std::vector<uint8_t> jpeg;
|
||||||
|
cv::imencode(".jpg", out_frame, jpeg, {cv::IMWRITE_JPEG_QUALITY, quality});
|
||||||
|
std::ofstream f(g_config.live_stream_frame_path, std::ios::binary);
|
||||||
|
f.write(reinterpret_cast<const char*>(jpeg.data()), jpeg.size());
|
||||||
|
} catch (...) {}
|
||||||
|
};
|
||||||
|
|
||||||
std::unordered_map<int, TrackedInfo> ayam_tracked;
|
std::unordered_map<int, TrackedInfo> ayam_tracked;
|
||||||
std::unordered_map<int, TrackedInfo> talenan_tracked;
|
std::unordered_map<int, TrackedInfo> talenan_tracked;
|
||||||
@@ -303,6 +403,16 @@ int main(int argc, char* argv[]) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!first_read_logged) {
|
||||||
|
first_read_logged = true;
|
||||||
|
std::cout << "[" << now_str() << "] [INFO] First frame decoded — "
|
||||||
|
<< "pos_frames=" << cap.get(cv::CAP_PROP_POS_FRAMES)
|
||||||
|
<< " pos_msec=" << cap.get(cv::CAP_PROP_POS_MSEC)
|
||||||
|
<< " frame_count=" << cap.get(cv::CAP_PROP_FRAME_COUNT)
|
||||||
|
<< " fps=" << cap.get(cv::CAP_PROP_FPS)
|
||||||
|
<< std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
auto elapsed_sec = std::chrono::duration<double>(
|
auto elapsed_sec = std::chrono::duration<double>(
|
||||||
std::chrono::steady_clock::now() - session_start).count();
|
std::chrono::steady_clock::now() - session_start).count();
|
||||||
auto mono = std::chrono::steady_clock::now();
|
auto mono = std::chrono::steady_clock::now();
|
||||||
@@ -346,6 +456,8 @@ int main(int argc, char* argv[]) {
|
|||||||
if (video_writer) video_writer->note_motion();
|
if (video_writer) video_writer->note_motion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (video_writer) video_writer->feed_frame(frame);
|
||||||
|
|
||||||
std::vector<Detection> detections;
|
std::vector<Detection> detections;
|
||||||
float inf_ms = 0.0f;
|
float inf_ms = 0.0f;
|
||||||
if (!skip_inference) {
|
if (!skip_inference) {
|
||||||
@@ -518,8 +630,6 @@ int main(int argc, char* argv[]) {
|
|||||||
ayam_tracked[tid] = {cx, mono};
|
ayam_tracked[tid] = {cx, mono};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video_writer) video_writer->feed_frame(frame);
|
|
||||||
|
|
||||||
// Draw talenan
|
// Draw talenan
|
||||||
for (int di = 0; di < nt; ++di) {
|
for (int di = 0; di < nt; ++di) {
|
||||||
auto it = talenan_tr.det_to_track.find(di);
|
auto it = talenan_tr.det_to_track.find(di);
|
||||||
@@ -593,21 +703,31 @@ int main(int argc, char* argv[]) {
|
|||||||
batch_pulse = std::max(0, batch_pulse - 1);
|
batch_pulse = std::max(0, batch_pulse - 1);
|
||||||
|
|
||||||
if (g_config.live_stream_enabled && frame_idx % g_config.live_stream_every_n == 0) {
|
if (g_config.live_stream_enabled && frame_idx % g_config.live_stream_every_n == 0) {
|
||||||
try {
|
cv::Mat out_frame = frame;
|
||||||
std::filesystem::path p(g_config.live_stream_frame_path);
|
if (blank_live_frame) {
|
||||||
if (p.has_parent_path())
|
out_frame = cv::Mat::zeros(frame.size(), frame.type());
|
||||||
std::filesystem::create_directories(p.parent_path());
|
blank_live_frame = false;
|
||||||
cv::Mat out_frame = frame;
|
}
|
||||||
if (blank_live_frame) {
|
write_live_jpeg(out_frame, g_config.live_stream_quality);
|
||||||
out_frame = cv::Mat::zeros(frame.size(), frame.type());
|
}
|
||||||
blank_live_frame = false;
|
|
||||||
|
if (g_config.pause_on_first_frame && !paused && frame_idx == 0) {
|
||||||
|
paused = true;
|
||||||
|
std::cout << "[" << now_str() << "] [PAUSE] First frame processed — "
|
||||||
|
<< "waiting for .continue in " << g_config.shm_dir << std::endl;
|
||||||
|
cv::Mat pause_frame = frame.clone();
|
||||||
|
while (!shutdown_requested && paused) {
|
||||||
|
write_live_jpeg(pause_frame, g_config.live_stream_quality);
|
||||||
|
std::string cont_file = g_config.shm_dir + "/.continue";
|
||||||
|
if (std::filesystem::exists(cont_file)) {
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove(cont_file, ec);
|
||||||
|
std::cout << "[" << now_str() << "] [PAUSE] .continue detected — resuming" << std::endl;
|
||||||
|
paused = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
std::vector<uint8_t> jpeg;
|
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||||
cv::imencode(".jpg", out_frame, jpeg,
|
}
|
||||||
{cv::IMWRITE_JPEG_QUALITY, g_config.live_stream_quality});
|
|
||||||
std::ofstream f(g_config.live_stream_frame_path, std::ios::binary);
|
|
||||||
f.write(reinterpret_cast<const char*>(jpeg.data()), jpeg.size());
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frame_idx++;
|
frame_idx++;
|
||||||
|
|||||||
+22
-5
@@ -16,11 +16,15 @@ VideoBatchWriter::VideoBatchWriter(const std::string& shm_dir,
|
|||||||
int w, int h, int fps,
|
int w, int h, int fps,
|
||||||
float delay_sec,
|
float delay_sec,
|
||||||
float end_delay_sec,
|
float end_delay_sec,
|
||||||
int retention_days)
|
int retention_days,
|
||||||
|
int crf,
|
||||||
|
const std::string& preset,
|
||||||
|
bool file_source)
|
||||||
: shm_dir_(shm_dir), output_dir_(output_dir),
|
: shm_dir_(shm_dir), output_dir_(output_dir),
|
||||||
w_(w), h_(h), fps_(fps),
|
w_(w), h_(h), fps_(fps),
|
||||||
delay_sec_(delay_sec), end_delay_sec_(end_delay_sec),
|
delay_sec_(delay_sec), end_delay_sec_(end_delay_sec),
|
||||||
retention_days_(retention_days)
|
retention_days_(retention_days), crf_(crf), preset_(preset),
|
||||||
|
file_source_(file_source)
|
||||||
{
|
{
|
||||||
ensure_dir(shm_dir_);
|
ensure_dir(shm_dir_);
|
||||||
ensure_dir(output_dir_);
|
ensure_dir(output_dir_);
|
||||||
@@ -94,7 +98,8 @@ void VideoBatchWriter::note_ayam_detection() {
|
|||||||
void VideoBatchWriter::on_batch_closed(int batch_number, const std::string& counting_date) {
|
void VideoBatchWriter::on_batch_closed(int batch_number, const std::string& counting_date) {
|
||||||
current_batch_ = batch_number;
|
current_batch_ = batch_number;
|
||||||
current_date_ = counting_date;
|
current_date_ = counting_date;
|
||||||
if (state_ == CONFIRMED) {
|
bool save = file_source_ ? (state_ != IDLE) : (state_ == CONFIRMED);
|
||||||
|
if (save) {
|
||||||
state_ = STOPPING;
|
state_ = STOPPING;
|
||||||
state_start_ = std::chrono::steady_clock::now();
|
state_start_ = std::chrono::steady_clock::now();
|
||||||
}
|
}
|
||||||
@@ -103,7 +108,7 @@ void VideoBatchWriter::on_batch_closed(int batch_number, const std::string& coun
|
|||||||
void VideoBatchWriter::feed_frame(const cv::Mat& raw) {
|
void VideoBatchWriter::feed_frame(const cv::Mat& raw) {
|
||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
if (state_ == SPECULATING) {
|
if (state_ == SPECULATING && !file_source_) {
|
||||||
float elapsed = std::chrono::duration<float>(now - state_start_).count();
|
float elapsed = std::chrono::duration<float>(now - state_start_).count();
|
||||||
if (elapsed >= delay_sec_) {
|
if (elapsed >= delay_sec_) {
|
||||||
stop_and_delete();
|
stop_and_delete();
|
||||||
@@ -135,6 +140,16 @@ void VideoBatchWriter::start_recording() {
|
|||||||
<< std::put_time(std::localtime(&t), "%Y%m%d_%H%M%S") << ".mp4";
|
<< std::put_time(std::localtime(&t), "%Y%m%d_%H%M%S") << ".mp4";
|
||||||
current_path_ = shm_dir_ + "/" + name.str();
|
current_path_ = shm_dir_ + "/" + name.str();
|
||||||
|
|
||||||
|
if (crf_ > 0 || !preset_.empty()) {
|
||||||
|
std::string opts;
|
||||||
|
if (crf_ > 0) opts += "crf;" + std::to_string(crf_);
|
||||||
|
if (!preset_.empty()) {
|
||||||
|
if (!opts.empty()) opts += "|";
|
||||||
|
opts += "preset;" + preset_;
|
||||||
|
}
|
||||||
|
setenv("OPENCV_FFMPEG_WRITE_OPTIONS", opts.c_str(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
writer_.open(current_path_,
|
writer_.open(current_path_,
|
||||||
cv::VideoWriter::fourcc('a','v','c','1'),
|
cv::VideoWriter::fourcc('a','v','c','1'),
|
||||||
fps_, cv::Size(w_, h_));
|
fps_, cv::Size(w_, h_));
|
||||||
@@ -208,7 +223,9 @@ void VideoBatchWriter::shutdown() {
|
|||||||
std::string src = current_path_;
|
std::string src = current_path_;
|
||||||
current_path_.clear();
|
current_path_.clear();
|
||||||
|
|
||||||
if (prev_state == CONFIRMED || prev_state == STOPPING) {
|
bool keep = file_source_ ? (prev_state != IDLE)
|
||||||
|
: (prev_state == CONFIRMED || prev_state == STOPPING);
|
||||||
|
if (keep) {
|
||||||
std::string day_dir = output_dir_ + "/" + (current_date_.empty() ? today_str() : current_date_);
|
std::string day_dir = output_dir_ + "/" + (current_date_.empty() ? today_str() : current_date_);
|
||||||
ensure_dir(day_dir);
|
ensure_dir(day_dir);
|
||||||
std::string dst = day_dir + "/" + build_mp4_name(current_batch_, current_date_);
|
std::string dst = day_dir + "/" + build_mp4_name(current_batch_, current_date_);
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ public:
|
|||||||
int w, int h, int fps,
|
int w, int h, int fps,
|
||||||
float delay_sec,
|
float delay_sec,
|
||||||
float end_delay_sec,
|
float end_delay_sec,
|
||||||
int retention_days);
|
int retention_days,
|
||||||
|
int crf,
|
||||||
|
const std::string& preset,
|
||||||
|
bool file_source);
|
||||||
|
|
||||||
~VideoBatchWriter();
|
~VideoBatchWriter();
|
||||||
|
|
||||||
@@ -35,6 +38,9 @@ private:
|
|||||||
float delay_sec_;
|
float delay_sec_;
|
||||||
float end_delay_sec_;
|
float end_delay_sec_;
|
||||||
int retention_days_;
|
int retention_days_;
|
||||||
|
int crf_;
|
||||||
|
std::string preset_;
|
||||||
|
bool file_source_ = false;
|
||||||
|
|
||||||
State state_ = IDLE;
|
State state_ = IDLE;
|
||||||
std::chrono::steady_clock::time_point state_start_;
|
std::chrono::steady_clock::time_point state_start_;
|
||||||
|
|||||||
Reference in new issue
Block a user