Skip to content
Nap Joseph Calub

A live-translation stack for Favor Church

Our church runs live Tagalog translation of English sermons through spf.io. I built the missing middle: a proxy that intercepts captions, an LLM that translates them into conversational Taglish with session context, and a live viewer — deployed where the congregation can reach it.

Deno / TypeScript / Qwen2.5-72B via DeepInfra / Supabase (Postgres) / Material Design 3 / Deno Deploy


Context

Favor Church preaches in English and translates live into Tagalog over spf.io, which streams captions to YouTube. That works, but three things were missing: the translation arriving on YouTube is machine-literal and hard to read; once captions scroll past, there is no record of what was actually shown; and attendees who wanted to follow along on their phones had nothing designed for that.

Constraints

  • The AV team's workflow could not change. spf.io and YouTube were fixed endpoints — whatever I built had to live between them, invisibly.
  • Live means fast enough: translation latency has to keep up with a preacher who does not pause for GPUs.
  • Volunteer budget: zero pesos, zero ops staff. It had to run itself on a free tier.
  • Translation quality is not a prompt problem alone — sermons reference people, places, and church-specific terms that must never be translated.

Decisions

Be the proxy. Instead of integrating with spf.io's API, the service simply poses as YouTube's caption-upload endpoint. spf.io posts captions to it; it forwards them upstream and keeps a copy. Zero workflow change, zero permission negotiation, and it degrades safely — if the proxy dies, the original path still works.

Translate with context, not per-caption. Captions arrive as fragments; a naive per-line translation loses pronouns and terminology. The service buffers a rolling session context and sends it along with each fragment, so the model (Qwen2.5-72B via DeepInfra) keeps references coherent across a whole sermon.

Put terminology in YAML, not in the prompt. Names, "Favor Church", and preferred renderings live in a reviewable, diffable config — no-translate lists and preferred-term maps — rather than buried in a prompt string. The config is the review surface for translation quality.

Persist everything, deliberately. Original and translated captions land in Supabase under a documented SQL schema with uniqueness constraints, so the service is replayable and auditable after the fact.

Pragmatic platform pivot. Server-sent events were the natural transport for the live viewer — and they fought Deno Deploy. Rather than heroics, I swapped the viewer to polling and moved on; the commit that does it says so plainly. The deployment constraint won, and that was the right call for a system nobody is paid to maintain.

What shipped

A complete pipeline in 892 lines of TypeScript across 34 commits of genuine iteration — KV-store availability fixes, encoding fixes, the SSE-to-polling pivot, context-window buffering, terminology pre-mapping — deployed on Deno Deploy, with a Material Design 3 live-caption viewer for the congregation and a branded landing page that embeds the live channel. It has run during actual services, which is the only acceptance test that matters.

What I'd do differently

Translation quality is currently judged by humans in the room. I'd add a replay harness — sample past services, re-translate, diff against what was shown — so terminology and context changes could be evaluated offline before Sunday. And the viewer deserves the streaming transport back, behind an abstraction that makes the polling fallback just another implementation.