Run on Cloud · backend
Audio transcription
Upload audio or podcasts, transcribe with Whisper, and search sentences semantically.
Scaffold locally
uvx pixeltable-new --template audio-transcription my-audio-transcriptionSame starter-kit files Cloud uses. Local UI (static HTML in some templates) is for uvx, not for Cloud. Cloud deploys schema + insert routes via pxt serve.
Cloud routes
- insert
/insert→ audiointel/audio
schema.py
"""Audio & Podcast Intelligence -- self-hosted transcription, summarization, and semantic search."""
import os
import pixeltable as pxt
from pixeltable.functions.audio import audio_splitter
from pixeltable.functions.huggingface import sentence_transformer
from pixeltable.functions.string import string_splitter
from pixeltable.functions.uuid import uuid7
HAVE_OPENAI = bool(os.environ.get("OPENAI_API_KEY"))
if HAVE_OPENAI:
from pixeltable.functions import openai
EMBED_FN = sentence_transformer.using(model_id="all-MiniLM-L6-v2")
# ---------------------------------------------------------------------------
# Namespace
# ---------------------------------------------------------------------------
pxt.create_dir("audiointel", if_exists="ignore")
# ---------------------------------------------------------------------------
# Base table -- one row per audio file
# ---------------------------------------------------------------------------
audio_files = pxt.create_table(
"audiointel.audio_files",
{
"audio": pxt.Audio,
"title": pxt.String,
"source": pxt.String,
"uuid": uuid7(),
"timestamp": pxt.Timestamp,
},
primary_key=["uuid"],
if_exists="ignore",
)
# ---------------------------------------------------------------------------
# Audio chunking -- 30-second segments with 5s overlap
# ---------------------------------------------------------------------------
chunks = pxt.create_view(
"audiointel.chunks",
audio_files,
iterator=audio_splitter(audio_files.audio, duration=30.0, overlap=5.0),
if_exists="ignore",
)
# ---------------------------------------------------------------------------
# Transcription (OpenAI Whisper API or local whisper)
# ---------------------------------------------------------------------------
if HAVE_OPENAI:
chunks.add_computed_column(
transcription=openai.transcriptions(chunks.audio_segment, model="whisper-1"),
if_exists="ignore",
)
chunks.add_computed_column(
transcript_text=chunks.transcription.text.astype(pxt.String),
if_exists="ignore",
)
# Local alternative:
# from pixeltable.functions.whisper import transcribe as whisper_transcribe
# chunks.add_computed_column(
# transcription=whisper_transcribe(chunks.audio_segment, model='base.en'),
# if_exists='ignore',
# )
# chunks.add_computed_column(transcript_text=chunks.transcription['text'].astype(pxt.String), if_exists='ignore')
# ---------------------------------------------------------------------------
# Sentence splitting on transcript text
# ---------------------------------------------------------------------------
if HAVE_OPENAI:
sentences = pxt.create_view(
"audiointel.sentences",
chunks,
iterator=string_splitter(chunks.transcript_text, separators="sentence"),
if_exists="ignore",
)
sentences.add_embedding_index(
"text",
idx_name="sentences_text_idx",
string_embed=EMBED_FN,
if_exists="ignore",
)
# ---------------------------------------------------------------------------
# Per-chunk summary (LLM)
# ---------------------------------------------------------------------------
if HAVE_OPENAI:
chunks.add_computed_column(
summary=openai.chat_completions(
messages=[
{
"role": "system",
"content": (
"You are a concise summarizer. Summarize the following audio transcript chunk "
"in 2-3 sentences. Focus on key points, decisions, and action items."
),
},
{"role": "user", "content": chunks.transcript_text},
],
model="gpt-4.1-mini",
),
if_exists="ignore",
)
chunks.add_computed_column(
summary_text=chunks.summary.choices[0].message.content.astype(pxt.String),
if_exists="ignore",
)
# ---------------------------------------------------------------------------
# Query functions
# ---------------------------------------------------------------------------
@pxt.query
def list_recordings():
"""List all audio files with metadata."""
return audio_files.select(audio_files.title, audio_files.source, audio_files.timestamp, audio_files.uuid)
# These require OPENAI_API_KEY (the views/columns they reference are created above)
if HAVE_OPENAI:
@pxt.query
def search_transcripts(query_text: str, limit: int = 10):
"""Semantic search across all transcripts."""
sim = sentences.text.similarity(string=query_text)
return sentences.order_by(sim, asc=False).limit(limit).select(sentences.text, score=sim)
@pxt.query
def search_in_recording(recording_title: str, query_text: str, limit: int = 10):
"""Semantic search within a specific recording."""
sim = sentences.text.similarity(string=query_text)
return (
sentences.where(sentences.title == recording_title)
.order_by(sim, asc=False)
.limit(limit)
.select(sentences.text, score=sim)
)
@pxt.query
def get_transcript(recording_title: str):
"""Full transcript of a recording, ordered by segment start time."""
return (
chunks.where(chunks.title == recording_title)
.order_by(chunks.segment_start)
.select(chunks.transcript_text, chunks.segment_start, chunks.segment_end)
)
@pxt.query
def get_summary(recording_title: str):
"""Per-chunk summaries for a recording, ordered by segment start time."""
return (
chunks.where(chunks.title == recording_title)
.order_by(chunks.segment_start)
.select(chunks.summary_text, chunks.segment_start, chunks.segment_end)
)
if __name__ == "__main__":
print("Schema initialized. Run: python app.py")
README
Audio & Podcast Intelligence
Ingest audio files, automatically transcribe, chunk, summarize, and search across recordings. Your own Otter.ai, self-hosted.
What it replaces
| Service | Cost |
|---|---|
| Otter.ai | $20/mo per user |
| Descript | $24/mo per user |
| AssemblyAI | $0.37/min |
| audio-transcription | Free + your compute |
Use cases
- Meeting intelligence -- transcribe and search across every meeting recording
- Podcast analytics -- find every mention of a topic across hundreds of episodes
- Call center QA -- search agent calls for compliance keywords in seconds
- Compliance search -- full-text + semantic search over recorded conversations
Quickstart
uv sync # install deps
OPENAI_API_KEY=sk-... uv run python app.py
# Open http://localhost:8000
That's it. app.py initializes the schema and starts the server with the web UI.
To use local Whisper instead of the OpenAI API, install with uv sync --extra local and uncomment the local whisper block in schema.py.
API-only mode (no UI)
OPENAI_API_KEY=sk-... uv run python schema.py
OPENAI_API_KEY=sk-... uv run pxt serve audiointel
Do not run both pxt serve and app.py at the same time -- they bind to the same port.
What Pixeltable handles
All of the following run automatically when you insert a row -- zero glue code:
- Audio splitting -- 30-second segments with 5s overlap via
audio_splitter - Transcription -- OpenAI Whisper API (or local
whisper.transcribe) - Sentence chunking -- spaCy sentence segmentation via
string_splitter - Embedding --
all-MiniLM-L6-v2sentence embeddings, computed on insert - Indexing -- HNSW vector index for sub-second semantic search
- Summarization -- per-chunk LLM summaries via
chat_completions
API routes
| Method | Path | Description |
|---|---|---|
POST |
/api/upload |
Upload and ingest an audio file (background job) |
GET |
/api/recordings |
List all recordings |
POST |
/api/search |
Semantic search across all transcripts (needs OPENAI_API_KEY) |
POST |
/api/search-in |
Search within one recording (needs OPENAI_API_KEY) |
GET |
/api/transcript |
Full transcript for a recording (needs OPENAI_API_KEY) |
GET |
/api/summary |
Per-chunk summaries for a recording (needs OPENAI_API_KEY) |
Architecture
audio file
└─ audio_files table (audio, title, source, uuid, timestamp)
└─ chunks view (30s segments via audio_splitter)
├─ transcription (openai.transcriptions)
├─ summary (chat_completions)
└─ sentences view (string_splitter)
└─ embedding index (all-MiniLM-L6-v2)
Files
audio-transcription/
├── schema.py Tables, views, indexes, computed columns, query functions
├── functions.py UDFs (generate_full_summary)
├── app.py FastAPI server — API + web UI
├── static/
│ └── index.html Frontend (Tailwind CSS, vanilla JS)
├── pyproject.toml Dependencies + pxt serve routes (API-only alternative)
└── README.md