Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

11 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SahurHub β€” a task reminder that works perfectly, and behaves completely unhinged about it

SahurHub

A task reminder that works perfectly, and behaves completely unhinged about it.
Talk normally. It captures the task. Then it will not let it go.

Qwen Brainrot Hackathon 2026 Most Creative Qwen Integration Qwen Bun TypeScript Three.js React Raspberry Pi

Pitch Deck Β· Demo Script Β· PRD Β· TRD Β· Runbook

Table of Contents

Expand
  1. About The Project
  2. Screenshots
  3. How It Works
  4. Features
  5. Architecture
  6. Tech Stack
  7. Hardware
  8. Getting Started
  9. Configuration
  10. Building The Device
  11. Project Structure
  12. License
  13. Team
  14. Acknowledgments

About The Project

SahurHub is a palm-sized desk companion: an animated 3D character living on a Raspberry Pi panel, driven end to end by Qwen. You talk to it from your phone. It talks back.

There is no "add task" button. You mention in passing that you should stretch in three minutes β€” it quietly captures that, schedules it, and then follows up. And follows up. And escalates.

The design principle is a sincerity inversion, and it is the whole product:

Behavior
The engine Never drops a task, never mistimes a follow-up. Task facts stay accurate.
The character Completely deranged about all of it. Escalating percussion menace.

The engine is the straight man. The character is the bit. The bit never breaks the tool.

Built for the Qwen Brainrot Hackathon 2026 β€” a competition whose brief is to build something deliberately useless that nonetheless genuinely works. SahurHub won Most Creative Qwen Integration at the Physical Final.

↑


Screenshots

The Kiosk

Sahur rendered on the 480Γ—320 kiosk panel, with the phone URL badge

Three.js Sahur at the panel's native 480Γ—320, with the connection badge and the phone URL to join from. On the device this runs rotated to upright portrait; tapping the badge shows a scannable QR.

The Remote

Conversation Tasks
Chatting with Sahur from the phone remote The Tasks drawer with live countdowns and escalation state
Two tasks captured from ordinary conversation β€” no command, no form. Live countdowns and escalation state: the deterministic half, doing its job.

↑


How It Works

1. Speak Or Type

Hold-to-talk from the phone, or just type. Audio goes to qwen3-asr-flash.

2. One Generation Does Everything

qwen3.6-flash streams the reply and its control tags in the same pass β€” no second classifier call, no JSON mode. Task capture is a side effect of talking.

3. The Kernel Takes Over

A captured task is scoped to its conversation and scheduled from its duration. The scheduler is plain deterministic code; the model never touches timing.

4. The Character Escalates

Reminders fire on schedule. Each ignored one advances an escalation level and re-arms. The wording gets worse; the facts do not change.

5. Sahur Reacts On The Panel

The kiosk consumes the same event stream: expression poses, amplitude-driven lip-sync, and a knock animation whose intensity tracks the escalation tier.

↑


Features

Feature What It Does
πŸŽ™οΈ Voice and text, both Hold-to-talk push-to-talk or plain typing, with barge-in that cuts an in-flight reply mid-sentence
πŸͺ€ Task capture without a command Tasks are extracted from ordinary conversational context β€” no add-task syntax to learn, and no second model call to pay for
⏰ Deterministic escalation pending β†’ reminding β†’ escalated Γ—2 β†’ missed, driven by a timer kernel that rebuilds itself from SQLite after a restart
🎭 Two characters, one seam Sahur (wooden log, percussion menace) and Tralala (blue sneakers, cardio menace) β€” each a manifest plus a bible that drives prompt, voice, colors, and animations
🧱 Procedural 3D, zero asset pipeline Characters are composed from Three.js primitives in code β€” nothing to download, nothing to rig, and it re-builds from scratch inside the 2-hour finals window
πŸ‘‰ Poke to retaliate Tap the character on the panel or from the phone and it swipes back, interrupting whatever it was saying
πŸ–ΌοΈ Uploads with a vision lane Text and images up to 5 MB β€” text enters context, images go through Qwen vision, and failures degrade to an in-character line instead of crashing
πŸ”Š Audio follows you Exactly one playback target at a time, Phone or Device, while the kiosk keeps receiving amplitude data for lip-sync either way
πŸ’¬ Real conversations Multiple threads with async-generated titles, editable and deletable, persisted in bun:sqlite
πŸŒ— Polished remote Light/dark tokens with no flash on load, self-hosted Fredoka, and a hand-rolled markdown renderer that never touches dangerouslySetInnerHTML
πŸ₯ Soundboard stings Task-completion and entrance beats, because the alternative was silence

↑


Architecture

One Bun process. It runs a plain HTTP listener for the kiosk, an mkcert-backed HTTPS listener for the phone (browser mic capture requires it), and a shared /ws endpoint on both.

flowchart TD
    subgraph Devices["Clients"]
        KIOSK["Kiosk panel<br/>Three.js Β· 480Γ—320"]
        PHONE["Phone remote<br/>React + Vite"]
    end

    subgraph Server["Bun process (single)"]
        WS["/ws Β· shared on both listeners"]
        ORCH["Orchestrator<br/>ordered turn queue"]
        PERSONA["Persona<br/>5-layer prompt compiler"]
        KERNEL["Kernel<br/>tags Β· scheduler Β· lifecycle"]
        DB[("bun:sqlite<br/>conversations Β· messages Β· tasks")]
    end

    subgraph Qwen["Qwen β€” Alibaba Model Studio"]
        ASR["qwen3-asr-flash"]
        CHAT["qwen3.6-flash"]
        TTS["qwen3-tts-flash"]
    end

    PHONE <-->|"text Β· PCM audio Β· events"| WS
    KIOSK <-->|"events Β· TTS chunks"| WS
    WS --> ORCH
    ORCH --> PERSONA --> CHAT
    ORCH -->|"audio in"| ASR
    ORCH -->|"clean text out"| TTS
    CHAT -->|"reply + inline tags"| KERNEL
    KERNEL --> DB
    KERNEL -->|"reminders on schedule"| ORCH
    TTS -->|"PCM16LE"| WS
Loading

The Split Brain

The separation that makes the joke safe to ship:

Owned By What It Controls
The kernel (code) Task existence, durations, wake times, escalation level, lifecycle transitions
The model (Qwen) Every word spoken, the expression, the animation trigger

The model can request a task via a tag, but the kernel owns the clock. A hallucinating model produces worse jokes β€” never a missed reminder.

The Tag Grammar

Control flows as streaming text, not JSON. The reply and its side effects arrive in one generation:

Tok. The chair is a trap. <|emotion:smug|> Stretch now. <|task:stretch|3|>
Tag Args Effect
emotion name Sets the expression pose on the panel
task name|minutes Captures a task and schedules it
task_done reference Completes an active task by id, id-prefix, or name
remind minutes Fires the character's remind animation (knock / stomp)
escalate β€” Fires the escalate animation (posture shift / strut)
schedule minutes Emits a schedule event, played immediately

Each character's manifest maps remind and escalate onto its own animation, so the same tag reads as a knock from Sahur and a stomp from Tralala.

The parser strips tag-shaped garbage from visible text, holds a trailing partial tag across streamed chunks, bounds every argument, and deduplicates events per turn. Malformed output degrades to plain speech rather than an error.

The Task Lifecycle

State Kernel Behavior
pending First duration interval elapses
reminding Reminder emitted; next wake is one duration later
escalated 1, 2 Each ignored follow-up advances a level and re-arms for one duration
missed A third ignored state ends the task and clears its wake time
done / dismissed Terminal manual actions that cancel scheduling

↑


Tech Stack

Layer Technologies
Runtime Bun Β· TypeScript Β· Bun workspaces Β· Biome + Prettier
Server Dual HTTP/HTTPS listeners Β· shared WebSocket Β· bun:sqlite Β· zero runtime deps
AI qwen3-asr-flash β†’ qwen3.6-flash β†’ qwen3-tts-flash
Kiosk Three.js Β· procedural primitive model Β· amplitude-driven lip-sync
Remote React 19 Β· Vite Β· hand-rolled CSS tokens Β· self-hosted Fredoka
Device Raspberry Pi 5 Β· Waveshare 3.5" SPI panel Β· labwc kiosk Β· systemd
Local HTTPS mkcert Β· Avahi mDNS (sahurhub.local)

↑


Hardware

Component Purpose
Raspberry Pi 5 Device host
Official Pi 5 PSU Required for stable demo power
Waveshare 3.5" RPi LCD (A) Rev4.0, SPI ILI9486 + XPT2046 panel
USB speaker Device-side audio output
Phone hotspot Demo network and remote access

No microphone or camera on the device. The phone webapp supplies text, push-to-talk audio, and uploads β€” which is also why the phone listener has to be HTTPS.

Want to build one? The runbook walks the whole thing top to bottom: assembly, SD-card flashing, provisioning, and operation.

↑


Getting Started

The app runs fully offline without an API key β€” a deterministic mock brain drives the whole UI, kiosk, and scheduler. Add a key only when you want real Qwen.

Prerequisites

  • Bun β€” package manager and runtime
  • mkcert β€” for the local HTTPS certificate
  • A POSIX shell (the setup scripts are bash)

Installation

1. Install mkcert (Debian/Ubuntu/WSL2)

sudo apt update && sudo apt install -y libnss3-tools
curl -JLO https://dl.filippo.io/mkcert/latest?for=linux/amd64
chmod +x mkcert-v*-linux-amd64 && sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert

2. Bootstrap the repository

git clone https://github.com/TolongLabs/SahurHub
cd SahurHub
./scripts/setup.sh

That runs bun install, builds both browser apps, installs the mkcert CA, mints cert/ with SANs for localhost + your LAN IP + sahurhub.local, and scaffolds .env.local without overwriting an existing one.

Daily Loop

bun run dev
Surface URL Notes
Kiosk http://localhost:8080/ Preview at 480Γ—320 in the device toolbar
Remote https://localhost:8443/phone The /phone path is required β€” / is the kiosk
Command Purpose
bun run dev Run the server
bun run dev:remote Vite dev server for the remote app, with HMR
bun run build:remote Build the app served at /phone
bun run build:kiosk Build the kiosk bundle
bun test Run tests
bun run typecheck Type-check without emitting
bun run lint Biome checks

WSL2: WebGL is commonly blocklisted under WSLg, so the kiosk shows RENDERER ERROR. Use the Windows browser against the forwarded localhost instead. Full notes in the runbook.

↑


Configuration

All settings live in .env.local, which is never committed. See .env.example for the annotated list.

Variable What It Does
DASHSCOPE_API_KEY Alibaba Model Studio key. Blank = mock brain, no network calls
QWEN_CHAT_MODEL Reply + tag generation β€” qwen3.6-flash
QWEN_ASR_MODEL Speech to text β€” qwen3-asr-flash
QWEN_TTS_MODEL Text to speech β€” qwen3-tts-flash
QWEN_VISION_MODEL Image-upload lane
SAHURHUB_WIFI_SSID / _PASS Hotspot credentials consumed by setup-pi.sh
SAHURHUB_STATIC_IP Pin a static LAN address for a stable demo URL

With a key present, each spoken reply costs roughly $0.002 after the free TTS quota.

PORT and HTTPS_PORT are shell overrides rather than .env entries β€” use them to run parallel instances:

PORT=8081 HTTPS_PORT=8444 bun run dev

↑


Building The Device

Hardware assembly, SD-card flashing, provisioning, and day-to-day operation all live in one place:

πŸ“– docs/runbook.md β€” build one yourself, top to bottom

Section Covers
Bill Of Materials Exactly what to buy, and the power supply that trips everyone
Assembling The Hardware Seating the 26-pin LCD HAT without bricking the boot
Flashing The SD Card Raspberry Pi OS Bookworm 64-bit, and the settings to preset
Provisioning SahurHub One idempotent script, and what each stage actually does
Troubleshooting Hardware, service, model, and network failure modes

The short version, on a freshly flashed Pi:

git clone https://github.com/TolongLabs/SahurHub
cd SahurHub
sudo ./scripts/setup-pi.sh --ssid "<hotspot>" --pass "<password>"

↑


Project Structure

SahurHub/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ server/
β”‚   β”‚   β”œβ”€β”€ index.ts        # dual listeners, routes, WS session wiring
β”‚   β”‚   β”œβ”€β”€ orchestrator.ts # ordered turn queue, barge-in, event dispatch
β”‚   β”‚   β”œβ”€β”€ kernel/         # tags Β· scheduler Β· task lifecycle
β”‚   β”‚   β”œβ”€β”€ persona/        # character registry + 5-layer prompt compiler
β”‚   β”‚   β”œβ”€β”€ llm/            # Qwen backends and the offline mock
β”‚   β”‚   └── db/             # bun:sqlite schema and DAO
β”‚   β”œβ”€β”€ kiosk/              # Three.js panel app + procedural model/
β”‚   └── shared/             # protocol.ts β€” the sole wire contract
β”œβ”€β”€ apps/remote/            # React + Vite phone webapp (Bun workspace)
β”œβ”€β”€ characters/             # <id>/character.json + bible.md
β”œβ”€β”€ scripts/                # setup.sh Β· setup-pi.sh Β· dev-kiosk.sh
β”œβ”€β”€ spikes/                 # qwen-probe Β· https-mic
β”œβ”€β”€ assets/screenshots/     # the images in this README
└── docs/                   # this README, PRD, TRD, runbook, demo deck

↑


License

Distributed under the MIT License. See LICENSE for details.

↑


Team

Built by TolongLabs for the Qwen Brainrot Hackathon 2026.

Tuna
Tuna
@AlaskanTuna
Chaos
Chaos
@chaosiris
Doraemon
Doraemon
@Doraemon-00
Software & integration β€” server, kernel, persona, kiosk, remote. Hardware & assembly β€” Pi build, panel, enclosure, demo rig. Documentation & testing.

↑


Acknowledgments

  • Qwen & Alibaba Cloud Model Studio β€” the ASR, chat, and TTS models doing the central work
  • Three.js β€” the kiosk renderer
  • Bun β€” runtime, bundler, test runner, and SQLite driver in one binary
  • mkcert β€” painless local HTTPS, without which the phone mic would not work
  • Shields.io β€” the badges above

↑


About

SahurHub is a deliberately useless task reminder that steals to-dos from ordinary conversation and nags through an animated 3D character on a Raspberry Pi, built on Bun, Three.js and Qwen.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages