agentbbs/internal/ascii/ascii.go
Anthony Ettinger 9b0f465946 Video calls (PairUX→ASCII), agent@ chat, and finger routes
ssh video-<code>@ joins a PairUX/LiveKit call rendered as truecolor
ASCII (▀ half-blocks, 2 pixels per cell); video@ prompts for a code.
Codes are minted by PairUX only. Pipeline: VP8 RTP → PLI keyframe
requests → ivfwriter remux → ffmpeg decode/scale → RGB24 → ANSI →
bubbletea over the SSH PTY. Subscriber-only, no audio in v1.

ssh agent@ opens a persisted chat with the operator's agent —
AGENTBBS_AGENT_CMD runs per message (stdin→stdout), e.g. `claude -p`.

ssh <member>@ with someone else's name prints a classic finger card
(.plan, member since, last seen) and disconnects; your own name still
enters the hub.

cmd/lkpublish: dev publisher for testing (explicit -fps pacing; lksdk
IVF replay mispaces from file timebase alone, measured 1fps from a
15fps file; dimensions required or dynacast pauses the track).

Verified end-to-end against livekit-server --dev: 128k truecolor cells
/ 20k distinct colors streamed over a real SSH session; chat round-trip
and finger card verified over SSH. Go toolchain pinned to 1.26 via
mise.toml (lksdk requirement).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 11:43:38 +00:00

52 lines
1.4 KiB
Go

// Package ascii converts raw video frames into truecolor terminal art.
//
// Each character cell renders two vertical pixels using the upper-half block
// (▀): foreground colors the top pixel, background the bottom — the same
// technique doom-ascii uses. A WxH terminal therefore displays a Wx(2H)
// pixel image.
package ascii
import (
"fmt"
"strings"
)
// FrameRGB renders a packed RGB24 frame (w*h*3 bytes, as ffmpeg's rawvideo
// rgb24 emits) sized exactly for the target cell grid: w columns, h*2 rows
// of pixels. Rows are joined with \r\n so the output is PTY-safe.
func FrameRGB(buf []byte, w, h int) string {
if len(buf) < w*h*3 || w <= 0 || h <= 0 {
return ""
}
rows := h / 2
var b strings.Builder
b.Grow(rows * w * 40)
for row := 0; row < rows; row++ {
top := row * 2
bot := top + 1
for x := 0; x < w; x++ {
ti := (top*w + x) * 3
bi := (bot*w + x) * 3
fmt.Fprintf(&b, "\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm▀",
buf[ti], buf[ti+1], buf[ti+2],
buf[bi], buf[bi+1], buf[bi+2])
}
b.WriteString("\x1b[0m")
if row != rows-1 {
b.WriteString("\r\n")
}
}
return b.String()
}
// FitEven clamps a terminal geometry to an even pixel height for the
// half-block renderer and returns pixel dimensions (pw, ph) for the decoder.
func FitEven(cols, rows int) (pw, ph int) {
if cols < 8 {
cols = 8
}
if rows < 4 {
rows = 4
}
return cols, (rows - 1) * 2 // leave one status line
}