Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```

Expand Down
4 changes: 4 additions & 0 deletions docs/uat-results-template.csv
Original file line number Diff line number Diff line change
@@ -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
258 changes: 258 additions & 0 deletions docs/uat-runbook.md

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions docs/uat-social.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
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

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)) 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…". (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
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 #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) |
| 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: <emoji> <first-person hook> <#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.>

<optional: one sentence of honest context, e.g. "This part of me is
still being built — you can watch it get better.">

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 <check-id> from my <date> full-feature test day.

Tags: stackchan, m5stack, esp32, robot, ai robot, self-hosted ai,
open source robot, <check-specific tags>
```

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. *(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
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 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 —
the CSV is the single record tying QA results, issues, and published
clips together.

Last verified: 2026-07-11.
9 changes: 8 additions & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
@@ -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/<date>/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

Expand Down
161 changes: 161 additions & 0 deletions scripts/uat-capture.sh
Original file line number Diff line number Diff line change
@@ -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/<date>/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=<XIAOZHI_USER>@<XIAOZHI_HOST> scripts/uat-capture.sh start
# XIAOZHI_SSH=<XIAOZHI_USER>@<XIAOZHI_HOST> 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/<YYYY-MM-DD>)
#
# 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:-<XIAOZHI_USER>@<XIAOZHI_HOST>}"
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 <<EOF

Capture running. Optional but recommended — serial monitor in a second
terminal (re-plug USB-C if /dev/ttyACM0 is missing):

docker run --rm -v "\$PWD/firmware/firmware:/project" -w /project \\
--device=/dev/ttyACM0 espressif/idf:v5.5.4 \\
bash -lc 'idf.py -p /dev/ttyACM0 monitor' | tee $LOG_DIR/serial.log

When the session ends: XIAOZHI_SSH=$XIAOZHI_SSH scripts/uat-capture.sh stop
EOF
;;

stop)
# 1. Kill the tails.
if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY-RUN: kill pids listed in $PID_FILE"
elif [[ -f "$PID_FILE" ]]; then
while read -r pid name; do
kill "$pid" 2>/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
Loading
Loading