Skip to content

How It Works

  1. UI wysyła init-upload i dostaje podpisany URL uploadu.
  2. Po uploadzie UI wywołuje complete-upload.
  3. Backend tworzy parent job typu dubbing (queued) i uruchamia workflow.
  4. Workflow tworzy child joby etapowe (kolejno: transcribe -> diarize -> voice-clone -> translate -> tts -> export) z parentJobId = <dubbing job id>.
  5. Worker konsument kolejki:
    • wywołuje providera,
    • normalizuje wynik,
    • zapisuje metadane do D1,
    • zapisuje payload JSON/audio do R2,
    • aktualizuje progress.
  6. Provider webhook może wznowić workflow i uruchomić kolejny etap.
  7. Export końcowy jest odkładany do dubbit-exports.

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 dubbing nie tworzy nowego parent joba: workflow jest wznawiany na tym samym jobId z nowym workflowInstanceId.
  • Gdy wznawiamy parent workflow, wcześniejsze child etapy ze statusem succeeded są pomijane (bez duplikacji rekordów i bez ponownego dispatchu).
  • W trakcie aktywnego joba (running lub waiting_webhook) UI blokuje tworzenie nowych jobów (upload/import/full workflow/individual steps/compare).

Oprócz pełnego pipeline dubbingowego, UI oferuje 6 oddzielnych przycisków do uruchamiania poszczególnych kroków:

  1. Transcribe — STT (typ joba: transcribe)
  2. Diarize — rozpoznawanie mówców (typ: diarize)
  3. Voice Clone — klonowanie głosu (typ: voice-clone)
  4. Translate — tłumaczenie segmentów (typ: translate)
  5. TTS — synteza mowy (typ: tts)
  6. 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.

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 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.

  1. Zalogowany user uruchamia youtubeImportCreate (ORPC) z adresem YouTube.
  2. Backend tworzy asset + ingest job i enqueue q_ingest z metadanymi źródła.
  3. 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).
  4. 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.
  5. Jeśli żaden provider nie zwróci poprawnego media/audio, job kończy się failed (bez noop i bez placeholderów).
  6. UI streamuje status przez SSE (/api/jobs/:jobId/events), a na jobach:
    • queued/running/waiting_webhook pozwala na Cancel (job zostaje oznaczony jako failed),
    • failed pozwala na Retry albo Delete.

Upload własnego wideo (domyślna ścieżka)

Section titled “Upload własnego wideo (domyślna ścieżka)”
  1. Użytkownik wybiera plik i akceptuje oświadczenie praw (rightsConsent).
  2. init-upload zwraca podpisany URL i tymczasowy token.
  3. Klient wykonuje PUT do upload URL.
  4. complete-upload zapisuje metadane + enqueue q_ingest.
  5. Dla assetu typu video ingest tworzy extracted audio (intermediate/.../transcribe-audio.wav) do STT.
  • jobs.idempotency_key zabezpiecza 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_calls ma unikalność (job_id, provider, endpoint, request_hash).
  • Retry kolejki nie duplikuje kosztu API, tylko aktualizuje status tego samego wywołania.

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 queued i ponowny dispatch.

Efekt: workflow kontynuuje od pierwszego nieskończonego etapu i nie powiela zakończonych sukcesem kroków.

jobCreate weryfikuje, czy zalogowany user jest właścicielem projektu (projects.userId) przed utworzeniem joba. Zapobiega to tworzeniu jobów w cudzych projektach.

  • 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).

JobStatusDO publikuje snapshot i strumień SSE dla UI (/api/jobs/:jobId/events).

  • Publiczne endpointy auth (/api/auth/sign-in/email, /api/auth/sign-up/email) wymagają poprawnego x-captcha-response (Better Auth captcha plugin) gdy ustawiony jest TURNSTILE_SECRET.
  • Upload init (/api/assets/init-upload) również waliduje token Turnstile (nagłówek cf-turnstile-token).
  • UI renderuje challenge na stronie logowania/rejestracji przez PUBLIC_TURNSTILE_SITE_KEY.
  • Interfejs web (apps/web) używa Tailwind CSS v4 + daisyUI.
  • Motywy są sterowane przez data-theme i 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

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:
    • provider
    • audioUrl/audioUrls lub videoUrl/videoUrls
    • contentType, 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_ID lub CLOUDFLARE_ACCOUNT_ID
  • CLOUDFLARE_STREAM_API_TOKEN lub CLOUDFLARE_API_TOKEN
  • YOUTUBE_RAPIDAPI_KEY
  • YOUTUBE_RAPIDAPI_HOST (domyślnie yt-api.p.rapidapi.com)
  • YOUTUBE_RAPIDAPI_FALLBACK_HOST (domyślnie youtube-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 serwisu video -> audio)
  • YOUTUBE_AUDIO_EXTRACTOR_API_KEY (opcjonalny bearer do tego serwisu)
  • Adapter: @dodopayments/better-auth (server plugin w packages/auth/src/index.ts).
  • Auth worker wystawia:
    • POST /api/auth/dodopayments/checkout-session
    • GET /api/auth/dodopayments/customer/portal
    • GET /api/auth/dodopayments/customer/subscriptions/list
    • GET /api/auth/dodopayments/customer/payments/list
    • POST /api/auth/dodopayments/webhooks
  • Wymagane sekrety:
    • DODO_PAYMENTS_API_KEY
    • DODO_PAYMENTS_WEBHOOK_SECRET
  • Uwaga: endpointy billingowe Dodo (portal/subscriptions/payments) wymagają, aby user miał emailVerified=true.
  • Uwaga: jeśli DODO_PAYMENTS_WEBHOOK_SECRET nie jest ustawiony, plugin webhooków jest wyłączony i top-upy nie będą automatycznie zapisywane do credit_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
  • 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 + metadata credits, 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_JSON z metadanymi:
    • metadata.plan_credits (kredyty miesięczne planu)
    • metadata.monthly_price_usd (cena miesięczna)
  • UI konta zapisuje wybrany plan przez ORPC (billingPlanSelect) do billing_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.succeeded automatycznie przyznaje top-up do credit_topups (idempotentnie po payment_ref).
    • Alternatywnie (fallback): ORPC billingSync pobiera listę paymentów i backfilluje credit_topups idempotentnie.
  • Kredyty są księgowane per job/stage w credit_usage_events:
    • transcribe
    • tts
    • export
  • Każdy event ma split:
    • credits_from_plan
    • credits_from_topup
    • credits_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 (purchasedAt ascending).
  • Dashboard /usage pokazuje:
    • 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:

Auto-heal jest wyzwalany na dwa sposoby:

  1. Lazy (status poll) — gdy klient polluje /api/jobs/:jobId/status, serwer wykrywa stuck joby i próbuje heal.
  2. Proactive (CRON) — scheduled handler (*/5 * * * *) skanuje wszystkie running joby nieaktualizowane przez >15 min i uruchamia heal dla każdego.
EtapStuck at progressClaim progressTypy jobów
transcribe2021transcribe, dubbing
diarize4546dubbing
diarize-done5556dubbing
voice-clone-skipped5859dubbing
voice-clone-done6263dubbing
translate6566dubbing
translate-done7576dubbing
tts8283dubbing
tts-done9091dubbing

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.

  • 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ć createdAt po provider call (guard na crash window między saveTranscript a rewriteSegmentsFromTranscript).

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.

  • STT + diarization: Azure Speech.
  • Translate/adapt: DeepL.
  • TTS: Inworld.ai (inworld-tts-1.5-max), LINEAR16 PCM output.
  • Voice cloning: Inworld.ai (voices:clone endpoint), 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).

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:done zawiera skipped: true.

Mechanizmy auto-heal są wyzwalane dwojako:

  1. Lazy — z GET /api/jobs/:jobId/status (przy każdym status poll).
  2. Proactive — CRON */5 * * * * skanuje wszystkie stuck running joby (>15 min bez aktualizacji) i wywołuje heal.
  • Trigger: running ingest job z progress < 72 i brak aktualizacji przez 20 minut.
  • Akcja: oznacza job jako failed, czyści provider_calls (running → failed), ustawia asset status = ingest_failed.
  • Dlaczego fail (nie complete): ingest nie ma odzyskiwalnych artefaktów R2 na tym etapie.

Registry-based healer pokrywający 9 deskryptorów (4 etapy główne + 5 luk inter-stage):

EtapStuck at progressTypy jobów
transcribe20transcribe, dubbing
diarize45dubbing
diarize-done55dubbing
voice-clone-skipped58dubbing
voice-clone-done62dubbing
translate65dubbing
translate-done75dubbing
tts82dubbing
tts-done90dubbing
  • 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.

cleanupStuckProviderCalls(env, jobId, reason) oznacza wszystkie running provider_calls dla joba jako failed. Wywoływane z error handlera consumeQueue przy każdym failurze joba.

Wszystkie zewnętrzne wywołania HTTP w Worker używają AbortSignal.timeout().

Typ wywołaniaTimeout
RapidAPI metadata30 s (RAPIDAPI_REQUEST_TIMEOUT_MS)
Media download60 s baza + 30 s na minutę wideo
Pipeline API120 s (PIPELINE_REQUEST_TIMEOUT_MS)

computeMediaDownloadTimeoutMs(durationMs) skaluje timeout pobierania dynamicznie na podstawie długości wideo.