Skip to content

Long-Form Podcast to Viral Shorts Studio ​

Transform 60-minute podcast episodes, interview streams, and YouTube webinars into ready-to-publish vertical viral shorts (TikTok, Instagram Reels, YouTube Shorts) with automated multi-speaker tracking, virality scoring, animated karaoke captions, and broadcast-grade audio mastering.

This recipe leverages FotoHUB's Shorts Engine, coordinating Whisper Large-v3 timestamped transcription, Claude 3.5 / DeepSeek virality scoring, computer-vision face tracking with multi-speaker split layouts, semantic B-roll cutaway insertion, and 2-pass EBU R128 loudness normalization (-14 LUFS).

TIP

All API usage in this guide is billed purely in USD. There are no synthetic credits or tokens to manage. You are charged directly against your wallet.available_usd balance.


1. Production Architecture Flowchart ​

The Shorts Engine orchestrates 10 specialized microservices, utilizing distinct GPU clusters to optimize memory access and inference speed.

mermaid
flowchart TD
    %% Define styles
    classDef ingest fill:#1e40af,stroke:#93c5fd,stroke-width:2px,color:#fff;
    classDef ai fill:#065f46,stroke:#6ee7b7,stroke-width:2px,color:#fff;
    classDef av fill:#9a3412,stroke:#fdba74,stroke-width:2px,color:#fff;
    classDef export fill:#4c1d95,stroke:#c4b5fd,stroke-width:2px,color:#fff;

    subgraph "1. Ingestion Phase"
        A["1. Long-Form Ingest<br/>(YouTube / S3 60-min MP4)"]:::ingest
    end

    subgraph "2. AI Analysis & Segmentation Phase"
        B["2. Whisper Large-v3<br/>(Word-level timestamps & Diarization)"]:::ai
        C["3. Acoustic Prosody & Topic Analysis<br/>(Arousal spikes, speaker turn-taking)"]:::ai
        D["4. Virality Scoring (B-Score)<br/>(Claude 3.5 / DeepSeek)"]:::ai
        E["5. Top Clip Extraction<br/>(15s–60s narrative units)"]:::ai
        
        B --> C
        C --> D
        D --> E
    end

    subgraph "3. Audio/Visual Pipeline"
        F["6. Face-Tracking & Multi-Cam<br/>(Auto camera cut / Stacked Split)"]:::av
        G["7. Word-by-Word Karaoke Captions<br/>(Dynamic ASS glow & emojis)"]:::av
        H["8. Contextual B-Roll Overlay<br/>(Transcript-triggered cutaways)"]:::av
        I["9. 8-Stage Audio Chain<br/>(EBU R128 -14 LUFS)"]:::av
        J["10. NVENC GPU Render<br/>(1080x1920 MP4)"]:::av
        
        F --> G
        G --> H
        H --> I
        I --> J
    end
    
    subgraph "4. Delivery Phase"
        K["Webhook Notification<br/>(HMAC-SHA256 Signed)"]:::export
        L["BYOB Export<br/>(S3 / R2 Bucket Delivery)"]:::export
        
        J --> K
        K --> L
    end

    A --> B
    E --> F

Dedicated GPU workers

Transcription, audio enhancement, optional lip-sync, B-roll matching and the final hardware-encoded render each run on dedicated FOTOhub GPU workers, so stages of one job do not compete for the same hardware.


2. API Endpoints Overview ​

The complete Shorts Engine lifecycle requires coordinating these REST endpoints:

  1. POST /v1/shorts/clips — Submit a long-form video for asynchronous clipping.
  2. GET /v1/shorts/clips/{job_id} — Poll for status, live progress, and final outputs. There is no separate SSE stream for a single job — poll this endpoint.
  3. POST /v1/ai/transcribe — Standalone endpoint for transcription.

3. Asynchronous Clipping: POST /v1/shorts/clips ​

Submits a 60-minute video for complete end-to-end processing. Because this process can take 3-10 minutes depending on the source length and requested clips, it returns immediately with a 202 Accepted and processes in the background.

Full Parameter Table ​

ParameterTypeRequiredDefaultDescription
source_urlstringYes—Public MP4 URL, presigned S3 link, or YouTube link to ingest. Max 2048 chars.
source_typestringYes—Type of source: "url", "youtube", "s3", "gcs".
titlestringNo"Untitled"Project title for metadata tracking.
languagestringNo"en"ISO 639-1 language code (e.g., "en", "es", "fr"). Auto-detected if omitted.
max_clipsintegerNo10Max clips to discover and render (range: 1–30).
min_durationintegerNo15Minimum duration in seconds (range: 5–180).
max_durationintegerNo60Maximum duration in seconds (range: 10–300).
aspect_ratiostringNo"9:16"Target aspect ratio: "9:16", "1:1", "4:5", or "16:9".
caption_stylestringNo"karaoke"Style: "karaoke", "hormozi", "beasty", "neon", "clean", "none".
captionsbooleanNotrueWhether to burn in captions. Set to false if platform captions are preferred.
reframebooleanNotrueEnable CV face-tracking to automatically crop 16:9 to 9:16.
hooksbooleanNotrueAuto-generate a 3-second animated text overlay for the hook.
coversbooleanNotrueGenerate high-CTR title card thumbnails.
brollbooleanNotrueSemantically inject B-roll cutaways during long monologue segments.
enhance_audiobooleanNotrueRun 8-stage DSP chain (denoise, EQ, limit).
remove_fillerbooleanNotrueAutomatically cut "ums", "ahs", and dead air > 1.2s.
retention_modelbooleanNotrueUse Claude 3.5 to evaluate pacing specifically for TikTok retention curves.
webhook_urlstringNo—HTTPS endpoint receiving event notifications on completion.
webhook_secretstringNo—Secret string used to sign HMAC-SHA256 headers.
referencestringNo—Your internal reference ID (e.g., user_123_proj_456).
settings.multicamstringNo"stack""stack" (Top/Bottom split) or "dynamic" (active speaker active cut).
settings.multicam_dimfloatNo0.18Brightness dimming applied to the inactive speaker (0.0 to 1.0).
settings.caption_highlight_colorstringNo"#FFDF00"Hex color for the active karaoke word.
settings.caption_fontstringNo"Montserrat"Google Font name for captions.
settings.broll_stylestringNo"cinematic"B-roll aesthetic: "cinematic", "memes", "minimalist".
settings.cover_textstringNo"auto"Text to place on the thumbnail, or "auto" for AI generation.
settings.target_platformstringNo"tiktok"Tunes safe-zones: "tiktok", "reels", "shorts".

4-Way Orchestration Examples ​

python
import os
import requests
import json

def submit_clipping_job(video_url: str):
    api_key = os.environ.get("FOTOHUB_API_KEY", "fh_live_your_api_key")
    url = "https://apis.fotohub.app/v1/shorts/clips"
    
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    
    payload = {
        "source_url": video_url,
        "source_type": "url",
        "title": "Lex Fridman #333",
        "max_clips": 5,
        "min_duration": 30,
        "max_duration": 60,
        "aspect_ratio": "9:16",
        "caption_style": "hormozi",
        "reframe": True,
        "hooks": True,
        "broll": True,
        "enhance_audio": True,
        "remove_filler": True,
        "webhook_url": "https://api.yourdomain.com/webhooks",
        "webhook_secret": "whsec_super_secret_key",
        "settings": {
            "multicam": "dynamic",
            "caption_highlight_color": "#00FF00",
            "target_platform": "shorts"
        }
    }
    
    response = requests.post(url, headers=headers, json=payload)
    response.raise_for_status()
    
    data = response.json()
    print(f"Accepted Job: {data['job_id']}")
    print(f"Cost Deducted: ${data['charged_usd']} USD")
    return data['job_id']
typescript
import fetch from "node-fetch";

async function submitClippingJob(videoUrl: string): Promise<string> {
  const apiKey = process.env.FOTOHUB_API_KEY || "fh_live_your_api_key";
  const url = "https://apis.fotohub.app/v1/shorts/clips";

  const payload = {
    source_url: videoUrl,
    source_type: "url",
    title: "Lex Fridman #333",
    max_clips: 5,
    min_duration: 30,
    max_duration: 60,
    aspect_ratio: "9:16",
    caption_style: "hormozi",
    reframe: true,
    hooks: true,
    broll: true,
    enhance_audio: true,
    remove_filler: true,
    webhook_url: "https://api.yourdomain.com/webhooks",
    webhook_secret: "whsec_super_secret_key",
    settings: {
      multicam: "dynamic",
      caption_highlight_color: "#00FF00",
      target_platform: "shorts"
    }
  };

  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(payload)
  });

  if (!res.ok) {
    throw new Error(`Failed to submit job: ${res.statusText}`);
  }

  const data = await res.json();
  console.log(`Accepted Job: ${data.job_id}`);
  console.log(`Cost Deducted: $${data.charged_usd} USD`);
  
  return data.job_id;
}
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

func submitClippingJob(videoUrl string) (string, error) {
	apiKey := os.Getenv("FOTOHUB_API_KEY")
	url := "https://apis.fotohub.app/v1/shorts/clips"

	payload := map[string]interface{}{
		"source_url":      videoUrl,
		"source_type":     "url",
		"title":           "Lex Fridman #333",
		"max_clips":       5,
		"min_duration":    30,
		"max_duration":    60,
		"aspect_ratio":    "9:16",
		"caption_style":   "hormozi",
		"reframe":         true,
		"hooks":           true,
		"broll":           true,
		"enhance_audio":   true,
		"remove_filler":   true,
		"webhook_url":     "https://api.yourdomain.com/webhooks",
		"webhook_secret":  "whsec_super_secret_key",
		"settings": map[string]interface{}{
			"multicam":                "dynamic",
			"caption_highlight_color": "#00FF00",
			"target_platform":         "shorts",
		},
	}

	body, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body))
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 202 {
		return "", fmt.Errorf("unexpected status: %s", resp.Status)
	}

	var data map[string]interface{}
	json.NewDecoder(resp.Body).Decode(&data)

	jobID := data["job_id"].(string)
	charged := data["charged_usd"].(float64)
	fmt.Printf("Accepted Job: %s\n", jobID)
	fmt.Printf("Cost Deducted: $%.2f USD\n", charged)

	return jobID, nil
}
bash
curl -X POST https://apis.fotohub.app/v1/shorts/clips \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "source_url": "https://example.com/podcast.mp4",
    "source_type": "url",
    "title": "Lex Fridman #333",
    "max_clips": 5,
    "min_duration": 30,
    "max_duration": 60,
    "aspect_ratio": "9:16",
    "caption_style": "hormozi",
    "reframe": true,
    "hooks": true,
    "broll": true,
    "enhance_audio": true,
    "remove_filler": true,
    "webhook_url": "https://api.yourdomain.com/webhooks",
    "webhook_secret": "whsec_super_secret_key",
    "settings": {
      "multicam": "dynamic",
      "caption_highlight_color": "#00FF00",
      "target_platform": "shorts"
    }
  }'

4. Polling for Job Status: GET /v1/shorts/clips/{job_id} ​

While webhooks are the recommended approach for production, you can manually poll the API to check job status.

Polling Limits

Do not poll more than once every 5 seconds. Excessive polling will trigger HTTP 429 Rate Limits.

Full Parameter Table ​

ParameterTypeRequiredLocationDescription
job_idstringYesPathThe ID returned from the POST request (e.g., job_948a01bf28).
include_metricsbooleanNoQueryIf true, includes detailed processing timing metrics.

4-Way Code Examples ​

python
import time
import requests

def poll_job_status(job_id: str):
    url = f"https://apis.fotohub.app/v1/shorts/clips/{job_id}"
    headers = {"Authorization": "Bearer fh_live_your_api_key"}
    
    while True:
        resp = requests.get(url, headers=headers)
        data = resp.json()
        
        status = data.get("status")
        pct = data.get("progress_pct", 0)
        
        print(f"Status: {status} ({pct}%)")
        
        if status == "completed":
            print("Finished!")
            return data["clips"]
        elif status in ["failed", "canceled"]:
            raise Exception(f"Job failed: {data.get('error')}")
            
        time.sleep(10)
typescript
async function pollJobStatus(jobId: string) {
  const url = `https://apis.fotohub.app/v1/shorts/clips/${jobId}`;
  
  while (true) {
    const res = await fetch(url, {
      headers: { "Authorization": "Bearer fh_live_your_api_key" }
    });
    const data = await res.json();
    
    console.log(`Status: ${data.status} (${data.progress_pct}%)`);
    
    if (data.status === "completed") {
      return data.clips;
    } else if (data.status === "failed") {
      throw new Error(data.error);
    }
    
    await new Promise(r => setTimeout(r, 10000));
  }
}
go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

func pollJob(jobID string) {
	url := "https://apis.fotohub.app/v1/shorts/clips/" + jobID
	client := &http.Client{}

	for {
		req, _ := http.NewRequest("GET", url, nil)
		req.Header.Set("Authorization", "Bearer fh_live_your_api_key")
		
		resp, _ := client.Do(req)
		var data map[string]interface{}
		json.NewDecoder(resp.Body).Decode(&data)
		
		status := data["status"].(string)
		pct := data["progress_pct"].(float64)
		
		fmt.Printf("Status: %s (%.0f%%)\n", status, pct)
		
		if status == "completed" {
			fmt.Println("Clips ready!")
			break
		}
		time.Sleep(10 * time.Second)
	}
}
bash
# Poll job status
curl -X GET "https://apis.fotohub.app/v1/shorts/clips/job_948a01bf28?include_metrics=true" \
  -H "Authorization: Bearer fh_live_your_api_key"

5. Real-Time UI Monitoring ​

Corrected 2026-09

This page previously documented a per-job GET /v1/shorts/clips/{job_id}/stream Server-Sent Events endpoint. It does not exist — there is no SSE stream for an individual shorts job anywhere in the API. Build your progress bar by polling GET /v1/shorts/clips/{job_id} (section 4 above), which already returns status and live progress.

python
import time
import httpx

def poll_job_progress(job_id: str):
    url = f"https://apis.fotohub.app/v1/shorts/clips/{job_id}"
    headers = {"Authorization": "Bearer fh_live_your_api_key"}
    with httpx.Client() as client:
        while True:
            data = client.get(url, headers=headers).json()
            print(f"Progress: {data.get('progress')}% - {data.get('status')}")
            if data.get("status") in ("completed", "failed"):
                return data
            time.sleep(3)

6. Delivering clips to your own storage (BYOB) ​

Corrected 2026-09

This page previously documented a per-job POST /v1/shorts/export call that took a job_id plus raw AWS/R2 credentials in the request body. That endpoint does not exist, and sending long-lived secret keys in a per-call JSON body would be a bad design if it did.

The real BYOB mechanism is account-wide, not per-job: register a destination once with POST /v1/destinations, and everything that API key generates — not just shorts clips — is mirrored there automatically. There is no per-job "export this one clip" call; you either receive the clip URL directly in the poll response, or attach a standing destination up front.

Registering a destination (one-time setup) ​

bash
curl -X POST https://apis.fotohub.app/v1/destinations \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production shorts bucket",
    "providerPreset": "aws",
    "bucketName": "my-production-shorts",
    "region": "us-west-2",
    "accessKeyId": "AKIA...",
    "secretAccessKey": "SECRET...",
    "pathPrefix": "2026/09/"
  }'

For Cloudflare R2, use "providerPreset": "r2" and add "accountId". See Cloud Storage for the full destination reference.


7. Standalone Audio Transcription: POST /v1/ai/transcribe ​

Sometimes you only need the Whisper Large-v3 data (e.g., for custom indexing).

ParameterTypeRequiredDescription
audio_urlstringYesURL to audio/video file.
diarizationbooleanNoTag speakers (e.g., SPEAKER_00).
word_timestampsbooleanNoReturn ms-level timing.

Code Examples ​

python
import requests

def transcribe_audio(audio_url: str):
    url = "https://apis.fotohub.app/v1/ai/transcribe"
    payload = {
        "audio_url": audio_url,
        "diarization": True,
        "word_timestamps": True
    }
    resp = requests.post(url, headers={"Authorization": "Bearer fh_live_your_api_key"}, json=payload)
    return resp.json()
typescript
// Fetch call for transcription
go
// Go code for transcription
bash
curl -X POST https://apis.fotohub.app/v1/ai/transcribe \
  -H "Authorization: Bearer fh_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"audio_url": "https://example.com/audio.mp3", "diarization": true, "word_timestamps": true}'

8. The Virality Scoring (B-Score) Formula ​

Rather than guessing solely from text, FotoHUB combines acoustic prosody measurements with Claude 3.5 / DeepSeek narrative evaluations across 5 weighted dimensions:

$$\text{B-Score} = 0.30 \cdot H + 0.25 \cdot V + 0.20 \cdot E + 0.15 \cdot P + 0.10 \cdot A$$

DimensionWeightDetection MethodDescription
Hook Power ($H$)30%First 3s NLP + Acoustic PitchProvocative opening query, controversy, hook words (secret, mistake, truth).
Content Value ($V$)25%Claude 3.5 Semantic AnalysisStandalone insight or takeaway that delivers a clear payoff.
Emotional Impact ($E$)20%Prosody Energy & ArousalVoice dynamic range, laughing, vocal conviction, and intensity peaks.
Pacing & Energy ($P$)15%Words-per-minute & Pause RatiosEliminates dead air and drags, targeting optimal 140–180 WPM tempo.
Visual Appeal ($A$)10%Landmark Expression ScoreEngaged facial expressions and body language cues.

9. Whisper Large-v3 Transcription Deep-Dive ​

Audio is demuxed and transcribed with word-level boundary detection. The acoustic model outputs normalized millisecond timestamps (start_ms, end_ms), word confidence scores, and diarization tags separating host (SPEAKER_00) from guest (SPEAKER_01).

json
{
  "word": "Autonomous",
  "start_ms": 12400,
  "end_ms": 13100,
  "confidence": 0.99,
  "speaker": "SPEAKER_01"
}

10. Multi-Speaker Reframing & Visual Cut Logic ​

When two or more people speak in a landscape frame, the engine provides two reframing strategies:

  • stack (Split Screen): Stacks Host on top and Guest on bottom in a 9:16 viewport. Inactive speakers are subtly dimmed (dim_inactive: 0.18) so the viewer's gaze automatically tracks the active speaker.
  • dynamic (Active Speaker Switch): Seamless camera cuts panning to whoever holds the floor with smoothing easing algorithms to prevent jerky jumps on fast interruptions.

11. 8-Stage DSP Audio Chain (-14 LUFS) ​

To conform to TikTok, YouTube, and Instagram loudness standards, audio passes through an 8-stage DSP pipeline using FFmpeg internal filters:

  1. High-Pass Filter: highpass=f=80 cutoff removes rumble and mic handling noise.
  2. FFT Denoiser: afftdn=nr=12:nf=-25 scrubs room hiss.
  3. Noise Gate: agate=threshold=0.01:ratio=10 removes breathing artifacts.
  4. De-Esser: firequalizer attenuates 4–9 kHz sibilance.
  5. Compressor: acompressor=threshold=-20dB:ratio=4:makeup=2 tightens dynamic range.
  6. Presence EQ: Boosts 2–5 kHz for vocal intelligibility on mobile phone speakers.
  7. Brickwall Limiter: Caps peaks at -0.95 dBFS to eliminate digital clipping.
  8. 2-Pass EBU R128 Loudness: Targets integrated -14 LUFS with $\le -1.5\text{ dBTP}$ true peak.

12. Karaoke Caption System (ASS Subtitles) ​

Captions are generated dynamically using the Advanced SubStation Alpha (.ass) format to provide bounce, glow, and color highlights precisely mapped to the start_ms of each Whisper word. Emoticons are injected semantically.


13. B-Roll Semantic Injection ​

When the broll: true flag is enabled, the pipeline identifies long contiguous monologues (over 8 seconds). It extracts keyword clusters and queries a massive internal video library to seamlessly inject contextual cutaways that keep the visual scene dynamic.


14. Hook Generation ​

A 3-second opening hook text overlay is generated using Claude 3.5 to summarize the core controversy or question in the clip, overlaying high-impact yellow/white text in the top 1/3 of the frame to stop the scroll.


15. Cover Thumbnail Generation ​

High-CTR title card designs are generated for YouTube Shorts and Reels covers. Faces are extracted, background blurred, and high-contrast text is overlaid based on the A/B tested settings.cover_text parameters.


16. Platform-Specific Settings ​

PlatformSafe ZonesMax LengthRecommended B-Score Focus
TikTokAvoid right edge (icons)10 minsHook Power (H)
IG ReelsAvoid bottom 20% (caption)90sVisual Appeal (A)
YT ShortsStandard60sContent Value (V)

17. Node.js Worker Pool (Batch Processing) ​

For users processing dozens of episodes, here is a Node.js p-limit implementation to manage concurrency safely:

typescript
import pLimit from "p-limit";

const limit = pLimit(5); // 5 concurrent jobs

const podcastUrls = ["url1", "url2", "url3"]; // up to 100 urls

const jobs = podcastUrls.map(url => 
  limit(() => submitClippingJob(url))
);

await Promise.all(jobs);

18. Python FastAPI Webhook Receiver (HMAC-SHA256) ​

FotoHUB signs webhook payloads using HMAC-SHA256 in the X-FotoHub-Signature header. Listen for the shorts.clip.rendered event.

python
from fastapi import FastAPI, Request, Header, HTTPException
import hmac
import hashlib

app = FastAPI()
WEBHOOK_SECRET = "whsec_super_secret_key"

@app.post("/webhooks/fotohub")
async def handle_webhook(request: Request, x_fotohub_signature: str = Header(None)):
    payload_bytes = await request.body()
    
    expected_mac = hmac.new(
        WEBHOOK_SECRET.encode("utf-8"), 
        payload_bytes, 
        hashlib.sha256
    ).hexdigest()
    
    if not hmac.compare_digest(expected_mac, x_fotohub_signature):
        raise HTTPException(status_code=400, detail="Invalid signature")
        
    payload = await request.json()
    if payload.get("event") == "shorts.clip.rendered":
        print("Clip ready:", payload["data"]["video_url"])
        
    return {"status": "ok"}

19. Retention Feedback Loop ​

If you have access to TikTok API analytics, you can feed retention drop-off data back into the pipeline by adjusting the settings.multicam_dim or caption pacing to optimize your next run.


20. Exact USD Pricing Breakdown & ROI ​

Billing is deducted from your unified prepaid USD wallet balance at job submission time.

OperationUnit Cost (USD)Notes
Full Clipping Job (shorts_clip_job)$0.75 / jobFlat rate covering ingest, transcription, virality scoring, multi-speaker reframing, captions, B-roll, audio mastering, and NVENC GPU rendering.
Ingest Only (shorts_ingest)$0.05 / videoStandalone step endpoint
Transcription (shorts_transcribe)$0.05 / videoStandalone WhisperX speech recognition
Scene Detection (shorts_detect_scenes)$0.08 / videoStandalone scene cut detection
Clip Generation (shorts_generate_clips)$0.12 / videoStandalone LLM moment scoring
Smart Reframe (shorts_reframe)$0.08 / clipStandalone face tracking and 9:16 crop
Karaoke Captions (shorts_captions)$0.05 / clipStandalone ASS subtitle compilation
GPU Render (shorts_render)$0.12 / clipStandalone NVENC composite

ROI Calculation Example ​

Extracting 5 high-converting vertical viral shorts from a 60-minute podcast episode costs $0.75 USD total, or $0.15 per short. If each short achieves 50,000 views on YouTube Shorts at a $0.05 RPM, the return is $2.50 per short, yielding a massive positive ROI on API usage.


21. Error Handling and DLQ Patterns ​

Use a Dead Letter Queue (DLQ) if the webhook fails to acknowledge a 200 OK. FotoHUB will retry webhooks with exponential backoff for up to 72 hours.

json
{
  "event": "shorts.job.failed",
  "job_id": "job_948a01bf28",
  "error": {
    "code": "transcription_timeout",
    "message": "The audio stream was corrupt."
  }
}

Timeout Risks

Ensure your webhook receiver endpoint responds with 200 OK within 5 seconds. Lengthy processing should be offloaded to a background queue.


22. Exhaustive Troubleshooting & Error Codes ​

When interacting with the Shorts Engine API, you may encounter the following error codes. This exhaustive list covers every possible failure mode across the 10-stage pipeline.

Ingestion Errors (Phase 1) ​

Error CodeHTTPDescriptionResolution
err_ingest_url_unreachable400Source URL returned 404 or timed out.Ensure URL is public or pre-signed properly.
err_ingest_format_invalid400File is not MP4/MOV/MKV/WebM.Convert source video to a supported format.
err_ingest_duration_short400Video is under 5 minutes.Shorts engine is optimized for long-form (>5m).

AI Analysis Errors (Phase 2) ​

Error CodeHTTPDescriptionResolution
err_whisper_oom500VRAM exceeded during transcription.Automatic retry on larger GPU instance.
err_no_speech_detected422Whisper found < 100 words.Verify audio track isn't silent or purely music.

Audio/Visual Errors (Phase 3) ​

Error CodeHTTPDescriptionResolution
err_face_track_lost422No faces detected for > 60s.Ensure video contains visible speakers for reframing.

(Additional 50 error codes omitted for brevity in this summary, but documented in full API spec)


23. Complete Webhook Payload Examples ​

Below are the exhaustive, un-truncated JSON payload examples for every event the system emits.

shorts.job.started ​

json
{
  "event": "shorts.job.started",
  "timestamp": "2026-09-06T16:15:02Z",
  "job_id": "job_948a01bf28",
  "data": {
    "source_url": "https://storage.fotohub.app/raw/tech_podcast_ep104.mp4",
    "detected_duration_s": 3612.4
  }
}

shorts.job.transcribing ​

json
{
  "event": "shorts.job.transcribing",
  "timestamp": "2026-09-06T16:16:10Z",
  "job_id": "job_948a01bf28",
  "data": {
    "status": "in_progress",
    "progress_pct": 12,
    "eta_seconds": 245
  }
}

shorts.clip.rendered (Extended) ​

json
{
  "event": "shorts.clip.rendered",
  "timestamp": "2026-09-06T16:18:22Z",
  "job_id": "job_948a01bf28",
  "reference": "podcast_ep104_batch",
  "data": {
    "clip_id": "clip_78a1bc09",
    "virality_score": 93.5,
    "score_breakdown": {
      "hook_power": 96,
      "content_value": 94,
      "emotional_impact": 91,
      "pacing_energy": 90,
      "visual_appeal": 88
    },
    "title": "Why Autonomous AI Agents Will Replace Middle Management",
    "hook": "Stop hiring managers for tasks software can execute in 3 seconds.",
    "duration_s": 44.8,
    "timestamps": { "start_ms": 742000, "end_ms": 786800 },
    "video_url": "https://storage.fotohub.app/shorts/job_948a01bf28/clip_01_9x16.mp4",
    "cover_url": "https://storage.fotohub.app/shorts/job_948a01bf28/cover_01.jpg",
    "metrics": {
      "integrated_lufs": -14.05,
      "true_peak_db": -1.5,
      "filler_words_removed": 6,
      "multicam_layout": "stack"
    },
    "metadata": {
      "faces_detected": 2,
      "broll_injected": 1,
      "broll_timestamps": [
        {"start_ms": 750000, "end_ms": 754000, "tag": "robotics"}
      ]
    }
  }
}

24. Detailed Whisper JSON Output Schema ​

When hitting POST /v1/ai/transcribe, the engine returns highly granular data.

json
{
  "operation": "transcription",
  "audio_duration_s": 3612.4,
  "language": "en",
  "language_probability": 0.998,
  "segments": [
    {
      "id": 0,
      "seek": 0,
      "start": 0.0,
      "end": 4.5,
      "text": " Welcome back to the AI Summit 2026.",
      "tokens": [50364, 4390, 770, 281, 264, 7318, 12795, 2378, 13, 50589],
      "temperature": 0.0,
      "avg_logprob": -0.154,
      "compression_ratio": 1.1,
      "no_speech_prob": 0.001,
      "speaker": "SPEAKER_00",
      "words": [
        {"word": "Welcome", "start": 0.1, "end": 0.6, "probability": 0.99},
        {"word": "back", "start": 0.6, "end": 0.9, "probability": 0.99},
        {"word": "to", "start": 0.9, "end": 1.1, "probability": 0.99},
        {"word": "the", "start": 1.1, "end": 1.2, "probability": 0.99},
        {"word": "AI", "start": 1.2, "end": 1.8, "probability": 0.99},
        {"word": "Summit", "start": 1.8, "end": 2.5, "probability": 0.99},
        {"word": "2026.", "start": 2.5, "end": 3.8, "probability": 0.98}
      ]
    }
  ]
}

25. Extensive Advanced Audio Customization ​

For power users, settings.audio exposes raw FFmpeg filter chain parameters.

ParameterTypeRequiredDefaultDescription
settings.audio.highpass_freqintegerNo80Cutoff frequency in Hz.
settings.audio.denoise_nrintegerNo12Noise reduction factor (1-97).
settings.audio.compressor_thresholdfloatNo-20.0dB threshold.
settings.audio.compressor_ratiofloatNo4.0Compression ratio.
settings.audio.limiter_ceilingfloatNo-0.95dBFS true peak cap.

This maps directly to:

bash
ffmpeg -i input.wav -af "highpass=f=80,afftdn=nr=12:nf=-25,agate=threshold=0.01:ratio=10,acompressor=threshold=-20dB:ratio=4:makeup=2,loudnorm=I=-14:TP=-1.5:LRA=11" output.wav