Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b80e3c1
Forward SHM transports to Rerun and unify Go2 replay IPC
bogwi May 24, 2026
c48366d
fix: mypy
bogwi May 25, 2026
928c08f
fix: Greptile P1
bogwi May 25, 2026
49e2b61
feat: add Go2 SeatGuide hardware flow
huaruic May 27, 2026
353b19f
merge: integrate macOS SHM replay routing
huaruic May 27, 2026
26dae6c
feat(mcp): support OpenRouter agent models
huaruic May 28, 2026
00c0ad4
feat(seat-guide): add Go2 hardware guidance flow
huaruic May 28, 2026
f87edf7
feat(seat-guide): add Vercel phone speaker relay
huaruic May 28, 2026
5d04493
Merge branch 'dimensionalOS:main' into feat/seat-guide-hardware
huaruic May 28, 2026
06a5bc6
chore(seat-guide): trim Vercel speaker dependencies
huaruic May 28, 2026
9e6fa3c
refactor(seat-guide): remove unsupported Go2 audio path
huaruic May 29, 2026
e0e6bed
fix(seat-guide): close phone relay guidance loop
huaruic May 29, 2026
a1167d6
chore(seat-guide): remove obsolete moondream camera demo
huaruic May 29, 2026
d018235
Merge pull request #1 from cheer-up-hackathon/feat/seat-guide-hardware
huaruic May 29, 2026
12ef54c
Merge branch 'dimensionalOS:main' into main
huaruic May 29, 2026
ff82f4c
feat: add SeatFinder and SeatPlanner skills
Ueti999 May 28, 2026
e2338a0
feat: add Go2 guide and seat-demo agentic blueprints
Ueti999 May 28, 2026
cf149ce
feat: add seat_check_webcam standalone YOLO test script
Ueti999 May 28, 2026
f3d9e39
chore: add seat-finder debug captures
Ueti999 May 28, 2026
5f65d10
chore: clean up seat finder branch artifacts
huaruic May 29, 2026
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
2 changes: 2 additions & 0 deletions apps/seat-guide-speaker-vercel/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vercel
node_modules
43 changes: 43 additions & 0 deletions apps/seat-guide-speaker-vercel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# SeatGuide Speaker Vercel App

This app lets an iPhone mounted on the Go2 act as the SeatGuide speaker.

Flow:

1. iPhone opens the deployed page with cellular data.
2. The page polls `/api/latest?device=go2-demo`.
3. Mac/DimOS posts arrival text to `/api/speak`.
4. The iPhone speaks the latest message with the local browser speaker.

This minimal Vercel version stores only the latest message in serverless memory.
It is enough for quick demos, but can lose messages on cold starts or instance
changes.

## Deploy

Create a Vercel project from this directory:

```bash
cd apps/seat-guide-speaker-vercel
npx vercel
```

No Redis or database is required for the quick demo version.

## iPhone

Open:

```text
https://<your-vercel-domain>/?device=go2-demo
```

Tap `Enable speaker`. Keep Safari open and unlocked.

## Mac Test

```bash
curl -X POST "https://<your-vercel-domain>/api/speak" \
-H "content-type: application/json" \
-d '{"device":"go2-demo","text":"我已经到了, 请坐。"}'
```
74 changes: 74 additions & 0 deletions apps/seat-guide-speaker-vercel/api/[...speaker].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const messages = globalThis.__seatGuideSpeakerMessages || new Map();
globalThis.__seatGuideSpeakerMessages = messages;

function json(res, status, body) {
res.statusCode = status;
res.setHeader("content-type", "application/json; charset=utf-8");
res.end(JSON.stringify(body));
}

function sanitizeDevice(value) {
const device = String(value || "go2-demo")
.trim()
.replace(/[^a-zA-Z0-9_-]/g, "-")
.slice(0, 80);
return device || "go2-demo";
}

async function readBody(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString("utf8");
return raw ? JSON.parse(raw) : {};
}

async function handleSpeak(req, res) {
if (req.method !== "POST") {
json(res, 405, { ok: false, error: "method_not_allowed" });
return;
}

try {
const body = await readBody(req);
const text = String(body.text || "").trim();
if (!text) {
json(res, 400, { ok: false, error: "missing_text" });
return;
}
const device = sanitizeDevice(body.device);
const message = {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
device,
text: text.slice(0, 800),
createdAt: new Date().toISOString(),
};
messages.set(device, message);
json(res, 200, { ok: true, storage: "memory", message });
} catch (error) {
json(res, 500, { ok: false, error: String(error.message || error) });
}
}

function handleLatest(req, res) {
if (req.method !== "GET") {
json(res, 405, { ok: false, error: "method_not_allowed" });
return;
}
const url = new URL(req.url, `https://${req.headers.host || "localhost"}`);
const device = sanitizeDevice(url.searchParams.get("device"));
json(res, 200, { ok: true, device, message: messages.get(device) || null });
}

export default async function handler(req, res) {
const url = new URL(req.url, `https://${req.headers.host || "localhost"}`);
const route = url.pathname.split("/").filter(Boolean).at(-1);
if (route === "speak") {
await handleSpeak(req, res);
return;
}
if (route === "latest") {
handleLatest(req, res);
return;
}
json(res, 404, { ok: false, error: "not_found" });
}
9 changes: 9 additions & 0 deletions apps/seat-guide-speaker-vercel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "seat-guide-speaker-vercel",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"lint": "node --check 'api/[...speaker].js'"
}
}
238 changes: 238 additions & 0 deletions apps/seat-guide-speaker-vercel/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SeatGuide Speaker</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: #f6f8fb;
background: #101316;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
main {
width: min(720px, 100%);
margin: 0 auto;
padding: 24px 16px;
}
header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding-bottom: 18px;
border-bottom: 1px solid #2a3036;
}
h1 {
margin: 0;
font-size: 26px;
line-height: 1.1;
letter-spacing: 0;
}
.state {
min-width: 150px;
border: 1px solid #3a454f;
border-radius: 8px;
padding: 8px 10px;
color: #cbd5df;
background: #171c21;
font: 13px ui-monospace, SFMono-Regular, Menlo, monospace;
text-align: center;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin: 20px 0;
}
input, button {
min-height: 48px;
border: 1px solid #46525e;
border-radius: 8px;
padding: 0 14px;
font-size: 16px;
}
input {
grid-column: 1 / -1;
background: #171c21;
color: #f6f8fb;
}
button {
background: #e8eef5;
color: #11161b;
font-weight: 700;
cursor: pointer;
}
button.secondary {
background: #20262c;
color: #f6f8fb;
}
.panel {
border: 1px solid #2a3036;
border-radius: 8px;
background: #171c21;
overflow: hidden;
}
.panel-head {
padding: 12px 14px;
border-bottom: 1px solid #2a3036;
color: #cbd5df;
font-size: 14px;
font-weight: 650;
}
.messages {
min-height: 280px;
max-height: 56vh;
overflow-y: auto;
padding: 12px;
}
.message {
margin-bottom: 10px;
padding: 12px;
border: 1px solid #333c45;
border-radius: 8px;
background: #11161a;
line-height: 1.45;
overflow-wrap: anywhere;
}
.time {
display: block;
margin-bottom: 5px;
color: #8e9baa;
font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
}
@media (max-width: 560px) {
header { flex-direction: column; }
.state { width: 100%; text-align: left; }
.controls { grid-template-columns: 1fr; }
input { grid-column: auto; }
}
</style>
</head>
<body>
<main>
<header>
<h1>SeatGuide Speaker</h1>
<div class="state" id="state">audio=locked</div>
</header>
<div class="controls">
<input id="device" autocomplete="off" value="go2-demo" aria-label="Device id">
<button id="enable">Enable speaker</button>
<button id="test" class="secondary">Play test</button>
</div>
<section class="panel">
<div class="panel-head">Messages</div>
<div class="messages" id="messages"></div>
</section>
</main>
<script>
const state = document.getElementById('state');
const deviceInput = document.getElementById('device');
const enable = document.getElementById('enable');
const test = document.getElementById('test');
const messages = document.getElementById('messages');
let audioContext = null;
let enabled = false;
let lastMessageId = localStorage.getItem('seatGuideLastMessageId') || '';

const params = new URLSearchParams(window.location.search);
if (params.get('device')) deviceInput.value = params.get('device');

function setState(text) {
state.textContent = text;
}

function isChinese(text) {
return /[\u3400-\u9fff]/.test(text);
}

function appendMessage(text, createdAt) {
const item = document.createElement('div');
item.className = 'message';
const stamp = document.createElement('span');
stamp.className = 'time';
stamp.textContent = createdAt ? new Date(createdAt).toLocaleTimeString() : new Date().toLocaleTimeString();
item.appendChild(stamp);
item.appendChild(document.createTextNode(text));
messages.prepend(item);
while (messages.children.length > 30) messages.removeChild(messages.lastChild);
}

function beep() {
if (!audioContext) return;
const now = audioContext.currentTime;
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();
oscillator.frequency.value = 880;
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(0.85, now + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.75);
oscillator.connect(gain).connect(audioContext.destination);
oscillator.start(now);
oscillator.stop(now + 0.8);
}

function speak(text, createdAt) {
appendMessage(text, createdAt);
if (!enabled) {
setState('audio=locked');
return;
}
beep();
if (!('speechSynthesis' in window)) {
setState('speech=missing');
return;
}
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = isChinese(text) ? 'zh-CN' : 'en-US';
utterance.volume = 1.0;
utterance.rate = 0.95;
utterance.pitch = 1.0;
utterance.onstart = () => setState('speech=playing');
utterance.onend = () => setState('speech=ready');
utterance.onerror = () => setState('speech=error');
window.speechSynthesis.speak(utterance);
}

async function unlockAudio() {
audioContext = audioContext || new (window.AudioContext || window.webkitAudioContext)();
if (audioContext.state !== 'running') await audioContext.resume();
enabled = true;
setState('speech=ready');
speak('SeatGuide speaker ready.');
}

async function poll() {
const device = encodeURIComponent(deviceInput.value.trim() || 'go2-demo');
try {
const response = await fetch(`/api/latest?device=${device}`, { cache: 'no-store' });
const payload = await response.json();
if (payload.ok && payload.message && payload.message.id !== lastMessageId) {
lastMessageId = payload.message.id;
localStorage.setItem('seatGuideLastMessageId', lastMessageId);
speak(payload.message.text, payload.message.createdAt);
}
if (enabled) setState('stream=ready');
} catch (error) {
setState('stream=error');
} finally {
setTimeout(poll, 700);
}
}

enable.onclick = () => unlockAudio().catch(error => setState(`audio=error ${error.message}`));
test.onclick = () => speak('SeatGuide speaker test.');
deviceInput.onchange = () => {
const url = new URL(window.location.href);
url.searchParams.set('device', deviceInput.value.trim() || 'go2-demo');
window.history.replaceState(null, '', url);
};

poll();
</script>
</body>
</html>
13 changes: 13 additions & 0 deletions apps/seat-guide-speaker-vercel/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "no-store"
}
]
}
]
}
Loading