Grace Community Church
Hội Thánh Tin Lành Ân Điển
Systems Map

Sermon Pipeline

Architecture Reference

A serverless weekly pipeline that detects each Sunday's sermon livestream, finds where the sermon starts and ends inside the full service, trims it losslessly, and publishes the clip to YouTube, Google Drive, and an email list. No VPS app, no database, no always-on process. State is per-sermon JSON committed to the repo; git history is the audit log. Every irreversible step waits behind a one-comment human gate.

Data flow

Services and the data between them

Read left to right. The week runs in three phases, split by the two approval gates (/approve, /publish) where a reviewer signs off before anything irreversible happens. Inside each phase the Runner on the church Mac is the hub, calling out to the boxed services beside it; every numbered arrow is one flow of data, keyed to the legend below. The Runner lives on a residential IP because YouTube blocks video downloads from datacenter addresses. Follow the numbers 1 to 15 in order.

PHASE 1 · DETECT AUTOMATED · EVERY MONDAY PHASE 2 · PROCESS AFTER /APPROVE PHASE 3 · PUBLISH AFTER /PUBLISH 1 2 3 4 4b 5 6 7 8 9 10 11 12 13 14 15 /approve /publish GITHUB GitHub Actions weekly cron · Mon 03:00 CHURCH MAC Runner detect job yt-dlp · captions The Runner is the hub. It sits on a residential IP so YouTube allows video downloads. GOOGLE YouTube find livestream + captions CHURCH vietgrace.org · Drupal sermon title + passages ANTHROPIC Claude AI boundary detection LOCAL · FALLBACK Whisper transcript, no-caption weeks GITHUB Repo + state/ write record + open Issue TELEGRAM Telegram → Reviewers review link to Abraham / John CHURCH MAC Runner process job download · trim · upload GOOGLE YouTube download sermon span LOCAL ffmpeg lossless trim + level GOOGLE YouTube upload UNLISTED (OAuth) GOOGLE Google Drive store mp3 + video GITHUB GitHub Actions publish.yml hosted runner API only, no video GOOGLE YouTube public + playlist ANTHROPIC Claude sermon summary (VN + EN) GMAIL SMTP → VietChristian mp3 email (publisher) GMAIL SMTP → Congregation sermon email CHURCH VPS sermons.vietgrace.org table + sermon pages
Legend · each number is one data flowdatahuman control (approval)
  1. 1GitHub Actions RunnerMonday 03:00 cron starts the detect job
  2. 2Runner YouTubefind the Sunday livestream (Data API) and pull captions (yt-dlp)
  3. 3Drupal Runnersermon title + scripture passages from vietgrace.org
  4. 4Runner Claudesend transcript, get sermon start / end + confidence
  5. 4bRunner Whisperlocal transcript, only when YouTube has no captions
  6. 5Runner Repo + state/write the sermon record and open the review Issue
  7. 6Runner Telegram Reviewerssend the review link
  8. /approve · gatea reviewer accepts the detected boundaries before any processing
  9. 7YouTube Runnerdownload just the sermon span with yt-dlp
  10. 8Runner ffmpeglossless trim + loudness level, local, no external call
  11. 9Runner YouTubeupload the sermon UNLISTED via OAuth
  12. 10Runner Google Drivestore the mp3 + video
  13. /publish · gatea reviewer approves going public after the unlisted preview looks right
  14. 11GitHub Actions YouTubeflip to public + add to the Sermons playlist
  15. 12GitHub Actions Claudegenerate the bilingual (VN + EN) summary shown on the sermon page
  16. 13GitHub Actions Gmail VietChristiansend the mp3 email to the publisher
  17. 14GitHub Actions Gmail Congregationsend the sermon email to the list
  18. 15GitHub Actions sermons.vietgrace.orgrsync the rebuilt table + sermon pages
  1. P1Phase 1 · Detectautomated, every Monday
  2. 1GitHub Actions → RunnerMonday 03:00 cron starts the detect job
  3. 2Runner ↔ YouTubefind the Sunday livestream (Data API) and pull captions (yt-dlp)
  4. 3Drupal → Runnersermon title + scripture passages from vietgrace.org
  5. 4Runner ↔ Claudesend transcript, get sermon start / end + confidence
  6. 4bRunner ↔ Whisperlocal transcript, only when YouTube has no captions
  7. 5Runner → Repo + state/write the sermon record and open the review Issue
  8. 6Runner → Telegram → Reviewerssend the review link
  9. /approve · gatea reviewer accepts the detected boundaries before any processing
  10. P2Phase 2 · Processafter /approve
  11. 7YouTube → Runnerdownload just the sermon span with yt-dlp
  12. 8Runner ↔ ffmpeglossless trim + loudness level, local, no external call
  13. 9Runner → YouTubeupload the sermon UNLISTED via OAuth
  14. 10Runner → Google Drivestore the mp3 + video
  15. /publish · gatea reviewer approves going public after the unlisted preview looks right
  16. P3Phase 3 · Publishafter /publish
  17. 11GitHub Actions → YouTubeflip to public + add to the Sermons playlist
  18. 12GitHub Actions → Claudegenerate the bilingual (VN + EN) summary shown on the sermon page
  19. 13GitHub Actions → Gmail → VietChristiansend the mp3 email to the publisher
  20. 14GitHub Actions → Gmail → Congregationsend the sermon email to the list
  21. 15GitHub Actions → sermons.vietgrace.orgrsync the rebuilt table + sermon pages

Reference

Where each piece runs

GitHubcloud
Repo + per-sermon state JSON, Actions cron and comment triggers, review Issues, the light publish / reconcile / retry / ci runners.
Church Macself-hosted
Residential-IP runner for the heavy steps: yt-dlp span + captions, ffmpeg trim, Whisper, site-deploy, and the Telegram bot.
Googlecloud
YouTube Data API (detect), YouTube OAuth (unlisted / public / playlist), Drive (mp3 + video), Gmail SMTP (VietChristian + list).
Anthropiccloud
Claude proposes the sermon start / end at detect, and writes the bilingual (VN + EN) summary at publish. Heuristic fallback if absent.
Telegramcloud
Sends status and review links, receives the owner-only approval commands.
Church infravietgrace.org
Drupal planning calendar (title + scripture) and the VPS that serves the public sermons.vietgrace.org page.

Live

System status

A heartbeat for each moving part, read from status.json. Grey means the check has not run recently.

    State

    Status machine

    There is no database. Each sermon is one JSON record in the repo, and git history is the audit log. A job checks the record's status at the top and refuses work not in its expected predecessor state, so re-running tomorrow is always safe.

    detected reviewed pending_upload processed published + *_failed download_blocked

    Rationale

    Design decisions

    Serverless, state in git

    There is no VPS app and no database. State is per-sermon JSON committed to the repo; the multi-GB video never crosses a job boundary. Git history is the audit trail, and a repo clone is the backup. The old FastAPI + SQLite service is retired one slice at a time.

    A residential runner for the heavy jobs

    YouTube bot-walls video downloads from datacenter IPs, which takes out both GitHub-hosted runners and the church VPS. The fix is a self-hosted runner on a Mac at the church, on a residential IP. Only the light, API-only jobs stay on cloud runners.

    Read-only vs production write secrets

    Detection runs with read-only keys. The credentials that can upload, go public, or send email live in a protected production Environment, branch-restricted to main and readable only by approve and publish. A runaway scan cannot touch the channel or the mailing list.

    Idempotency by receipt and probe

    Before every irreversible call, the job checks a per-effect receipt and then probes the destination itself (a marker in the YouTube description, a Drive name lookup, the email flag). A torn or stale commit cannot cause a duplicate upload or a double-send. Re-running tomorrow is always safe.

    Captions first, Whisper as fallback

    Free YouTube captions are the primary transcript, so a normal week finishes in minutes. Whisper is off by default and runs only on request, locally on the Mac, so a no-caption week degrades gracefully instead of blowing the time budget.

    Human gates before anything irreversible

    Nothing goes public without a person. The upload is unlisted first; an owner-only /approve then /publish stands between detection and the congregation. Auto-pilot, off by default, can only ever produce the unlisted preview.

    Approval without a server

    The Telegram bot is a webhook-free dispatcher: it long-polls for updates, so there is no inbound port and nothing to host. An owner message is validated (chat id, then a regex on the video id and timestamps) and turned into a gh workflow run via an argument list, never a shell string, so a chat message cannot inject a command. The same review loop runs from the phone or from a GitHub Issue comment.

    Quota deferral, not failure

    YouTube's upload quota is small. When it is exhausted mid-run the record parks at processed_pending_upload instead of erroring; a daily job drains those back into an upload as quota returns. The status gate plus per-effect receipts make every step re-runnable with no duplicate upload or double email.