---
title: "SafeStream: UGC Video Moderation as One Row, Two Signals"
date: "2026-09-09"
author: "Pierre Brunelle"
tags:
  - Video
  - Moderation
  - UGC
  - Whisper
  - TableModel
  - Multimodal AI
  - Pixeltable Cloud
description: "Extract audio, transcribe, flag the transcript, sample frames, then fold both into a risk level. Pixeltable is the video graph — not a fake NSFW API plus a queue."
url: "https://www.pixeltable.com/blog/safestream-ugc-video-moderation"
---

# SafeStream: UGC Video Moderation as One Row, Two Signals

**Summary:** SafeStream is the UGC gate: an upload is one row. Audio comes off the file. Whisper writes a transcript. Gemini (or a small UDF) flags language. Frames at 1 fps get visual labels. A parent UDF folds both into `risk_level`. You do not buy a mystery NSFW API and a review queue as two vendors. You declare the graph. `pip install pixeltable`.

 
## The Product

 

 - Ingest a creator clip with `upload_id`

 - Speech path: extract audio → transcribe → flag the text

 - Vision path: 1 fps frames with a short visual label

 - One parent column: `ok` / `review` / `high`

 

 
The kitchen-sink tutorial is [video intelligence](/blog/video-intelligence-pipeline-tutorial) — frames plus audio, not a risk rollup. If you need a real ordered aggregate across frames, that pattern is [PixelGolf UDA aggregation](/blog/one-swing-many-frames-pixelgolf-uda-aggregation). This post’s fold is a UDF on the parent, not a fake classifier name.

 
## The Stitch You Delete

 
Object store. A speech job. A vision job. A spreadsheet of scores you reconcile in ops. Pixeltable is the video, the transcript, the frame labels, and the fold. Insert the MP4. The gate follows.

 
## The Receipt

 
```python
import pixeltable as pxt
from pixeltable.functions import gemini, whisper
from pixeltable.functions.video import frame_iterator
from pixeltable.serving import FastAPIRouter

TableModel = pxt.model_base()

AUDIO_FLAG = (
 'Flag policy-relevant speech. Reply with one line: ok, review, or high, '
 'then a short reason. Do not invent a classifier name.'
)
VISUAL_FLAG = (
 'Describe visible policy-relevant content in one sentence, then ok, review, or high.'
)

@pxt.udf
def combine_signals(transcript_flags: str, visual_summary: str) -> str:
 blob = f'{transcript_flags} {visual_summary}'.lower()
 if 'high' in blob:
 return 'high'
 if 'review' in blob:
 return 'review'
 return 'ok'

class Videos(TableModel, name='videos'):
 upload_id: pxt.String
 video: pxt.Video

 audio = video.extract_audio()
 transcript = whisper.transcribe(audio, model='base')
 transcript_flags = gemini.generate_content(
 [transcript, AUDIO_FLAG],
 model='gemini-2.5-flash',
 )
 visual_summary = gemini.generate_content(
 [video, VISUAL_FLAG],
 model='gemini-2.5-flash',
 )
 risk_level = combine_signals(transcript_flags, visual_summary)

class Frames(
 TableModel,
 name='frames',
 base=Videos,
 iterator=frame_iterator(video=Videos.video, fps=1),
):
 visual_label = gemini.generate_content(
 [frame, 'One-sentence visual label for review.'],
 model='gemini-2.5-flash',
 )

@pxt.query
def review_queue():
 return (
 Videos.where(Videos.risk_level != 'ok')
 .select(Videos.upload_id, Videos.risk_level, Videos.transcript_flags, Videos.visual_summary)
 )

api = FastAPIRouter(name='safestream')
api.add_insert_route(Videos, path='/uploads', inputs=[Videos.video, Videos.upload_id])
api.add_query_route(path='/queue', query=review_queue)
```

 
`risk_level` is the parent fold: transcript flags plus a video-level visual summary. `Frames` is the audit trail — one label per sampled second — not a second vendor. A UDF that greps `high` / `review` is honest about what it is. Ordered sport metrics across frames are PixelGolf. Same `app.py` on Cloud: db → schema → service.

 
## What This Is Not

 
Not a hosted trust-and-safety suite. Not a named NSFW model we do not ship. Not legal advice. Your policy text goes in the prompts; the catalog stores the evidence. Human review still exists — that is the `review` bucket.

 
## People Also Ask

 
**Why not aggregate frame labels into the parent?** Cross-table rollup is the PixelGolf post. SafeStream keeps the fold as a UDF on signals the parent already has, plus a view you can inspect.

 
**Can I swap Gemini for a local UDF?** Yes. Keep the column names. Change the function.

 
**How do I go to Cloud?** Same file. `PIXELTABLE_API_KEY`, `pxt://org:db`, then `pxt db update` → `pxt schema update` → `pxt service update`.

 
**Is this an AI automation workflow?** Yes. Insert the upload; transcript and frame labels run as columns. The category is [AI automation workflow](/blog/ai-automation-workflow).

 
## Two Signals, One Gate

 
Declare the upload. Extract audio. Label frames. Fold the risk. Keep the evidence. Delete the mystery API.

 

 - **[Pixeltable on GitHub](https://github.com/pixeltable/pixeltable)**: `pip install pixeltable`

 - **[ClipFinder](/blog/clipfinder-semantic-video-moment-search)** — NL video moments

 - **[CallSense](/blog/callsense-sales-call-intelligence)** — sales-call auditor

 - **[DocuVision](/blog/docuvision-pdf-chart-qa)** — PDF Q&A

 - **[SnapCatalog](/blog/snapcatalog-product-visual-search)** — shop-the-look

 - **[ClaimBot](/blog/claimbot-multimodal-fnol-triage)** — claims triage

 - **[PixelGolf UDA aggregation](/blog/one-swing-many-frames-pixelgolf-uda-aggregation)** — when the fold must be an ordered aggregate

 - **[Pixeltable documentation](https://docs.pixeltable.com)**