the-committee / app.py
Alptraum's picture
The Committee — two cats judge your webcam (local MiniCPM + Llama via llama.cpp)
3481136 verified
Raw
History Blame Contribute Delete
6.82 kB
"""Desk Companion — a context-aware ASCII visual diary.
HF "Build Small" submission, Thousand Token Wood track. One small local model
(llama.cpp) senses the room — through YOUR browser's webcam/mic (light + sound
computed client-side; only two numbers reach the app), through a real Arduino
over serial, or through a simulated day — and writes a moody diary + ASCII art,
pushing a short line to the (virtual or physical) OLED screen.
"""
from __future__ import annotations
import math
import gradio as gr
import threading
from companion import CompanionRunner, DeskPersona
from companion.runner import POLL_S
from companion.text import bar
from companion.ui import CSS, FORCE_DARK_JS, theme
WELCOME_ART = r"""
.--------------.
| hello, you |
| ( o_o ) |
| watching |
| your room |
'--------------'
"""
EXPLAINER = """
**Your desk has a tiny spirit that keeps a diary.** It feels the room —
brightness, sound, the hour — and when something shifts (you switch the
lights off, the night gets long) it writes a short moody diary entry, draws
ASCII art on its little screen, and leaves you a two-line note. One small
3B model, running locally. No cloud.
**Try it:** ① press **🎥 sense my room** and allow camera+mic (nothing is
recorded — light & sound levels are computed in your browser) → ② turn your
lights off → ③ watch the screen react. Too shy? Press **✎ write now** and
it writes about the room as-is.
"""
# Runs once when the user presses "sense my room": asks for camera+mic, then
# keeps window._dc.{light,sound} updated twice a second. All processing stays
# in the browser — frames and audio never leave the page.
START_SENSING_JS = """
async () => {
if (window._dc && window._dc.started) return;
window._dc = {light: -1, sound: -1, started: true};
try {
const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
const video = document.createElement('video');
video.srcObject = stream; video.muted = true; video.playsInline = true;
await video.play();
const canvas = document.createElement('canvas');
canvas.width = 32; canvas.height = 24;
const ctx = canvas.getContext('2d', {willReadFrequently: true});
const ac = new AudioContext();
const analyser = ac.createAnalyser();
analyser.fftSize = 256;
ac.createMediaStreamSource(stream).connect(analyser);
const buf = new Uint8Array(analyser.fftSize);
setInterval(() => {
try {
ctx.drawImage(video, 0, 0, 32, 24);
const d = ctx.getImageData(0, 0, 32, 24).data;
let lum = 0;
for (let i = 0; i < d.length; i += 4)
lum += 0.2126*d[i] + 0.7152*d[i+1] + 0.0722*d[i+2];
window._dc.light = Math.round(lum / (d.length/4) / 255 * 100);
analyser.getByteTimeDomainData(buf);
let rms = 0;
for (let i = 0; i < buf.length; i++) {
const v = (buf[i] - 128) / 128; rms += v*v;
}
window._dc.sound = Math.round(Math.min(100, Math.sqrt(rms/buf.length) * 400));
} catch (e) { /* keep last values */ }
}, 500);
} catch (e) {
window._dc.started = false; // permission denied — stay on simulation
}
}
"""
# Every poll tick: hand the latest browser readings to Python (-1 = not live).
READ_SENSORS_JS = """
(a, b) => {
const d = window._dc || {};
return [d.light ?? -1, d.sound ?? -1];
}
"""
def build():
runner = CompanionRunner(DeskPersona(), mock_mode="day", browser=True)
def status_md():
r = runner.latest()
clk = r.clock if r else "--:--"
return runner.status_md() + f" · **desk clock** `{clk}`"
def gauges_md():
r = runner.latest()
if r is None:
return "```\nwaiting for the desk to wake up...\n```"
temp = "" if math.isnan(r.temp) else f"temp {r.temp:5.1f}C\n"
return ("```\n"
f"light {r.light:3d}% {bar(r.light)}\n"
f"sound {r.sound:3d}% {bar(r.sound)}\n"
f"{temp}```")
def art_text():
return runner.get("ascii") or WELCOME_ART
def screen_text():
return (runner.get("screen") or "...").replace("|", "\n")
def feed_md():
entries = runner.feed()
if not entries:
return "_warming up — the first entry writes itself in a few seconds..._"
return "\n\n---\n\n".join(
f"**`{e['clock']}`** · _{e['trigger']}_ \n{e['entry']} \n"
f"<sub>{e['context']}</sub>" for e in entries
)
def on_tick(light, sound):
runner.ingest_browser(light, sound)
return status_md(), gauges_md(), art_text(), screen_text(), feed_md()
# First entry writes itself shortly after boot so the page is never blank.
def first_entry():
for _ in range(20):
if runner.latest() is not None:
runner.generate(auto=True)
return
import time
time.sleep(0.5)
threading.Thread(target=first_entry, daemon=True).start()
with gr.Blocks(title="Desk Companion") as demo:
gr.Markdown("# ▚ desk companion")
gr.Markdown(EXPLAINER, elem_id="hero")
status = gr.Markdown(status_md())
with gr.Row():
with gr.Column(scale=1):
sense_btn = gr.Button("🎥 sense my room", variant="secondary")
gr.Markdown("### sensors")
gauges = gr.Markdown(gauges_md())
gr.Markdown("### screen (OLED)")
art = gr.Markdown(elem_id="oled", value=art_text())
screen = gr.Markdown(elem_id="screen", value=screen_text())
with gr.Accordion("hardware (optional Arduino)", open=False):
port = gr.Textbox(label="serial port",
placeholder="/dev/tty.usbmodem... (blank = no device)")
connect = gr.Button("connect")
write_btn = gr.Button("✎ write now", variant="primary")
with gr.Column(scale=2):
gr.Markdown("### diary")
feed = gr.Markdown(feed_md())
h_light = gr.Number(value=-1, visible=False)
h_sound = gr.Number(value=-1, visible=False)
sense_btn.click(None, js=START_SENSING_JS)
gr.Timer(POLL_S).tick(
on_tick,
inputs=[h_light, h_sound],
outputs=[status, gauges, art, screen, feed],
js=READ_SENSORS_JS,
)
write_btn.click(lambda: (runner.generate(False), art_text(), screen_text(), feed_md())[1:],
outputs=[art, screen, feed])
connect.click(runner.connect, inputs=[port], outputs=[status])
return demo
if __name__ == "__main__":
build().launch(css=CSS, theme=theme(), js=FORCE_DARK_JS)