The Dreams API provides access to a live AI-generated art stream. Frames are generated on a GPU and streamed to viewers in real-time through a WebSocket-based architecture.
Overview
Base URL: https://aetherawi.red
The Dreams system consists of:
- VPS Server (aethera) - Hosts the API, manages viewer connections, controls GPU lifecycle
- GPU Worker (dream_gen) - Generates frames using Stable Diffusion, pushes to VPS
- Viewers (browsers/clients) - Connect to consume and display frames
Key Features
- Real-time frame streaming via WebSocket
- Smart GPU lifecycle management (auto-start when viewers connect, auto-stop when idle)
- Frame caching for instant delivery to new viewers
- Rate limiting to prevent abuse
- Multiple consumption methods (WebSocket, SSE, polling)
REST Endpoints
GET /dreams
Returns the Dreams viewer HTML page.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
embed |
int | 0 | If 1, renders minimal embed version without header/footer |
Example:
GET /dreams?embed=1
GET /api/dreams/status
Returns comprehensive status information about the Dreams system.
Rate Limit: 60 requests/minute per IP
Response:
{
"status": "ready",
"gpu": {
"active": true
},
"generation": {
"frame_count": 12500,
"current_frame": 12500,
"current_keyframe": 625,
"fps": 4.8,
"session_fps": 4.7,
"resolution": [1024, 512]
},
"viewers": {
"websocket_count": 3,
"api_active": true
},
"cache": {
"frames_cached": 30,
"total_bytes": 1572864
},
"playback": {
"queue_depth": 5,
"buffer_seconds": 1.0,
"target_fps": 5.0,
"actual_fps": 4.8
}
}
Status Values:
| Status | Description |
|---|---|
idle |
GPU not running, no viewers |
starting |
GPU is starting up |
ready |
GPU running, frames flowing |
stopping |
GPU shutting down |
error |
An error occurred |
GET /api/dreams/current
Returns the most recent frame as a WebP image.
Rate Limit: 60 requests/minute per IP
Response Headers:
| Header | Description |
|---|---|
X-Frame-Number |
Sequential frame number |
X-Keyframe-Number |
Associated keyframe number |
X-Generation-Time-Ms |
Time taken to generate (ms) |
Content-Type |
image/webp |
Response Codes:
| Code | Description |
|---|---|
| 200 | Frame returned successfully |
| 204 | No frames available yet |
Example:
curl -o frame.webp https://your-domain.com/api/dreams/current
GET /api/dreams/health
Health check endpoint for monitoring and load balancers. Does not trigger GPU lifecycle.
Response (Healthy):
{
"status": "healthy",
"gpu_connected": true,
"viewer_count": 3,
"frames_cached": true
}
Response (Unhealthy):
{
"status": "unhealthy",
"error": "error message"
}
Response Codes:
| Code | Description |
|---|---|
| 200 | Service healthy |
| 503 | Service unavailable |
GET /api/dreams/frames/recent
Returns metadata (and optionally data) for recent frames from the cache.
Rate Limit: 60 requests/minute per IP
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
count |
int | 5 | Number of frames (1-30) |
format |
string | metadata |
metadata or urls |
Response (metadata format):
{
"frames": [
{
"frame_number": 12498,
"keyframe_number": 625,
"timestamp": 1702489200.123,
"generation_time_ms": 180,
"size_bytes": 48256
},
// ... more frames
],
"count": 5
}
Response (urls format):
{
"frames": [
{
"frame_number": 12498,
"keyframe_number": 625,
"timestamp": 1702489200.123,
"generation_time_ms": 180,
"size_bytes": 48256,
"data_url": "data:image/webp;base64,UklGR..."
}
],
"count": 5
}
GET /api/dreams/embed
Returns embeddable code snippets for Dream Window.
Response:
{
"iframe": "<iframe src=\"https://your-domain.com/dreams?embed=1\" width=\"1024\" height=\"512\" frameborder=\"0\" allow=\"autoplay\" loading=\"lazy\"></iframe>",
"image_url": "https://your-domain.com/api/dreams/current",
"stream_url": "wss://your-domain.com/ws/dreams",
"status_url": "https://your-domain.com/api/dreams/status",
"resolution": {
"width": 1024,
"height": 512
}
}
Server-Sent Events (SSE)
GET /api/dreams/sse
Alternative to WebSocket for simpler clients. Returns an SSE stream with frame updates.
Rate Limit: Initial connection counts against rate limit
Events:
| Event | Data | Description |
|---|---|---|
status |
JSON | Status updates (every 5 seconds) |
frame |
JSON | New frame data (base64 encoded) |
Event: status
{
"status": "ready",
"gpu_connected": true,
"viewer_count": 3,
"frame_count": 12500
}
Event: frame
{
"frame_number": 12501,
"data": "UklGRlQ..." // base64-encoded WebP
}
Example (JavaScript):
const source = new EventSource('/api/dreams/sse');
source.addEventListener('status', (e) => {
const status = JSON.parse(e.data);
console.log('Status:', status.status);
});
source.addEventListener('frame', (e) => {
const frame = JSON.parse(e.data);
const img = document.getElementById('dream-img');
img.src = `data:image/webp;base64,${frame.data}`;
});
Note: SSE polls for new frames at 10 Hz. WebSocket is more efficient for high-frequency updates.
WebSocket Endpoints
WS /ws/dreams
Primary WebSocket endpoint for browser viewers. Provides real-time frame streaming.
Connection:
const ws = new WebSocket('wss://your-domain.com/ws/dreams');
ws.binaryType = 'arraybuffer';
Incoming Messages:
-
Binary (Frame Data)
- First byte:
0x01(message type) - Remaining bytes: WebP image data
ws.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { const view = new Uint8Array(event.data); if (view[0] === 0x01) { const frameData = event.data.slice(1); const blob = new Blob([frameData], { type: 'image/webp' }); // Display blob... } } }; - First byte:
-
JSON (Status/Config)
{ "type": "status", "status": "ready", "message": "Dreams flowing...", "viewer_count": 3 } { "type": "config", "target_fps": 5.0 } { "type": "pong" }
Outgoing Messages:
- Ping (Keepalive)
{ "type": "ping" }
Example Client:
class DreamClient {
constructor(url) {
this.ws = new WebSocket(url);
this.ws.binaryType = 'arraybuffer';
this.ws.onmessage = this.handleMessage.bind(this);
// Keepalive ping every 30 seconds
setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
}
handleMessage(event) {
if (event.data instanceof ArrayBuffer) {
const view = new Uint8Array(event.data);
const msgType = view[0];
if (msgType === 0x01) {
// Frame data
const frameData = event.data.slice(1);
this.onFrame(frameData);
}
} else {
// JSON message
const msg = JSON.parse(event.data);
if (msg.type === 'status') {
this.onStatus(msg);
} else if (msg.type === 'config') {
this.onConfig(msg);
}
}
}
onFrame(data) {
// Override this method
}
onStatus(status) {
// Override this method
}
onConfig(config) {
// Override this method
}
}
WS /ws/gpu (GPU Worker Only)
WebSocket endpoint for the GPU worker. Requires authentication.
Authentication:
- Set
DREAM_GEN_AUTH_TOKENenvironment variable on both VPS and GPU - GPU sends token in Authorization header:
Authorization: Bearer <token> - If env var is not set, authentication is disabled (development mode only)
Connection:
import websockets
async def connect():
headers = {'Authorization': f'Bearer {auth_token}'}
ws = await websockets.connect(
'wss://your-domain.com/ws/gpu',
additional_headers=headers
)
Message Protocol:
All messages are binary with a type byte prefix:
| Type Byte | Direction | Description |
|---|---|---|
0x01 |
GPU → VPS | Frame data (WebP bytes) |
0x02 |
GPU → VPS | State snapshot (msgpack) |
0x03 |
GPU → VPS | Heartbeat |
0x04 |
GPU → VPS | Status update (JSON) |
0x10 |
VPS → GPU | Pause generation |
0x11 |
VPS → GPU | Resume generation |
0x12 |
VPS → GPU | Save state |
0x13 |
VPS → GPU | Shutdown |
0x14 |
VPS → GPU | Load state (msgpack payload) |
Sending Frames:
# Frame message: type byte + WebP data
message = bytes([0x01]) + webp_bytes
await ws.send(message)
Sending Status:
import json
status = {"target_fps": 5.0, "keyframe": 123}
message = bytes([0x04]) + json.dumps(status).encode()
await ws.send(message)
Receiving Control Messages:
message = await ws.recv()
msg_type = message[0]
if msg_type == 0x10:
pause_generation()
elif msg_type == 0x11:
resume_generation()
elif msg_type == 0x12:
save_state()
elif msg_type == 0x13:
shutdown_gracefully()
elif msg_type == 0x14:
state_data = message[1:]
load_state(state_data)
GPU Lifecycle
The Dreams system automatically manages GPU resources:
- Viewer Connects → GPU starts (if not running)
- Frames Flow → GPU sends frames to VPS → VPS broadcasts to all viewers
- All Viewers Disconnect → Grace period timer starts (5 minutes)
- Grace Period Expires → GPU receives shutdown command, saves state, stops
This ensures cost-effective usage: GPU only runs when viewers are present.
API Activity: Accessing /api/dreams/status, /api/dreams/current, or other API endpoints also counts as viewer activity and keeps the GPU warm.
Rate Limiting
Most API endpoints are rate limited to 60 requests per minute per IP.
Rate Limit Response:
{
"detail": "Rate limit exceeded. Max 60 requests per 60s."
}
HTTP Status: 429 Too Many Requests
WebSocket connections are not rate limited (connection frequency is inherently limited).
Example: Simple Polling Client
For environments where WebSocket isn't available:
import requests
import time
BASE_URL = "https://your-domain.com"
def poll_frames():
last_frame = 0
while True:
# Check status
status = requests.get(f"{BASE_URL}/api/dreams/status").json()
if status["status"] != "ready":
print(f"Status: {status['status']}")
time.sleep(5)
continue
# Get current frame
current = status["generation"]["current_frame"]
if current > last_frame:
response = requests.get(f"{BASE_URL}/api/dreams/current")
if response.status_code == 200:
with open(f"frame_{current}.webp", "wb") as f:
f.write(response.content)
last_frame = current
print(f"Saved frame {current}")
time.sleep(0.2) # 5 Hz polling
poll_frames()
Example: Display on Canvas (Browser)
<canvas id="dream-canvas" width="1024" height="512"></canvas>
<script>
const canvas = document.getElementById('dream-canvas');
const ctx = canvas.getContext('2d');
const ws = new WebSocket('wss://your-domain.com/ws/dreams');
ws.binaryType = 'arraybuffer';
ws.onmessage = async (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new Uint8Array(event.data);
if (view[0] === 0x01) {
const frameData = event.data.slice(1);
const blob = new Blob([frameData], { type: 'image/webp' });
const img = await createImageBitmap(blob);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
}
};
</script>
Example: Embed in External Site
<iframe
src="https://your-domain.com/dreams?embed=1"
width="1024"
height="512"
frameborder="0"
loading="lazy">
</iframe>
Or use the image endpoint for a static snapshot:
<img
src="https://your-domain.com/api/dreams/current"
alt="AI Dream"
style="width: 100%; max-width: 1024px;"
>
Frame Format
All frames are delivered as WebP images:
- Resolution: 1024×512 pixels
- Quality: 80-85%
- Average size: 40-70 KB per frame
- Color space: RGB (no alpha)
Error Handling
WebSocket Disconnection:
- Implement exponential backoff for reconnection
- Start with 1 second delay, max 30 seconds
- Reset delay counter on successful connection
No Frames Available:
/api/dreams/currentreturns204 No Content- WebSocket sends initial status, then frames when GPU starts
GPU Not Running:
- Status will show
idleorstarting - Connecting via WebSocket or hitting API triggers GPU start
- First frame typically arrives within 30-120 seconds
Environment Variables
| Variable | Description |
|---|---|
DREAM_GEN_AUTH_TOKEN |
Shared secret for GPU authentication |
VPS_HOST |
VPS hostname (default: aetherawi.red) |
Technical Notes
Frame Buffering
The VPS maintains a playback queue to smooth network jitter:
- Frames queue at receive rate
- Playback releases at steady FPS
- Client-side also implements frame queue for smooth display
Presence Tracking
- WebSocket connections tracked individually
- API access refreshes presence timer
- 5-minute grace period after last activity
Binary Protocol
Using binary WebSocket messages with type prefixes is ~50% more efficient than base64-encoded JSON for image data.