Procedure capture and guidance#
Overview#
Use this reference for applications that record task demonstrations and guide users through approved procedures. It shows how to capture images and narration, author a guide from the saved evidence, and run that guide with spoken instructions and progress checks.
Guide mode is at an early stage of development. For a more comprehensive example of procedural guidance, refer to the separate Tea-making guidance — building deterministic visual workflows.
Run a demonstration#
Run commands from agent-samples/procedure-capture-guide/:
uv run --project model-servers procedure_capture_guide_model_servers
uv run main.py --mode capture
The sample’s model-servers launcher starts batch Parakeet STT on port 8103,
Pocket TTS on 8105, Nemotron Omni LLM on 8108, and Cosmos VLM on 8100.
Batch STT accepts narration without speaker enrollment. The capture worker uses
STT, TTS, and Cosmos; the LLM is available but does not participate in capture
or generate guides automatically. Guide mode uses Omni for guide questions and
model-driven observations, alongside the same STT, TTS, and Cosmos services.
The launcher combines shared server defaults with
model-servers/yaml/<gpu-profile>.yaml; use --gpu-profile NAME to select a
profile explicitly. The worker’s yaml/models.json configures its client
adapters and endpoints, not server deployment. Keep those endpoints aligned
with server ports when changing configuration. Refer to
Customizing model servers for the shared launch workflow.
Wait for the shared models to be ready before starting the sample. Open the authenticated web-client URL printed by DeviceIOHub, enable the microphone, and connect. Say start capture, demonstrate and narrate, then say stop capture. Say start capture again for another task packet without reconnecting. The worker logs each finalized packet’s path and status.
Use live video or on-demand image capture. Speak commands directly, without a wake phrase. After start capture, the worker says “Capture started.” and stays silent during narration. After stop capture, it says “Capture stopped.” The evidence packet, transcript, and summary then finish saving silently; processing errors are logged. Disconnecting finalizes the packet without announcements.
On connection, capture mode says: “Say start capture to start the recording and demonstrate your procedure. To finish recording, say stop capture.” This welcome is only sent on connection. Other speech while idle does not trigger a reply, and the welcome is not repeated after “Capture stopped.”
To stop the sample, press Ctrl+C. The shared model servers remain running. To stop the services selected by this sample’s deployment:
uv run --project model-servers procedure_capture_guide_model_servers --stop
Stop the model stack before changing server settings, switching samples, or
updating the checkout. If you changed ports in a GPU profile, pass the same
--gpu-profile NAME on stop.
Output destination#
Choose an output root at launch without editing configuration:
uv run main.py --mode capture --output-root /path/to/task-evidence
Packets are saved under PATH/sessions/<session-id>/. Relative command-line
paths resolve against the launch directory; ~ is expanded. Without this option,
artifacts_dir from yaml/capture-agent-config.yaml is used, relative to that YAML. The override
does not relocate model or voice configuration and does not edit the YAML.
The sample saves JPEG snapshots and text evidence. Audio is transcribed by STT; captions use the configured VLM, and recording acknowledgements use TTS. Refer to Requirements for model and hub deployment. The sample does not require NVENC for its snapshots. Session recording is a separate opt-in feature described below.
Session recording#
To record the whole participant connection in either mode, run:
uv run main.py --mode capture --session-recording
uv run main.py --mode guide example-guides/tea-making/guide.yaml --session-recording
The shared recorder starts immediately after DeviceIOHub and records participant video, device and agent audio, and data-channel traffic from join to leave. Recording continues while the selected agent is idle, capturing a demonstration, or following a guide. start capture and stop capture control only procedure packets; start guide and stop guide control only guided execution. Neither pair starts or stops session recording, and enabling session recording does not start either procedure mode’s work. Guide mode still waits for start guide and retains its verification and timer rules. Disconnecting closes the raw bundle and then starts rendering while the recorder remains running.
yaml/media_capture.yaml selects session_mode: participant and the demo
profile. It saves recordings under
~/.local/share/xr-ai/captures/procedure-capture-guide/, retaining raw media and
rendering a captioned MP4 after disconnect. This option requires an NVIDIA GPU with NVENC support
and ffmpeg on PATH. Refer to Server runtime for the shared
recorder’s output and encoding contract.
For a captioned video, disconnect the client first and wait for the bundle’s
video/session.mp4 and its renderings.captioned_mp4 manifest entry before
pressing Ctrl+C. Stack shutdown commits active raw bundles without starting new
renders. A render already running can also exceed the launcher’s 20-second
shutdown timeout and be interrupted. For a completed raw-only bundle, use
device_io_capture_render
to render it separately after the stack stops.
Session recordings have a separate retention policy: the shared recorder
automatically prunes the oldest completed bundles under out_dir to meet its
default 10 GiB limit. Set max_total_bytes in yaml/media_capture.yaml to
change that limit; 0 disables automatic pruning. Copy recordings you need to
keep outside that directory, and monitor disk usage if pruning is disabled.
This policy does not prune procedure evidence packets.
The flag does not enable the client camera or microphone. Enable live video for
continuous camera recording. In capture mode, --output-root can be combined with this flag and
changes only the procedure-packet destination; configure session recording output
through out_dir in yaml/media_capture.yaml.
Recording boundaries#
After connecting, the worker waits for start capture to open a packet. stop capture finalizes it. Repeat the pair to record another demonstration on the same connection; duplicate starts during recording are ignored.
Say each command as a standalone utterance. Matching ignores case, surrounding whitespace, and trailing sentence punctuation. Other speech is saved as narration while recording, including phrases such as “stop” or “stop recording.” The two control commands are excluded from narration.
Recording and narration continue while the camera is off or images are unavailable. When a fresh live frame is unavailable, the worker requests a still from clients supporting on-demand capture; this can briefly activate the camera. Captioning resumes when a distinct image is saved.
Disconnect or graceful shutdown finalizes the active packet. Reconnecting returns the worker to waiting for start capture.
Commands take effect after speech recognition finishes. Keep the microphone enabled and pause after the final narration before saying stop capture or disconnecting. A late transcript can miss the recording boundary; source audio is retained only when session recording is enabled.
A new recording waits for the preceding packet to finish saving. Narration after its start command is queued in order, but snapshots during this brief restart gap are not buffered.
Outputs#
Generated data remains local and Git-ignored. Defaults are relative to the sample:
Frame, caption, and transcript logs append during recording. Packet counts and the summary are sealed at stop after pending writes drain; they are not live progress indicators.
Location |
Contents |
|---|---|
|
Recording status, participant, timestamps, counts, and activity hierarchy |
|
JPEG samples targeting 2 fps and timestamped |
|
Timestamped captions, activities, phases, and visible changes |
|
Final user transcripts inside the task recording boundary |
|
Human-readable activity and phase summary |
|
Frame, caption, or persistence errors, when present |
Evidence-packet contract#
packet.json uses schema_version: 1. A finalized complete packet contains
packet.json, summary.md, and transcript.jsonl (which can be empty). The
frames/ directory exists, but frames/index.jsonl and captions.jsonl are
created only when records are appended. An absent log with a zero corresponding
count means no observations of that kind were saved. A missing log with a
nonzero count is inconsistent and must not be silently interpreted as empty.
errors.jsonl is optional; its absence is not proof of sufficient evidence.
Cancelled startup or failed storage can leave an incomplete packet with fewer
files; consumers must check status and file availability before conversion.
JSONL files contain one object per nonempty line. files paths in the packet,
image path values in the frame index, and caption frame_path values are
relative to the packet directory. Preserve these paths when copying a packet.
Frame IDs and caption reference IDs are local to that packet. Timestamps ending
in _us are Unix microseconds; human-readable timestamps are UTC ISO 8601.
started_at is the accepted start command’s transcript timestamp. During a
recording, ended_at is null. At finalization it records the effective end,
after recording producers stop and further evidence admission closes, before
pending writes and derived summaries finish draining. stop_command.timestamp_us
preserves the accepted stop utterance’s transcript timestamp, which can precede
the last captured frames because speech recognition takes time. stop_command
is null when no spoken stop was accepted. Neither
timestamp is a claim that the demonstrated task itself finished successfully.
Activity/phase grouping, captions, and summary.md are fallible model-derived
interpretations. Use the frame index and saved images to inspect their evidence.
Downstream coding-agent conversion consumes the packet as immutable input and
writes generated guide material separately. A guide capability consumes that
generated package rather than importing capture internals.
The captioner starts when the first sampled frame is available, then waits 5 seconds after each caption attempt before checking for a new frame. Each request compares two saved images in chronological order: the previous successfully captioned frame, then the current frame. The first caption of each recording uses only the initial image. Requests describe visible actions, object details, and changes, and run when a distinct saved image is available. Model latency adds to the caption interval; on-demand sampling also depends on client response time. Review captions against the images: they are model observations, not verified procedural steps.
caption describes the current image; delta compares earlier and current
states. Previous activity and phase names are supplied only to stabilize summary
grouping, not as visual evidence. Grouping is a navigation aid, not a step list.
Malformed responses are recorded as caption_error in errors.jsonl, including
the raw response, and are excluded from captions and summaries. The next request
still compares against the last successfully captioned image.
Each saved caption includes pipeline-generated delta_reference_frame_id and
delta_reference_timestamp_us for the earlier image actually supplied to the
model. Both are null on the first observation. Resolve the reference ID through
frames/index.jsonl; it need not be the preceding sampled frame, especially
after slow or failed inference. Current-image provenance remains in frame_id,
frame_timestamp_us, and frame_path. These references identify the evidence
pair; they do not verify the model’s interpretation or establish intervening
actions. Older packets without the reference fields do not explicitly record
that comparison provenance.
The transcript contains admitted narration between the spoken commands. The commands themselves are recorded as boundaries rather than narration.
packet.json.stop_command records an accepted spoken stop. Packet status
complete means local writes finished; evidence can still be missing or
incorrect. Review the counts, errors, and source images before using the packet.
Persistence failures mark a packet incomplete when storage permits saving that
status; otherwise the process log reports the failure. Failed packets are closed
so subsequent recording commands create separate packets.
Procedure packets are retained until manually removed, so monitor disk usage.
Caption evaluation#
With the configured VLM running, run the synthetic visual controls from the sample directory. Choose a new output directory for each evaluation:
PYTHONPATH=worker uv run --project worker python evaluation/capture/evaluate_captions.py --output artifacts/caption-eval
The cases exercise initial observations, chronological direction, unchanged images, object appearance and disappearance, and grouping labels that disagree with visual evidence. The runner uses the production query, system prompt, multi-image tool, and parser. It saves source images, raw responses, and expected observations for inspection. The command exits nonzero if any case fails the response contract or cannot obtain a model response; all cases are still saved for inspection. Its automatic result measures response structure, not visual accuracy: compare every output with its images and expected observations. Synthetic controls supplement, rather than replace, evaluations on recorded demonstrations.
Generate a draft guide#
Use a coding agent with local file and image access to follow
guide-generation-skills/SKILL.md. This orchestration skill coordinates the
initial draft, runtime evaluation, evidence-supported revisions, and human-review
handoff. Its draft-guide/ child writes YAML directly; evaluate-guide/ runs a
fixed draft against captured images with the configured XR-AI models and tools,
then reports findings without editing the guide. The orchestrator owns revisions.
These skills, their references, the validator, and packet-playback support belong
to the generation workflow. No separate sample regression suite is needed.
The authoring model observes runtime results and edits the guide. It does not replace the runtime VLM, invent tool answers, compile a plan into YAML, or approve the result. The capture worker does not invoke the skills.
From the repository root, give the agent this request:
Read agent-samples/procedure-capture-guide/guide-generation-skills/SKILL.md
and follow its draft-guide and evaluate-guide child skills, shared schema,
and applicable examples.
Use agent-samples/procedure-capture-guide/artifacts/sessions/<session-id>/packet.json.
Create <slug>-guide/ under agent-samples/procedure-capture-guide/ and write
guide.yaml and review.md inside it. Use a lowercase, hyphen-separated slug
derived from the guide name, such as assemble-toy-guide/guide.yaml.
Keep evaluation.md and per-run evaluation artifacts in the same guide directory.
Keep the guide draft.
Reuse narration and captions; inspect original images where evidence is unclear.
Interpret all capture sources together as complementary, fallible evidence of
the intended task, not observations to translate literally into steps. Use
judgment for ordinary organization and wording. Document consequential assumptions,
their effects and questions needing human confirmation in review.md, while
continuing to draft sufficiently understood parts.
Preserve distinct actions, counts, orientation, order, and required durations.
Validate with the runtime parser, then follow
agent-samples/procedure-capture-guide/guide-generation-skills/evaluate-guide/SKILL.md to test the
draft with the captured images and configured local XR-AI models and tools.
Assess fidelity to the intended task and clarity for someone performing it
afresh, not just whether the recording passes. Distinguish recording coverage
gaps, uncertain requirements and actual runtime verification limitations.
Observe the actual outputs and revise only the guide when findings justify it.
Revalidate and rerun revised guides without weakening requirements to fit the
demonstration. Follow the skill's iteration and final-verification rules.
Report paths, actual results, revision rounds, coverage gaps and approval
blockers. Leave task.status draft. Do not edit source recordings, substitute
your own visual answers, or invent successful tests when models are unavailable.
No installation is required for this explicit-file request. To install the
workflow, copy the entire guide-generation-skills/ directory, preserving its
top-level SKILL.md, child skills, references, and metadata. Start with
the top-level skill, which links to the child instructions; do not depend on
nested skills being independently discovered. Keep the target checkout and
packet paths explicit; the evaluator runs that checkout’s implementation.
The outputs are local and Git-ignored:
<slug>-guide/guide.yaml: model-authored draft.<slug>-guide/review.md: action-to-evidence mapping, capture ID, source frame references, corrections, revisions, consequential assumptions and questions needing human confirmation.<slug>-guide/evaluation.md: task fidelity and clarity assessment, runtime observations, evaluated hash, untested checks and findings for the next revision.
Source session IDs and recording timestamps belong in review notes, not executable YAML. Update the request, orchestration and child skills, schema reference and examples together when changing this workflow.
Validate and refine#
Run the read-only validator from the repository root:
uv run --project agent-samples/procedure-capture-guide/worker python -m procedure_capture_guide_worker._validate_guide agent-samples/procedure-capture-guide/<slug>-guide/guide.yaml
It imports the runtime parser directly; there is no authoring-specific schema. It checks state and completion rules, step links, trigger arguments, permitted tools, strings and duplicate keys. It neither rewrites nor approves a guide. A schema-valid guide can still omit an action or accept an incorrect result.
With this sample’s configured model servers running, evaluate a fixed draft:
PYTHONPATH=agent-samples/procedure-capture-guide/worker uv run --project agent-samples/procedure-capture-guide/worker python agent-samples/procedure-capture-guide/evaluation/guide/evaluate_recorded_packet.py --packet /path/to/packet.json --guide agent-samples/procedure-capture-guide/<slug>-guide/guide.yaml --output agent-samples/procedure-capture-guide/<slug>-guide/evaluation/run-001
The existing runtime evaluator supplies each captured frame once at its original
relative time and sends start and Next utterances through the conversation
model. It records exact guide bytes, source hashes, frame selections, actual
model and tool outputs, guidance state, speech and failures. Draft eligibility
is temporary and in memory. It does not write YAML or change approval status.
The camera endpoint and user text are simulated; hub IPC, browser, STT and TTS
audio are not tested. Refer to guide-generation-skills/evaluate-guide/SKILL.md
for artifact details and timing limits.
Use the findings to refine instructions and checks, then rerun from fresh state. Default to at most three evaluation-and-update rounds and always verify the final revision. Report actual rounds, not retries or formatting-only edits. Missing models or evidence are blocked tests, never a pass. A fixed recording cannot react to guidance, so distinguish timing gaps from guide defects. Missing visual evidence in the recording does not by itself invalidate an appropriate completion check for a future worker. Preserve that requirement and disclose the coverage gap rather than weakening the SOP to make the test pass.
The timer example verifies a visual setup, then starts the timer on entry to
the wait step. It accepts duration_s only; no model-written start timestamp.
Each step has its own initially false completion field. A current image cannot
prove a past action sequence, and elapsed time does not prove continuous contact.
Retain unsupported requirements as review blockers rather than weakening them.
Review the YAML and evidence notes before manually setting task.status to
approved. Resolve every blocker, including any never-matching draft gate.
Switch to agent-samples/procedure-capture-guide/ and run an approved guide with
uv run main.py --mode guide my-procedure-guide/guide.yaml, then say “start guide”.
Check generation changes#
The sample’s evaluation/guide/authoring_cases.yaml contains text-only authoring probes with
separate inputs and withheld review criteria. They are not image-based runtime
tests. This data stays in YAML so an external authoring model can receive each
capture excerpt without executing test code or seeing the separate reviewer criteria.
Tea execution scenarios cannot test this capture-to-guide authoring behavior.
Refer to the evaluation skill for the authoring comparison procedure.
The offline checks validate the fixture structure, all examples and shipped
guides, and the complete packet-runner invocation:
uv run --project tests python -m pytest tests/test_sop_guide_generation.py tests/test_sop_recorded_replay.py -q
Deterministic model doubles test runtime integration, not runtime-model perception or authoring-model guide quality. Keep guide-specific refinement results distinct from sample-level end-to-end regression results.
Follow an approved guide#
Select a reviewed guide YAML file or its containing directory by path. Generated guide packages live in
local, Git-ignored <name>-guide/ directories; maintained examples live under
example-guides/. Guides can also live outside the sample. Set task.status to
approved only after reviewing the instructions, completion checks, timers,
and safety constraints. A directory must contain exactly one top-level .yaml
or .yml file; otherwise, provide a specific file path.
The worker loads the selected file and rejects
missing, draft, or invalid guides. It does not search by task.name or task.id.
The launcher resolves relative paths from agent-samples/procedure-capture-guide/
and passes an absolute path to the worker. Symbolic links are supported. Quote paths
containing spaces. Run from the sample directory:
uv run main.py --mode guide my-procedure-guide/guide.yaml
uv run main.py --mode guide example-guides/tea-making
Start the shared models first, as for capture. Guide mode uses the LLM, VLM, STT,
and TTS endpoints in yaml/models.json. Connect using the hub’s web
client URL and enable microphone and camera access. The command line selects
the guide, but connecting does not start it. Say start guide to begin step
one with independent progress for that participant. No wake phrase is required.
Until then, no verification or timer is running. Say stop guide at any time
to cancel guidance, then start guide to begin a fresh run of the same guide.
Turning off live video does not reset the guide. The shared current
frame tool may request a still image from clients that support on-demand capture;
if no fresh image is available, visual verification waits. Timer checks continue
without a camera. Disconnect releases
that participant’s progress and pending work; reconnect waits for start guide.
Like Tea-making guidance — building deterministic visual workflows, guide separates foreground conversation from
background verification. The foreground answers guide questions using the
current step and its allowed tools. It cannot directly commit completion.
Periodic observations use a fresh camera view or a deterministic elapsed-time
check. The observer can commit only declared writable fields, with any required
consecutive evidence. Late observations from a previous step or run cannot
change current progress. The agent announces step completion once, without
requiring a user question. Advancement is explicit unless the terminal step
sets finish_on_complete: true, which ends the run automatically.
Request |
Behavior |
|---|---|
“Start guide” |
Begin a fresh run of the command-line-selected guide from step one |
“Stop guide” |
Cancel guidance and verification, clear progress, and keep the selected guide loaded but inactive |
“What should I do?” |
Explain the current step |
“Does this look right?” |
Use the current step’s visual tool if available |
“How much time remains?” |
Use the current step’s timer tool if available |
“Next” or “continue” |
Advance only if the current step is verified complete |
“Skip” |
Explicitly bypass the current step; this is not evidence of success |
“Status” |
Report current progress |
“Reset guide” |
Same as stop guide |
“Restart” |
Same as start guide |
After “next” on the completed final step, the agent announces workflow completion and leaves the guide inactive, ready for another start guide. No relaunch or reconnect is needed. Explicitly skipping to the end also leaves the guide inactive, with an explicit skipped-steps message rather than a verified-completion claim. While inactive, status and start guide remain available, but there is no active step or timer. Recording commands and selecting another guide are not supported inside guide mode.
The worker pins the validated guide and SHA-256 from the same captured bytes at startup. File edits do not change a running guide; restart the sample to use a new version or revoke approval. Approval is a local review convention, not a cryptographic signature. Model-based visual checks can be wrong; reviewers must describe observable evidence, including orientation and count where relevant. This demo is not a safety interlock. A still image cannot prove a hidden action.
Included tea-making guide#
example-guides/tea-making/guide.yaml is a hand-authored, approved example with task
name Tea Making and ID tea_making. Its four stages guide brewing
chamomile tea: read the packet, verify the water temperature, put the tea in
water, and start the steeping timer. It is not generated from a user recording.
Start the sample’s shared models, then run:
uv run main.py --mode guide example-guides/tea-making/guide.yaml
Connect and say start guide. Say next after each verified visual step and stop guide to cancel. The guide finishes automatically when the five-minute timer expires. Say start guide to repeat.
Use a tea packet or box labeled chamomile, a stable heat-safe vessel, and a kettle or thermometer with a readable current water-temperature display. The example uses the chamomile settings in the tea sample’s brewing reference: 100°C (212°F) water and a 5–7 minute steep. It selects five minutes and verifies a displayed 100°C (212°F) current reading. If the packet requires a different recipe or local boiling temperature cannot reach that target, stop and adapt a local copy before using it. Do not tilt or lift hot vessels to satisfy a camera check.
The example is adapted to guide’s existing tools, not a drop-in copy of the tea sample’s workflow YAML:
It accepts the printed word chamomile on a tea packet or box, including blend names such as Chamomile Lemon, regardless of brand. The brand does not need to be visible. The brewing settings are fixed in the guide; the brewing reference is not retrieved at runtime.
Its three visual steps require two distinct fresh frames. Identification accepts only
YES(case-insensitive) to visibleCHAMOMILEtext, notNO,UNKNOWN, or explanatory prose; the second step reads a currentLABEL | NUMBER UNITreading and acceptsCURRENT,ACTUAL, orTHERMOMETERwith100 Cor212 F. It preservesTARGETandSETlabels in the visual result so those readings cannot satisfy completion. Unlabeled displays are unavailable rather than assumed to show current temperature. Matching tolerates equivalent letter case, spacing, degree symbols, and trailing decimal zeros, not different temperatures. Immersion requiresYESfor visible bag contact with visible water, including a floating bag. Missing frames and responses outside each step’s accepted format cannot satisfy its evidence requirement. A well-formed answer can still be a visual misclassification; the response contract does not prove visual accuracy. These bounded evidence commits do not use an observation LLM; the foreground LLM still handles questions and controls.The heating check verifies a displayed current temperature at the brewing target, not a target setting or a “heating underway” threshold. Guide does not expose the tea sample’s typed temperature tools: this is a VLM judgment, not a deterministic temperature measurement or safety guarantee.
Immersion detection does not start the timer. Say next promptly after immersion is confirmed to enter the timer step. The runtime then measures a full 300 seconds; time spent waiting at the immersion step is not subtracted. Timer verification does not inspect images or prove continued immersion.
The final step sets
finish_on_complete: true, so expiry ends the active run, clears progress, and announces completion without another next command.Skipping a visual prerequisite ends the run as skipped instead of assuming the prerequisite was satisfied. Skipping the timer also cannot produce a verified-completion announcement.
Regression tests load this exact example and exercise its state transitions, distinct-frame evidence, skip behavior, and timer with simulated observations. Those tests do not establish accuracy on real tea-making images or appliances.
Local-model guide evaluation#
example-guides/tea-making/evaluation/run_scenario.py exercises a manifest’s selected guide through
the actual guide agent, runtime, frame selection, and model
tools. It uses the sample’s configured LLM and VLM. Fixture images replace the
hub camera endpoint, and typed requests enter the same runtime topic as
transcribed speech. Microphones, STT, network camera delivery, and audible TTS
are not covered. The script never manages external model-server processes.
Both the scenario runner and the skill’s recorded-packet runner use the production frame freshness and request
timeouts from yaml/worker.yaml, and the visual verification timeout from
yaml/guide-agent-config.yaml (15 seconds by default).
Scenarios continue scoring later probes after an answer or tool-call
mismatch unless fail_fast: true; a runtime exception or timeout still ends the run. Packet evaluation stops
after a failed check because later steps depend on that progress.
The tea step-verification and full-run scenarios cover visual progress, timers,
completion, and start/stop behavior. example-guides/tea-making/evaluation/scenarios/routing.yaml
exercises the composed foreground prompt with controls, polite requests,
quoted commands, negation, requests to explain guide content, unsupported capture
requests, and visual questions.
These scenarios run with the full permitted tool set; the evaluator does not preselect
tools based on the utterance.
Both guide evaluators tag model events with their origin and foreground turn ID.
Routing assertions check only calls belonging to the simulated user turn;
concurrent background observation calls remain in the trace but cannot count
as foreground routing successes or failures.
example-guides/tea-making/evaluation/scenarios/label_reading.yaml tests the included tea guide with generated label
cards: chamomile alone, longer chamomile names, other teas, and an unreadable
label. It checks real VLM responses, rejection of premature advancement, and
identification announcements. These cards test the response contract, not
photographic accuracy.
A YAML or JSON manifest selects a guide_path (relative to the manifest’s
directory, or absolute) and lists ordered actions:
image: show an imagepath, or a named syntheticscene. Optionalscenesmap names to a rectangle color, display text, or a tea scene.scenes_fileloads a shared scene map relative to the manifest.observe: wait forcountvisual results (default two) from the displayed image and compare them to the allowedanswers. Alternatively, name acasefromvisual_cases_file(relative to the manifest) to reuse its scene, step, and expected-answer regex. Recognition comparisons ignore whitespace layout and case; unavailable observations never count as correct answers.say: sendtextas a user turn. Optionalexpectchecks the delivered answer against a regex;toolsandargumentscheck model tool calls. Status assertions use What is my status? andworkflow__status.wait_speech: wait for anexpectregex in speech delivered since the last user turn, with optionaltimeout_s. This checks completion notices, including automatic timer completion, without reading private backend state. Optionalafter_speechandminimum_elapsed_scheck the elapsed delivery time from the first matching origin to completion within the most recent restart. Later conversational paraphrases do not move that origin.quiet: check for additional speech since the precedingsayresponse, including intervening image changes, and for a furtherduration_s. Use it after stopping the guide while a matching image remains visible. Onlyimageactions may appear between thatsayandquiet; observation, speech-wait, or other silence checks require a newsaybefore anotherquiet.
Timing fields are validated before model calls. after_speech and
minimum_elapsed_s must appear together; the minimum must be finite and
nonnegative. Timeouts and silence durations must be finite and positive.
The runner makes no assumptions about step names, step count, or a final timer. Timers use real elapsed time. Include negative observations before positive ones and explicitly test early advance, completion, stop, and restart as needed.
From the sample directory, with its model servers already running:
uv run --project worker python example-guides/tea-making/evaluation/run_scenario.py \
--manifest example-guides/tea-making/evaluation/scenarios/step_verification.yaml --output artifacts/tea-verification-eval
uv run --project worker python example-guides/tea-making/evaluation/run_scenario.py \
--manifest example-guides/tea-making/evaluation/scenarios/routing.yaml --output artifacts/tea-routing-eval
uv run --project worker python example-guides/tea-making/evaluation/run_scenario.py \
--manifest example-guides/tea-making/evaluation/scenarios/label_reading.yaml --output artifacts/tea-identification-eval
Use a new output directory for each run. Supply --manifest /path/to/scenario.yaml
to evaluate another guide and its images.
Outputs include the exact guide, manifest, image hashes, model tool calls, visual
answers, speech text, and result JSON. Synthetic controls can isolate label or
setpoint mistakes but do not establish accuracy on a user’s actual camera views.
The manifest’s optional boolean fail_fast defaults to false: mismatched
answers or tool calls fail the run but do not hide later independent probes.
scenarios/routing.yaml uses this continuation policy and restarts between probes.
The dependent tea scenarios set fail_fast: true, so a failed
check stops the scenario and later actions are reported as not scored rather
than run against an assumed successful history. Runtime exceptions and timeouts
stop either mode; they do not count as ordinary answer mismatches.
Generated tea-image evaluation#
Tea-specific evaluation data lives under example-guides/tea-making/evaluation/. The executable procedure is
example-guides/tea-making/guide.yaml; the YAML files below describe test inputs and
assertions, not additional guides.
File under |
Purpose |
|---|---|
|
Presents incorrect then correct scenes for each visual step; checks blocked advancement and verified completion, then stops before the timer |
|
Independent successful run through the five-minute timer, completion, restart, and stop |
|
Checks chamomile name variations, other tea names, and unreadable labels in the identification step |
|
Independent conversation probes for controls, negation, quoted instructions, and visual questions; restarts before each probe |
|
Expected recognition answers and acceptance decisions for isolated and multi-turn visual checks |
|
Image descriptions for packets, kettle readings, and tea-bag placement |
The adjacent run_scenario.py executes the scenario files; evaluate_visual_checks.py runs isolated
VLM checks selected by --manifest. _scene_rendering.py draws their fixtures,
and _visual_expectations.py supplies shared scoring. Automated runtime and
evaluator regressions live directly under the repository’s tests/.
tests/test_procedure_indicator_guide.yaml supplies a minimal visual-and-timer
guide for isolated evidence, timing, and failure-reporting checks, not a second
end-to-end scenario.
The sample’s standalone evaluation/capture/evaluate_captions.py loads synthetic caption inputs
from evaluation/capture/caption_cases.yaml. evaluation/guide/authoring_cases.yaml contains the
text-only authoring probes described above; tests/test_procedure_indicator_guide.yaml
is the only procedure-specific YAML fixture left in tests/.
The sample-owned evaluation/guide/_runtime_harness.py is an evaluation harness,
not the production guide runtime. It provides simulated camera and speech
boundaries, production agent and tool setup, and trace collection. Its speech
boundary records each published output without running the voice floor, so
update combining and withdraws are not exercised: a notice that production
withdraws on a step transition still appears in the trace.
tests/test_sop_guide.py covers those paths through the real floor. Both the
recorded-packet runner and tea scenario runner consume this harness.
Recorded-packet playback and orchestration stay in
evaluation/guide/; tea-specific scene rendering,
scripted actions, and assertions stay with the example guide. Neither runner
imports the other’s implementation or regression fixtures. The coding-agent
skill invokes evaluation/guide/evaluate_recorded_packet.py from the target
checkout rather than maintaining executable evaluation code inside the skill.
Capture evaluation and its cases live in evaluation/capture/; text-only
guide-authoring probes live in evaluation/guide/authoring_cases.yaml.
Keep the full run independent of step-verification failures so timer and lifecycle checks remain reachable. Label-reading has its own scenario because it isolates name variations using text cards without running the rest of the procedure. Image descriptions and expected answers are separate so both evaluators share the same fixtures and scoring contract without supplying expected answers to the renderer or model.
The tea evaluation separates visual recognition from guide execution. All images
are schematic drawings generated by example-guides/tea-making/evaluation/_scene_rendering.py from
example-guides/tea-making/evaluation/fixtures/image_scenes.yaml. Packet labels contain only tea names and a generic TEA heading,
without brand names or logos. Inspect the drawings before interpreting model scores.
The manifests use guide_path: ../../guide.yaml, relative to their own directory,
loading example-guides/tea-making/guide.yaml through the production guide runtime.
They exercise the same procedure, prompts, and verification rules that users run,
without a separate copy of the tea guide. The purpose-built
tests/test_procedure_indicator_guide.yaml remains a separate, simplified test procedure.
Identification checks whether chamomile is visibly printed on the packet,
using the shipped guide’s YES, NO, or UNKNOWN contract.
Layer |
What it checks |
What it does not establish |
|---|---|---|
CPU regression tests |
Reproducible images, shipped-guide selection, scoring, timeout handling, and existing guide state-machine guards |
Live model accuracy |
Isolated visual cases |
Configured VLM responses to the shipped guide’s exact questions and evidence gates |
Conversation, consecutive-frame enforcement, or task completion |
Multi-turn guide scenario |
Actual guide agent, frame tools, local LLM and VLM, controls, announcements, and the five-minute timer |
Microphone, STT, network camera transport, or audible TTS |
From the sample directory, generate images for inspection without model calls:
uv run --project worker python example-guides/tea-making/evaluation/evaluate_visual_checks.py \
--manifest example-guides/tea-making/evaluation/fixtures/visual_expectations.yaml \
--render-only --output artifacts/tea-preview
With the existing model servers running, evaluate the 25 visual cases three times each, then run the complete conversational scenario:
uv run --project worker python example-guides/tea-making/evaluation/evaluate_visual_checks.py \
--manifest example-guides/tea-making/evaluation/fixtures/visual_expectations.yaml \
--repeats 3 --output artifacts/tea-visual
uv run --project worker python example-guides/tea-making/evaluation/run_scenario.py \
--manifest example-guides/tea-making/evaluation/scenarios/step_verification.yaml --output artifacts/tea-step-verification
uv run --project worker python example-guides/tea-making/evaluation/run_scenario.py \
--manifest example-guides/tea-making/evaluation/scenarios/full_run.yaml --output artifacts/tea-full-run
The visual suite covers five tea blends, incomplete and covered labels, cold through boiling readings, Celsius and Fahrenheit, target versus current labels, competing readings, and dry, immersed, floating, and obscured tea bags. Expected answers describe scene content and are used only for scoring, not supplied to the VLM. Isolated and conversational checks share this answer table. The suite reports recognition accuracy (allowing whitespace layout and equivalent temperature notation), evidence-gate accuracy, false accepts, false rejects, and unavailable requests separately. An unavailable model cannot pass a negative case. Correctly rejecting a scene while misreading its contents is not a recognition pass. Repetitions measure response variability on the same images, not additional independent scenes. Evidence-gate scores apply the production regex to the raw stripped answer; recognition normalization never changes whether the guide would accept it.
The step-verification scenario tests early advance rejection, completion announcements, and explicit advancement for the three visual steps, then stops. The full-run scenario tests successful visual verification, the real five-minute steep, return to inactive state, restart, stop, and silence after stop. Its completion check requires at least 295 seconds between timer-entry and completion speech, allowing five seconds for speech delivery while leaving the runtime’s 300-second timer unchanged. Allow more than five minutes for a complete run. Fixture pixels are republished as a simulated live stream: repeated source pixels do not demonstrate recognition across different camera views. Existing CPU tests separately enforce distinct-frame identity and stale-observation rejection. Post-stop checks include the image switch and ten additional seconds of silence. This is a bounded observation window, not a guarantee against announcements delayed beyond it by slow inference.
Both runners exit nonzero on failed checks and preserve evidence in a new output directory. The visual runner saves PNGs, scene definitions, guide and source hashes, guide questions, raw answers, timing, and per-step scores. Compare runs using the same guide, fixtures, model configuration, and renderer versions. A synthetic pass is not evidence of safe operation with real appliances or general photographic accuracy.
Guide structure#
The declarative shape follows the tea-making workflow: a task, typed sparse state, and ordered steps with separate observation and voice policies. This is a guide-specific schema inspired by that design, not a compatible loader for tea-making or legacy workflow-recorder YAML. Adapt guides to the contract below.
Text fields must be actual strings. Duplicate mapping keys are rejected at
every level, including overrides introduced through YAML merges, instead of
silently replacing earlier requirements. Invalid guides cannot be used in guide mode,
even when their task.status is approved.
schema_versionmust be1.taskdeclaresid,name, positiveversion,status(draftorapproved),start_step,foreground_prompt, andcomplete_message.Each
statefield declares a type (boolean,integer,number, orstring), a description, and optionally an initial value.Each step declares
id,title,reads,writes,trigger,voice, a non-emptycomplete_whenmapping,next, andmessagescontainingenter,complete, andskip. Thenextchain must visit every step once and end withnull.agentandvoiceeach contain a prompt and a tool list.agentis optional whenevidence.commitsupplies a deterministic completion patch; that path does not run an observation LLM. Otherwiseagentis required. Allowed step tools arecurrent_view,clock__now, andclock__timer. Only the observer receivesworkflow__commit. Foreground workflow controls are supplied by the runner, not listed in the guide’s step tools.Optional
evidencecontains a full-match regexpattern, positiveconsecutivecount, and optionalcommitmapping. Withcommit, matching observations directly apply that bounded patch without an observation LLM. The patch must satisfy everycomplete_whenrequirement. Each step must have at least one initially unmet completion field that no preceding step can write; use a dedicated completion flag rather than reusing a preceding step’s flag. Withoutcommit, the observation LLM proposes state changes and the evidence count gates completion. Use a precise closed-set VLM answer contract if a regex is used. Visual evidence counts distinct source frames, identified by participant, track, timestamp, and sequence. Repeated or older frames do not increment the count or run another observation-model update. Unavailable observations reset the consecutive evidence count.Optional
state_on_skipwrites only declared step fields;complete_on_skip: trueends the run when explicitly skipped.Optional
finish_on_complete: trueis valid only on a terminal step. Once verified, it ends the run automatically and announces the task’s completion message. If an earlier step was skipped, it reports skipped steps instead. The default isfalse, preserving explicit advancement after verification.
current_view triggers require arguments.question (1–500 characters), a
positive interval_s, and optionally result_field: text. Timer triggers use
function: clock__timer with arguments.duration_s (positive integer seconds). They may
select elapsed_s, remaining_s, or expired as result_field.
Arguments can reference declared readable or writable fields using
$state.field_name. On entering a timer step, guide resolves and freezes its
duration and starts a monotonic runtime clock. Missing or invalid durations
leave the step blocked. Start timestamps are not writable timer state;
started_at_us in timer trigger arguments is rejected. Model commits and
wall-clock changes cannot backdate the timer or shorten an active wait.
The foreground clock__timer tool takes no arguments and reads the active
runtime timer. Restarting the guide starts a fresh timer on the next timer-step
entry; clock__now is informational and cannot start or modify timers.
An unavailable or superseded timer returns available: false and a reason,
not an invented elapsed time or a tool exception.
Timer-only steps use elapsed-time evidence, not a visual substitute. Completion
commits for timer triggers are rejected until their configured duration expires.
Composition and configuration#
The outer main.py launches the hub and worker, plus the shared recorder when
--session-recording is enabled. The worker’s __main__.py
accepts configuration and readiness arguments; procedure_capture_guide_worker/app.py constructs
the hub endpoint, model clients, voice runtime, and frame/image tools.
The worker uses VoiceAgent for transcripts, CurrentFrameTool for snapshots,
and MultiImageQueryTool for captions.
The registered CaptureAgent in procedure_capture_guide_worker/capture/agent.py owns commands,
narration admission, participant generations, and acknowledgements. Its private
_Capture helper in capture/capturing.py owns sampling, caption tasks,
persistence, and finalization.
capture/captioning.py defines the request and response contract used by both
recording and evaluation. Application cleanup explicitly drains capture work
before closing its frame endpoint and VLM client.
The worker keeps shared model, voice, image, and lifecycle wiring in app.py
and shared settings in config.py. The sibling capture/ and guide/ packages
own their capability-specific agents, settings, and helpers. Only the selected
agent is registered. With no mode flag, capture remains the default.
guide/agent.py handles connection events, foreground questions, and conversational
controls. Its _Guide backend in guide/guiding.py owns guide progress,
runtime timers, frame evidence, and background verification. Transitions clear
local notices under the session lock, so obsolete completion notices cannot
follow the new step’s instruction.
The foreground receives detached guidance context and scoped tools. Observation
updates have a state revision; controls have a separate run-and-step revision.
Completing the same step during inference does not invalidate a stop or advance
request, but a concurrent step transition does. The backend rechecks those scopes
under its own lock when controls execute and answers finish. It owns accepted control tasks
through completion even if a new utterance cancels the foreground turn. The
cancelled turn drains accepted controls and delivers their still-current transition
response; new guidance snapshots also drain controls first. Departure and shutdown
drain those tasks without speaking to disconnected participants.
Stop guide cancels the observer and clears timer
state without unloading the selected guide. Connecting only loads an inactive
session; start guide activates it and starts its observer and any first-step timer.
Guide mode gives VoiceAgent an LLM update combiner for unprompted guide
announcements, and uses ImageQueryTool, native tools, and the
shared tool loop. The conversation prompt governs intent without an English
wording filter. Polite action requests can use controls; questions about controls
do not authorize them. This is model policy, not a deterministic intent guarantee.
State-changing tools independently check session freshness and completion.
Guide loading and validation live in guide/guides.py
and guide/_workflow_spec.py. Guide mode never constructs the capture backend or
writes demonstration packets. Both modes start the same hub and worker processes
and use the same separately launched model stack and yaml/models.json.
The always-on voice gate passes commands and narration through the same accepted input path. Recording acknowledgements follow voice-input handling, so a command cannot cancel its own response.
Configuration |
Settings |
|---|---|
|
Shared frame and speech detection settings; capability configuration paths |
|
Task output directory, sampling rate, and caption interval |
|
Visual verification timeout |
|
Shared batch STT, LLM, VLM, and TTS services and endpoints |
|
Opt-in session recording output, profile, and participant lifetime |
|
Shared wake-word-free speech input; each mode handles its own commands |
|
Room and web server |
worker.yaml selects the capture YAML through capture_config, resolved relative
to the worker YAML. artifacts_dir resolves relative to the selected capture
YAML, even when it is in another directory. Model and voice configuration remain
beside the worker YAML. Absolute configuration and output paths are also supported.
The worker loads shared settings into WorkerConfig and capture-specific settings
into CaptureConfig under capture/config.py; app.py supplies each to its
owning component. Guide mode selects its YAML through guide_config and loads
GuideConfig from guide/config.py. The required positional GUIDE_PATH selects
a guide file or a directory containing exactly one top-level YAML file. The
launcher resolves sample-relative paths and passes an absolute path to the worker.
task.name and task.id remain guide metadata, not CLI selectors.
--output-root applies only to capture. Restart after configuration edits.
Refer to Sample configuration reference and Command-line reference for fields and CLI options.
Guide regression tests#
From the repository root:
uv --config-file uv.toml run --project tests pytest -q \
tests/test_procedure_capture_guide.py tests/test_sop_guide.py -m "not gpu"
These tests cover mode isolation, approval, guide validation, participant-local
progress, repeated runs, stale observations, timers, and runtime-to-voice delivery.
They use synthetic fixtures and validate the shipped hand-authored tea guide,
not private user recordings. Offline CLI tests exercise the evaluation runner
with controlled model responses, including visual-only and visual-and-timer guides.
These tests establish runtime behavior, not model accuracy. Run the standalone
example-guides/tea-making/evaluation/run_scenario.py scenarios above against existing model servers to measure live
tool routing and visual judgments; the runner never manages their lifecycle.