yuzu-block
by studiocon
README.md
# yuzu-block (working title)
An open-source public site and auth-free MCP server that render anonymous,
aggregate-only data from a voice journal built on ElevenLabs Scribe
transcription as a 3D block sculpture, maintained by STUDIO CON. This repo
is independent from the voice journal's own codebase; it consumes aggregate
numbers only.
## What it shows / what it never shows
Shows weekly aggregate rings:
- record count → block height
- silence-day ratio → gaps in the ring
- words-per-record → a second block color
The sculpture itself carries no text, numbers, or labels. Page chrome
around it is limited to the YUZU logo (linking to
[yuzu.style](https://yuzu.style)), a two-line lead, and a footer; no
metrics — record counts, per-user data, consecutive-day counts — are
displayed anywhere. No counters, no congratulatory effect, no light or
sound cue. The sculpture never exceeds the server snapshot it started
from — see "Rendering" for what it does show while a visitor watches.
Weeks whose cohort is below the anonymity threshold render as empty
rings.
## Concept
Open the material, never the interpretation.
Annual rings: layers are cut by time passing, not by effort. Numbers are
counted, not praised.
## Setup
```
npm install
npm run dev
```
Open `/`. The MCP endpoint is at `/api/mcp`.
```
npm run check
npm run build
```
Requires Node 20+.
## MCP server
No authentication. Streamable HTTP at `/api/mcp`. One tool: `get_ring_data`.
Input:
```json
{ "year": 2026 }
```
Trimmed example output (`RingAggregate`, buckets truncated):
```json
{
"year": 2026,
"unit": "week",
"minCohort": 50,
"generatedAt": "2026-09-12T00:00:00.000Z",
"buckets": [
{
"index": 0,
"start": "2025-12-29",
"sufficient": true,
"recordCount": 214,
"wordCount": 18904,
"silenceDayRatio": 0.412
},
{
"index": 1,
"start": "2026-01-05",
"sufficient": false,
"recordCount": null,
"wordCount": null,
"silenceDayRatio": null
},
{
"index": 2,
"start": "2026-01-12",
"sufficient": true,
"recordCount": 233,
"wordCount": 21172,
"silenceDayRatio": 0.388
}
]
}
```
### Custom connector
Add as a custom connector using the URL only:
```
https://<your-deployment>/api/mcp
```
### curl example
Initialize:
```
curl -s http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "curl", "version": "0.0.1" }
}
}'
```
Call the tool:
```
curl -s http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": { "name": "get_ring_data", "arguments": { "year": 2026 } }
}'
```
## Data contract
```ts
export interface WeekBucket {
index: number;
/** ISO date, YYYY-MM-DD, Monday of the bucket's week. */
start: string;
/** False when the cohort for this bucket is below the anonymity threshold. */
sufficient: boolean;
recordCount: number | null;
wordCount: number | null;
/** 0..1 — days with zero records / days in the bucket. */
silenceDayRatio: number | null;
}
export interface RingAggregate {
year: number;
unit: "week";
minCohort: number;
generatedAt: string;
buckets: WeekBucket[];
}
```
Buckets are ISO weeks: Monday-start, week 1 is the week containing
January 4, and a year has 52 or 53 of them. The reference upstream
computes these boundaries in Asia/Tokyo.
Anonymization policy: week-level granularity only. Buckets whose cohort
falls below `minCohort` are returned with every numeric field set to `null`
and `sufficient: false`. No per-user field exists anywhere in this shape, by
construction.
The data source is selected by the `DATA_SOURCE_URL` environment variable
(`lib/data-source.ts`):
- Unset: a deterministic mock (`lib/mock-aggregate.ts`), seeded by year and
date.
- Set (e.g. `DATA_SOURCE_URL=https://app.yuzu.style/api/public/weekly`):
`GET {DATA_SOURCE_URL}?year=YYYY`, expected to return JSON matching
`RingAggregate` exactly (52 or 53 `WeekBucket`s, `unit: "week"`, nulls
agreeing with `sufficient`). The response is validated against that shape;
any failure — network error, non-2xx, timeout, or invalid shape — falls
back to the mock and logs one line server-side. The upstream is expected
to set long CDN cache headers; no auth is sent or required.
When a validated upstream payload has no week above the anonymity
threshold at all, the site and MCP tool fall back to the mock as well and
report `source: "mock"`.
The MCP tool's JSON output carries a top-level `source` field
(`"upstream"` or `"mock"`) alongside the aggregate, so a caller can see
which one produced a given response. The page never renders this field.
`wordCount`'s unit is defined by whichever source produced it. For the
reference upstream (the voice journal above), it is transcribed
characters, not words.
## Rendering
The scene is a snapshot: `SceneSpec` is generated server-side on every
request and handed to the client once. A slow, continuous camera orbit
runs throughout. On top of the snapshot, the client holds back about 10%
of its blocks at first paint and reveals them one at a time on a bounded
schedule, and independently re-colors whole cell columns between the two
tokens while holding the overall color balance close to the snapshot's —
the sculpture reads as continuously carved, but it converges to the
snapshot and never adds anything beyond it. `prefers-reduced-motion`
renders the finished snapshot with no camera motion and no carving.
Blocks are unlit, per-face tints of the two block tokens defined in
`lib/palette.ts` — side faces are fixed, darker multiples of the same
top-face color, not a separate token or a lighting effect. Colors
actually rendered:
- `YUZU_YELLOW` `#F5D84A`
- `YUZU_ZEST` `#E8A020`
- `YUZU_WHITE` `#FAFAF5` (background)
## ElevenLabs
The words that become blocks are transcribed by ElevenLabs Scribe in the
upstream journal. This repo is intended for submission to the ElevenLabs
Grant / Showcase as a standalone, free, open-source demo.
## Deploy
Vercel, zero configuration (`next build`). To use a real upstream instead
of the mock, set `DATA_SOURCE_URL` in the Vercel project's environment
variables (see `.env.local.example`).
## License
MIT © 2026 STUDIO CON.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues