@reactor-models/sana-streaming
v0.1.12
Published
Strongly-typed SDK for the SanaStreaming model on Reactor
Maintainers
Readme
@reactor-models/sana-streaming
Typed JavaScript + React SDK for the SanaStreaming model on Reactor. Version v0.1.12.
Get started
Scaffold a starter app for SanaStreaming with create-reactor-app:
npx create-reactor-app my-app --model=sana-streamingpnpm dlx create-reactor-app my-app --model=sana-streamingInstall
npm install @reactor-models/sana-streamingpnpm add @reactor-models/sana-streamingThe package exports a plain-JavaScript client and a set of React bindings. Import whichever you need from @reactor-models/sana-streaming:
import { SanaStreamingModel } from "@reactor-models/sana-streaming";import { SanaStreamingProvider, useSanaStreaming } from "@reactor-models/sana-streaming";React 18 or later is required when using the provider and hooks. The token-loading examples below use React 19's use(); on React 18, fetch the JWT in a useEffect and pass it to the provider once it resolves.
Authenticate
Reactor uses short-lived JWTs for session auth. You hold your API key on your server, mint a token on demand, and the client never sees the raw key. Tokens are valid for 6 hours — if one leaks, it expires on its own.
Mint a JWT with POST https://api.reactor.inc/tokens and the Reactor-API-Key header; the response JSON is { "jwt": "..." }.
JavaScript (Next.js route handler)
// app/api/reactor/token/route.ts
import { NextResponse } from "next/server";
export async function POST() {
const res = await fetch("https://api.reactor.inc/tokens", {
method: "POST",
headers: { "Reactor-API-Key": process.env.REACTOR_API_KEY! },
});
const { jwt } = await res.json();
return NextResponse.json({ jwt });
}React (provider)
Call the /api/reactor/token route above from a client component and pass the result to the provider:
"use client";
import { use } from "react";
import { SanaStreamingProvider } from "@reactor-models/sana-streaming";
import { ReactorView } from "@reactor-team/js-sdk";
async function getToken() {
const r = await fetch("/api/reactor/token", { method: "POST" });
const { jwt } = await r.json();
return jwt;
}
const tokenPromise = getToken();
export default function App() {
const token = use(tokenPromise);
return (
<SanaStreamingProvider jwtToken={token} connectOptions={{ autoConnect: true }}>
<ReactorView className="w-full aspect-video" />
</SanaStreamingProvider>
);
}Connect
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);React
The provider takes the JWT as a prop; fetch it from the same /api/reactor/token route the Authenticate example mints:
"use client";
import { use } from "react";
import { SanaStreamingProvider, useSanaStreaming } from "@reactor-models/sana-streaming";
async function getToken() {
const r = await fetch("/api/reactor/token", { method: "POST" });
const { jwt } = await r.json();
return jwt;
}
const tokenPromise = getToken();
function Controller() {
const { status } = useSanaStreaming();
return <span>Status: {status}</span>;
}
export default function App() {
const token = use(tokenPromise);
return (
<SanaStreamingProvider jwtToken={token}>
<Controller />
</SanaStreamingProvider>
);
}Events
Client-to-model commands. The typed surface is SanaStreamingModel (one method per event) in plain JS, and useSanaStreaming() in React — every field name below matches the parameter name the method accepts.
pause
Pause editing after the current chunk; frames stop until resume. Valid only while generating. Emits generation_paused + state, or command_error if not generating or already paused.
Emits: generation_paused, state, command_error
No parameters.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.pause();React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { pause } = useSanaStreaming();
return <button onClick={() => pause()}>pause</button>;
}reset
Abort the current run, clear the prompt and source video, and return to the waiting state. Valid at any time. Emits generation_reset + state; re-arm with set_video / set_prompt / start.
Emits: generation_reset, state
No parameters.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.reset();React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { reset } = useSanaStreaming();
return <button onClick={() => reset()}>reset</button>;
}start
Begin streaming on main_video. In file mode requires a source video (set_video); in live mode transforms the camera track (no upload needed). A prompt is OPTIONAL in both. Emits generation_started + state, or command_error. No effect while already generating.
Emits: generation_started, state, command_error
No parameters.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.start();React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { start } = useSanaStreaming();
return <button onClick={() => start()}>start</button>;
}resume
Resume editing after a pause, continuing from where it stopped. Valid only while paused. Emits generation_resumed + state, or command_error if not paused.
Emits: generation_resumed, state, command_error
No parameters.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.resume();React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { resume } = useSanaStreaming();
return <button onClick={() => resume()}>resume</button>;
}setMode
Select the input source for the next start: file (edit an uploaded clip set via set_video) or live (transform the live camera input track / webcam). Set before start; treat as immutable for the run — call reset to switch. Emits state, or command_error for an unknown mode.
Emits: state, command_error
| Parameter | Type | Required | Description |
|---|---|---|---|
| mode | "file" \| "live" | | Input source: file or live. (default "file") |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.setMode({ mode: "file" });React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { setMode } = useSanaStreaming();
return <button onClick={() => setMode({ mode: "file" })}>setMode</button>;
}setSeed
Set the random seed that controls the edit's randomness, so the same source, prompt, and seed reproduce the same result. Valid at any time, but read once when generation begins — change it then reset to re-seed an in-progress run. Emits state.
Emits: state
| Parameter | Type | Required | Description |
|---|---|---|---|
| seed | number | | Non-negative noise seed. (min 0, default 0) |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.setSeed({ seed: 0 });React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { setSeed } = useSanaStreaming();
return <button onClick={() => setSeed({ seed: 0 })}>setSeed</button>;
}setVideo
Provide the source video to edit, in file mode. Required before start. Returns immediately no matter how long the clip is: the video is consumed gradually as the edit streams, not all at once up front. Emits video_accepted + state, or command_error if the file is missing or is not a video the model can read.
Emits: video_accepted, state, command_error
| Parameter | Type | Required | Description |
|---|---|---|---|
| video | FileRef | | Reference to a file uploaded via the Reactor presigned-URL protocol. (default null) |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
const fileRef = await sanaStreaming.uploadFile(blob);
await sanaStreaming.setVideo({ video: fileRef });React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { setVideo, uploadFile } = useSanaStreaming();
async function handlePick(file: File) {
const ref = await uploadFile(file);
await setVideo({ video: ref });
}
return <input type="file" onChange={(e) => handlePick(e.target.files![0])} />;
}setPrompt
Set the natural-language instruction that steers the edit. Optional and valid at any time — with no prompt the output stays close to the source video; set or change it (including while streaming) to steer the edit live, visible about one chunk later on a not-yet-shown chunk. Emits prompt_accepted + state, or command_error (the previous prompt is kept) if the prompt cannot be processed.
Emits: prompt_accepted, state, command_error
| Parameter | Type | Required | Description |
|---|---|---|---|
| prompt | string | | Natural-language edit instruction. (default "") |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.setPrompt({ prompt: "A sunset over the ocean" });React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { setPrompt } = useSanaStreaming();
return <button onClick={() => setPrompt({ prompt: "A sunset over the ocean" })}>setPrompt</button>;
}setAnchorInterval
Periodically re-ground the edit on the source video every chunks chunks (0 = never). Over a long edit the output can slowly drift away from the source; re-grounding pulls it back, at the cost of a brief visible refresh on the following chunk. Valid at any time, including while streaming (it takes effect at the next chunk boundary). Each re-ground emits anchored.
Emits: anchored
| Parameter | Type | Required | Description |
|---|---|---|---|
| chunks | number | | Chunks between anchors; 0 disables anchoring. (min 0, default 20) |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
await sanaStreaming.setAnchorInterval({ chunks: 20 });React
"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";
function Example() {
const { setAnchorInterval } = useSanaStreaming();
return <button onClick={() => setAnchorInterval({ chunks: 20 })}>setAnchorInterval</button>;
}Messages
Model-to-client messages. Register a typed listener with on… on SanaStreamingModel, or a useSanaStreaming… hook in React, to receive only the messages you care about.
state
Emitted as a full snapshot of the session's observable state.
Sent on connect, after every accepted state-changing command, and at each chunk boundary, so a client can render its UI from one message instead of accumulating individual events.
Listener: onState · React hook: useSanaStreamingState
| Field | Type | Description |
|---|---|---|
| seed | number | Current value of the noise seed. |
| paused | boolean | True while generation is paused via pause. |
| running | boolean | True while the chunk loop is actively producing frames. |
| started | boolean | True once start has been accepted; reset to false by reset. |
| has_video | boolean | True once a source video has been set. |
| has_prompt | boolean | True once an edit prompt has been set. |
| current_chunk | number | Zero-based index of the last completed chunk; 0 before the first. |
| current_prompt | unknown | The prompt currently steering the edit, or null. |
| anchor_interval | number | Re-ground generation every this many chunks (0 = never). Set via set_anchor_interval. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onState((msg) => {
console.log(
"state",
msg.seed,
msg.paused,
msg.running,
msg.started,
msg.has_video,
msg.has_prompt,
msg.current_chunk,
msg.current_prompt,
msg.anchor_interval,
);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingState } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingState((msg) => {
console.log(
"state",
msg.seed,
msg.paused,
msg.running,
msg.started,
msg.has_video,
msg.has_prompt,
msg.current_chunk,
msg.current_prompt,
msg.anchor_interval,
);
});anchored
Emitted each time the edit re-grounds on the source video to pull back drift. Fires every anchor_interval chunks; the following chunk may show a brief visible refresh.
Listener: onAnchored · React hook: useSanaStreamingAnchored
| Field | Type | Description |
|---|---|---|
| chunk_index | number | Zero-based index of the first chunk generated after this re-ground. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onAnchored((msg) => {
console.log("anchored", msg.chunk_index);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingAnchored } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingAnchored((msg) => {
console.log("anchored", msg.chunk_index);
});command_error
Emitted when a command is rejected; the session's prior state is unchanged.
Listener: onCommandError · React hook: useSanaStreamingCommandError
| Field | Type | Description |
|---|---|---|
| reason | string | Why the command was rejected. |
| command | string | Name of the command that was rejected. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onCommandError((msg) => {
console.log("command_error", msg.reason, msg.command);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingCommandError } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingCommandError((msg) => {
console.log("command_error", msg.reason, msg.command);
});chunk_complete
Emitted once per completed chunk of main_video.
Listener: onChunkComplete · React hook: useSanaStreamingChunkComplete
| Field | Type | Description |
|---|---|---|
| chunk_index | number | Zero-based index of the chunk that just completed. |
| active_prompt | string | The prompt that drove this chunk; empty means the output stayed close to the source. |
| frames_emitted | number | Edited pixel frames emitted by this chunk. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onChunkComplete((msg) => {
console.log(
"chunk_complete",
msg.chunk_index,
msg.active_prompt,
msg.frames_emitted,
);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingChunkComplete } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingChunkComplete((msg) => {
console.log(
"chunk_complete",
msg.chunk_index,
msg.active_prompt,
msg.frames_emitted,
);
});video_accepted
Emitted after set_video accepts + probes the source clip.
Listener: onVideoAccepted · React hook: useSanaStreamingVideoAccepted
| Field | Type | Description |
|---|---|---|
| width | number | Width in pixels of the source video, before cropping to the output aspect ratio. |
| height | number | Height in pixels of the source video, before cropping to the output aspect ratio. |
| num_frames | number | Always 0 — the source is not scanned up front, so its total length is not reported here. |
| num_latent_frames | number | Always 0 — reserved; the source length is not reported here. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onVideoAccepted((msg) => {
console.log(
"video_accepted",
msg.width,
msg.height,
msg.num_frames,
msg.num_latent_frames,
);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingVideoAccepted } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingVideoAccepted((msg) => {
console.log(
"video_accepted",
msg.width,
msg.height,
msg.num_frames,
msg.num_latent_frames,
);
});prompt_accepted
Emitted when set_prompt accepts a new edit prompt.
Listener: onPromptAccepted · React hook: useSanaStreamingPromptAccepted
| Field | Type | Description |
|---|---|---|
| prompt | string | The prompt text that was accepted. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onPromptAccepted((msg) => {
console.log("prompt_accepted", msg.prompt);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingPromptAccepted } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingPromptAccepted((msg) => {
console.log("prompt_accepted", msg.prompt);
});conditions_ready
Emitted after set_prompt / set_video so the client can tell if start will succeed (only a video is required; a prompt is optional).
Listener: onConditionsReady · React hook: useSanaStreamingConditionsReady
| Field | Type | Description |
|---|---|---|
| has_video | boolean | True once a source video has been set. |
| has_prompt | boolean | True once an edit prompt has been set. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onConditionsReady((msg) => {
console.log("conditions_ready", msg.has_video, msg.has_prompt);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingConditionsReady } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingConditionsReady((msg) => {
console.log("conditions_ready", msg.has_video, msg.has_prompt);
});generation_reset
Emitted when reset aborts the run and clears the prompt and source video.
Listener: onGenerationReset · React hook: useSanaStreamingGenerationReset
| Field | Type | Description |
|---|---|---|
| reason | string | Short human-readable reason the reset was issued. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationReset((msg) => {
console.log("generation_reset", msg.reason);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationReset } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationReset((msg) => {
console.log("generation_reset", msg.reason);
});generation_paused
Emitted when pause succeeds and frames stop until resume.
Listener: onGenerationPaused · React hook: useSanaStreamingGenerationPaused
| Field | Type | Description |
|---|---|---|
| chunk_index | number | Index of the last completed chunk before pausing. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationPaused((msg) => {
console.log("generation_paused", msg.chunk_index);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationPaused } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationPaused((msg) => {
console.log("generation_paused", msg.chunk_index);
});generation_resumed
Emitted when resume succeeds and frames continue from where they stopped.
Listener: onGenerationResumed · React hook: useSanaStreamingGenerationResumed
| Field | Type | Description |
|---|---|---|
| chunk_index | number | Index of the last completed chunk before resuming. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationResumed((msg) => {
console.log("generation_resumed", msg.chunk_index);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationResumed } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationResumed((msg) => {
console.log("generation_resumed", msg.chunk_index);
});generation_started
Emitted once when start succeeds and frames begin streaming.
Listener: onGenerationStarted · React hook: useSanaStreamingGenerationStarted
| Field | Type | Description |
|---|---|---|
| prompt | string | The prompt active at the start of generation (may be empty). |
| chunk_num | number | Total chunks the run will produce (0 — not known up front). |
| frame_num | number | Approximate total edited pixel frames (0 — not known up front). |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationStarted((msg) => {
console.log(
"generation_started",
msg.prompt,
msg.chunk_num,
msg.frame_num,
);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationStarted } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationStarted((msg) => {
console.log(
"generation_started",
msg.prompt,
msg.chunk_num,
msg.frame_num,
);
});generation_complete
Emitted when the whole source clip has been edited.
Listener: onGenerationComplete · React hook: useSanaStreamingGenerationComplete
| Field | Type | Description |
|---|---|---|
| total_chunks | number | Total chunks produced by the run. |
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationComplete((msg) => {
console.log("generation_complete", msg.total_chunks);
});
await sanaStreaming.connect(jwt);React
import { useSanaStreamingGenerationComplete } from "@reactor-models/sana-streaming";
// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationComplete((msg) => {
console.log("generation_complete", msg.total_chunks);
});Tracks
Named media channels between your app and the SanaStreaming model. Use the typed helpers below — SanaStreamingModel.publish<Track> / on<Track> in plain JS, and useSanaStreamingTrack or the per-track <SanaStreaming<Track>View> components in React — so track names are checked at compile time.
camera
A video channel you push media into (for example, the user's webcam).
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);
const media = await navigator.mediaDevices.getUserMedia({ video: true });
const videoTrack = media.getVideoTracks()[0];
await sanaStreaming.publishCamera(videoTrack);
// later, to stop sending:
await sanaStreaming.unpublishCamera();React
"use client";
import { SanaStreamingCameraView } from "@reactor-models/sana-streaming";
// Inside a component wrapped by <SanaStreamingProvider>:
export function Example() {
return <SanaStreamingCameraView />;
}main_video
A video channel you subscribe to — the model publishes this for your app to render.
JavaScript
import { SanaStreamingModel } from "@reactor-models/sana-streaming";
const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onMainVideo((track, stream) => {
// attach to a <video> element, pipe to a canvas, etc.
videoEl.srcObject = stream;
});
await sanaStreaming.connect(jwt);React
"use client";
import { SanaStreamingMainVideoView } from "@reactor-models/sana-streaming";
// Inside a component wrapped by <SanaStreamingProvider>:
export function Example() {
return <SanaStreamingMainVideoView className="w-full aspect-video" />;
}