Per-participant moderation
Review every participant separately, so a finding points at a person rather than a composite.
Live social and creator platforms that have to answer "who did this, and when" — for trust and safety, for a takedown request, or for a regulator. This is the one pipeline here that deliberately never mixes.
livekit room live-show
- ari
- mei
- luca
n:n no mixer — one output per participant
-
image → S3
640×360 jpeg per participant
every 10s
-
websocket → your service
16 kHz mono PCM, one socket each
continuous
The pipeline
Sources
- livekit
Nodes
- audio_resample (16 kHz mono)
Sinks
- image → S3
- websocket
livekit(room) ─┬→ image ────────────────────────→ S3 (one jpeg per participant)
└→ audio_resample → websocket ────→ your service (one socket per participant) What makes it work
The parts that are not obvious from the JSON — usually because the naive approach does not do what you would expect.
These are the only two sinks that do not force a mixer
image and websocket are n:n — they keep one output per upstream stream. Every other sink is 1:1, so feeding it a room means composing first and losing track of who is who. segment is 1:1 on purpose: it would otherwise run a full encoder per participant.
Per-participant audio cannot go through an encoder
An audio_encoder is 1:1, so livekit → audio_encoder is rejected at submit time with a hint to insert a mixer — which is exactly what you are trying to avoid. Per-participant audio therefore leaves as PCM. audio_resample is the one n:n audio node, so it is what stands between the room and the socket.
Resampling is a cost decision, not just a format one
PCM is billed as egress. At 48 kHz stereo each participant is ~1.5 Mbit/s; at 16 kHz mono it is ~0.26 Mbit/s — about 6× cheaper, and already the format speech models want. For a ten-person room that difference is most of the bill.
Attribution comes from the object key
The pathPrefix is templated, so moderation/live-show/{identity} gives every participant their own folder and a finding does not depend on parsing a filename.
The image sink has no webhook
It uploads to s3, gcp, or azure and nothing else, so the loop is AVFlow → bucket → your bucket event → your service. With intervalSec at 10 this is sampling for evidence and audit, not a real-time gate. The audio socket is the low-latency half.
Neither sink carries captions
Routing an asr node into image is rejected — it does not support subtitles. Transcript-based moderation means running speech-to-text on the PCM you receive, or adding a separate asr → livekit path for data events.
The Job
Replace tokens, URLs, and storage credentials, then submit it. This is the same file /examples/25-moderation-per-participant.json serves.
{
"name": "moderation-review",
"sources": [
{
"name": "room",
"type": "livekit",
"config": {
"serverUrl": "wss://your-project.livekit.cloud",
"token": "<subscribe-token>",
"select": {
"mediaTypes": [
"audio",
"video"
]
}
}
}
],
"nodes": [
{
"name": "for_review",
"type": "audio_resample",
"inputs": [
{
"name": "room",
"select": {
"mediaTypes": [
"audio"
]
}
}
],
"config": {
"sampleRate": 16000,
"channels": 1
}
}
],
"sinks": [
{
"name": "frames",
"type": "image",
"inputs": [
{
"name": "room",
"select": {
"mediaTypes": [
"video"
]
}
}
],
"config": {
"storageType": "s3",
"storageConfig": {
"bucket": "your-bucket",
"region": "us-east-1",
"accessKeyId": "<access-key-id>",
"secretAccessKey": "<secret-access-key>",
"pathPrefix": "moderation/live-show/{identity}"
},
"format": "jpeg",
"intervalSec": 10,
"quality": 80,
"width": 640,
"height": 360
}
},
{
"name": "audio_review",
"type": "websocket",
"inputs": [
"for_review"
],
"config": {
"url": "wss://moderation.your-app.example.com/audio",
"headers": {
"Authorization": "Bearer <moderation-service-token>"
}
}
}
],
"policies": {
"maxDurationSec": 14400,
"idleTimeoutSec": 300
}
} Submit it
curl -X POST "https://api.avflow.dev/v1/jobs" \
-H "Authorization: Bearer ${AVFLOW_API_KEY}" \
-H "Content-Type: application/json" \
-d @25-moderation-per-participant.json
# check status
curl "https://api.avflow.dev/v1/jobs/moderation-review" \
-H "Authorization: Bearer ${AVFLOW_API_KEY}"
# stop
curl -X DELETE "https://api.avflow.dev/v1/jobs/moderation-review" \
-H "Authorization: Bearer ${AVFLOW_API_KEY}"