Files
silver_pose/docs/superpowers/plans/2026-07-22-t304-go-demo-delivery.md
T

20 KiB
Raw Blame History

T-304 Go 实时演示、报警和打包 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a Windows Go V2 application that safely reads one local RTSP stream, performs ONNX Pose and fall-event processing off the UI thread, visibly alerts once per confirmed event, persists evidence, and can be assembled into a self-contained release folder.

Architecture: internal/config resolves a credential-free JSON configuration and the RTSP URL only from an environment variable. internal/source owns FFprobe/FFmpeg subprocesses, reconnection and a one-slot latest-frame channel. internal/monitor owns ONNX Pose and the existing fall engine; it publishes completed render frames and events. internal/render and internal/alert draw/persist evidence on worker goroutines. internal/ui is a Walk-only observer and command surface; it receives already-computed state via Synchronize and never opens a stream or runs inference.

Tech Stack: Go 1.24, github.com/yalue/onnxruntime_go v1.31.0, ONNX Runtime CPU DLL, FFmpeg/FFprobe, github.com/lxn/walk, Windows MessageBeep, standard-library JSON/PNG/SHA-256, existing internal/pose and internal/fall.


Scope and acceptance boundary

  • One Windows camera stream only. The public configuration stores an environment-variable name, never an RTSP URL, user name or password.
  • The target scene remains a fixed, downward-viewing hall/corridor with most of a body visible. demo/1.mp4 remains a documented out-of-scope false-negative risk sample, not a T-304 success test.
  • The user manually closed T-303. This plan must not describe that as a new positive-video or event-latency validation result.
  • Release dependencies are copied from explicit operator-supplied paths and their hashes are written into the generated release manifest. No DLL, FFmpeg binary, ONNX file, customer video, credential or event artifact is committed.

File map

Path Responsibility
v2/config.example.json Public, credential-free V2 runtime shape and default V1-equivalent event values.
v2/internal/config/config.go Parse/validate JSON, resolve RTSP URL from environment, calculate a non-secret configuration version, and validate hashes/paths.
v2/internal/source/stream.go Probe source metadata, launch/relaunch FFmpeg, expose status and a latest-frame channel, and hide subprocess details that could contain a URL.
v2/internal/monitor/monitor.go Compose source, reusable ONNX Runtime, Pose parse and the existing fall engine; publish display-ready results.
v2/internal/render/overlay.go Convert BGR to RGBA and draw box, 17-point skeleton, state/ID/time and red confirmed border.
v2/internal/alert/dispatcher.go One-shot PNG + JSONL evidence writer, MessageBeep adapter and UI popup payload.
v2/internal/ui/window.go Light Windows Walk main window, Monitor/Settings tabs and UI-thread-safe update methods.
v2/cmd/silver-pose/main.go CLI lifecycle, dependency setup and app/UI composition.
v2/scripts/build-v2-release.ps1 Build an isolated release directory, copy explicit runtime dependencies and emit their versions/hashes.
docs/v2-demo-runbook.md Preflight, local secret setup, start/stop, normal/fall/connection-error demo sequence, evidence review and scope statement.

Task 1: Safe V2 configuration and dependency manifests

Files:

  • Create: v2/config.example.json

  • Create: v2/internal/config/config.go

  • Create: v2/internal/config/config_test.go

  • Create: v2/assets/runtime-manifest.example.json

  • Modify: docs/api.md

  • Step 1: Write failing configuration tests

func TestLoadResolvesOnlyNamedEnvironmentURL(t *testing.T) {
    t.Setenv("SILVER_POSE_RTSP_URL", "rtsp://operator:secret@192.0.2.9/Streaming/Channels/101")
    cfg, err := Load(writeConfig(t, `{"source":{"id":"lobby","rtsp_url_env":"SILVER_POSE_RTSP_URL"},"model":{"onnx":"assets/best.onnx","sha256":"`+strings.Repeat("a", 64)+`","ort_dll":"runtime/onnxruntime.dll"},"tools":{"ffmpeg":"runtime/ffmpeg.exe","ffprobe":"runtime/ffprobe.exe"},"event":`+defaultEventJSON+`,"artifacts":{"event_dir":"../artifacts/events"}}`))
    if err != nil || cfg.Source.URL == "" || strings.Contains(cfg.RuntimeConfigVersion, "secret") { t.Fatalf("unexpected config: %#v, %v", cfg, err) }
}

func TestLoadRejectsEmbeddedRTSPURL(t *testing.T) {
    _, err := Load(writeConfig(t, `{"source":{"id":"lobby","url":"rtsp://secret"}}`))
    if err == nil || !strings.Contains(err.Error(), "rtsp_url_env") { t.Fatal(err) }
}
  • Step 2: Run the focused test and verify the missing package fails

Run: Set-Location v2; go test ./internal/config -run TestLoad -v
Expected: FAIL because internal/config does not exist.

  • Step 3: Implement the minimal public contract
type Config struct {
    Source SourceConfig; Model ModelConfig; Tools ToolConfig
    Event fall.EngineConfig; Artifacts ArtifactConfig; RuntimeConfigVersion string
}
type SourceConfig struct { ID string; URL string; Transport string; Timeout time.Duration; LowLatency bool }

func Load(path string) (Config, error) {
    // Decode into a private raw type, reject raw source.url/host/user/password,
    // resolve only os.LookupEnv(raw.Source.RTSPURLEnv), and hash canonical
    // non-secret source/model/event values as cfg-<sha256>.
}

Validate all required files and 64-hex model hash before monitoring starts. Return generic path/configuration errors; do not include the resolved URL in errors. Document the V2 field meanings and unchanged FallEvent fields in docs/api.md.

  • Step 4: Re-run focused and package tests

Run: Set-Location v2; go test ./internal/config -v
Expected: PASS, including missing environment, embedded URL, invalid hash and version-redaction cases.

  • Step 5: Commit the configuration slice
git add v2/config.example.json v2/internal/config v2/assets/runtime-manifest.example.json docs/api.md
git commit -m "feat(v2): add safe runtime configuration"

Task 2: FFprobe/FFmpeg source worker with reconnect and latest-frame semantics

Files:

  • Create: v2/internal/source/stream.go

  • Create: v2/internal/source/stream_test.go

  • Modify: docs/04-architecture.md

  • Step 1: Write deterministic source tests before subprocess integration

func TestLatestFrameDropsStaleFrame(t *testing.T) {
    out := make(chan Frame, 1)
    publishLatest(out, Frame{Sequence: 1})
    publishLatest(out, Frame{Sequence: 2})
    if got := <-out; got.Sequence != 2 { t.Fatalf("got sequence %d", got.Sequence) }
}

func TestBackoffIsBoundedAndCancellationStopsRetry(t *testing.T) {
    if got := retryDelay(0); got != time.Second { t.Fatal(got) }
    if got := retryDelay(99); got != 8*time.Second { t.Fatal(got) }
}
  • Step 2: Verify the tests fail

Run: Set-Location v2; go test ./internal/source -run 'Test(Latest|Backoff)' -v
Expected: FAIL because internal/source does not exist.

  • Step 3: Implement probe and worker ownership
type Status string
const (Connecting Status = "CONNECTING"; Connected Status = "CONNECTED"; Retrying Status = "RETRYING"; Stopped Status = "STOPPED"; Failed Status = "FAILED")
type Frame struct { BGR []byte; Width, Height int; Timestamp time.Duration; Sequence uint64 }
type Stream interface { Frames() <-chan Frame; Statuses() <-chan Update; Close() error }

func Start(ctx context.Context, cfg Config, commands CommandFactory) (Stream, error) {
    // ffprobe JSON obtains width/height/FPS once per connection; ffmpeg emits
    // bgr24 rawvideo. ReadFull exactly W*H*3 bytes, time-stamp with time.Since
    // (monotonic), publish only the newest frame, and retry 1/2/4/8 seconds.
}

Use -rtsp_transport, -rw_timeout, low-latency FFmpeg input flags when configured, and -f rawvideo -pix_fmt bgr24 - output. Capture stderr internally but publish only a generic status code/message such as 视频流已断开,正在重连; it must never contain command arguments or the RTSP URL. Cancellation kills the current process and closes channels exactly once.

  • Step 4: Run source tests and a safe invalid-source smoke

Run: Set-Location v2; go test ./internal/source -v
Expected: PASS.
Run: go run ./cmd/silver-pose --config config.example.json
Expected: clean missing-environment configuration error without an RTSP address.

  • Step 5: Commit the source slice
git add v2/internal/source docs/04-architecture.md
git commit -m "feat(v2): add reconnecting FFmpeg stream source"

Task 3: Display overlay, event artifacts and exactly-once alert boundary

Files:

  • Create: v2/internal/render/overlay.go

  • Create: v2/internal/render/overlay_test.go

  • Create: v2/internal/alert/dispatcher.go

  • Create: v2/internal/alert/dispatcher_test.go

  • Modify: docs/api.md

  • Step 1: Write failing rendering and alert tests

func TestRenderUsesRedBorderOnlyForConfirmed(t *testing.T) {
    normal := Render(grayBGR(), 2, 2, fall.FrameResult{})
    confirmed := Render(grayBGR(), 2, 2, fall.FrameResult{People: []fall.PersonAnalysis{{State: fall.Confirmed}}})
    if normal.RGBAAt(0, 0) == critical || confirmed.RGBAAt(0, 0) != critical { t.Fatal("critical border semantics broken") }
}

func TestDispatchWritesOnePNGAndRedactedJSONL(t *testing.T) {
    d := NewDispatcher(t.TempDir(), "lobby", silentNotifier{})
    event := fall.Event{EventID:"FALL-run-000001", TrackID:"P-0001", ConfigVersion:"cfg-safe", State:fall.Confirmed}
    if got := d.Dispatch(event, image.NewRGBA(image.Rect(0,0,2,2))); !got.Written { t.Fatal(got) }
    if got := d.Dispatch(event, image.NewRGBA(image.Rect(0,0,2,2))); got.Written { t.Fatal("duplicate") }
}
  • Step 2: Verify tests fail

Run: Set-Location v2; go test ./internal/render ./internal/alert -v
Expected: FAIL because both packages are absent.

  • Step 3: Implement deterministic evidence outputs
func Render(bgr []byte, width, height int, result fall.FrameResult) *image.RGBA
type Record struct { Event fall.Event; ScreenshotPath, LogPath string; CreatedAtUTC time.Time; Written bool }
func (d *Dispatcher) Dispatch(event fall.Event, image *image.RGBA) (Record, error)

Draw a 17-point COCO skeleton, box and ASCII track/state/time label in the image. Normal/SUSPECT/RETRYING use green/amber/slate respectively; only CONFIRMED draws critical red skeleton and a six-pixel image border. Create YYYYMMDD/FALL-*.png and append one UTF-8 JSONL line with the V1-compatible public event fields plus source ID, UTC time and relative screenshot path. The dispatcher holds a seen-event-ID set before writing so retried UI messages cannot overwrite an image or replay an alert. Its notifier interface only reports a one-shot notification request; it does not own UI controls.

  • Step 4: Run focused tests

Run: Set-Location v2; go test ./internal/render ./internal/alert -v
Expected: PASS, including red-only-confirmed, PNG decodability, one JSONL line and no rtsp/credential substring.

  • Step 5: Commit the rendering and alert slice
git add v2/internal/render v2/internal/alert docs/api.md
git commit -m "feat(v2): add annotated evidence and alert dispatch"

Task 4: Background monitor composition and latest completed UI updates

Files:

  • Create: v2/internal/monitor/monitor.go

  • Create: v2/internal/monitor/monitor_test.go

  • Modify: v2/internal/spike/ort.go only if an interface adapter is needed

  • Step 1: Write failing orchestration tests using fakes

func TestMonitorPublishesLatestCompletedFrameAndOneAlert(t *testing.T) {
    stream := newFakeStream(frameA, frameB)
    runtime := fakeRuntime{output: poseOutputForPersistentFall()}
    monitor := New(stream, runtime, fixedEngine(t), newFakeDispatcher())
    go monitor.Run(context.Background())
    got := eventually(t, monitor.Updates())
    if got.Image == nil || got.SourceStatus != source.Connected { t.Fatalf("bad update: %#v", got) }
}

func TestMonitorDoesNotCreateFallEvidenceOnSourceFailure(t *testing.T) {
    monitor := New(newStatusOnlyStream(source.Retrying), fakeRuntime{}, fixedEngine(t), newFakeDispatcher())
    go monitor.Run(context.Background())
    if got := eventually(t, monitor.Updates()); len(got.Events) != 0 || got.SourceStatus != source.Retrying { t.Fatalf("bad update: %#v", got) }
}
  • Step 2: Verify the monitor tests fail

Run: Set-Location v2; go test ./internal/monitor -v
Expected: FAIL because internal/monitor does not exist.

  • Step 3: Implement the composition boundary
type PoseRuntime interface { Run([]float32) ([]float32, error); Close() }
type Update struct { Image *image.RGBA; Result fall.FrameResult; SourceStatus source.Status; SourceMessage string; Events []alert.Record }
type Monitor struct { /* stream, pose runtime, engine, renderer, dispatcher, one-slot updates */ }

func (m *Monitor) Run(ctx context.Context) { /* source frame -> pose -> fall -> render -> dispatch -> publishLatest */ }

Create/open one ONNX Runtime instance before consuming frames and close it only when monitoring stops. Treat Pose/preprocess errors as non-fall status updates and continue/reconnect according to source state; do not terminate the Walk message loop. Only completed rendered frames enter the one-slot Updates channel, so a slow UI never queues stale video frames.

  • Step 4: Run monitor, fall and full Go tests

Run: Set-Location v2; $env:CGO_ENABLED='1'; go test ./internal/monitor ./internal/fall ./...
Expected: PASS.

  • Step 5: Commit the monitor slice
git add v2/internal/monitor v2/internal/spike/ort.go
git commit -m "feat(v2): compose realtime monitor pipeline"

Task 5: Implement the accessible light Walk operator window

Files:

  • Create: v2/internal/ui/window.go

  • Create: v2/internal/ui/window_test.go

  • Modify: v2/internal/ui/spec.go

  • Modify: v2/cmd/ui-spike/main.go

  • Step 1: Write UI state-model tests without launching a native window

func TestViewModelUsesCriticalOnlyForConfirmed(t *testing.T) {
    if NewViewModel().Apply(fall.Normal, source.Connected).Critical { t.Fatal("normal is critical") }
    if !NewViewModel().Apply(fall.Confirmed, source.Connected).Critical { t.Fatal("confirmed must be critical") }
}

func TestSettingsNeverExposeResolvedRTSPURL(t *testing.T) {
    vm := NewViewModel()
    vm.SetSourceReady("SILVER_POSE_RTSP_URL", true)
    if strings.Contains(vm.SettingsSummary, "rtsp://") { t.Fatal(vm.SettingsSummary) }
}
  • Step 2: Verify the pure UI tests fail

Run: Set-Location v2; go test ./internal/ui -v
Expected: FAIL until the view-model additions are created.

  • Step 3: Implement the UI model and window
type ViewModel struct { ConnectionText, StateText, EventText, SettingsSummary string; Critical bool }
type Window struct { /* MainWindow, ImageView, labels, tabs, current bitmap and onStart/onStop callbacks */ }
func NewWindow(config config.Config, commands Commands) (*Window, error)
func (w *Window) Present(update monitor.Update)

Use a 1120×720 default / 960×640 minimum window with a standard top TabWidget named “监控” and “设置”. Monitor tab: ImageViewModeZoom, source/status/event cards, a clearly labeled Start/Stop button and recent-event summary. Settings tab: read-only source environment-variable readiness, model hash prefix, runtime dependency readiness and the effective event values; do not present editable credentials or a resolved URL in T-304. Apply semantic brushes from the documented light palette (#EAF1F8 base, white surfaces, blue focus/action); state text and labels always accompany color. Use standard controls so Tab/Shift+Tab/Enter/Space/Escape behavior is inherited. Present must use MainWindow.Synchronize, dispose the previous bitmap after ImageView.SetImage, and show exactly one modal walk.MsgBox with “我已知晓” semantics per dispatcher record. Call win.MessageBeep when the record reaches the UI; no normal, suspect or reconnect path may beep or show a critical dialog.

  • Step 4: Build the UI and perform the manual keyboard/sizing smoke

Run: Set-Location v2; $env:CGO_ENABLED='0'; go test ./internal/ui; go build ./cmd/ui-spike
Expected: PASS.
Run: go run ./cmd/ui-spike
Expected: light two-tab shell; verify focus traversal, Enter/Space activation, Escape closes the alert dialog, and no red element appears before a confirmed update.

  • Step 5: Commit the UI slice
git add v2/internal/ui v2/cmd/ui-spike
git commit -m "feat(v2): implement operator monitoring window"

Task 6: Production command, release script and operator runbook

Files:

  • Create: v2/cmd/silver-pose/main.go

  • Create: v2/scripts/build-v2-release.ps1

  • Create: docs/v2-demo-runbook.md

  • Modify: docs/03-tech-stack.md

  • Modify: docs/04-architecture.md

  • Modify: docs/current-state.md

  • Modify: docs/06-tasks.md

  • Modify: progress.md

  • Step 1: Write the command preflight test

func TestValidateStartupRejectsMissingRuntimeWithoutLeakingSource(t *testing.T) {
    err := validateStartup(config.Config{Source: config.SourceConfig{URL: "rtsp://secret@example"}})
    if err == nil || strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "rtsp://") { t.Fatal(err) }
}
  • Step 2: Verify it fails before adding the command package

Run: Set-Location v2; go test ./cmd/silver-pose -run TestValidateStartup -v
Expected: FAIL because the command package is absent.

  • Step 3: Implement preflight, lifecycle and release assembly
func main() {
    cfg := mustLoadConfig(flagConfig)
    mustValidateStartup(cfg)
    window := mustNewWindow(cfg, commands)
    go consumeMonitorUpdates(window, monitor)
    window.Run()
}

build-v2-release.ps1 must require -OnnxPath, -OrtDllPath, -FfmpegPath, -FfprobePath and -OutputDirectory; it must reject a non-empty target directory, build silver-pose.exe with CGO/MinGW, copy the four explicit binaries plus config.example.json, write runtime-manifest.json with SHA-256 and tool version output, and fail when a required input is missing. The runbook must document a local ignored config.local.json, setting the RTSP environment variable outside the repository, normal/reconnect/confirmed demonstrations, where event evidence is written, a 1–3 second stated target, and the fixed-view scope limitation.

  • Step 4: Run automated and release-folder smoke verification

Run: Set-Location v2; $env:CGO_ENABLED='1'; go test ./...; go build ./cmd/silver-pose
Expected: PASS.
Run: ./scripts/build-v2-release.ps1 -OnnxPath <locked-onnx> -OrtDllPath <release-dll> -FfmpegPath <ffmpeg.exe> -FfprobePath <ffprobe.exe> -OutputDirectory <empty-temporary-directory>
Expected: silver-pose.exe, all runtime files, public config and hash manifest present; no RTSP URL, video or event artifact appears in the folder.

  • Step 5: Run the task acceptance sequence and update facts

Run: ./init.ps1
Expected: V1 baseline passes.
Run: Set-Location v2; $env:CGO_ENABLED='1'; go test ./...; go build ./cmd/silver-pose
Expected: PASS.
Run: launch the release executable with a local configuration and approved live RTSP stream.
Expected: live frame and skeleton/ID; disconnect causes reconnect text without fall alert; one approved fall produces red overlay, one sound, one acknowledgment popup, one PNG and one JSONL line; normal activity does not create a confirmed event.

  • Step 6: Commit delivery evidence
git add v2/cmd/silver-pose v2/scripts docs/03-tech-stack.md docs/04-architecture.md docs/current-state.md docs/06-tasks.md docs/v2-demo-runbook.md progress.md
git commit -m "feat(v2): deliver realtime demo and release tooling"

Plan self-review

  • Coverage: Tasks 1–2 implement secure RTSP configuration and reconnect; Tasks 3–4 add Pose/event-to-evidence flow; Task 5 provides the light, accessible operator UI and one-shot alert; Task 6 builds the release directory, documents operation and verifies the complete T-304 acceptance path.
  • Boundary checks: release binaries and local credentials/videos remain untracked; alerts are event-ID idempotent; red is exclusive to CONFIRMED; source failures never enter the fall engine; UI thread only observes completed updates.
  • Deliberate gap: actual camera/fall acceptance requires the local camera, credentials and safe on-site action. The application and release can be built/tested without those assets, but T-304 must remain DOING until the runbook's final live sequence is observed and recorded.