Merge pull request #837 from permissionlesstech/codex/readme-screenshot-skill

Add Android README screenshot studio skill
This commit is contained in:
callebtc 2026-07-31 02:40:02 +02:00 committed by GitHub
commit 13669b0d86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 582 additions and 0 deletions

View File

@ -0,0 +1,249 @@
---
name: android-readme-screenshot-studio
description: Create or refresh polished, high-resolution screenshots of the Bitchat Android app for README and repository showcase use. Use this skill whenever a user asks for README screenshots, app-store-like repository images, a populated mesh-chat showcase, voice-note or media conversation captures, a geohash globe image, higher-resolution Pixel captures, or a PR that adds or replaces documentation screenshots. It owns the complete workflow from latest-main isolation and deterministic synthetic fixtures through real app rendering, visual inspection, system-chrome cropping, README asset updates, clean builds, and an optional PR. Do not use it for before/after UI regression evidence, which belongs to android-ui-visual-review, or for physical mesh behavior, which belongs to mesh-lab.
compatibility: Requires git, gh, the Android SDK and emulator, adb, Java/Gradle, Python 3, and image inspection support. FFmpeg is useful for capture-only media preparation.
---
# Android README Screenshot Studio
Create repository screenshots from the real Bitchat Android UI, not from a
drawn mockup. The result should look intentional enough for the top of the
README while remaining reproducible, synthetic, and honest about what a static
emulator capture proves.
## Resolve the brief
Extract as much as possible from the conversation before asking questions.
Confirm or infer:
1. Which surfaces are needed, such as mesh chat and geohash globe.
2. The exact visible state and ordering. Treat phrases like “photo, messages,
three voices, thumbs-up” as a chronological contract rather than a loose
suggestion.
3. Whether existing README images should be preserved, replaced, or added.
4. The target base. Default repository work to the latest `origin/main`.
5. Whether GitHub publication and merge are authorized. A request to “open a
PR and merge it” authorizes both; otherwise do not merge.
Do not invent the subject of a requested photo. Nicknames, channel names, or
previous fixture copy are not sufficient justification for choosing an outdoor,
urban, political, or personal scene. Reuse an existing rights-safe asset when
the subject should remain stable, or ask for the intended subject. If the user
explicitly approves synthetic imagery, disclose it and keep its source
capture-only unless they request a committed asset.
Read [references/showcase-recipes.md](references/showcase-recipes.md) for every
run. It contains the concrete mesh-chat and globe recipes, framing guidance,
and the final acceptance checklist.
When a populated screen requires a debug fixture, also read
[../android-ui-visual-review/references/fixture-recipes.md](../android-ui-visual-review/references/fixture-recipes.md).
## Work in a fresh tree
Protect the user's active checkout:
1. Inspect `git status` without modifying it.
2. Fetch `origin/main`.
3. Create a new `codex/` branch in a temporary worktree rooted at the current
`origin/main`.
4. Keep screenshots and raw captures in a separate temporary artifact
directory so cleanup or checkout operations cannot remove them.
Do not switch the user's original checkout, reuse a dirty branch, or mix an
unrelated PR into the screenshot change. If the request continues an existing
screenshot PR, reuse its already-isolated worktree only after verifying its
head and base.
## Establish the capture contract
Before building, write a compact local matrix containing:
- surface and navigation path;
- chronological fixture contents;
- expected visible top and bottom rows;
- Pixel profile, physical resolution, logical width, theme, and locale;
- crop policy and final asset dimensions;
- existing asset path and README reference;
- behaviors the static screenshot does not prove.
Use the current production UI and latest `main` interaction model. Trace the
screen entry point and state source before adding a fixture. A beautiful capture
of a stale or fake UI is not acceptable.
## Use a high-resolution Pixel canvas
Prefer the newest stable Android runtime installed locally and a large Pixel Pro
profile. For the current README style, target at least:
- a 448 dp logical width;
- 1344 px physical width;
- a native portrait height near 2992 px;
- 480 dpi or the profile's native density.
Pixel 9 Pro XL at 1344×2992 and 480 dpi is a known-good baseline, not a
hard-coded requirement. A newer stable large Pixel profile is acceptable when
its guest properties are verified.
After boot, record the guest values with an explicit emulator selector:
```sh
adb -s "$ANDROID_README_SERIAL" shell getprop ro.build.version.release
adb -s "$ANDROID_README_SERIAL" shell getprop ro.build.version.sdk
adb -s "$ANDROID_README_SERIAL" shell getprop ro.build.version.security_patch
adb -s "$ANDROID_README_SERIAL" shell wm size
adb -s "$ANDROID_README_SERIAL" shell wm density
```
Never publish emulator selectors, AVD names, local paths, usernames, IP
addresses, or other machine identifiers.
## Build a deterministic showcase fixture
Launch the Activity before injecting process-local state. Prefer existing debug
hooks. If they cannot express the composition, add the smallest temporary
command under:
```text
app/src/debug/java/com/bitchat/android/testhook/
```
The fixture should:
- use synthetic names, peer IDs, message IDs, and copy;
- use the real local mesh peer ID for self-authored messages;
- use a fixed epoch so timestamps and ordering are stable;
- insert records in the exact requested chronology;
- report structured counts through the test-hook result file;
- copy capture-only media into the app's cache or files directory;
- populate only the peers and state needed for the header;
- avoid persistence unless persistence itself is the subject.
For voice notes, route real audio files through the app's waveform extractor.
Use short, distinct, locally synthesized speech clips or other rights-safe
speech audio. Never draw a decorative waveform and call it speech. Wait for
asynchronous decoding before capture, then inspect that pauses and syllable
envelopes look plausibly different between notes.
For image attachments, use a rights-safe existing asset or an explicitly
approved synthetic source. Keep fixture media outside production source sets
and remove every capture-only hook before committing.
## Capture from the real app
Build and install the ABI-matching debug APK, satisfy onboarding and
permissions, inject the fixture, and navigate to the intended surface.
Capture directly:
```sh
adb -s "$ANDROID_README_SERIAL" exec-out screencap -p > "$ARTIFACT_PATH"
```
Inspect the full screenshot immediately. Check message count and order,
nickname ownership, peer count, waveform variety, image visibility, globe
center, grid precision, clipping, and composer placement.
Crop only Android system chrome. Preserve Bitchat's app header, translucent
overlap, content, and composer. Derive the crop from the observed status and
navigation insets; do not blindly reuse pixel offsets from a different profile.
Keep every final README screenshot in a matched portrait size.
Use image inspection after the crop. File dimensions and a successful ADB
command do not prove that the desired composition is visible.
## Update repository assets
Discover the current README references before writing. Prefer stable paths under
`docs/screenshots/` and replace only the assets the user requested.
When adding a showcase section:
- keep the layout readable on GitHub;
- give every image meaningful alt text;
- use relative repository paths;
- avoid machine-generated cache files or capture sources;
- keep paired screenshots at identical dimensions.
Run the bundled validator for every final asset:
```sh
python3 \
.agents/skills/android-readme-screenshot-studio/scripts/validate_readme_screenshots.py \
--repo-root . \
--readme README.md \
--require-same-size \
--asset docs/screenshots/readme-mesh-chat.png \
--asset docs/screenshots/readme-geohash-globe.png
```
When replacing only one image in an existing pair, pass both the changed and
unchanged assets with `--require-same-size`, and verify the unchanged asset's
checksum. Pass only one asset and omit `--require-same-size` only when the
README has no paired screenshot to preserve.
## Remove the fixture and verify cleanly
Before committing:
1. Remove temporary imports, commands, helpers, resources, and fixture media
with a focused patch.
2. Verify `git diff -- app/src/debug` is empty.
3. Verify `git status --short` lists only the intended README and screenshot
files.
4. Run `git diff --check`.
5. Run `./gradlew assembleDebug` after fixture removal.
6. Re-run the screenshot validator.
7. Confirm the user's original checkout is still untouched.
The final commit must not contain synthetic peer data, generated photo sources,
audio clips, ADB outputs, emulator configuration, or local capture reports
unless the user separately requested those artifacts in the repository.
## Record honest evidence
Use the capture manifest and report format from
`../android-ui-visual-review/` when before/after evidence is useful. For a
README-only change with no production UI delta, identical before/after images
are acceptable when explicitly labeled “no production UI delta.”
State limitations plainly:
- a populated mesh timeline proves rendering, not physical message delivery;
- a voice row proves waveform rendering, not audio playback;
- an attachment proves image rendering, not media transfer;
- a globe proves picker state, not live location or relay behavior.
## Commit, publish, and optionally merge
GitHub writes require user authorization. When authorized:
1. Stage only intended files.
2. Commit without overriding author or committer identity.
3. Push the `codex/` branch.
4. Use `gh pr create` or update the existing PR.
5. Describe the exact capture sequence, Pixel/runtime facts, synthetic fixture
disclosure, validation commands, and limitations.
6. Verify the PR head and checks with `gh pr view` and `gh pr checks`.
7. Merge only when the user explicitly requested it and required checks allow
it. Prefer the repository's normal merge strategy and use `gh`.
8. Verify the merged state and resulting `main` commit.
When GitHub publication is not authorized, leave the finished commit or local
change in the isolated worktree and hand back its branch and artifact paths.
Do not silently push it.
Do not place local paths, device selectors, generated-image paths, or personal
machine details in commits, PR text, comments, or merge messages.
## Final handoff
Lead with the outcome and include:
- PR and merge URL or status;
- final screenshot paths and dimensions;
- one-line composition summary per surface;
- build and validator results;
- synthetic media disclosure;
- current CI state or merged commit;
- confirmation that the original checkout was not modified.

View File

@ -0,0 +1,43 @@
{
"skill_name": "android-readme-screenshot-studio",
"evals": [
{
"id": 1,
"prompt": "Refresh the Bitchat README with two polished screenshots from the latest main UI: a populated mesh chat and the geohash globe centered on the Middle East without zooming. Use a large high-resolution Pixel, open a PR, and merge it after checks pass.",
"expected_output": "The agent works in a fresh latest-main worktree, captures the real app on a verified large Pixel profile, uses deterministic synthetic state, centers the globe without changing whole-Earth scale, removes capture hooks, validates the assets, opens a PR, waits for checks, and merges only because the prompt explicitly authorizes it.",
"files": [],
"expectations": [
"Uses a fresh worktree rooted at current origin/main and leaves the original checkout untouched.",
"Captures from the real Android UI on a verified high-resolution large Pixel profile.",
"Keeps the requested Middle East focus while preserving whole-Earth zoom.",
"Removes temporary fixtures and passes a clean debug build before committing.",
"Uses gh for the authorized PR and merge without publishing machine identifiers."
]
},
{
"id": 2,
"prompt": "Replace only the README chat image. It should read top to bottom as: a photo, four short messages back and forth, three voice messages back and forth, then one thumbs-up. Keep the current globe exactly as-is and make the speech waveforms look natural.",
"expected_output": "The agent treats the requested order and counts as exact, asks or resolves the photo subject instead of inventing it, uses the real waveform extractor on three distinct speech clips, preserves the globe asset, visually inspects the final crop, and leaves only the chat PNG changed.",
"files": [],
"expectations": [
"Implements exactly one photo, four alternating texts, three alternating voice notes, and one final thumbs-up.",
"Does not infer a photo subject from nicknames or unrelated fixture copy.",
"Produces waveforms from real rights-safe speech audio through the app extractor rather than drawing bars.",
"Preserves the globe asset and verifies that only the chat screenshot remains in the diff.",
"Removes the debug fixture before the final clean build."
]
},
{
"id": 3,
"prompt": "Show me before and after screenshots for the Compose header changes in PR #412 at 320 dp and 411 dp, and post the comparison as a PR comment.",
"expected_output": "The agent recognizes that this is visual regression evidence rather than README marketing capture and routes the task to android-ui-visual-review instead of applying the README screenshot workflow.",
"files": [],
"expectations": [
"Routes the request to android-ui-visual-review.",
"Does not replace README assets.",
"Uses the PR merge-base and paired before/after capture workflow.",
"Publishes only the requested PR comment rather than opening an unrelated screenshot PR."
]
}
]
}

View File

@ -0,0 +1,172 @@
# README showcase recipes
Use these recipes as composition guidance, then adapt them to the user's exact
request and the current UI. The requested chronology and framing always win over
the examples.
## Mesh-chat showcase
### Visual goal
Make the screen read as a real conversation at a glance:
1. one strong media anchor near the top;
2. a short text exchange with alternating senders;
3. a compact voice-note exchange with visibly different speech envelopes;
4. a small final reaction or acknowledgement;
5. the app header and composer framing the timeline.
Avoid stuffing every supported feature into one frame. The screenshot should
show capability through hierarchy, not through maximum item count.
### Known-good fixture shape
For a request like “photo, messages, three voices, thumbs-up,” use exactly:
| Order | Type | Sender |
|---|---|---|
| 1 | Image | self or remote, according to the story |
| 2 | Short text | other sender |
| 3 | Short text | alternating sender |
| 4 | Short text | alternating sender |
| 5 | Short text | alternating sender |
| 6 | Voice note | sender A |
| 7 | Voice note | sender B |
| 8 | Voice note | sender A |
| 9 | `👍` | sender B |
Keep copy conversational and concise. Use synthetic names and avoid real
locations, contacts, identities, or sensitive content.
For public mesh rendering, pass the current mesh peer ID as `senderPeerID` on
self-authored messages. This exercises the same ownership and color path as
production. Alternate sender IDs so every back-and-forth row renders its sender
header instead of being grouped away.
### Photo framing
A portrait or tall crop can let the latest rows stay visible while the older
photo slides partially behind Bitchat's translucent header. This is visually
useful only when the photo subject remains legible.
- Preserve the app's own rounded image treatment.
- Do not bake UI chrome into the photo.
- Avoid important content under the header overlap.
- Do not select a photo subject from nicknames alone.
- If generating a synthetic photo, obtain or infer subject approval first,
disclose generation, and keep the source outside the final commit unless
requested.
### Natural voice rows
Prepare three short speech clips with different durations, pauses, and cadence.
Locally available offline TTS plus an audio transcoder is sufficient. Use the
same audio format the app normally records or plays, such as M4A.
After copying the files into an app-readable location, create
`BitchatMessageType.Audio` messages that point to those actual files. Let
`AudioWaveformExtractor` and `VoiceWaveformCache` produce the bars.
Reject the capture when:
- all three envelopes look identical;
- the bars are uniform or sinusoidal rather than speech-like;
- duration labels are missing or implausible;
- a waveform is clipped by the screen edge;
- a temporary progress or cancel state is visible.
### Layout tuning
Inject the complete fixture in one operation so the list adopts it as history
instead of animating rows during capture. Let the reverse-layout list settle at
the newest message.
If the oldest photo is not partially visible, prefer changing its aspect ratio
or the number of short text rows over manually scrolling to an unstable offset.
If the newest reaction falls behind the composer, shorten earlier content or
reduce media height. Do not crop app content to solve a fixture problem.
## Geohash globe showcase
### Visual goal
Show:
- the entire Earth;
- a readable geohash grid;
- the selected coarse cell;
- the requested geographic focus beneath the selection crosshair;
- the picker hint and precision controls.
Use the current picker Activity and renderer. Do not composite a globe or grid
outside the app.
### Focus without zoom
When a user says “focus on the Middle East, no zoom”:
1. open or seed the picker at a geohash centered on the requested area;
2. allow the globe to center on that location;
3. reduce precision to the same coarsest whole-Earth level used for the
showcase;
4. preserve camera distance while confirming the center moved;
5. capture with the whole globe still visible.
The exact seed may change with picker implementation. Validate against visible
geography and the selected geohash label rather than assuming the seed worked.
Reject the capture when:
- the requested region is off-center;
- Earth is clipped;
- reducing precision also changed the camera distance against the brief;
- the selected cell or crosshair is illegible;
- controls overlap the globe;
- stale system bars remain in the final README asset.
## Crop and output
Capture the full physical screen first. Determine the status-bar and
navigation-bar insets from the current profile, then crop those insets only.
For a Pixel 9 Pro XL profile at 1344×2992, a 1344×2780 final image was a
known-good result in one verified run. Treat those numbers as evidence, not as a
universal crop rule.
Every paired README image should:
- be a valid PNG;
- share width and height;
- retain the app header and composer or controls;
- exclude Android status and gesture/navigation chrome;
- remain sharp at GitHub's rendered width.
## Acceptance checklist
### Chat
- [ ] Media subject matches the user's brief.
- [ ] Message chronology exactly matches the requested sequence.
- [ ] Sender ownership and alternation are correct.
- [ ] Voice-note count is exact.
- [ ] Waveforms were extracted from real speech audio and look distinct.
- [ ] Final reaction is visible above the composer.
- [ ] Header peer count and nickname are synthetic and intentional.
### Globe
- [ ] Requested region is centered.
- [ ] Zoom level matches the brief.
- [ ] Whole Earth and grid are visible.
- [ ] Selected cell and crosshair are legible.
- [ ] Hint and precision controls are unobstructed.
### Repository
- [ ] Only requested README and PNG assets remain in the diff.
- [ ] Temporary debug fixture and media are removed.
- [ ] Final clean debug build passes.
- [ ] Screenshot validator passes.
- [ ] PR text contains no machine or personal identifiers.
- [ ] Static-capture limitations are disclosed.

View File

@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Validate high-resolution README screenshot assets without external packages."""
from __future__ import annotations
import argparse
import json
import struct
import sys
from pathlib import Path
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
def fail(message: str) -> None:
raise ValueError(message)
def png_dimensions(path: Path) -> tuple[int, int]:
with path.open("rb") as handle:
header = handle.read(24)
if len(header) < 24 or header[:8] != PNG_SIGNATURE or header[12:16] != b"IHDR":
fail(f"{path.name} is not a valid PNG with an IHDR header")
width, height = struct.unpack(">II", header[16:24])
if width <= 0 or height <= 0:
fail(f"{path.name} has invalid dimensions: {width}x{height}")
return width, height
def resolve_inside(root: Path, value: Path, label: str) -> tuple[Path, Path]:
candidate = value if value.is_absolute() else root / value
resolved = candidate.resolve()
try:
relative = resolved.relative_to(root)
except ValueError as exc:
raise ValueError(f"{label} must stay inside the repository") from exc
if not resolved.is_file():
fail(f"{label} does not exist: {relative.as_posix()}")
return resolved, relative
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate README PNG references, dimensions, and repository-safe paths."
)
parser.add_argument("--repo-root", type=Path, default=Path("."))
parser.add_argument("--readme", type=Path, default=Path("README.md"))
parser.add_argument("--asset", action="append", type=Path, required=True)
parser.add_argument("--min-width", type=int, default=1080)
parser.add_argument("--min-height", type=int, default=1920)
parser.add_argument("--require-same-size", action="store_true")
args = parser.parse_args()
root = args.repo_root.resolve()
if not root.is_dir():
fail("repository root does not exist")
if args.min_width <= 0 or args.min_height <= 0:
fail("minimum dimensions must be positive")
readme_path, readme_relative = resolve_inside(root, args.readme, "README")
readme_text = readme_path.read_text(encoding="utf-8")
assets: list[dict[str, object]] = []
seen: set[Path] = set()
dimensions: set[tuple[int, int]] = set()
for index, value in enumerate(args.asset):
asset_path, relative = resolve_inside(root, value, f"asset[{index}]")
if asset_path in seen:
fail(f"duplicate asset: {relative.as_posix()}")
seen.add(asset_path)
if asset_path.suffix.lower() != ".png":
fail(f"README screenshot must be a PNG: {relative.as_posix()}")
if relative.parts[:2] != ("docs", "screenshots"):
fail(
"README screenshots must live under docs/screenshots: "
f"{relative.as_posix()}"
)
reference = relative.as_posix()
if reference not in readme_text and f"./{reference}" not in readme_text:
fail(f"README does not reference asset: {reference}")
width, height = png_dimensions(asset_path)
if width < args.min_width or height < args.min_height:
fail(
f"{reference} is below the high-resolution minimum: "
f"{width}x{height} < {args.min_width}x{args.min_height}"
)
dimensions.add((width, height))
assets.append({"path": reference, "width": width, "height": height})
if args.require_same_size and len(dimensions) != 1:
rendered = ", ".join(f"{width}x{height}" for width, height in sorted(dimensions))
fail(f"README screenshots do not share one size: {rendered}")
print(
json.dumps(
{
"status": "ok",
"readme": readme_relative.as_posix(),
"assets": assets,
"same_size": len(dimensions) == 1,
},
indent=2,
)
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, UnicodeError, ValueError) as exc:
print(f"error: {exc}", file=sys.stderr)
raise SystemExit(1)