mobileapp

Shir ya Khat — mobile app

Bilingual (Farsi / English), RTL-first archive and community app for Shir ya Khat (شیر یا خط, “Heads or Tails”), a non-profit Persian blockchain podcast recorded as live panels since 2017.

What is in v1

Area What it does
Episodes 41 episodes grouped by season, bilingual cards, Jalali dates in Farsi, continue-listening card
Player Video (YouTube) / audio (podcast RSS) toggle, background audio with lock-screen controls, resume per episode, chapter markers, speed control
Transcript Bilingual, synced to playback, tap a line to seek, long-press to bookmark / share / copy
Search Full-archive search over every transcript (FA + EN) returning episode + timestamp hits
Glossary 358 crypto terms with FA + EN definitions; tap-to-define inline in transcripts
Bookmarks Per episode and per timestamp, local-first
Downloads Resumable offline audio downloads with storage management
Share On-device image card (art + quote + QR) and plain quote + link sharing
Community Supabase schema, GitHub sign-in, minute comments, questions, bookmark sync — flagged off

Quick start

npm install
npm start          # then press i (iOS simulator), a (Android) or scan the QR with Expo Go

No .env is needed. Everything the app shows is read from content/.

npm run check      # lint + typecheck + tests (what CI runs)
npm run format     # prettier

Deep links (scheme shiryakhat://):

Link Opens
shiryakhat://episode/s07e07?t=600 An episode at 10:00
shiryakhat://episode/s07e07?pane=transcript An episode on its transcript (about, chapters, transcript)
shiryakhat://?tab=search&q=tornado Search with a query (tab = episodes, search, glossary, library, settings)

Project layout

content/            committed static bundle the app reads (generated by the pipeline)
  episodes.json       canonical episode list
  transcripts/        one bilingual cue list per episode
  enrichment/         Claude output per episode + manifest.json (resume state)
  glossary.json       merged glossary
  search-index.json   token -> episodes inverted index
  types.ts            shapes shared by the pipeline and the app
pipeline/           Node/TS scripts that build content/ (own package.json)
src/
  app/                expo-router routes (home, episode/[id], share, questions)
  screens/            the five home panes
  components/         feature components and components/ui primitives
  player/             unified player (expo-audio + YouTube iframe)
  content/            typed access to the bundle, search, glossary matching
  state/              local-first stores (settings, progress, bookmarks, downloads)
  community/          Supabase client, auth and API (only used when the flag is on)
  theme/, i18n/       design tokens, FA/EN dictionaries
supabase/           SQL migrations, RLS policies, local config
docs/release.md     tag -> TestFlight / Play internal flow

Media strategy

Nothing heavy is re-hosted.

The corpus holds 49 videos for 40 episodes plus one special, because seasons 4 to 6 were uploaded twice. The pipeline de-duplicates by episode code, prefers the upload that has chapters and then the original upload, and keeps the others in altYoutubeIds.

RTL

Direction is set with the direction style on every screen root (components/ui/Screen), driven by the persisted locale, so the whole UI mirrors instantly when the language changes.

Content pipeline

cd pipeline && npm install
Step Command Needs Output
Fetch sources npm run fetch-sources network pipeline/.cache/website, pipeline/.cache/rss.xml
Episodes npm run episodes corpus, cache content/episodes.json
Transcripts npm run transcripts corpus content/transcripts/*.json
Enrichment npm run enrich ANTHROPIC_API_KEY content/enrichment/*.json
Finalize npm run finalize nothing glossary, corrected transcripts, search index, index.generated.ts

npm run build runs episodes, transcripts and finalize. The raw corpus (/Volumes/Beezhan/data/podcasts, override with SYK_CORPUS_DIR) is read-only and never copied.

Enrichment sends each episode’s bilingual transcript to Claude (claude-sonnet-4-6, override with SYK_ENRICH_MODEL) and gets back a bilingual summary, 3 to 5 timestamped key quotes, glossary terms, and a list of mis-transcribed names and terms. It is resumable: manifest.json records finished episodes and re-running skips them.

npm run enrich                          # everything still pending, 3 at a time
npm run enrich -- --only s07e07,s07e06  # specific episodes
npm run enrich -- --batch               # Message Batches API (cheaper, asynchronous, resumable)
npm run enrich -- --force               # redo finished episodes

Finalize is offline and idempotent. It merges glossaries, applies the corrections to the transcripts (for example the mis-heard show name “Shenakht”), rebuilds the search index and regenerates the require map Metro uses to load transcripts lazily.

Transcripts are raw Whisper output with automatic name fixes only. They are not proofread, and the app says so under each transcript.

Community backend (off by default)

Set these in .env once a Supabase project exists (see supabase/README.md):

EXPO_PUBLIC_COMMUNITY_ENABLED=true
EXPO_PUBLIC_SUPABASE_URL=...
EXPO_PUBLIC_SUPABASE_ANON_KEY=...

With the flag off the Supabase SDK is never initialised and the app shows a short note in Settings.

Build and release

npx eas-cli build --profile development --platform ios   # dev client for the simulator
npx eas-cli build --profile preview                      # internal builds

Pushing a v* tag runs .github/workflows/release.yml: lint, typecheck and test, then EAS Build and EAS Submit to TestFlight and the Play internal track. The app version comes from package.json (app.config.js), so npm version patch && git push --follow-tags is the whole release command. One-time setup, the secrets table and troubleshooting are in docs/release.md.

Background audio and lock-screen controls need a development or store build to behave as they will in production; Expo Go is fine for everything else.