How It Works
Pipeline
Section titled “Pipeline”Full dubbing pipeline
Section titled “Full dubbing pipeline”- UI wysyła
init-uploadi dostaje podpisany URL uploadu. - Po uploadzie UI wywołuje
complete-upload. - Backend tworzy parent job typu
dubbing(queued) i uruchamia workflow. - Workflow tworzy child joby etapowe (kolejno:
transcribe->diarize->voice-clone->translate->tts->export) zparentJobId = <dubbing job id>. - Worker konsument kolejki:
- wywołuje providera,
- normalizuje wynik,
- zapisuje metadane do D1,
- zapisuje payload JSON/audio do R2,
- aktualizuje progress.
- Provider webhook może wznowić workflow i uruchomić kolejny etap.
- Export końcowy jest odkładany do
dubbit-exports.
Workflow timeline + retry semantics
Section titled “Workflow timeline + retry semantics”Widok projektu rozdziela joby workflow na trzy obszary:
- Workflow runs: parent joby
dubbing(karta uruchomienia workflow). - Workflow timeline: tylko child joby wybranego parenta, w kolejności etapów.
- Other jobs: joby poza wybranym workflow (np. ingest/manual/compare).
Reguły retry:
- Retry child kroku workflow jest dozwolony wyłącznie dla ostatniego kroku danego runa (egzekwowane w UI i backendzie).
- Retry parenta
dubbingnie tworzy nowego parent joba: workflow jest wznawiany na tym samymjobIdz nowymworkflowInstanceId. - Gdy wznawiamy parent workflow, wcześniejsze child etapy ze statusem
succeededsą pomijane (bez duplikacji rekordów i bez ponownego dispatchu). - W trakcie aktywnego joba (
runninglubwaiting_webhook) UI blokuje tworzenie nowych jobów (upload/import/full workflow/individual steps/compare).
Individual step execution
Section titled “Individual step execution”Oprócz pełnego pipeline dubbingowego, UI oferuje 6 oddzielnych przycisków do uruchamiania poszczególnych kroków:
- Transcribe — STT (typ joba:
transcribe) - Diarize — rozpoznawanie mówców (typ:
diarize) - Voice Clone — klonowanie głosu (typ:
voice-clone) - Translate — tłumaczenie segmentów (typ:
translate) - TTS — synteza mowy (typ:
tts) - Export — mix i export audio (typ:
export)
Każdy krok wymaga danych z poprzednich kroków — przyciski są automatycznie wyłączone (disabled) z tooltipem wskazującym brakujące dane.
sourceJobId — łączenie kroków
Section titled “sourceJobId — łączenie kroków”Standalone joby (diarize, voice-clone, tts, export) potrzebują artefaktów z wcześniejszego joba. Frontend przekazuje sourceJobId (ID wybranego joba z poprzedniego kroku) w metadanych tworzenia joba. Queue consumer rozwiązuje:
const sourceJobId = payload.metadata?.sourceJobId ?? payload.jobId;i ładuje artefakty z R2/D1 po sourceJobId zamiast payload.jobId.
Standalone job completion
Section titled “Standalone job completion”Standalone joby (np. diarize, tts) po zakończeniu pracy są oznaczane jako succeeded i emitują event ${task}:done:
await updateJobStatus(env, payload.jobId, "succeeded", 100);await emitWorkflowEvent(env, payload.jobId, "diarize:done", { status: "succeeded",});Jeśli job nie ma workflowName/workflowInstanceId, emitWorkflowEvent wykrywa to i kończy się bez-op (log workflow.event.skipped). Dzięki temu ten sam kod działa dla jobów standalone i workflow child.
YouTube import (MVP)
Section titled “YouTube import (MVP)”- Zalogowany user uruchamia
youtubeImportCreate(ORPC) z adresem YouTube. - Backend tworzy
asset+ingest jobi enqueueq_ingestz metadanymi źródła. - Consumer ingest wykonuje chain providerów:
- RapidAPI
yt-api.p.rapidapi.com(/dl?id=...), - fallback:
youtube-media-downloader.p.rapidapi.com(/v2/video/details?videoId=...), - opcjonalnie na końcu: zewnętrzny worker (
YOUTUBE_PIPELINE_URL->POST /youtube/extract-audio).
- RapidAPI
- Dla każdego kroku:
- media są pobierane z URL providera (bez ciężkiego przetwarzania lokalnego),
- source video jest zawsze archiwizowane do R2 (
raw/.../source.*), - audio do dalszego STT jest uzyskiwane przez Cloudflare Stream (
/stream/copy+/downloads/audio) gdy trzeba, - audio trafia do
dubbit-assets, - metadane assetu i status joba są aktualizowane w D1.
- Jeśli żaden provider nie zwróci poprawnego media/audio, job kończy się
failed(beznoopi bez placeholderów). - UI streamuje status przez SSE (
/api/jobs/:jobId/events), a na jobach:queued/running/waiting_webhookpozwala naCancel(job zostaje oznaczony jakofailed),failedpozwala naRetryalboDelete.
Upload własnego wideo (domyślna ścieżka)
Section titled “Upload własnego wideo (domyślna ścieżka)”- Użytkownik wybiera plik i akceptuje oświadczenie praw (
rightsConsent). init-uploadzwraca podpisany URL i tymczasowy token.- Klient wykonuje
PUTdo upload URL. complete-uploadzapisuje metadane + enqueueq_ingest.- Dla assetu typu video ingest tworzy extracted audio (
intermediate/.../transcribe-audio.wav) do STT.
Idempotency
Section titled “Idempotency”jobs.idempotency_keyzabezpiecza tworzenie jobów pełnego pipeline.- Standalone step joby (diarize, voice-clone, tts, export) używają unikalnego klucza z
Date.now(), ponieważ mogą być uruchamiane wielokrotnie na tych samych danych. provider_callsma unikalność(job_id, provider, endpoint, request_hash).- Retry kolejki nie duplikuje kosztu API, tylko aktualizuje status tego samego wywołania.
Resume bez duplikacji etapów
Section titled “Resume bez duplikacji etapów”Przy wznowieniu parenta dubbing workflow wykrywa istniejące child joby po deterministycznym idempotencyKey (<parentJobId>:<stage>):
succeeded-> skip dispatch + przejście do kolejnego etapu,- inne statusy terminalne -> reset do
queuedi ponowny dispatch.
Efekt: workflow kontynuuje od pierwszego nieskończonego etapu i nie powiela zakończonych sukcesem kroków.
Job ownership
Section titled “Job ownership”jobCreate weryfikuje, czy zalogowany user jest właścicielem projektu (projects.userId) przed utworzeniem joba. Zapobiega to tworzeniu jobów w cudzych projektach.
Video uploads
Section titled “Video uploads”- Upload wideo usera zawsze zostaje w R2 jako source of truth.
- W kroku ingest tworzymy osobny plik audio (
intermediate/.../extracted-audio.wav) przez Cloudflare Stream audio export. - Transcribe używa extracted audio key (nie surowego wideo).
Realtime status
Section titled “Realtime status”JobStatusDO publikuje snapshot i strumień SSE dla UI (/api/jobs/:jobId/events).
Turnstile (auth + upload)
Section titled “Turnstile (auth + upload)”- Publiczne endpointy auth (
/api/auth/sign-in/email,/api/auth/sign-up/email) wymagają poprawnegox-captcha-response(Better Authcaptchaplugin) gdy ustawiony jestTURNSTILE_SECRET. - Upload init (
/api/assets/init-upload) również waliduje token Turnstile (nagłówekcf-turnstile-token). - UI renderuje challenge na stronie logowania/rejestracji przez
PUBLIC_TURNSTILE_SITE_KEY.
Frontend UI (daisyUI)
Section titled “Frontend UI (daisyUI)”- Interfejs web (
apps/web) używaTailwind CSS v4+daisyUI. - Motywy są sterowane przez
data-themei wspierają tryby:auto,light,dark. - W navbarze tryb motywu jest przełączany ikonami (auto/light/dark) z zachowaniem
aria-label(a11y). - Komponenty UI (
btn,card,menu,dropdown,input) są utrzymywane w standardzie daisyUI dla spójności. - Referencja docs dla agentów/modeli: https://daisyui.com/llms.txt
External YouTube pipeline (optional)
Section titled “External YouTube pipeline (optional)”W repo jest gotowy worker: apps/youtube-pipeline.
- Endpoint:
POST /youtube/extract-audio - Auth:
Authorization: Bearer <PIPELINE_INTERNAL_API_KEY>(opcjonalnie wymagane, jeśli sekret ustawiony) - Request:
sourceUrl(wymagane)outputFormat(opcjonalne, np.wav)requestId(opcjonalne)
- Response:
provideraudioUrl/audioUrlslubvideoUrl/videoUrlscontentType,durationMs,costUsd(opcjonalne)
Sekrety dla pipeline:
RAPIDAPI_YT_API_KEY(z RapidAPI subscription)PIPELINE_INTERNAL_API_KEY(Twój własny losowy sekret, np.openssl rand -hex 32)
Sekrety dla API worker (ingest + Stream extractor):
CLOUDFLARE_STREAM_ACCOUNT_IDlubCLOUDFLARE_ACCOUNT_IDCLOUDFLARE_STREAM_API_TOKENlubCLOUDFLARE_API_TOKENYOUTUBE_RAPIDAPI_KEYYOUTUBE_RAPIDAPI_HOST(domyślnieyt-api.p.rapidapi.com)YOUTUBE_RAPIDAPI_FALLBACK_HOST(domyślnieyoutube-media-downloader.p.rapidapi.com)YOUTUBE_PIPELINE_URL(opcjonalnie)YOUTUBE_PIPELINE_API_KEY(opcjonalnie)TURNSTILE_SECRET(opcjonalnie, ale zalecane dla publicznego produktu)
Zmienne dla web:
PUBLIC_TURNSTILE_SITE_KEY
Token Stream musi mieć uprawnienia konta do Stream API (co najmniej odczyt/zapis dla Stream).
Jeśli w jobach widzisz Cloudflare API request failed (401) z code:10000, token jest nieprawidłowy albo bez właściwych scope.
Fallback opcjonalny (zewnętrzny extractor):
YOUTUBE_AUDIO_EXTRACTOR_URL(endpoint Twojego własnego serwisuvideo -> audio)YOUTUBE_AUDIO_EXTRACTOR_API_KEY(opcjonalny bearer do tego serwisu)
Billing integration (Dodo Payments)
Section titled “Billing integration (Dodo Payments)”- Adapter:
@dodopayments/better-auth(server plugin wpackages/auth/src/index.ts). - Auth worker wystawia:
POST /api/auth/dodopayments/checkout-sessionGET /api/auth/dodopayments/customer/portalGET /api/auth/dodopayments/customer/subscriptions/listGET /api/auth/dodopayments/customer/payments/listPOST /api/auth/dodopayments/webhooks
- Wymagane sekrety:
DODO_PAYMENTS_API_KEYDODO_PAYMENTS_WEBHOOK_SECRET
- Uwaga: endpointy billingowe Dodo (portal/subscriptions/payments) wymagają, aby user miał
emailVerified=true. - Uwaga: jeśli
DODO_PAYMENTS_WEBHOOK_SECRETnie jest ustawiony, plugin webhooków jest wyłączony i top-upy nie będą automatycznie zapisywane docredit_topups.- W takim wypadku użyj akcji Sync billing na
/account(po weryfikacji email) albo skonfiguruj webhook w Dodo:- URL:
https://api.dubbit.ai/api/auth/dodopayments/webhooks - nagłówki podpisu:
webhook-id,webhook-timestamp,webhook-signature
- URL:
- W takim wypadku użyj akcji Sync billing na
- Konfiguracja produktu checkout:
DODO_PAYMENTS_DEFAULT_PRODUCT_SLUG+DODO_PAYMENTS_DEFAULT_PRODUCT_ID- albo
DODO_PAYMENTS_PRODUCTS_JSON(tablica slug->productId)
- Top-up katalog:
DODO_PAYMENTS_TOPUPS_JSON(produkty top-up + metadatacredits,price_usd,embedded_discount_pct)
- Planowe rabaty top-up:
DODO_PAYMENTS_TOPUP_PLAN_DISCOUNTS_JSON(np. Pro -10%, Max -15%)
- Dla planów kredytowych rekomendowany jest
DODO_PAYMENTS_PRODUCTS_JSONz metadanymi:metadata.plan_credits(kredyty miesięczne planu)metadata.monthly_price_usd(cena miesięczna)
- UI konta zapisuje wybrany plan przez ORPC (
billingPlanSelect) dobilling_profiles. - UI konta:
- pokazuje cenę top-up przed/po rabatach i koszt per credit,
- wymaga aktywnej subskrypcji Go/Pro/Max do uruchomienia checkout top-up.
- Webhook
payment.succeededautomatycznie przyznaje top-up docredit_topups(idempotentnie popayment_ref).- Alternatywnie (fallback): ORPC
billingSyncpobiera listę paymentów i backfillujecredit_topupsidempotentnie.
- Alternatywnie (fallback): ORPC
Credits model (Usage)
Section titled “Credits model (Usage)”- Kredyty są księgowane per job/stage w
credit_usage_events:transcribettsexport
- Każdy event ma split:
credits_from_plancredits_from_topupcredits_overage
- Plan monthly resetuje się co miesiąc (UTC), top-up ma ważność 12 miesięcy od zakupu (
credit_topups.expires_at). - Konsumpcja kredytów:
- najpierw miesięczny plan,
- potem top-up od najstarszego zakupu (
purchasedAtascending).
- Dashboard
/usagepokazuje:- zużycie kredytów globalnie,
- split plan/top-up/overage,
- rozbicie per service/task i per projekt,
- aktualny balance planu i top-upów.
- Referencja dokumentacji:
Email delivery (Resend)
Section titled “Email delivery (Resend)”- Auth wysyła maile transakcyjne przez Resend:
- verification email (auto po rejestracji),
- reset password email.
- Wymagane env:
RESEND_API_KEY(secret),RESEND_FROM_EMAIL,RESEND_REPLY_TO(opcjonalnie).
- Resend docs:
Stuck job auto-healing
Section titled “Stuck job auto-healing”Auto-heal jest wyzwalany na dwa sposoby:
- Lazy (status poll) — gdy klient polluje
/api/jobs/:jobId/status, serwer wykrywa stuck joby i próbuje heal. - Proactive (CRON) — scheduled handler (
*/5 * * * *) skanuje wszystkierunningjoby nieaktualizowane przez >15 min i uruchamia heal dla każdego.
Heal registry (STAGE_DESCRIPTORS)
Section titled “Heal registry (STAGE_DESCRIPTORS)”| Etap | Stuck at progress | Claim progress | Typy jobów |
|---|---|---|---|
| transcribe | 20 | 21 | transcribe, dubbing |
| diarize | 45 | 46 | dubbing |
| diarize-done | 55 | 56 | dubbing |
| voice-clone-skipped | 58 | 59 | dubbing |
| voice-clone-done | 62 | 63 | dubbing |
| translate | 65 | 66 | dubbing |
| translate-done | 75 | 76 | dubbing |
| tts | 82 | 83 | dubbing |
| tts-done | 90 | 91 | dubbing |
Deskryptory z sufiksem -done i -skipped pokrywają luki między etapami (inter-stage gaps), gdy artefakty już istnieją ale pipeline nie przeszedł do następnego kroku.
Weryfikacja per etap
Section titled “Weryfikacja per etap”- provider call — wymagane jest zakończone sukcesem wywołanie providera (
stt.transcribe,diarize.run,translate.adapt, lub manifest TTS). - R2 freshness — artefakt R2 musi być uploadowany po provider call (guard na crash window między providerem a
saveTranscript). - D1 segment freshness — segmenty w D1 muszą istnieć i mieć
createdAtpo provider call (guard na crash window międzysaveTranscriptarewriteSegmentsFromTranscript).
Mechanizm 2-fazowy
Section titled “Mechanizm 2-fazowy”Atomic claiming zapobiega wyścigom: pierwszy poll claimuje job (progress → claim progress), drugi poll (>60s później) potwierdza i wykonuje heal. Workflow event jest emitowany przed aktualizacją statusu, aby failure zostawił job w stanie retryowalnym.
Provider defaults (aktualnie)
Section titled “Provider defaults (aktualnie)”- STT + diarization: Azure Speech.
- Translate/adapt: DeepL.
- TTS: Inworld.ai (
inworld-tts-1.5-max), LINEAR16 PCM output. - Voice cloning: Inworld.ai (
voices:cloneendpoint), instant cloning. - Audio mix/export:
mixPcmSegments()w Worker (PCM stitching), zastępuje ElevenLabs Dubbing API. - ElevenLabs: wykomentowany fallback w
packages/voice-providers/src/registry.ts(nieaktywny w produkcji).
Diarize auto-skip
Section titled “Diarize auto-skip”processDiarize automatycznie pomija wywołanie providera diarization, jeśli transcript wejściowy ma pełne pokrycie speakerId we wszystkich utterance. Wtedy:
- transcript jest kopiowany pod
results/<diarizeJobId>/transcript.normalized.json, - segmenty są przepisywane do
segments(jobId=<diarizeJobId>), - job kończy się
succeeded, - workflow event
diarize:donezawieraskipped: true.
Reliability: Auto-heal
Section titled “Reliability: Auto-heal”Mechanizmy auto-heal są wyzwalane dwojako:
- Lazy — z
GET /api/jobs/:jobId/status(przy każdym status poll). - Proactive — CRON
*/5 * * * *skanuje wszystkie stuckrunningjoby (>15 min bez aktualizacji) i wywołuje heal.
Stuck ingest jobs
Section titled “Stuck ingest jobs”- Trigger:
runningingest job zprogress < 72i brak aktualizacji przez 20 minut. - Akcja: oznacza job jako
failed, czyściprovider_calls(running → failed), ustawia assetstatus = ingest_failed. - Dlaczego fail (nie complete): ingest nie ma odzyskiwalnych artefaktów R2 na tym etapie.
Stuck dubbing pipeline stages
Section titled “Stuck dubbing pipeline stages”Registry-based healer pokrywający 9 deskryptorów (4 etapy główne + 5 luk inter-stage):
| Etap | Stuck at progress | Typy jobów |
|---|---|---|
| transcribe | 20 | transcribe, dubbing |
| diarize | 45 | dubbing |
| diarize-done | 55 | dubbing |
| voice-clone-skipped | 58 | dubbing |
| voice-clone-done | 62 | dubbing |
| translate | 65 | dubbing |
| translate-done | 75 | dubbing |
| tts | 82 | dubbing |
| tts-done | 90 | dubbing |
- Faza 1: claim (progress → claim progress) po 15-minutowym progu.
- Faza 2: weryfikacja artefaktów R2/D1; jeśli istnieją → advance job i emit workflow event; jeśli nie → fail.
- 1-minutowe okno reclaim zapobiega podwójnemu heal z równoczesnych status polls.
Provider call cleanup
Section titled “Provider call cleanup”cleanupStuckProviderCalls(env, jobId, reason) oznacza wszystkie running provider_calls dla joba jako failed. Wywoływane z error handlera consumeQueue przy każdym failurze joba.
Fetch Timeouts
Section titled “Fetch Timeouts”Wszystkie zewnętrzne wywołania HTTP w Worker używają AbortSignal.timeout().
| Typ wywołania | Timeout |
|---|---|
| RapidAPI metadata | 30 s (RAPIDAPI_REQUEST_TIMEOUT_MS) |
| Media download | 60 s baza + 30 s na minutę wideo |
| Pipeline API | 120 s (PIPELINE_REQUEST_TIMEOUT_MS) |
computeMediaDownloadTimeoutMs(durationMs) skaluje timeout pobierania dynamicznie na podstawie długości wideo.