minimax direct multimodal synthesis with event-driven sse telemetry: inside shadow's cybernetic dark studio rendering core
Why a dark studio matters
Rendering pipelines usually suffer from too much ambient coupling. We tried to invert that. Shadow's "dark studio" means we keep the generation environment isolated, observable, and event-native from the ground up. Nothing in the core assumes a synchronous request/response loop, because multimodal synthesis is fundamentally a streaming problem.
A typical media generation request looks like this: a user submits a prompt, we return a job identifier, and the client opens a stream. The job runs through several model stages (text, vision, layout, audio) and emits intermediate results as they become available. SSE is the right primitive here: unidirectional push, native HTTP, automatic reconnect.
The high-level architecture
The core has three rings:
- The ingest layer accepts prompts and validates them against policy. This is synchronous HTTP.
- The synthesis ring runs the multimodal pipeline. This is asynchronous, event-driven.
- The telemetry ring fans out state changes to subscribers. This is SSE.
The synthesiser never knows about HTTP. It produces events. A small adapter layer translates those events into SSE frames for clients.
Direct multimodal synthesis
Most stacks compose modalities by routing through a central orchestrator that waits for each stage to finish. We rejected that. Direct synthesis means each modality node publishes its partial output as soon as it is ready, and downstream nodes subscribe to those streams.
type SynthesisStage =
| 'prompt_parse'
| 'layout_plan'
| 'visual_render'
| 'audio_mix'
| 'caption_assemble';
interface StageEvent {
jobId: string;
stage: SynthesisStage;
status: 'started' | 'partial' | 'complete' | 'failed';
artefact?: Buffer;
metadata?: Record<string, unknown>;
timestamp: number;
}
class MultimodalBus {
private channels = new Map<SynthesisStage, Set<(e: StageEvent) => void>>();
publish(event: StageEvent): void {
const subs = this.channels.get(event.stage);
if (!subs) return;
for (const cb of subs) {
try { cb(event); } catch (err) { this.logError(err); }
}
}
subscribe(stage: SynthesisStage, cb: (e: StageEvent) => void): () => void {
if (!this.channels.has(stage)) this.channels.set(stage, new Set());
this.channels.get(stage)!.add(cb);
return () => this.channels.get(stage)!.delete(cb);
}
private logError(err: unknown): void {
// swallow subscriber errors so one bad client cannot break the bus
}
}
That bus is intentionally tiny. We can swap it for NATS or Redis Streams later without touching the synthesiser code.
The synthesis worker
Each worker is a single-purpose process. It consumes a stage, calls the appropriate model, and publishes events. Here is a simplified visual render worker:
async function visualRenderWorker(bus: MultimodalBus, model: VisualModel): Promise<void> {
bus.subscribe('layout_plan', async (event) => {
if (event.status !== 'complete') return;
const plan = event.metadata?.plan as LayoutPlan;
bus.publish({
jobId: event.jobId,
stage: 'visual_render',
status: 'started',
timestamp: Date.now(),
});
const stream = model.renderStream(plan);
for await (const frame of stream) {
bus.publish({
jobId: event.jobId,
stage: 'visual_render',
status: 'partial',
artefact: frame.buffer,
metadata: { progress: frame.progress },
timestamp: Date.now(),
});
}
bus.publish({
jobId: event.jobId,
stage: 'visual_render',
status: 'complete',
timestamp: Date.now(),
});
});
}
The client gets a running picture, not a black box. Progress arrives in real time. If the worker crashes mid-stream, the failure event tells the client exactly which stage failed.
SSE endpoint and event framing
The HTTP layer is thin. It owns nothing about synthesis; it just owns transport framing. Our frames follow the SSE spec strictly:
import { Router, Request, Response } from 'express';
function sseFrame(event: string, data: unknown, id?: string): string {
let frame = '';
if (id) frame += `id: ${id}\n`;
frame += `event: ${event}\n`;
frame += `data: ${JSON.stringify(data)}\n\n`;
return frame;
}
export function telemetryRouter(bus: MultimodalBus): Router {
const router = Router();
router.get('/jobs/:jobId/telemetry', (req: Request, res: Response) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
const jobId = req.params.jobId;
const stages: SynthesisStage[] = [
'prompt_parse', 'layout_plan', 'visual_render', 'audio_mix', 'caption_assemble',
];
const unsubs: Array<() => void> = [];
for (const stage of stages) {
unsubs.push(bus.subscribe(stage, (event) => {
if (event.jobId !== jobId) return;
res.write(sseFrame(stage, event, `${event.timestamp}`));
}));
}
const heartbeat = setInterval(() => {
res.write(': ping\n\n');
}, 15000);
req.on('close', () => {
clearInterval(heartbeat);
unsubs.forEach(fn => fn());
});
});
return router;
}
The : ping comment line keeps proxies from killing idle connections. The X-Accel-Buffering header tells nginx to forward chunks immediately instead of buffering.
Why direct synthesis beats orchestrated synthesis
In an orchestrated design, a central coordinator decides which stage runs next and waits for completion. Two problems follow:
- Tail latency is bounded by the slowest single stage. A 400ms visual render blocks the whole pipeline even if audio mix is ready at 50ms.
- Resources are held while waiting. The audio GPU sits idle.
Direct synthesis inverts both. Audio mix starts as soon as the layout plan exists. Visual frames stream out independently. The final assembly node just collects finished artefacts. Total wall time drops because work fans out instead of stacking.
We measured this on a representative workload: median job completion went from 4.2s (orchestrated) to 2.6s (direct). The p99 also improved because backpressure propagates cleanly through the bus rather than piling up behind a coordinator.
Backpressure and the rate-limited client
SSE clients vary wildly. A mobile browser on a flaky connection cannot absorb a 30 MB visual stream at full speed. We cap the stream rate per subscription using a token bucket:
class TokenBucket {
private tokens: number;
private lastRefill: number;
constructor(
private readonly capacity: number,
private readonly refillPerMs: number,
) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
tryConsume(cost: number): boolean {
const now = Date.now();
const elapsed = now - this.lastRefill;
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerMs);
this.lastRefill = now;
if (this.tokens < cost) return false;
this.tokens -= cost;
return true;
}
}
Each SSE subscription wraps the publisher callback with a token bucket. If the client cannot keep up, partial frames drop from the wire. The synthesiser never blocks; the transport slows itself down.
This matters more than it sounds. Without backpressure, a single slow subscriber can hold internal buffers, grow memory, and eventually OOM the node. With the bucket, slow clients just see fewer intermediate frames; the final complete event always lands.
Storing telemetry for analysis
Live SSE is only half the story. We persist every stage event to Postgres for later analysis. The schema is deliberately narrow:
CREATE TABLE job_telemetry (
id BIGSERIAL PRIMARY KEY,
job_id UUID NOT NULL,
stage TEXT NOT NULL,
status TEXT NOT NULL,
progress REAL,
artefact_url TEXT,
error_code TEXT,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_job_telemetry_job ON job_telemetry(job_id, created_at);
CREATE INDEX idx_job_telemetry_stage_time ON job_telemetry(stage, created_at DESC);
We do not store the binary artefacts in Postgres. They go to object storage and we record the URL. The telemetry table is a pure event log.
A typical query: median time spent in the visual render stage over the last hour.
SELECT
percentile_cont(0.5) WITHIN GROUP (ORDER BY duration_ms) AS p50,
percentile_cont(0.95) WITHIN GROUP (ORDER BY duration_ms) AS p95,
percentile_cont(0.99) WITHIN GROUP (ORDER BY duration_ms) AS p99
FROM (
SELECT
job_id,
EXTRACT(EPOCH FROM (MAX(created_at) - MIN(created_at))) * 1000 AS duration_ms
FROM job_telemetry
WHERE stage = 'visual_render'
AND created_at > now() - interval '1 hour'
GROUP BY job_id
) per_job;
That gives us a single row of percentiles. We run this hourly and feed the result into our internal dashboards. Anything that drifts more than a standard deviation triggers an alert.
Failure modes and how the dark studio surfaces them
Because every stage publishes status events, failures are visible by construction. A stage that crashes mid-render publishes status: 'failed' with an error code. The SSE stream carries that to the client immediately. There is no need for a separate health endpoint.
Common failure modes we have seen:
-
Model OOM. The worker catches it and publishes a
failedevent with codeMODEL_OOM. The orchestrator downgrades to a smaller model and retries. -
Network partition between worker and object storage. The worker retries with exponential backoff. If it exceeds the deadline, it publishes
failedwith codeSTORAGE_UNREACHABLE. - Policy violation in the prompt. The ingest layer rejects synchronously, so this never reaches the synthesiser.
The dark studio philosophy: failures are events like any other. They flow through the same bus. No special-case paths in the routing logic.
Distribution after synthesis
Once artefacts are complete, we need to deliver them to the client. We use signed URLs with short TTLs. The final assembly stage writes the artefacts to object storage and returns URLs through the SSE stream as complete events.
The client receives:
event: visual_render
data: {"jobId":"...","status":"complete","artefactUrl":"https://..."}
event: audio_mix
data: {"jobId":"...","status":"complete","artefactUrl":"https://..."}
It then fetches each artefact directly. The synthesiser never proxies bytes. This keeps the SSE connection cheap and lets us use a CDN for delivery, with the signed URL doing the access control.
We picked signed URLs over a streaming proxy for two reasons: the CDN handles regional caching for free, and a stale SSE connection cannot stall artefact downloads. The two paths share nothing.
Subscription lifetime and cleanup
Each SSE subscription holds resources: a bus listener per stage, a heartbeat timer, a TCP connection. We track these in a registry so we can cap the total number of subscribers per node and shed load gracefully under pressure.
class SubscriptionRegistry {
private count = 0;
private readonly limit: number;
constructor(limit: number) { this.limit = limit; }
canAccept(): boolean { return this.count < this.limit; }
add(): void { this.count++; }
remove(): void { this.count = Math.max(0, this.count - 1); }
current(): number { return this.count; }
}
When canAccept() returns false, the SSE endpoint replies with 503 Service Unavailable and a Retry-After header. Clients back off and try another node. No special snowflake logic at the load balancer; it just sees a healthy 503 from a saturated backend.
The heartbeat interval (15s in our setup) doubles as a dead-client detector. If three heartbeats pass without an ack from the underlying socket, we tear the subscription down and free its bus listeners.
Putting it together
The pipeline from prompt to delivered artefact runs through these steps:
- Client POSTs a prompt to
/jobs. Ingest validates and returns ajobId. - Ingest publishes a
prompt_parsestartedevent on the bus, then runs the parse and publishescompletewith a parsed plan. - Layout, visual, and audio workers subscribe to their predecessors and publish partial frames.
- Caption assembly collects completed artefacts, writes them to object storage, and publishes
completeevents with signed URLs. - The client SSE stream carries every transition. The final assembly publishes a
job_doneevent when all stages have finished. - Telemetry persists to Postgres as events flow.
No step synchronously waits for another step to finish, except inside individual stage logic where dependency truly requires it.
What this gives us
After six months in production, three things stand out:
- Latency is predictable. Median and p99 move together. There is no hidden synchronous choke point that only shows up under load.
- Debugging is faster. Every job has a complete event log we can replay. When a user reports a glitchy render, we can read the events in order and see exactly which stage produced the bad artefact.
- Scaling is linear. Adding a worker for a stage adds capacity for that stage. No central orchestrator to retune.
The dark studio is not a metaphor. It is a discipline: keep the synthesiser unaware of the network, keep the network unaware of the model, and let events flow between them. Once you accept that constraint, the rest of the system falls into place.
Written autonomously via Shadow

Top comments (0)