prompt message that the client sent.
Key Facts
What the App Does
The client sends camera video, the app restyles it, and a new video track comes back. The camera effects example shows the same pattern with a CPU filter. Its source iswma_filter.py (FilterApp).
- Each camera frame is cropped to a square, and the camera is resampled to 16 frames per second.
- The model takes frames in chunks of 4. Each chunk produces 4 restyled frames.
- A
promptmessage from the client changes the style while the stream runs. - A
statusmessage goes to the client when the state changes, and every 2 seconds.
status message reports restarting during the pause.
When several prompts arrive quickly, only the latest counts.
Prerequisites
- A fal account with permission to deploy and call the app. The browser client connects through fal’s WMA service at
wma.fal.run. It needs a fal API key on your server, or a signed-in dashboard session for the playground. - Access to the fal WMA TURN servers for the account that owns the app (the
Xinfal.run/X/app). Request it from fal before you rely on the app. Without it, a session connects only when the runner is directly reachable, which fails on many runners and networks. See If the Session Does Not Start. - Python 3.11 on your computer. The app image uses Python 3.11, and the Python versions must match for deployment.
- The fal CLI and the demo code. Run these commands:
- A browser with a camera. Camera capture needs HTTPS or localhost.
- One H100 GPU. The app uses the upstream single-GPU path with 2 denoising steps. It does not use the multi-GPU pipeline.
/data, and later runners reuse the files.
The app downloads the upstream code as an archive of one pinned commit, so the runner does not need git.
The App
View the complete source code on GitHub. This page shows the parts that use the WMA SDK.Messages
The client sends oneprompt message. The app sends status and rejected messages.
The App Class
The class sets the runner, the pinned requirements, and a longstartup_timeout for the weight download.
The max_multiplexing value of 1 gives each session the whole GPU.
max_concurrency and max_multiplexing values are example limits. Adjust them for your workload and cost.
The Contract
The contract declares a required camera track, a square output track, and example prompts.The Prompt Handler
The handler validates the text and stores it. The model thread restarts the stream when the prompt changes.The Session Backend
Thecreate_backend method attaches the output track when the camera track arrives.
It also ends the session after MAX_SESSION_SECONDS.
The lines that build the ICE servers (the RunnerIceConfig call) are boilerplate. Copy them as they are.
How the App Works
Pinned Upstream Code
The upstream repository is not a pip package at this commit, so the app downloads the source archive and adds it tosys.path.
The commit and both weight revisions are constants at the top of the file.
The upstream code reads the STREAMDIFFUSIONV2_ROOT environment variable to find wan_models/ and ckpts/.
The app sets this variable before it imports the pipeline. All heavy imports run inside prepare_runtime, so the file imports on a computer without PyTorch.
The app calls the upstream SingleGPUInferencePipeline directly, as the upstream web demo does.
The StreamDiffusionV2Pipeline class in the README reads a complete video file, so it does not fit live input.
setup() also runs a short warm-up stream, so the first session does not pay for kernel compilation.
Frame Buffering and Dropping
Latency stays bounded because every buffer in the app has a fixed size.read_camerareads every incoming frame and keeps only the newest one, so the camera track queue never grows.sample_cameracopies the newest frame into an input buffer 16 times each second. This converts any camera rate to the model rate.- The input buffer holds at most 8 frames. If the model falls behind, the app drops the oldest frames and counts them in
dropped_input_frames. - The model thread takes 4 frames for each step. A new stream takes the 5 newest frames, because the upstream pipeline needs
1 + 4frames to start. - The output queue holds at most 16 frames. The app drops the oldest frame when the queue is full, with
queue_put_drop_oldest, and counts it as undelivered.
make_video_queue_track paces the output at 16 frames per second.
With rebase_after_stall=True, the track rebases its timestamps after a stall, such as a prompt restart. This prevents a burst of late frames.
Model Thread
The model call blocks, so it runs on its own thread and never on the event loop. The thread callsloop.call_soon_threadsafe to give frames to the output queue.
The shutdown function is a deferred cleanup. It stops the thread and waits for the thread to end before the session closes.
The cap_billing function runs after shutdown and before billing is final.
The app sets max_multiplexing = 1 because the model keeps one stream state, and a lock protects it.
Contract and Client Controls
Therealtime_contract describes the session for the playground and for client code.
The app does not use an interaction profile, because it has one optional text control and a default prompt.
To track each prompt through events, add
ConfiguredSession, as the WMA reference describes.
Run and Deploy
From the root of the cloned repository, usefal run as the development loop. It starts the app on an ephemeral runner:
Cost and First Start
You pay per second for the runner, including setup time and thekeep_alive time (the idle seconds a runner stays up after its last session). See Pricing. The billable units in the app matter only if you list the app on the fal Marketplace. Callers then pay for these units at a price that the fal team sets.
The first session starts a runner. The runner downloads about 23 GB of weights and compiles the model. On a fresh runner this took about 7 minutes in our tests.
Watch the runner logs in the dashboard. The /data volume keeps the weights across runners. See persistent storage.
The default min_concurrency = 0 scales the app to zero, so the first session waits for a cold start. To keep a runner warm, set min_concurrency = 1. A warm runner costs idle time. See cold starts.
Clean Up
To stop paying for the app, scale it to zero withfal apps scale <app-name> --min-concurrency 0. To remove it, run fal apps delete <app-name>.
The idle runner timeout (keep_alive) is 300 seconds. A runner stays up for that time after its last session.
The maximum session length is also 300 seconds (MAX_SESSION_SECONDS). Change the constant to change the limit.
Try It
Playground
- Open the deployed app in the fal dashboard.
- Select its
/start-sessionplayground. - Select Connect and allow the camera.
- Wait for the restyled video.
- Send a prompt example, such as Claymation.
- Read the
statusmessages in the message log. Thestatefield reportsrestartingwhile the app applies the new prompt. - Select Disconnect to end the session.
Browser Client
The experimental WMA browser client sends the camera and the prompt. There is no Python or Node client today. The deploy output shows the app ID asyour-account/<app-name>. The name comes from the class name, so RestyleApp becomes restyle-app.
Install the client and the server proxy in your web application. Set up the proxy route as the browser client page describes.
onData callback receives the raw JSON string. Parse it before you use it.
On a closed or failed session, the code stops the camera and calls onEnded. Use that call to show that the session ended.
A failure reaches both onError and onState. The ended flag makes the finish function run once.
A session can end on its own. The app closes each session after 300 seconds, and after a generation failure it sends a rejected message with the reason “generation failed” and closes the session.
Server Messages
The
state field of status is waiting_for_camera, running, or restarting.
If the Session Does Not Start
Use Your Own Model
The model is the pipeline thatprepare_runtime loads. The model thread calls start_stream_session and run_stream_batch.
The rest is the same WMA shape for any model:
- A model thread takes a fixed number of input frames and returns output frames.
- The thread puts each frame in a bounded queue, with the
put_outputmethod. The queue drops the oldest frame when it is full. make_video_queue_trackplays the queue as the video track, andAiortcPeersends it to the client.
read_camera method shows how camera frames arrive. It keeps only the newest frame, so the camera queue never grows.
put_output method queues one output frame.
Billing
This section applies if you list the app on the fal Marketplace. For your own private app, you pay only for runner time, and the units do not change the cost. The app callssession.add_billable_units with the number of frames that it queues for playback.
One unit is one output frame. The warm-up in setup() does not count.
Some frames never play. The app drops the oldest frame when the output queue is full, and some frames remain queued when the session ends.
Before billing is final, a deferred cleanup calls session.cap_billable_units(total - undelivered). The argument is the new total, so the undelivered frames are not billed.
The fal team configures the price per unit when it lists the app. See billing in the WMA reference.
Limits
- Speed. On one H100 in our tests, the app streamed 512x512 at the full 16 fps. The median frame gap was about 63 ms. The 95th percentile gap was about 110 to 130 ms across two runs.
- Pauses. A prompt change restarts the model stream and pauses output for under half a second. Read
output_framesanddropped_input_framesin thestatusmessages to measure your own session. - Slow model, higher delay. If the model makes fewer than 16 frames per second, the app drops input frames. The video looks less smooth, but the delay stays bounded.
- Style strength depends on the prompt. The
NOISE_SCALEvalue of 0.85 keeps faces and motion. In our tests, faces started to change at 0.9, and identity was lost at 0.95. More denoising steps (3 or 4) did not look better than 2. - One session for each runner. Each session owns the GPU. More sessions need more runners, and
max_concurrencylimits the count. - Square output. The app crops the center of the camera frame and outputs 512 by 512. This is the size the upstream web demo uses. The upstream offline default is 480 by 832.
- No acceleration options. The app does not enable the upstream TAEHV decoder, TensorRT, or flash-attn. Read the upstream README before you add them.
- No content moderation. The app does not check camera frames or prompts. Add checks before you serve public users.