Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4ab3c95
Run scope server in fal serverless
mjh1 Jan 27, 2026
c41b553
wip: add plan
emranemran Jan 27, 2026
f27ec9b
Phase 1: Add FalClient module for fal.ai cloud integration
emranemran Jan 27, 2026
e349250
Phase 2: Add FalOutputTrack and FalInputTrack for WebRTC frame handling
emranemran Jan 28, 2026
7df1eed
Phase 3: Integrate FalClient with FrameProcessor for cloud inference …
emranemran Jan 28, 2026
ce9b46e
Phase 4: Add REST API endpoints for fal.ai cloud configuration
emranemran Jan 28, 2026
17f4d6c
Phase 5: Fix Spout receiver to route frames through fal when enabled
emranemran Jan 28, 2026
a75a554
Phase 6: Add parameter forwarding and CloudModeToggle UI
emranemran Jan 28, 2026
6dc0380
fixes to handle WSL (linux encoding) vs windows
emranemran Jan 28, 2026
d449699
Fix: Wire up CloudModeToggle props in StreamPage
emranemran Jan 28, 2026
deaf741
fixes: wss fails to connect
emranemran Jan 28, 2026
d7d0849
update githash
emranemran Jan 28, 2026
0687917
trigger
emranemran Jan 28, 2026
ec6c130
workflow
emranemran Jan 28, 2026
bdcb538
Fix: WebSocket connection to fal cloud backend
emranemran Jan 29, 2026
7079f98
debug prints
emranemran Jan 29, 2026
9c0146b
model download fix
emranemran Jan 29, 2026
1de95bc
fal: Update Docker image and increase keep_alive to 30 min
emranemran Jan 29, 2026
8db96e9
fal: Wait for pipeline to fully load before WebRTC offer
emranemran Jan 29, 2026
802e944
fal: Fix pipeline status check to use correct field name
emranemran Jan 29, 2026
d117be7
fal: Keep pipeline_ids in initialParameters for frame_processor
emranemran Jan 29, 2026
7ee70e6
fal: Simplify offer handling - let backend manage pipeline loading
emranemran Jan 29, 2026
1359976
fix: relay mode bugs in fal_app and tracks
emranemran Jan 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
59 changes: 59 additions & 0 deletions .github/workflows/docker-build-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Build and Push Docker Image (Branch)

on:
push:
branches-ignore:
- main

jobs:
build-and-push:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Clean up before build
run: |
# Clean up unused packages to avoid disk space issues
sudo rm -rf /usr/local/.ghcup
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo rm -rf /usr/local/lib/android/sdk/ndk
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost

# Show disk usage before build
echo "Disk usage before build:"
df -h

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: daydreamlive
password: ${{ secrets.DOCKER_HUB_TOKEN }}

- name: Generate Docker tags
id: tag
run: |
# Replace / with - for valid Docker tag (e.g., feature/foo -> feature-foo)
BRANCH_TAG=$(echo "${{ github.ref_name }}" | sed 's/\//-/g')
# Short SHA (first 7 characters)
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
echo "branch_tag=$BRANCH_TAG" >> $GITHUB_OUTPUT
echo "sha_tag=$SHORT_SHA" >> $GITHUB_OUTPUT

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
push: true
tags: |
daydreamlive/scope:${{ steps.tag.outputs.branch_tag }}
daydreamlive/scope:${{ steps.tag.outputs.sha_tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
188 changes: 188 additions & 0 deletions FAL_DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Deploying Scope to fal.ai

This guide explains how to deploy the Scope backend to fal.ai serverless.

## Architecture

```
┌──────────────────────────────────────────────────────────────────┐
│ fal.ai Runner │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ fal_app.py │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────┐│ │
│ │ │ WebSocket │ ──────► │ Scope Backend ││ │
│ │ │ Endpoint │ HTTP │ (uv run daydream-scope) ││ │
│ │ │ /ws │ Proxy │ localhost:8000 ││ │
│ │ └────────┬────────┘ └──────────────┬──────────────┘│ │
│ │ │ │ │ │
│ └───────────┼─────────────────────────────────┼────────────────┘ │
└──────────────┼─────────────────────────────────┼─────────────────┘
│ │
WebSocket │ WebRTC │
(signaling │ (video) │
+ API) │ │
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Browser │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Frontend with FalAdapter │ │
│ │ - API calls go through WebSocket │ │
│ │ - WebRTC signaling goes through WebSocket │ │
│ │ - Video frames flow directly via WebRTC │ │
│ └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```

## How It Works

1. **Single WebSocket Connection**: All communication (API calls + WebRTC signaling) goes through one WebSocket connection to prevent fal from spawning new runner instances.

2. **Scope Runs as Subprocess**: The Scope backend runs inside the fal container via `uv run daydream-scope --no-browser`.

3. **WebRTC Video Flows Directly**: Once signaling is complete, video frames flow directly via WebRTC (UDP/RTP) between browser and fal runner.

## Deployment

### 1. Deploy to fal.ai

```bash
cd scope
fal deploy fal_app.py
```

This will output a URL like: `https://fal.run/your-username/scope-app`

### 2. Update Frontend to Use FalAdapter

In your frontend initialization (e.g., `main.tsx` or `App.tsx`):

```typescript
import { initFalAdapter } from "./lib/falAdapter";

// Initialize when running on fal
const FAL_WS_URL = "wss://fal.run/your-username/scope-app/ws";

async function initApp() {
// Check if we should use fal mode
const useFal = import.meta.env.VITE_USE_FAL === "true";

if (useFal) {
const adapter = initFalAdapter(FAL_WS_URL);
await adapter.connect();
console.log("Connected to fal.ai backend");
}
}

initApp();
```

### 3. Use the FalAdapter in Components

For API calls, use the adapter's API methods:

```typescript
import { getFalAdapter, isFalMode } from "./lib/falAdapter";
import { getPipelineStatus } from "./lib/api";

async function fetchStatus() {
if (isFalMode()) {
const adapter = getFalAdapter()!;
return adapter.api.getPipelineStatus();
} else {
return getPipelineStatus();
}
}
```

For WebRTC, use the `useWebRTCFal` hook:

```typescript
import { useWebRTC } from "./hooks/useWebRTC";
import { useWebRTCFal } from "./hooks/useWebRTCFal";
import { getFalAdapter, isFalMode } from "./lib/falAdapter";

function VideoStream() {
// Choose the right hook based on deployment mode
const adapter = getFalAdapter();

const webrtc = isFalMode() && adapter
? useWebRTCFal({ adapter })
: useWebRTC();

// Use webrtc.startStream, webrtc.stopStream, etc.
}
```

## WebSocket Protocol

All messages are JSON with a `type` field.

### WebRTC Signaling

```typescript
// Get ICE servers
{ "type": "get_ice_servers" }
// Response: { "type": "ice_servers", "data": { "iceServers": [...] } }

// Send SDP offer
{ "type": "offer", "sdp": "...", "sdp_type": "offer", "initialParameters": {...} }
// Response: { "type": "answer", "sdp": "...", "sdp_type": "answer", "sessionId": "..." }

// Send ICE candidate
{ "type": "icecandidate", "sessionId": "...", "candidate": { "candidate": "...", "sdpMid": "...", "sdpMLineIndex": 0 } }
// Response: { "type": "icecandidate_ack", "status": "ok" }
```

### API Proxy

```typescript
// Make API request
{
"type": "api",
"method": "GET", // or POST, PATCH, DELETE
"path": "/api/v1/pipeline/status",
"body": null, // for POST/PATCH
"request_id": "req_123" // for correlating responses
}

// Response
{
"type": "api_response",
"request_id": "req_123",
"status": 200,
"data": { ... }
}
```

### Keepalive

```typescript
{ "type": "ping" }
// Response: { "type": "pong" }
```

## Environment Variables

The fal container inherits environment variables. Set these in your fal deployment:

- `HF_TOKEN` - Hugging Face token for TURN server access
- `PIPELINE` - Default pipeline to pre-warm (optional)

## Limitations

1. **File Downloads**: Binary file downloads (recordings, logs) need special handling. The adapter provides URLs that the browser can fetch directly.

2. **File Uploads**: Files are base64-encoded when sent through WebSocket, which increases size by ~33%.

3. **Connection Persistence**: The WebSocket connection must stay open to keep the runner alive. If it disconnects, you may get a new runner.

## Troubleshooting

### "WebSocket not connected"
Make sure `adapter.connect()` completes before making API calls.

### WebRTC connection fails
Check that TURN servers are configured. The fal runner needs a public IP or TURN relay for WebRTC to work.

### New runner spawned for each request
Make sure ALL API calls go through the FalAdapter WebSocket, not direct HTTP fetch.
Loading
Loading