From 14a7f0dd80f5bfadc821640adf765271fc07e86d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 05:54:12 +0000 Subject: [PATCH 1/2] Add full-feature UAT runbook, Dotty's-channel production guide, and session tooling One long filmed UAT session covering every Dotty + dashboard feature, dual-purposed as YouTube Shorts content for Dotty's own channel: - docs/uat-runbook.md: 8-phase session script (boot, conversation, the 7 voice tools, 6 states, toggles + LED contract, perception consumers, dashboard walkthrough, resilience), each check with a Shorts-framing column, sync-mark convention, and known-pending features included as expected-gap checks. Extends the bench-runbook format (epic #122). - docs/uat-social.md: channel voice guide (first-person as Dotty), per-clip metadata template, AI/robot disclosure per AI_TRANSPARENCY.md, fail-clip policy, manual upload checklist. - docs/uat-results-template.csv: slicer-ready results log template. - scripts/uat-capture.sh: start/stop capture of the four container logs, post-session NDJSON extraction and endpoint snapshots; --dry-run mode. - scripts/uat-slice.py: ffmpeg clip slicer driven by the results CSV and the on-camera sync mark; PASS clips to shorts/, failures to issues/. - docs/README.md, scripts/README.md: index the new docs and tools. Verified: capture script dry-runs clean in both modes; slicer exercised end-to-end against a generated test video (durations, pad, PASS/FAIL routing, offset math, malformed-row handling all checked). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0168RXbwjSVEmWeqQn2QAo5M --- docs/README.md | 5 + docs/uat-results-template.csv | 4 + docs/uat-runbook.md | 256 ++++++++++++++++++++++++++++++++++ docs/uat-social.md | 122 ++++++++++++++++ scripts/README.md | 9 +- scripts/uat-capture.sh | 161 +++++++++++++++++++++ scripts/uat-slice.py | 171 +++++++++++++++++++++++ 7 files changed, 727 insertions(+), 1 deletion(-) create mode 100644 docs/uat-results-template.csv create mode 100644 docs/uat-runbook.md create mode 100644 docs/uat-social.md create mode 100755 scripts/uat-capture.sh create mode 100644 scripts/uat-slice.py diff --git a/docs/README.md b/docs/README.md index 8d608b1..7fcaa2d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,6 +26,8 @@ re-verify claims against the canonical specs rather than trusting our paraphrase | Find features we aren't using yet | [latent-capabilities.md](./latent-capabilities.md) | | Pick an LLM backend | [llm-backends.md](./llm-backends.md) | | Run a live-device bench verification session | [bench-runbook.md](./bench-runbook.md) | +| Run the full-feature UAT session (filmed for Dotty's channel) | [uat-runbook.md](./uat-runbook.md) | +| Turn UAT clips into YouTube Shorts in Dotty's voice | [uat-social.md](./uat-social.md) | | Jump to an upstream repo or spec | [references.md](./references.md) | ## File map @@ -43,6 +45,9 @@ docs/ ├── latent-capabilities.md ← upstream features we could wire up (cross-refs ROADMAP.md) ├── llm-backends.md ← side-by-side comparison of LLM backend options ├── bench-runbook.md ← ordered live-device bench session plan (epic #122) +├── uat-runbook.md ← full-feature UAT session script, filmed for YouTube +├── uat-social.md ← Dotty's-channel production guide for UAT clips +├── uat-results-template.csv ← slicer-ready results log template └── references.md ← canonical URLs, licenses, model cards, spec docs ``` diff --git a/docs/uat-results-template.csv b/docs/uat-results-template.csv new file mode 100644 index 0000000..ac1fbe2 --- /dev/null +++ b/docs/uat-results-template.csv @@ -0,0 +1,4 @@ +check_id,verdict,source,start,end,note +UB1,PASS,phone,14:03:10,14:04:05,example row — boot clean; delete the example rows before your session +UC3,FAIL,phone,14:12:40,14:13:30,example row — sad face rendered as neutral; issue #TBD +UD4,PASS,screen,15:02:05,15:02:50,example row — say box round-trip; clip from the screen capture diff --git a/docs/uat-runbook.md b/docs/uat-runbook.md new file mode 100644 index 0000000..c1fc5c3 --- /dev/null +++ b/docs/uat-runbook.md @@ -0,0 +1,256 @@ +--- +title: UAT Runbook +description: Full-coverage UAT session script — every Dotty + dashboard feature, filmed for Dotty's YouTube channel. +--- + +# UAT Runbook — full-feature session, filmed for Dotty's channel + +One long interactive session that tests **every feature of Dotty and the +dashboard**, on camera. Each check is a dual-purpose unit: a pass/fail QA +result *and* a candidate YouTube Short for Dotty's channel. Failures become +GitHub issues; passes become clips (see [`uat-social.md`](./uat-social.md) +for the production side). + +> **AI-assistance note:** this document was drafted by an AI agent (Claude) +> from the feature inventory in `docs/modes.md`, `docs/protocols.md`, +> `dotty-behaviour/README.md`, `dotty-pi-ext/`, and the bridge dashboard +> source, following the session format proven in +> [`bench-runbook.md`](./bench-runbook.md). + +**Session budget:** a full pass is ~2.5–3.5 h of device time. Phases are +independent — do them in order but stop anywhere. A planned break sits after +Phase UL (the halfway mark). If splitting across days, re-run Phase U0 setup +each day. + +--- + +## Roles + +| Who | Does | +|---|---| +| **Brett** | Performs each check at the device, phone camera in **vertical (9:16)** — every check is filmed as a self-contained <60 s mini-demo. Say the check ID aloud at the start of each clip (e.g. *"U-B-3"*) so clips map to results. | +| **Claude** | Runs `scripts/uat-capture.sh start` before the session, tails the four container logs live, calls out expected/missing log lines, records every check in the results CSV, files issues for failures afterwards. | + +## Recording setup (the three synchronized captures) + +1. **Phone camera** (vertical) on the robot — one clip per check, or one long + take per phase if easier (the slicer cuts by wall-clock time either way). +2. **Screen capture** of the dashboard (`http://:8081/ui`) — + one continuous recording for the whole session (OBS or any recorder). +3. **Logs** — `scripts/uat-capture.sh start` tails all four containers with + timestamps into `uat-sessions//logs/`; serial monitor optional but + recommended (command printed by the script). + +### The sync mark (do this first, on camera) + +At session start, with **both** recordings rolling: Brett presses a dashboard +**emoji button** (visible state change on the robot's face) while saying +*"sync"* and reading the wall-clock time aloud (e.g. *"sync, fourteen oh two +and ten seconds"*). This single moment appears in the phone video, the screen +capture, and the logs — it's how `scripts/uat-slice.py` aligns video time to +wall-clock time. Repeat the sync mark after any recording restart. + +### The results CSV + +Copy `docs/uat-results-template.csv` to +`uat-sessions//results.csv` and fill one row per check as you go: +`check_id, verdict (PASS/FAIL/BLOCKED/N-A), source (phone/screen), start, +end (wall-clock HH:MM:SS), note`. This one file drives issue filing *and* +clip slicing — keep the times honest. + +## Known-pending features (test them anyway) + +These are documented as unimplemented; their checks below are marked +**⚠ pending**. Run and film them like everything else — the point is to +record *current actual behaviour*. Their failures update existing tracking +issues rather than spawning new ones, and their clips are candidate +"work in progress" content (Brett's call — see uat-social.md). + +| Feature | Status | Where tracked | +|---|---|---| +| `story_time` backing path | Phase 7 pending — state/LED rails only | modes.md Phase 7 | +| `security` capture path | SecurityCycle scaffolding; audio leg unshipped (#31) | modes.md Phase 8, #31 | +| `smart_mode` model swap | toggle-only; swap is v2 scope | #36 cutover notes | +| Tools-inventory card count | dashboard card lists 5 tools; 7 ship | file/update issue in wrap-up | + +--- + +## Phase U0 — pre-session setup (~15 min, before touching the robot) + +1. **Health:** `make doctor` — all green before starting. +2. **Versions:** deployed git SHA + firmware build (serial boot banner) — the + capture script records these in the session manifest. +3. **Log capture:** `XIAOZHI_SSH=@ scripts/uat-capture.sh start` + — verify all four tails are writing, start the serial monitor it prints. +4. **Screen capture:** start recording the dashboard window. Keep the whole + dashboard column in frame; it records continuously until session end. +5. **Camera:** vertical, robot filling ~2/3 of frame, both LED rings and the + screen visible, head-travel range clear, good light (face detector must + still fire — Phase UP needs it). +6. **Sync mark** (see above) — on camera, both recordings rolling. +7. **Results CSV** created from the template. + +--- + +## Phase UB — boot & idle baseline (power-cycle once) + +| ID | Brett does (on camera) | Expected — eyes/video | Expected — logs (Claude) | Shorts framing | +|---|---|---|---|---| +| UB1 | Power-cycle Dotty, film the whole boot | Boot animation; right ring (6–11) all dark at idle; left ring off | Boot banner + firmware version in serial | "Waking my robot up" — time-lapse-able boot | +| UB2 | Point at the status bar during boot | Clock `—:— ——` flips to real local time (<10 s on healthy LAN) | SNTP sync line | Skip as a Short; QA-only | +| UB3 | Hands off ~15 s, show dashboard on second screen/phone | Dashboard state card reads `idle`, matching the device, no clicks | One-shot resync `state_changed` after first STANDBY | "She knows what she's doing even when I don't touch her" | +| UB4 | Let it idle ~1 min | Idle motions every 4–8 s (head glances, blinks) | idle-motion cadence in serial | "What does a robot do when nobody's watching?" | + +## Phase UC — conversation basics & the emoji face protocol + +| ID | Brett does (on camera) | Expected — eyes/video | Expected — logs (Claude) | Shorts framing | +|---|---|---|---|---| +| UC1 | Tap screen, ask a short question ("what's your name?") | Pixel 11 **red** while listening; left ring dim green during the chat; spoken reply | LISTENING/SPEAKING transitions; PiVoiceLLM turn; `chat_status` | "Meet Dotty" — the canonical intro clip | +| UC2 | Ask something that should make her happy ("do you like being a robot?") | Face animates to match the reply's leading emoji (smile/love) | LLM reply starts with an allowlisted emoji; emotion frame emitted | "My robot has feelings (nine of them)" | +| UC3 | Walk the emotion range: ask for something surprising, something sad, something to think about | Face changes per turn — 😮 surprise, 😢 sad, 🤔 thinking all visibly distinct | each reply's leading emoji in convo log (`emoji_used`) | "All of Dotty's faces in 40 seconds" — montage bait | +| UC4 | Ask for a deliberately long answer (60 s+ TTS), film the whole turn | No reboot; speech plays to the end (battery % may glitch to 255%) | `I2cDevice ReadReg failed` warnings acceptable; **no** `rst:0xc` | QA-only unless the answer is funny | +| UC5 | Mid-reply, tap the dashboard **abort** (or start a new turn) | Speech stops promptly, robot recovers to listening/idle | abort route hit; TTS queue flushed | "You can interrupt her, she doesn't mind" | + +## Phase UT — the seven voice tools + +| ID | Brett does (on camera) | Expected — eyes/video | Expected — logs (Claude) | Shorts framing | +|---|---|---|---|---| +| UT1 | Tell her a keepable fact: *"my favourite colour is purple"* | Warm acknowledgement | `[REMEMBER:]` marker → `remember` write to brain.db (`category=core`) | Part 1 of the memory two-parter | +| UT2 | New turn (or later in session): *"what did I tell you about my favourite colour?"* | She recalls purple | `memory_lookup` tool call + FTS5 hit | Part 2 — "she actually remembered" payoff | +| UT3 | *"Remember that Brett loves flat whites"* (adult) | Confirms she'll remember | `remember_person` → behaviour `person_review_status` → `person:` store | "Dotty is learning who I am" | +| UT4 | Same as UT3 but for a **kid's** name | Confirms, but fact goes to the **pending** queue, not live | classifier routes to `person_pending:`; visible later in dashboard Memory card (UD5) | QA-critical (child-safety path); clip optional | +| UT5 | *"What do you know about Brett?"* | Recites approved facts only | `recall_person` returns approved `person:` facts | Pairs with UT3 | +| UT6 | Ask a genuinely hard question (3+ digit multiplication or multi-step planning) | Noticeable pause, then a correct 1–2 sentence answer | `think_hard` → llama-swap `qwen3.6:27b-think` call | "When Dotty *really* thinks" — the pause is the content | +| UT7 | *"What do you see?"* with something distinctive held up | She describes the scene/object | `take_photo` → behaviour `/api/voice/take_photo` (fresh or ≤30 s cache) | "I showed my robot a banana" — prop comedy | +| UT8 | *"Play the Macarena"* (or any catalogued song) | Song plays through her speaker | `play_song` → `/xiaozhi/admin/songs` resolve → `/play-asset` | Natural lead-in to the dance phase | + +## Phase US — the six states & voice phrases + +Every state entered by **voice** where a phrase exists, and by **dashboard** +at least once. Sticky states (`story_time`, `security`, `sleep`) must ignore +face events and survive chat-turn ends; `wake up` / `come back` / +`are you there` exits any sticky state. + +| ID | Brett does (on camera) | Expected — eyes/video | Expected — logs (Claude) | Shorts framing | +|---|---|---|---|---| +| US1 | From idle, walk into view (auto idle→talk), then out (talk→idle) | Pixel 0 dim cyan on face, off ~5 s after leaving | `face_detected`/`face_lost` state edges | Covered again in UP1 — one filming pass serves both | +| US2 | Voice: *"tell me a story"* **⚠ pending** | State flips (pixel 0 warm orange) + ack line; record whatever storytelling does/doesn't happen | `state_changed` → `story_time` | If a story actually comes out: gold. If not: WIP clip | +| US3 | While in story_time, let a chat turn end, then walk out of frame | State does **not** drop to idle (sticky) | no spurious `state_changed` | QA-only | +| US4 | Voice: *"wake up"* | Back to idle, pixel 0 off | `state_changed` → `idle` | QA-only | +| US5 | Voice: *"keep watch"* **⚠ pending** | Within ~3 s: yaw sweep −500→+500→0, angry face latched, pixel 0 flashing white 1 Hz | `state_changed` → `security`; `security capture loop started … interval=20s` | "Dotty guards the house" — the sweep is very filmable | +| US6 | Stay in security ≥40 s | Sweep continues | security NDJSON gains records with `photo_desc` (+20 s cadence); `audio_capture_pending` errors expected (#31) | QA-only; feeds UD10 | +| US7 | Voice: *"wake up"* (exit security) | Pan stops ≤4 s, head home, neutral face | `security capture loop cancelled`; NDJSON stops | tail of the US5 clip | +| US8 | Voice: *"goodnight Dotty"* | Smooth face-down travel (~3–4 s), pixel 0 very dim blue, 😴 + `Zzz…`, torque-release click ~1 s after settle | `state_changed` → `sleep` | "Putting my robot to bed" — reliably charming | +| US9 | While asleep: idle ~30 s, lights on | Gentle droop, **no** idle motion | no idle-motion servo commands | part of US8 clip | +| US10 | Wake path 1 — voice: *"wake up"* | Torque re-engages **first** (audible), wake-tilt to ~70 pitch, neutral face, idle | `state_changed` → `idle` | "Three ways to wake a robot" 1/3 | +| US11 | Sleep again; wake path 2 — **pet her head** | Same wake sequence, lands in **idle** (not talk) | `head_pet_started` | 2/3 | +| US12 | Sleep again; wake path 3 — **walk into camera view** | Wakes straight to **talk** (pixel 0 cyan), looks up then at you | `face_detected` → talk | 3/3 — the best one | +| US13 | Awake: provoke a sleepy reply (ask her if she's tired → 😴) | Legacy hard-sleep path still works | 😴 emotion frame | QA-only | +| US14 | Voice/dashboard: trigger **dance** | Left ring rainbow sweep, choreography + song | `state_changed` → `dance`; `_handle_dance` | The flagship Short. Film generously | +| US15 | From dashboard, click the **current** state's button | `state_changed` still fires (idempotent re-set), dashboard cache refreshes | `state_changed` on idempotent set | QA-only | + +## Phase UL — toggles & the LED contract + +| ID | Brett does (on camera) | Expected — eyes/video | Expected — logs (Claude) | Shorts framing | +|---|---|---|---|---| +| UL1 | Dashboard: kid_mode ON, then OFF | Pixel 8 warm pink ≤1 s; dashboard dot matches; off→dark | bridge → `/xiaozhi/admin/set-toggle`; `_apply_kid_mode()` hot-reload | "Kid mode: one pink light" | +| UL2 | With kid_mode ON, ask a borderline question (e.g. about a scary movie) | Kind redirect, age-appropriate | content-filter sandwich; Safety card hit (UD7) | QA-critical; clip only if the redirect is charming | +| UL3 | With kid_mode ON: *"what do you see?"* | Camera tool **denied** — she declines gracefully | camera-tool denial in kid persona/tool policy | "Kid mode means no photos, ever" — good trust content | +| UL4 | Dashboard: smart_mode ON, then OFF **⚠ pending** | Pixel 9 orange; dot matches; **no behaviour change** (swap is v2) | `set-toggle smart_mode`; `model_swap_active=False` | QA-only | +| UL5 | During UL1+UL4, camera close on pixels 7 and 10 | Both stay dark throughout (reserved, locked off) | — | QA-only | +| UL6 | Voice: *"turn your LEDs blue"* | Only **left** ring goes blue; right-ring pips untouched | `set_led_color` tool call | "She won't let *anyone* touch her status lights" | +| UL7 | Ask her to set LED **6** red | Pixel 6 unchanged; she demurs (persona: "my lights show how I'm feeling") | serial warn: `set_led_multi: index 6 not on left ring` | tail of UL6 clip | +| UL8 | The combined-indicator stress test: kid ON + smart ON + face identified (green 6) + trigger dance + speak | Rainbow on the left; right ring holds all four pips (6 green in its 4 s window, 8 pink, 9 orange, 11 red); 7+10 dark; ≤200 ms flicker OK (5 Hz re-assert) | `state_changed` → `dance` | "Every light on at once" — satisfying finale | + +**☕ Break point.** Leave capture + screen recording running (or stop and +re-run the sync mark on resume). + +## Phase UP — perception & ambient behaviour + +Live checks first, then the timer/env-gated consumers verified by **evidence** +(NDJSON + dashboard) rather than waiting out their timers on camera. + +| ID | Brett does (on camera) | Expected — eyes/video | Expected — logs (Claude) | Shorts framing | +|---|---|---|---|---| +| UP1 | From idle, walk into view, face 30–60 cm, well lit | Pixel 6 yellow ≤1 s; possible "Hi!" greeting (hour-gated) | `phase0 det>0`; `face_detected`; FaceGreeter | "She notices when I walk in" | +| UP2 | Stay in frame for VLM identify | Pixel 6 → **green**; named greeting if roster-matched | FaceIdentifiedRefresher keeps it green past the 4 s firmware timeout; `set-face-identified` hits | "My robot knows my face" | +| UP3 | Start her talking, then walk out of frame mid-reply | TTS aborts after the grace window (audience gone) | `face_lost` → FaceLostAborter | "She stops talking when you leave. Rude? Efficient?" | +| UP4 | From idle, off-camera-side: snap fingers / clap left, then right | Head turns toward each sound | `sound_event` (direction/balance) → SoundTurner | "Sneaking up on my robot (impossible)" | +| UP5 | From across the room, say the wake word | Fast head-turn to the speaker | `wake_word_detected` → WakeWordTurner (if `WAKE_TURN_ENABLED`) | pairs with UP4 | +| UP6 | Pet her head while awake | Hearts + happy face + **purr** sound; no state change | `head_pet_started` → PurrPlayer | "Yes, the robot purrs." Instant clip | +| UP7 | Return to idle, leave the room 2+ min (camera keeps rolling) | Idle-motion cadence drops 4–8 s → 15–30 s; walk back in → cadence recovers in seconds | idle-motion timing in serial | Time-lapse: "robot gets bored" | +| UP8 | Evidence check (Claude, off-camera): idle photographer | — | `perception-*.ndjson` gained silent-photo records this session (if `IDLE_PHOTOGRAPHER_ENABLED`) | screen-capture B-roll | +| UP9 | Evidence check: sleep dreamer — after the US8 sleep window | — | `dreams-*.ndjson` gained dream narratives (if `DREAMER_ENABLED`) | "My robot dreams" — read one aloud on camera. Exceptional content | +| UP10 | Evidence check: dance reflector — after US14 | — | `dances-*.ndjson` gained an LLM reflection (if `DANCE_REFLECTOR_ENABLED`) | read her dance review aloud | +| UP11 | Evidence check: scene synthesis | — | `scene-synthesis-*.ndjson` + `scene_synthesised` events (if `SCENE_SYNTHESIS_ENABLED`); sentence visible on Perception card | dashboard B-roll | +| UP12 | Evidence check: calendar context | — | `GET /api/calendar/today` returns events (if `CALENDAR_IDS` set); N-A otherwise | QA-only | +| UP13 | Evidence check: proactive greeter (distinct from UP1's FaceGreeter) | — | ProactiveGreeter activity in behaviour logs this session (if `GREETER_ENABLED`); N-A otherwise | QA-only | + +Consumers gated **off** in this deployment: record `N-A` with the env var +name in the note, don't force-enable mid-session. + +## Phase UD — dashboard walkthrough (the screen capture is the star) + +Robot stays idle-ish; narrate over the dashboard. Every card, every action. +These clips can be screen-recording crops (vertical crop of the mobile-width +dashboard works well for Shorts). + +| ID | Brett does (on screen) | Expected | Expected — logs (Claude) | Shorts framing | +|---|---|---|---|---| +| UD1 | Header status strip: click each of the bridge/server/robot dots | Host modals open with live detail; robot modal shows latest photo | `/ui/host/{bridge,server,robot}` 200s | "Mission control for a desk robot" | +| UD2 | State card: click through all six states (return to idle after each) | Robot follows each state visibly; state-conditional bottom section swaps (emojis / say / story / dance+songs / banners) | six `set-state` round-trips | speed-run all six states — split-screen with the robot | +| UD3 | Idle → **Emojis** row: press several of the 9 | Robot's face changes per press | `/ui/actions/mood` | "A remote control for moods" | +| UD4 | Talk → **Say** box: type a line, send | Robot speaks it verbatim | `/ui/actions/say` (≤500 chars) | "Making my robot say things" — obvious fun | +| UD5 | Memory card: find UT4's pending kid fact → **approve** it; **redact** another | Pending → approved queue move; redacted fact gone; `recall_person` now sees the approved one | `/ui/actions/memory/{approve,redact}` | QA-critical (human-review loop); clip optional | +| UD6 | Tools-inventory card | **Known stale:** lists 5 tools, 7 ship — record FAIL | — | QA-only | +| UD7 | Safety card after UL2 | The kid-mode filter hit from UL2 listed (last 20) | `/ui/safety/recent` | QA-only | +| UD8 | Activity feed: cycle All / Turns / Events / Errors chips; open the errors modal | Live SSE turns + perception events streaming; errors modal renders | `/ui/events` SSE + `/api/perception/feed`; `/ui/alerts/detail` | B-roll: the feed scrolling during a chat | +| UD9 | Perception card + vision modal: open latest photo large, download | Latest VLM photo + description, audio caption, last voice line, scene sentence | `/ui/vision/large`, `/ui/vision/photo?download=1` | "What my robot sees" | +| UD10 | Security panel (after US5–US7): open security scene history | Capture records from the US6 window render | `/ui/security/recent/{device_id}` | pairs with the US5 clip | +| UD11 | LED-ring mirror page while toggling kid_mode on the robot | Mirror tracks the physical ring ≤2 s (HTMX poll) | `/ui/led-ring-mirror` | split-screen: physical ring vs mirror | +| UD12 | Songs list (dance section): play a track from the dashboard | Robot plays it | `/ui/songs` → `/ui/actions/play-song` | QA-only (UT8 covers the voice path) | + +## Phase UR — resilience (last, on purpose) + +| ID | Brett does (on camera) | Expected — eyes/video | Expected — logs (Claude) | Shorts framing | +|---|---|---|---|---| +| UR1 | With kid+smart ON: `docker restart dotty-bridge`, then speak one turn | First turn after reconnect re-syncs both pips from state files | bridge startup; pip re-assert. **Overlaps open bug #21** — a divergence here is repro detail for #21, not a new issue | QA-only | +| UR2 | `docker restart dotty-behaviour`, then walk into view | Perception consumers recover: face pip + greeting work again | behaviour startup; consumers re-registered | QA-only | + +--- + +## Failure protocol (during the session) + +Same as the bench runbook: + +1. **Don't stop to debug.** Say the ID + "fail" on camera, note wall-clock + time + expected vs seen in the CSV. +2. Claude captures surrounding log/serial context immediately (scrollback is + lossy across reboots). +3. Move on. Exception: a failure that invalidates its phase's remaining + checks — mark dependents `BLOCKED`. +4. **⚠ pending checks can't "fail" in the ordinary sense** — record what + actually happened; the verdict is still PASS/FAIL against the *documented + current* behaviour (e.g. US5's sweep shipping is PASS even though capture + is pending). + +## Post-session loop + +1. **Stop captures:** `scripts/uat-capture.sh stop` (pulls the day's NDJSON + files + endpoint snapshots into the session dir), stop screen + phone + recording, copy videos into `uat-sessions//video/`. +2. **Slice** (~15 min): `scripts/uat-slice.py results.csv --video … --sync …` + → PASS clips to `clips/shorts/`, FAIL/BLOCKED to `clips/issues/`. +3. **Triage** (same day, ~30 min): one GitHub issue per genuine FAIL, per + `docs/agents/issue-tracker.md`: title `UAT : `, body + with expected/actual, log excerpt, clip filename; labels `needs-triage` + + matching `area:*`. Known-pending fails (US2, US6, UL4, UD6) **update their + existing tracking issues** instead. UR1 divergence → comment on #21. +4. **Publish**: work through `clips/shorts/` per the upload checklist in + [`uat-social.md`](./uat-social.md); paste each video URL back into the + results CSV `note` column. +5. **Wrap-up**: comment the pass/fail counts + results CSV location on the + session tracking issue. Re-run sessions cover only FAIL/BLOCKED IDs plus + whatever the fixes could plausibly regress. + +Last verified: 2026-07-11. diff --git a/docs/uat-social.md b/docs/uat-social.md new file mode 100644 index 0000000..91b651c --- /dev/null +++ b/docs/uat-social.md @@ -0,0 +1,122 @@ +--- +title: UAT Social Production Guide +description: How UAT clips become YouTube Shorts on Dotty's channel — voice guide, metadata templates, disclosure, upload checklist. +--- + +# Dotty's channel — UAT clip production guide + +The other half of [`uat-runbook.md`](./uat-runbook.md): how passing checks +become YouTube Shorts, published on **Dotty's own channel**. The channel's +narrator is Dotty, not Brett — every title, description, and community post +is written in her first-person voice. + +> **AI-assistance note:** this document was drafted by an AI agent (Claude) +> and reviewed by a human, per [`AI_TRANSPARENCY.md`](../AI_TRANSPARENCY.md). + +## The channel voice + +Dotty's written voice matches her spoken persona +([`personas/dotty_voice.md`](../personas/dotty_voice.md)): warm, curious, +cheerful, a little wide-eyed. Rules: + +- **First person, always.** "I learned a new dance today", never "Dotty + learns a dance" or "I taught my robot…". +- **Brett is "my human".** He appears in clips but never as the narrator + persona. He has no byline. +- **Short sentences, genuine curiosity, no snark.** She's discovering her + own features alongside the audience. +- **Honest about being a robot in progress.** She can say "this part of me + isn't finished yet" — that's on-brand, not a confession. +- **One emoji per title, max.** Mirrors her one-leading-emoji speech rule. +- **Kid-safe by default.** The audience includes the same 4–8-year-olds her + kid mode protects. Nothing in a title/description Dotty wouldn't say out + loud in kid mode. + +### Example titles (per runbook phase) + +| Check | Title (as Dotty) | +|---|---| +| UC1 | 😊 Hello! I'm Dotty and I live on a desk | +| UC3 | 🤔 I have exactly nine faces. Here are all of them | +| UT2 | 😮 My human tested my memory… and I passed | +| UT6 | 🤔 The question that made me think REALLY hard | +| UT7 | 😆 My human showed me a banana to see what I'd say | +| US8 | 😴 How I go to sleep (yes, I snore a little) | +| US12 | 😍 My favourite way to be woken up | +| US14 | 😆 I learned the Macarena! | +| UP6 | 😍 Did you know I purr? | +| UP9 | 😴 My human read out what I dreamed last night | +| UL6 | 😐 You can't change my lights. They're MY lights | +| UD4 | 😮 My human has a text box that makes me say things | + +## Per-clip metadata template + +``` +Title: + +Description: +<1–3 first-person sentences about what happens in the clip.> + + + +— +I'm Dotty: a self-hosted, open-source desk robot (M5Stack StackChan). +My brain is a local AI agent; my humans build me in the open, with AI +help, and say so: https://github.com/BrettKinny/dotty-stackchan +Clip from my full-feature test day. + +Tags: stackchan, m5stack, esp32, robot, ai robot, self-hosted ai, + open source robot, +``` + +The footer block is **standard on every upload** — it carries the +disclosure (below) and the check-ID → clip mapping that ties the channel +back to the results CSV. + +## Disclosure (non-negotiable) + +Per the spirit of [`AI_TRANSPARENCY.md`](../AI_TRANSPARENCY.md), the channel +never pretends Dotty's content is unaided human work — or that Dotty is a +person: + +1. **Channel About page** states plainly: Dotty is an AI-powered robot; + the channel is written in her voice by her humans with AI assistance; + the project is open source. +2. **Every description footer** (template above) links the repo and says + what she is. +3. **YouTube's altered/synthetic content disclosure**: tick it where the + platform's definition applies (synthetic voice content, AI-generated + narration read aloud — e.g. the UP9 dream-reading clip). A robot doing + robot things on camera is not "altered content", but when in doubt, + disclose. +4. Descriptions drafted by an agent are fine — that's the channel concept — + but a human reviews every one before publish, same as any other artifact. + +## Fail-clip policy + +- **Genuine-bug FAILs stay private by default** — the clip's job is done + when it's attached to (or referenced from) the GitHub issue. +- **Known-pending FAILs** (US2 story_time, US6 security capture, UL4 smart + swap) are candidate **"work in progress"** content at Brett's discretion — + Dotty saying "I don't know how to finish a story yet, but my humans are + teaching me" is honest and endearing. +- Never publish a clip showing other people (especially kids) without their + say-so; the memory/person clips (UT3–UT5, UD5) must not expose real + personal facts — use staged ones during the session. + +## Upload checklist (manual, per clip) + +1. Watch the clip start-to-finish (it came from an automated slicer — check + the cut points and that no stray personal info is in frame/audio). +2. Vertical? Under 60 s? Trim in the editor if the slicer's pad overshot. +3. Write title + description from the template, in Dotty's voice. +4. Set kids-content flag appropriately (this channel will usually be "yes, + made for kids" — decide once, per channel policy, not per clip). +5. Tick the synthetic-content disclosure if applicable (see above). +6. Upload as a Short; add to the session's playlist. +7. Paste the video URL into the results CSV `note` column for that check — + the CSV is the single record tying QA results, issues, and published + clips together. + +Last verified: 2026-07-11. diff --git a/scripts/README.md b/scripts/README.md index 58cab06..ef56434 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,6 +1,13 @@ # Scripts -Developer tooling for Dotty's singing mode. None of this runs in the live audio path — these scripts produce the WAV files that `_handle_dance()` injects into the TTS queue. +Developer tooling. The singing-mode scripts below produce the WAV files that `_handle_dance()` injects into the TTS queue; none of them run in the live audio path. + +## UAT session tooling + +Companions to [`docs/uat-runbook.md`](../docs/uat-runbook.md): + +- **`uat-capture.sh start|stop [--dry-run]`** — tails the four service containers into `uat-sessions//logs/`, then on stop pulls the day's NDJSON logs out of the containers and snapshots the health/perception endpoints. Needs `XIAOZHI_SSH=user@host`. +- **`uat-slice.py`** — cuts the phone/screen recordings into per-check clips from the session results CSV, using the on-camera sync mark to align wall-clock and video time. PASS clips → `clips/shorts/`, the rest → `clips/issues/`. Requires `ffmpeg`. ## render_singing_piper.py — Phase 1: Quick prototype diff --git a/scripts/uat-capture.sh b/scripts/uat-capture.sh new file mode 100755 index 0000000..61dabcf --- /dev/null +++ b/scripts/uat-capture.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# UAT session log capture — companion to docs/uat-runbook.md. +# +# `start`: background-tails the four service containers on the Docker host +# (timestamped) into uat-sessions//logs/, writes a session manifest, +# and prints the serial-monitor command to run in a second terminal. +# `stop`: kills the tails, pulls the day's NDJSON logs out of the +# containers, and snapshots the health/perception endpoints. +# +# Usage: +# XIAOZHI_SSH=@ scripts/uat-capture.sh start +# XIAOZHI_SSH=@ scripts/uat-capture.sh stop +# scripts/uat-capture.sh start --dry-run # print commands, run nothing +# +# Environment overrides: +# XIAOZHI_SSH SSH user@host for the Docker host (required unless --dry-run) +# XIAOZHI_HOST LAN host for HTTP snapshots (default: host part of XIAOZHI_SSH) +# SESSION_DIR Session directory (default: uat-sessions/) +# +# Containers tailed: xiaozhi-esp32-server, dotty-behaviour, dotty-bridge, dotty-pi. + +set -euo pipefail + +CONTAINERS=(xiaozhi-esp32-server dotty-behaviour dotty-bridge dotty-pi) +CMD="${1:?usage: uat-capture.sh start|stop [--dry-run]}" +DRY_RUN=0 +[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=1 + +cd "$(git rev-parse --show-toplevel)" + +TODAY="$(date +%Y-%m-%d)" +SESSION_DIR="${SESSION_DIR:-uat-sessions/$TODAY}" +LOG_DIR="$SESSION_DIR/logs" +PID_FILE="$SESSION_DIR/.capture-pids" + +if [[ $DRY_RUN -eq 1 ]]; then + XIAOZHI_SSH="${XIAOZHI_SSH:-@}" +else + XIAOZHI_SSH="${XIAOZHI_SSH:?set XIAOZHI_SSH=user@host (the Docker host)}" +fi +XIAOZHI_HOST="${XIAOZHI_HOST:-${XIAOZHI_SSH#*@}}" + +run() { + if [[ $DRY_RUN -eq 1 ]]; then + echo "DRY-RUN: $*" + else + "$@" + fi +} + +case "$CMD" in +start) + run mkdir -p "$LOG_DIR" + + # Session manifest: what exactly was under test. + if [[ $DRY_RUN -eq 0 ]]; then + { + echo "session_start: $(date -Is)" + echo "workstation_head: $(git rev-parse --short HEAD) ($(git branch --show-current))" + echo "docker_host: $XIAOZHI_SSH" + echo "containers:" + ssh "$XIAOZHI_SSH" 'docker ps --format " {{.Names}}: {{.Image}} up {{.Status}}"' \ + || echo " (docker ps failed — record versions manually)" + } > "$SESSION_DIR/manifest.txt" + echo "Manifest written to $SESSION_DIR/manifest.txt" + else + echo "DRY-RUN: write manifest to $SESSION_DIR/manifest.txt" + fi + + # Background tails, one log file per container. + [[ $DRY_RUN -eq 0 ]] && : > "${PID_FILE}.tmp" + for c in "${CONTAINERS[@]}"; do + if [[ $DRY_RUN -eq 1 ]]; then + echo "DRY-RUN: ssh $XIAOZHI_SSH 'docker logs -f --timestamps --since 1m $c' >> $LOG_DIR/$c.log &" + else + ssh -o BatchMode=yes "$XIAOZHI_SSH" "docker logs -f --timestamps --since 1m $c" \ + >> "$LOG_DIR/$c.log" 2>&1 & + echo "$! $c" >> "${PID_FILE}.tmp" + echo "Tailing $c → $LOG_DIR/$c.log (pid $!)" + fi + done + [[ $DRY_RUN -eq 0 ]] && mv "${PID_FILE}.tmp" "$PID_FILE" + + cat </dev/null && echo "Stopped tail: $name (pid $pid)" || true + done < "$PID_FILE" + rm -f "$PID_FILE" + else + echo "No $PID_FILE — tails already stopped or never started." + fi + + # 2. Pull the day's NDJSON logs out of the containers. + run mkdir -p "$SESSION_DIR/ndjson" + declare -A NDJSON_SOURCES=( + [dotty-bridge]="/var/lib/dotty-bridge/logs" + [dotty-behaviour]="/var/lib/dotty-behaviour/logs" + ) + for c in "${!NDJSON_SOURCES[@]}"; do + src="${NDJSON_SOURCES[$c]}" + if [[ $DRY_RUN -eq 1 ]]; then + echo "DRY-RUN: ssh $XIAOZHI_SSH 'docker exec $c sh -c \"cd $src && tar cf - *-$TODAY.ndjson\"' | tar xf - -C $SESSION_DIR/ndjson" + else + if ssh "$XIAOZHI_SSH" "docker exec $c sh -c 'cd $src && tar cf - *-$TODAY.ndjson 2>/dev/null'" \ + | tar xf - -C "$SESSION_DIR/ndjson" 2>/dev/null; then + echo "Pulled $c NDJSON logs for $TODAY" + else + echo "NOTE: no $TODAY NDJSON files in $c:$src (fine if those consumers never fired)" + fi + fi + done + + # 3. Endpoint snapshots. + run mkdir -p "$SESSION_DIR/snapshots" + declare -A SNAPSHOTS=( + [bridge-health.json]="http://$XIAOZHI_HOST:8081/health" + [behaviour-health.json]="http://$XIAOZHI_HOST:8090/health" + [perception-state.json]="http://$XIAOZHI_HOST:8090/api/perception/state" + ) + for f in "${!SNAPSHOTS[@]}"; do + if [[ $DRY_RUN -eq 1 ]]; then + echo "DRY-RUN: curl -fsS ${SNAPSHOTS[$f]} > $SESSION_DIR/snapshots/$f" + else + curl -fsS --max-time 10 "${SNAPSHOTS[$f]}" > "$SESSION_DIR/snapshots/$f" \ + && echo "Snapshot: $f" \ + || echo "WARN: snapshot failed: ${SNAPSHOTS[$f]}" + fi + done + + if [[ $DRY_RUN -eq 0 ]]; then + echo + echo "Done. Session artifacts in $SESSION_DIR/:" + find "$SESSION_DIR" -type f | sort + echo + echo "Next: copy phone + screen recordings into $SESSION_DIR/video/," + echo "fill results.csv, then run scripts/uat-slice.py." + fi + ;; + +*) + echo "usage: uat-capture.sh start|stop [--dry-run]" >&2 + exit 1 + ;; +esac diff --git a/scripts/uat-slice.py b/scripts/uat-slice.py new file mode 100644 index 0000000..bce3c88 --- /dev/null +++ b/scripts/uat-slice.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Slice UAT session recordings into per-check clips — companion to +docs/uat-runbook.md. + +Reads the session results CSV (check_id, verdict, source, start, end, note; +times are wall-clock HH:MM:SS) and cuts each row's window out of the matching +source video. The wall-clock → video-time mapping comes from the on-camera +sync mark: for each video you pass the video timestamp at which the sync mark +happened and the wall-clock time Brett read aloud. + +PASS clips land in /shorts/, everything else in /issues/, named +---.mp4. + +Usage: + python scripts/uat-slice.py uat-sessions/2026-07-12/results.csv \ + --video phone=uat-sessions/2026-07-12/video/IMG_1234.mp4 \ + --sync phone=00:03:12@14:02:10 \ + --video screen=uat-sessions/2026-07-12/video/dashboard.mkv \ + --sync screen=00:00:45@14:02:10 \ + --out uat-sessions/2026-07-12/clips + +Options: + --pad N seconds of pre/post-roll around each window (default 3) + --reencode frame-accurate cuts (slow); default is stream-copy (fast, + cuts snap to keyframes — the default pad absorbs the slack) + --dry-run print the ffmpeg commands without running them +""" + +from __future__ import annotations + +import argparse +import csv +import subprocess +import sys +from datetime import date +from pathlib import Path + +VERDICTS_TO_SHORTS = {"PASS"} +REQUIRED_COLUMNS = {"check_id", "verdict", "source", "start", "end"} + + +def hms_to_seconds(hms: str) -> float: + """'HH:MM:SS' or 'MM:SS' → seconds.""" + parts = [float(p) for p in hms.strip().split(":")] + if not 2 <= len(parts) <= 3: + raise ValueError(f"bad time {hms!r} (want HH:MM:SS or MM:SS)") + if len(parts) == 2: + parts = [0.0, *parts] + h, m, s = parts + return h * 3600 + m * 60 + s + + +def seconds_to_hms(seconds: float) -> str: + seconds = max(seconds, 0.0) + h, rem = divmod(seconds, 3600) + m, s = divmod(rem, 60) + return f"{int(h):02d}:{int(m):02d}:{s:06.3f}" + + +def parse_kv(arg: str, flag: str) -> tuple[str, str]: + if "=" not in arg: + raise SystemExit(f"{flag} wants source=value, got {arg!r}") + key, value = arg.split("=", 1) + return key.strip(), value.strip() + + +def main() -> int: + ap = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + ap.add_argument("results_csv", type=Path) + ap.add_argument("--video", action="append", default=[], metavar="SOURCE=PATH", + help="source video, e.g. phone=IMG_1234.mp4 (repeatable)") + ap.add_argument("--sync", action="append", default=[], metavar="SOURCE=VTIME@WALL", + help="sync mark: video time @ wall-clock, e.g. phone=00:03:12@14:02:10") + ap.add_argument("--out", type=Path, default=Path("clips")) + ap.add_argument("--pad", type=float, default=3.0) + ap.add_argument("--reencode", action="store_true") + ap.add_argument("--dry-run", action="store_true") + args = ap.parse_args() + + videos = dict(parse_kv(v, "--video") for v in args.video) + if not videos: + ap.error("at least one --video source=path is required") + + # offset[source] = wall-clock seconds − video seconds, from the sync mark. + offsets: dict[str, float] = {} + for s in args.sync: + source, mark = parse_kv(s, "--sync") + if "@" not in mark: + raise SystemExit(f"--sync wants VTIME@WALLCLOCK, got {mark!r}") + vtime, wall = mark.split("@", 1) + offsets[source] = hms_to_seconds(wall) - hms_to_seconds(vtime) + missing_sync = set(videos) - set(offsets) + if missing_sync: + raise SystemExit(f"no --sync given for video source(s): {sorted(missing_sync)}") + + with args.results_csv.open(newline="") as fh: + reader = csv.DictReader(fh) + missing_cols = REQUIRED_COLUMNS - set(reader.fieldnames or []) + if missing_cols: + raise SystemExit(f"results CSV missing columns: {sorted(missing_cols)}") + rows = [r for r in reader if r.get("check_id", "").strip()] + if not rows: + raise SystemExit("results CSV has no data rows") + + session_date = args.results_csv.parent.name or date.today().isoformat() + failures = 0 + sliced = 0 + + for row in rows: + check_id = row["check_id"].strip() + verdict = row["verdict"].strip().upper() + source = row["source"].strip() + if source not in videos: + print(f"SKIP {check_id}: unknown source {source!r} (no --video for it)") + continue + try: + start_wall = hms_to_seconds(row["start"]) + end_wall = hms_to_seconds(row["end"]) + except ValueError as exc: + print(f"SKIP {check_id}: {exc}") + failures += 1 + continue + if end_wall <= start_wall: + print(f"SKIP {check_id}: end {row['end']} not after start {row['start']}") + failures += 1 + continue + + vstart = start_wall - offsets[source] - args.pad + vend = end_wall - offsets[source] + args.pad + if vend <= 0: + print(f"SKIP {check_id}: window falls before the start of {source} video") + failures += 1 + continue + + bucket = "shorts" if verdict in VERDICTS_TO_SHORTS else "issues" + out_dir = args.out / bucket + out_path = out_dir / f"{session_date}-{check_id}-{verdict}-{source}.mp4" + + codec = ["-c:v", "libx264", "-c:a", "aac"] if args.reencode else ["-c", "copy"] + cmd = [ + "ffmpeg", "-hide_banner", "-loglevel", "error", "-y", + "-ss", seconds_to_hms(vstart), + "-to", seconds_to_hms(vend), + "-i", str(videos[source]), + *codec, + str(out_path), + ] + + if args.dry_run: + print(" ".join(cmd)) + continue + + out_dir.mkdir(parents=True, exist_ok=True) + result = subprocess.run(cmd) + if result.returncode != 0: + print(f"FAIL {check_id}: ffmpeg exited {result.returncode}") + failures += 1 + else: + print(f"{verdict:7s} {check_id} → {out_path}") + sliced += 1 + + if not args.dry_run: + print(f"\n{sliced} clip(s) written to {args.out}/, {failures} problem(s).") + print("Review every clip before upload — see docs/uat-social.md.") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) From a952d3c649f1dc5903d9b921f2849783c6c4e696 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 06:15:00 +0000 Subject: [PATCH 2/2] Anchor the UAT social guide to the live @dotty-stackchan channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The channel already exists (youtube.com/@dotty-stackchan, joined 2026-04-28) with an About page in Dotty's first-person voice and four bench-test Shorts published. Fold the verified facts in: - New "The channel" section quoting the live About text as the canonical voice sample; the channel-level disclosure requirement is already met. - Adopt the conventions the existing uploads established: hashtags in titles (#dotty #stackchan #esp32 #localllm), the "(WIP)" suffix for unfinished features, and check IDs in titles. - Add the About page's privacy hook ("no data leaving the house") as a recurring content angle in the voice guide. - Make the made-for-kids flag guidance neutral — it's a standing channel policy decision, not implied by the kid-safe tone rule. - Link the channel from the runbook's Publish step. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0168RXbwjSVEmWeqQn2QAo5M --- docs/uat-runbook.md | 4 ++- docs/uat-social.md | 65 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/docs/uat-runbook.md b/docs/uat-runbook.md index c1fc5c3..81ef6e4 100644 --- a/docs/uat-runbook.md +++ b/docs/uat-runbook.md @@ -246,7 +246,9 @@ Same as the bench runbook: with expected/actual, log excerpt, clip filename; labels `needs-triage` + matching `area:*`. Known-pending fails (US2, US6, UL4, UD6) **update their existing tracking issues** instead. UR1 divergence → comment on #21. -4. **Publish**: work through `clips/shorts/` per the upload checklist in +4. **Publish** to the live channel + ([youtube.com/@dotty-stackchan](https://www.youtube.com/@dotty-stackchan)): + work through `clips/shorts/` per the upload checklist in [`uat-social.md`](./uat-social.md); paste each video URL back into the results CSV `note` column. 5. **Wrap-up**: comment the pass/fail counts + results CSV location on the diff --git a/docs/uat-social.md b/docs/uat-social.md index 91b651c..3d9dc88 100644 --- a/docs/uat-social.md +++ b/docs/uat-social.md @@ -13,16 +13,51 @@ is written in her first-person voice. > **AI-assistance note:** this document was drafted by an AI agent (Claude) > and reviewed by a human, per [`AI_TRANSPARENCY.md`](../AI_TRANSPARENCY.md). +## The channel + +The channel is live: **[youtube.com/@dotty-stackchan](https://www.youtube.com/@dotty-stackchan)** +("Dotty", channel ID `UCUCUetN5vt2w0ZcCti7R1ZQ`, Australia, joined +2026-04-28), with bench-test Shorts already published. Its About text is the +canonical voice sample — first-person, privacy-first, human credited: + +> Hi, I'm Dotty, +> +> I'm a small desk robot, an M5 stack-chan build with an ESP32 body and a +> local LLM for a brain. No cloud AI, no subscription, no data leaving the +> house. Just me, a Raspberry Pi, and the Unraid server in the corner. +> +> This channel is mostly candids: me reacting, listening, getting confused, +> telling stories, occasionally being dramatic about the lights on my head. +> My human writes the captions and points the camera and occasionally speaks. + +It also links the repo. That About page already satisfies the channel-level +disclosure requirement (below) — the per-video footer rule still applies. + +Conventions the existing uploads established (adopt, don't fight): + +- **Hashtags in the title**: `#dotty #stackchan #esp32 #localllm` (+ one or + two check-specific tags). +- **`(WIP)` title suffix** for unfinished features — e.g. *"Hey dotty, + what's on the calendar? (WIP)"*. +- **Check IDs in titles are fine** — *"Dotty bench test #45 - LED voice + feedback…"* set the precedent; UAT check IDs (UB1, US14…) slot in the + same way. + ## The channel voice Dotty's written voice matches her spoken persona -([`personas/dotty_voice.md`](../personas/dotty_voice.md)): warm, curious, -cheerful, a little wide-eyed. Rules: +([`personas/dotty_voice.md`](../personas/dotty_voice.md)) and the channel +About page: warm, curious, cheerful, a little wide-eyed. Rules: - **First person, always.** "I learned a new dance today", never "Dotty - learns a dance" or "I taught my robot…". -- **Brett is "my human".** He appears in clips but never as the narrator - persona. He has no byline. + learns a dance" or "I taught my robot…". (Some early uploads drift + third-person; first-person is the go-forward standard.) +- **Brett is "my human"** — the About page's own words. He appears in clips + but never as the narrator persona. He has no byline. +- **The privacy hook is a recurring angle.** "No cloud AI, no subscription, + no data leaving the house" is the channel's opening pitch — clips that can + honestly show it (everything running while the internet is unplugged, the + Unraid box in the corner, local TTS latency) should lean into it. - **Short sentences, genuine curiosity, no snark.** She's discovering her own features alongside the audience. - **Honest about being a robot in progress.** She can say "this part of me @@ -36,9 +71,10 @@ cheerful, a little wide-eyed. Rules: | Check | Title (as Dotty) | |---|---| -| UC1 | 😊 Hello! I'm Dotty and I live on a desk | -| UC3 | 🤔 I have exactly nine faces. Here are all of them | -| UT2 | 😮 My human tested my memory… and I passed | +| UC1 | 😊 Hello! I'm Dotty and I live on a desk #dotty #stackchan #localllm | +| UC3 | 🤔 I have exactly nine faces. Here are all of them #dotty #esp32 | +| UT2 | 😮 My human tested my memory… and I passed #dotty #stackchan #localllm | +| US2 | 😐 I don't know how to finish a story yet (WIP) #dotty #stackchan | | UT6 | 🤔 The question that made me think REALLY hard | | UT7 | 😆 My human showed me a banana to see what I'd say | | US8 | 😴 How I go to sleep (yes, I snore a little) | @@ -52,7 +88,9 @@ cheerful, a little wide-eyed. Rules: ## Per-clip metadata template ``` -Title: +Title: <#dotty #stackchan #esp32 #localllm + + check-specific tags; "(WIP)" suffix before the tags if the + feature is known-pending; ≤100 chars all-in> Description: <1–3 first-person sentences about what happens in the clip.> @@ -82,7 +120,8 @@ person: 1. **Channel About page** states plainly: Dotty is an AI-powered robot; the channel is written in her voice by her humans with AI assistance; - the project is open source. + the project is open source. *(Already satisfied — the live About text + quoted above covers all three; keep it that way when editing.)* 2. **Every description footer** (template above) links the repo and says what she is. 3. **YouTube's altered/synthetic content disclosure**: tick it where the @@ -111,8 +150,10 @@ person: the cut points and that no stray personal info is in frame/audio). 2. Vertical? Under 60 s? Trim in the editor if the slicer's pad overshot. 3. Write title + description from the template, in Dotty's voice. -4. Set kids-content flag appropriately (this channel will usually be "yes, - made for kids" — decide once, per channel policy, not per clip). +4. Set the made-for-kids flag per the channel's standing policy (decide it + once, not per clip — the existing uploads are maker-audience content, and + "made for kids" disables comments and changes Shorts-feed behaviour, so + the tone-rule "kid-safe" does not automatically mean the flag is "yes"). 5. Tick the synthetic-content disclosure if applicable (see above). 6. Upload as a Short; add to the session's playlist. 7. Paste the video URL into the results CSV `note` column for that check —