---
title: "ReelForge: Podcast Chapters and a Highlight Clip in One app.py"
date: "2026-09-17"
dateModified: "2026-09-17"
author: "Pierre Brunelle"
tags:
  - Podcast
  - Video
  - Whisper
  - TableModel
  - Multimodal AI
  - Pixeltable Cloud
  - Clipping
description: "Ingest MP4 or WAV, transcribe, chapter the episode, cut a highlight. Pixeltable is the episode row — not FFmpeg plus a transcription fleet plus a vector DB."
url: "https://www.pixeltable.com/blog/reelforge-podcast-chaptering-viral-clips"
---

# ReelForge: Podcast Chapters and a Highlight Clip in One app.py

**Summary:** ReelForge is the show-notes loop: drop a video episode or a WAV, get a transcript, chapter titles, a hook, and a highlight MP4 with a thumbnail. You do not stand up Whisper workers, an FFmpeg box, and a search index as three products. You declare `Podcasts`. Insert the file. The rest follows. `pip install pixeltable`.

 
## The Product

 

 - Ingest an episode with `episode_id`, show, title, guest

 - Accept an MP4, a WAV, or both

 - Transcribe, chapter, pick a highlight window

 - Search past episodes by what was said

 

 
Natural-language *visual* moments are [ClipFinder](/blog/clipfinder-semantic-video-moment-search). The sales-call transcript object is [CallSense](/blog/callsense-sales-call-intelligence). This post is the episode: audio resolution, chapters, a clipped file.

 
## The Stitch You Delete

 
Object store for the master. A job that calls Whisper. Another job that asks an LLM for chapters. FFmpeg on a worker for the 45-second cut. Embeddings in a sidecar so last week’s show is searchable. Pixeltable is the row those joints were syncing by hand. That is the [Frankenstein stack](/blog/deconstructing-ai-frankenstein-stack).

 
## The Receipt

 
```python
import json
import pixeltable as pxt
from pixeltable.catalog.model import Column, EmbeddingIndex
from pixeltable.functions import gemini, whisper
from pixeltable.functions.huggingface import sentence_transformer
from pixeltable.functions.video import clip, extract_audio, extract_frame
from pixeltable.serving import FastAPIRouter
from pixeltable.type_system import AudioType, VideoType

TableModel = pxt.model_base()
text_embed = sentence_transformer.using(model_id='all-MiniLM-L6-v2')

CHAPTER_PROMPT = (
 'Return JSON only: chapters (list of title, start, end in seconds), '
 'highlight_start, highlight_end, highlight_hook (one sentence).'
)

@pxt.udf
def pick_audio(from_video, file):
 return file if file is not None else from_video

@pxt.udf
def parse_chapters(raw: str) -> dict:
 try:
 return json.loads(raw)
 except json.JSONDecodeError:
 return {
 'chapters': [],
 'highlight_start': 0.0,
 'highlight_end': 30.0,
 'highlight_hook': raw,
 }

@pxt.udf
def highlight_start(chapters: dict) -> float:
 return float(chapters.get('highlight_start') or 0)

@pxt.udf
def highlight_end(chapters: dict) -> float:
 return float(chapters.get('highlight_end') or 30)

class Podcasts(TableModel, name='podcasts'):
 episode_id = Column(pxt.String, primary_key=True)
 podcast_name: pxt.String
 title: pxt.String
 guest: pxt.String
 video = Column(VideoType(nullable=True))
 audio_file = Column(AudioType(nullable=True))

 extracted_audio = extract_audio(video)
 audio = pick_audio(extracted_audio, audio_file)
 transcript = whisper.transcribe(audio, model='base').text.astype(pxt.String)
 chapters_raw = gemini.generate_content(
 [transcript, CHAPTER_PROMPT],
 model='gemini-2.5-flash',
 )
 chapters = parse_chapters(chapters_raw)
 start = highlight_start(chapters)
 end = highlight_end(chapters)
 highlight_clip = clip(video, start_time=start, end_time=end)
 highlight_thumbnail = extract_frame(video, timestamp=start)

 __indexes__ = [
 EmbeddingIndex(column=transcript, embedding=text_embed, name='rf_transcript_idx'),
 ]

@pxt.query
def search_episodes(query: str, top_k: int = 8):
 sim = Podcasts.transcript.similarity(string=query)
 return (
 Podcasts.order_by(sim, asc=False)
 .limit(top_k)
 .select(
 Podcasts.episode_id,
 Podcasts.title,
 Podcasts.chapters,
 Podcasts.highlight_clip,
 Podcasts.highlight_thumbnail,
 )
 )

api = FastAPIRouter(name='reelforge')
api.add_insert_route(
 Podcasts,
 path='/episodes',
 inputs=[
 Podcasts.video,
 Podcasts.audio_file,
 Podcasts.episode_id,
 Podcasts.podcast_name,
 Podcasts.title,
 Podcasts.guest,
 ],
)
api.add_query_route(path='/search', query=search_episodes)
```

 
Gemini returns JSON. `parse_chapters` is the object you query — not a shipped virality model. `clip` and `extract_frame` follow the highlight window. Audio-only rows still transcribe; the cut is empty without a video. Same file on Cloud: `pxt db update` → `pxt schema update` → `pxt service update`.

 
## What This Is Not

 
Not Descript. Not a TikTok scheduler. Auth and the CMS stay where they are. Frame-level visual search is ClipFinder. This is chapters plus one cut on the parent row.

 
## People Also Ask

 
**Video or audio?** Either column can be null. `pick_audio` prefers the WAV when you have one.

 
**Which Whisper?** `model='base'` matches the homepage. Swap the string; the column stays.

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

 
**Is this an AI automation workflow?** Yes. Insert the master; Whisper, chapters, and the cut run as columns. The category is [AI automation workflow](/blog/ai-automation-workflow).

 
## Chapter the Episode, Cut the Hook

 
Declare the show. Apply the file. Insert the master. Search the library. Keep the clip. Delete the FFmpeg farm.

 

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

 - **[MedDossier](/blog/meddossier-clinical-intake-triage)** — clinical intake row

 - **[LectureSync](/blog/lecturesync-slide-lecture-qa)** — slides + lecture video

 - **[AdRadar](/blog/adradar-creative-fatigue-visual-search)** — creative fatigue

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

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

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