No description
  • TypeScript 63.2%
  • Vue 31.1%
  • Astro 5.3%
  • JavaScript 0.4%
Find a file
Maksym Zlatohorskyi 97060a0605 Add API documentation as markdown content collection at /docs/api
Docs live in src/content/docs/api.md (Astro content collection); the
page is a thin styled renderer. Linked from the nav and Settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 20:59:14 +02:00
migrations Add v1 HTTP API with Bearer API keys 2026-07-16 20:51:48 +02:00
src Add API documentation as markdown content collection at /docs/api 2026-07-16 20:59:14 +02:00
test Fix TMDB year filter and add season/episode metadata 2026-07-16 20:33:09 +02:00
.dev.vars.example Add D1 schema, typed queries, and Cloudflare Access auth middleware 2026-07-14 16:08:24 +02:00
.gitignore Scaffold Astro 7 + Vue + Cloudflare adapter with Workflows smoke test 2026-07-14 16:02:40 +02:00
.nvmrc Scaffold Astro 7 + Vue + Cloudflare adapter with Workflows smoke test 2026-07-14 16:02:40 +02:00
astro.config.mjs Add v1 HTTP API with Bearer API keys 2026-07-16 20:51:48 +02:00
bun.lock Scaffold Astro 7 + Vue + Cloudflare adapter with Workflows smoke test 2026-07-14 16:02:40 +02:00
package.json Polish: re-translate to another language, prod auth hardening, README 2026-07-14 17:04:34 +02:00
README.md Add v1 HTTP API with Bearer API keys 2026-07-16 20:51:48 +02:00
tsconfig.json Scaffold Astro 7 + Vue + Cloudflare adapter with Workflows smoke test 2026-07-14 16:02:40 +02:00
wrangler.jsonc Configure Cloudflare Access: team domain, AUD, disable workers.dev 2026-07-14 17:29:22 +02:00

Subtitle Translator

A private, invite-only web app that translates .srt subtitle files with Gemini — built on Astro + Vue, running serverless on Cloudflare (Workers, Workflows, D1, R2, Access).

How it works

  1. Upload an .srt file and pick a target language.
  2. An AI pre-pass (AnalyzeWorkflow) reads the whole dialogue and builds a character sheet: movie title guess, characters with genders inferred from dialogue evidence, speech styles, tone, and a glossary of terms to translate consistently.
  3. You review and edit the sheet — genders drive grammatical agreement in gendered languages (Ukrainian «він з'їв» vs «вона з'їла»). Optionally search TMDB to confirm the movie and merge real cast genders into the sheet.
  4. Translation (TranslateWorkflow) runs as a durable background job: the file is split into chunks translated in concurrency-limited parallel waves. Every chunk call carries the shared character sheet + glossary and a tail of preceding source cues for local context, so chunks stay consistent and parallel. Safe to close the tab.
  5. Download the translated .srt — timestamps, numbering, formatting tags, BOM and line endings are preserved byte-for-byte; only cue text changes. Files stay in your history for later download.

Failed chunks retry automatically with exponential backoff (free-tier friendly). If a job still fails, completed chunks are kept — "Start again" only re-translates what's missing. Editing the sheet or switching language invalidates the cache and re-translates fully.

Stack

  • Astro 7 + Vue 3 (SSR) on Cloudflare Workers via @astrojs/cloudflare; the custom worker entry (src/worker.ts) also exports both WorkflowEntrypoint classes.
  • Cloudflare Workflows for durable analyze/translate jobs.
  • D1 for jobs, per-chunk progress, and user settings; R2 for the SRT files.
  • Cloudflare Access (Zero Trust) for auth — no registration UI; users are allowlisted in the Cloudflare dashboard. The Worker verifies the Access JWT (jose).
  • Gemini Interactions API via a thin fetch client with structured JSON output.
  • Per-user settings: personal Gemini API key (AES-GCM-encrypted at rest) and translation concurrency, with shared-key defaults.

Local development

Prereqs: Bun, Node ≥ 22.15 (nvm use picks it up from .nvmrc).

bun install
cp .dev.vars.example .dev.vars     # fill in keys; generate SETTINGS_ENC_KEY with: openssl rand -base64 32
bunx wrangler d1 migrations apply DB --local
bun run dev                        # http://localhost:4321 — D1/R2/Workflows run locally

Without Cloudflare Access locally, DEV_USER_EMAIL from .dev.vars is your identity.

bun run test        # vitest (SRT parser, chunker)
bunx tsc --noEmit   # type check
bun run preview     # production build under wrangler dev (shares local state)

Deploying

bunx wrangler login
bunx wrangler d1 create subtitle-translator      # put the returned id in wrangler.jsonc "database_id"
bunx wrangler r2 bucket create subtitle-translator-files
bunx wrangler secret put GEMINI_API_KEY          # shared default key (free tier is fine)
bunx wrangler secret put TMDB_API_TOKEN          # v3 api key or v4 read access token
bunx wrangler secret put SETTINGS_ENC_KEY        # openssl rand -base64 32
bunx wrangler d1 migrations apply DB --remote
bun run deploy

Then lock it behind Cloudflare Access:

  1. Add a custom domain to the Worker (Workers → your worker → Domains & Routes) and disable the workers.dev route.
  2. In Zero Trust → Access → Applications, add a self-hosted app for that domain with an allowlist policy (the emails you invite).
  3. Copy your team domain (<team>.cloudflareaccess.com) and the app's AUD tag into ACCESS_TEAM_DOMAIN / ACCESS_AUD in wrangler.jsonc, then bun run deploy again.

Never set DEV_USER_EMAIL as a production secret — it is the local-dev auth bypass.

Notes:

  • Workers Paid ($5/mo) is recommended for full movies: the free plan caps a workflow instance at 50 outbound requests (≈ 45 chunks ≈ 2,200 cues) and 10 ms CPU per step.
  • GEMINI_MODEL, TRANSLATE_CONCURRENCY, CHUNK_SIZE, CONTEXT_TAIL are tunable in wrangler.jsonc (users can override key/concurrency per-account in Settings).

HTTP API (v1)

Programmatic access for external systems. Create an API key on the Settings page and send it as Authorization: Bearer <key> to /api/v1/*. Keys are stored as SHA-256 hashes (shown once at creation); jobs created with a key belong to the key's owner.

/api/v1 requires a Cloudflare Access bypass policy so machine traffic reaches the Worker (Zero Trust → Access → Applications → add a self-hosted app for <your-domain>/api/v1 with a single Bypass policy including Everyone). The Bearer key check in the middleware is then the only gate on those paths.

BASE=https://subtitles.example.com/api/v1
AUTH="Authorization: Bearer st_..."

# 1. Upload — body is the raw .srt (≤ 2 MB). Returns {"id": "...", "status": "analyzing"}
JOB=$(curl -s -X POST -H "$AUTH" -H "content-type: application/octet-stream" \
  --data-binary @movie.srt \
  "$BASE/jobs?target_lang=Ukrainian&filename=Show.S01E02.srt" | jq -r .id)

# 2. Poll GET $BASE/jobs/$JOB until status is "ready" (includes character_sheet)
# 3. Optionally adjust the sheet:   PUT $BASE/jobs/$JOB/sheet
# 4. Start translation:
curl -s -X POST -H "$AUTH" -H "content-type: application/json" -d '{}' \
  "$BASE/jobs/$JOB/translate"
# 5. Poll until "done" (progress.done / progress.total = chunks), then:
curl -s -H "$AUTH" -o translated.srt "$BASE/jobs/$JOB/download?which=translated"
Endpoint Method Notes
/api/v1/jobs?target_lang=&filename= POST Raw .srt body. Season/episode auto-parsed from filename.
/api/v1/jobs GET List your jobs.
/api/v1/jobs/:id GET Status, character sheet, chunk progress, failed chunks.
/api/v1/jobs/:id/sheet PUT Update sheet / title / season / episode before translating.
/api/v1/jobs/:id/translate POST Start or restart. Optional {"target_lang": "..."} — a new language re-translates fully; a retry after error reuses completed chunks.
/api/v1/jobs/:id/download?which=translated|original GET Streams the .srt.
/api/v1/jobs/:id DELETE Delete the job and its files.

Job statuses: uploaded → analyzing → ready → translating → done | error.

This product uses the TMDB API but is not endorsed or certified by TMDB.