MediaRouter / README.md
basyx's picture
Upload 629 files
0d8ef05 verified
|
Raw
History Blame Contribute Delete
77.3 kB
metadata
title: MediaRouter
emoji: 🎬
colorFrom: blue
colorTo: purple
sdk: docker
app_port: 7860
pinned: false

MediaRouter

MediaRouter is a production-oriented, CPU-optimized REST, Model Context Protocol (MCP), and YAML workflow API for FFmpeg, FFprobe, yt-dlp, and faster-whisper. It is designed to run unchanged as a Hugging Face Docker Space and to act as a reusable media backend for AI clients, n8n, and other automation systems.

The service accepts multipart uploads, JSON URLs, JSON Base64, n8n binary objects, and streamed raw request bodies. Every input becomes an InputMedia before it enters the operation layer, so operations never need to know how media arrived.

Architecture

REST /v1/*                       MCP stdio or /mcp/
     │                                  │
     ├───────────────┬──────────────────┘
                     ▼
        API-key authentication + scopes
          rate limits + audit logging
                     │
            request-ID + JSON logging
                     │
                InputResolver
          ├── streamed multipart/raw upload
          ├── Base64/n8n decoding
          ├── HTTP(S) streaming downloader + SSRF guard
          └── automatic yt-dlp extractor selection
                    │
                    ▼
             normalized InputMedia
                    │
          FFprobe validation/metadata
                    │
      optional versioned Template Engine
                    │
          operation (video/audio/image)
                    │
       concurrency-limited FFmpeg/Whisper
                    │
       published output + streaming URL
                    │
          background expiry cleanup

REST routes and MCP tools contain transport concerns only. Both use the same dependency container, InputResolver, MediaProcessor, operation functions, and infrastructure services. MediaProcessor orchestrates resolution, probing, validation, publishing, and metrics; operation modules build safe FFmpeg argument arrays; infrastructure services own subprocesses, downloads, model loading, and storage. No command is executed through a shell.

Project layout

media-api/
├── app/
│   ├── api/              # versioned, thin FastAPI routes
│   ├── core/             # settings, errors, logging, response models
│   ├── generation/       # provider-neutral durable generation foundation
│   ├── mcp/              # MCP server, registry, tools, resources, prompts
│   ├── models/           # InputMedia and request/result models
│   ├── operations/       # reusable FFmpeg operation functions
│   ├── security/         # API keys, scopes, roles, limits, audit, migrations
│   ├── services/         # FFmpeg, FFprobe, resolver, downloads, Whisper
│   ├── templates/        # YAML schemas, loader, registry, executor, categories
│   ├── workers/          # asynchronous expiry cleanup worker
│   └── container.py      # dependency construction
├── api/                   # requested top-level import compatibility
├── services/              # requested top-level import compatibility
├── operations/            # requested top-level import compatibility
├── workers/               # requested top-level import compatibility
├── core/                  # requested top-level import compatibility
├── models/                # requested top-level import compatibility
├── outputs/              # published, expiring request outputs
├── temp/                 # request/{uploads,outputs,logs}
├── tests/
├── Dockerfile
├── requirements.txt
├── requirements-dev.txt
├── .env.example
└── main.py

The top-level api/, services/, operations/, workers/, core/, and models/ packages mirror the canonical app/ modules as import-compatible entry points for integrations that use the requested layout. Runtime composition uses the single implementation under app/, so business logic is not duplicated.

Generation foundation

MediaRouter includes a tenant-scoped generation request/job foundation at /v1/generation. Optional wan and flux providers implement audited WAN 2.2 image-to-video and FLUX.2 Klein image-generation worker contracts. Each model stays unavailable until its backend-only URL/token are configured and live readiness verifies its exact worker identity. Requests use an idempotency key and may reference only canonical MediaRouter assets by opaque ID. See docs/generation-foundation.md, docs/generation-wan.md, and docs/generation-flux.md for the state machine, PostgreSQL migration, scopes, worker contracts, and security boundary.

Each request creates TEMP_DIR/<uuid>/{uploads,outputs,logs}. Completed files are atomically moved to OUTPUT_DIR/<uuid> so they can be streamed. Both locations expire after CLEANUP_MINUTES; active requests are protected from the cleanup worker.

Run locally

FFmpeg, FFprobe, and ImageMagick must be installed on the host.

python3.10 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
python -m app.security.cli generate-bootstrap --environment test
# Put the printed AUTH_BOOTSTRAP_* values in .env and save API_KEY securely.
uvicorn main:app --host 0.0.0.0 --port 7860

Open http://localhost:7860/docs for OpenAPI/Swagger or http://localhost:7860/redoc for ReDoc.

Docker

docker build -t media-api .
docker run --rm -p 7860:7860 --env-file .env media-api

The image uses python:3.10-slim, installs FFmpeg, FFprobe, ImageMagick and the system libraries needed by CTranslate2/faster-whisper, clears apt and pip caches, runs as UID 1000, exposes port 7860, and includes a container health check.

The Docker image defaults to APP_ENVIRONMENT=development so it can start without external PostgreSQL or a Vercel CORS origin. For production, set APP_ENVIRONMENT=production and configure external PostgreSQL/Supabase, apply the explicit migrations, exact CORS origins, and the required RLS/worker roles before startup. See the production deployment gate.

Deploy to Hugging Face Spaces

  1. Create a new Space and select Docker as the SDK.
  2. Push the contents of this directory to the Space repository. Keep the YAML block at the top of this README; app_port is already 7860.
  3. Apply the PostgreSQL migrations in the documented dependency order, using a controlled administrative connection.
  4. For a quick start, the Space can run with the default development environment and local SQLite storage. For production, configure the required PostgreSQL/RLS/CORS variables and roles in Space Settings and set APP_ENVIRONMENT=production. Keep SECURITY_AUTO_MIGRATE=false and SOCIAL_AUTO_MIGRATE=false.
  5. Generate the initial administrator key locally with python -m app.security.cli generate-bootstrap --environment live. Store API_KEY in a password manager. Add only the printed AUTH_BOOTSTRAP_KEY_HASH, AUTH_BOOTSTRAP_KEY_PREFIX, and AUTH_BOOTSTRAP_ENVIRONMENT under Settings → Secrets. Do not commit them.
  6. Wait for the Docker build. The first Whisper call downloads the selected model to the Hugging Face cache. Persistent storage is optional for the model cache, but avoids downloading models again after a cold rebuild.
  7. Check the public https://<owner>-<space>.hf.space/health, then run scripts/deployment_smoke.py against the Space before accepting traffic.

If using the supplied media-api-huggingface.zip, extract it first and push the extracted files so Dockerfile and this README.md are at the Space repository root. Do not commit the ZIP as the only repository file.

Use one Uvicorn process in a CPU Space. FFmpeg and Whisper concurrency is managed in-process by MAX_WORKERS; multiple Uvicorn workers duplicate Whisper models and memory.

Production keys, workspaces, projects, audit records, and rate aggregates live in external PostgreSQL/Supabase. SQLite remains a local-development and test backend only. Media files in TEMP_DIR/OUTPUT_DIR are expiring working data; see the deployment gate for their filesystem lifecycle.

Connect the Vercel frontend to Hugging Face

The Next.js frontend is an authenticated backend-for-frontend (BFF): browser requests go to its same-origin /api/backend/* route, and only that server-side route attaches a backend API key. The browser must never receive an API key. All FFmpeg, Whisper, yt-dlp, uploads, and media processing remain in the Hugging Face Space.

For the current hosted backend, the public origin is:

https://basyx-mediarouter.hf.space

First verify that the Space has started successfully. This endpoint is public and must return HTTP 200 with "status": "healthy" before the frontend can connect:

curl -i https://basyx-mediarouter.hf.space/health

Hugging Face Space secrets

Create the bootstrap administrator locally, save its plaintext API_KEY in a password manager, and add only the three generated AUTH_BOOTSTRAP_* values to Hugging Face Space → Settings → Secrets. Enter each value in the value field only: no quotes, no NAME=, and no line breaks. Never commit these values.

AUTH_BOOTSTRAP_KEY_HASH=<64-character lowercase SHA-256 hash>
AUTH_BOOTSTRAP_KEY_PREFIX=mp_live_<first-eight-secret-characters>
AUTH_BOOTSTRAP_ENVIRONMENT=live

The bootstrap fields are optional after the first successful start. If a Space fails at startup with Bootstrap key hash or prefix is malformed, remove duplicate or stale AUTH_BOOTSTRAP_* entries from both the Space Variables and Secrets panels, restart once, then recreate the three exact values above as Secrets. A blank hash and blank prefix deliberately disable bootstrap creation; a non-empty value must match the format exactly.

Recommended backend deployment settings are:

APP_ENVIRONMENT=production
BASE_URL=https://basyx-mediarouter.hf.space
DATABASE_URL=postgresql+asyncpg://<security-role>:<password>@<host>/<database>
SECURITY_DATABASE_ROLE=mediarouter_security_service
SECURITY_ENFORCE_RLS=true
SECURITY_AUTO_MIGRATE=false
CORS_ALLOWED_ORIGINS=https://<your-vercel-project>.vercel.app
SOCIAL_AUTO_MIGRATE=false
AUTH_ROLE_SCOPES={}
MCP_STDIO_API_KEY=<a-valid-mp_live-or-mp_test-key-if-stdio-MCP-is-enabled>

When social automation is enabled, configure its existing separate tenant and worker PostgreSQL URLs/roles as described in the deployment gate. BASE_URL is the public Hugging Face origin, not the Vercel frontend URL.

Vercel environment variables

Import this repository with frontend as the Vercel root directory. Configure the following values for Production and for Preview if preview deployments need to connect to the backend. Redeploy after changing an environment variable.

# Server-only backend connection and credential. Never use NEXT_PUBLIC_ for a key.
MEDIAROUTER_API_URL=https://basyx-mediarouter.hf.space
MEDIAROUTER_MCP_URL=https://basyx-mediarouter.hf.space
MEDIAROUTER_API_TOKEN=mp_live_<saved-bootstrap-admin-key-or-role-key>
MEDIAROUTER_API_TIMEOUT=120000

# Auth.js / OAuth (server only)
AUTH_SECRET=<output-of-openssl-rand-base64-32>
NEXTAUTH_URL=https://<your-vercel-project>.vercel.app
AUTH_GITHUB_ID=<github-oauth-client-id>
AUTH_GITHUB_SECRET=<github-oauth-client-secret>
# Optional: configure both values to enable Google sign-in.
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_SESSION_MAX_AGE=28800

# Human-role mapping. Put your own OAuth email in the Admin list.
AUTH_USER_ROLES={}
AUTH_ADMIN_EMAILS=<your-verified-oauth-email>
AUTH_DEVELOPER_EMAILS=
AUTH_OPERATOR_EMAILS=
AUTH_VIEWER_EMAILS=
AUTH_DEFAULT_ROLE=Viewer

# Public, non-secret display and capability settings.
NEXT_PUBLIC_API_URL=https://basyx-mediarouter.hf.space
NEXT_PUBLIC_MCP_URL=https://basyx-mediarouter.hf.space
NEXT_PUBLIC_APP_NAME=MediaRouter
NEXT_PUBLIC_ENABLE_MCP=true
NEXT_PUBLIC_ENABLE_MARKETPLACE=true

MEDIAROUTER_API_TOKEN is the server-only fallback for every human role. In a least-privilege production setup, create dedicated backend role keys after bootstrap and replace it with the appropriate keys instead:

MEDIAROUTER_ADMIN_API_TOKEN=mp_live_<admin-key>
MEDIAROUTER_DEVELOPER_API_TOKEN=mp_live_<developer-key>
MEDIAROUTER_OPERATOR_API_TOKEN=mp_live_<operator-key>
MEDIAROUTER_VIEWER_API_TOKEN=mp_live_<viewer-key>

GitHub’s production callback URL is https://<your-vercel-project>.vercel.app/api/auth/callback/github; Google’s is the equivalent /api/auth/callback/google. Generate AUTH_SECRET with openssl rand -base64 32. Never put API keys, OAuth client secrets, or AUTH_SECRET in a NEXT_PUBLIC_* variable.

Verify the complete connection

  1. Confirm https://basyx-mediarouter.hf.space/health returns 200.
  2. Redeploy Vercel after its environment variables are set.
  3. Sign in using an OAuth account assigned to AUTH_ADMIN_EMAILS.
  4. Visit the frontend /api/backend/health while signed in. It should proxy the healthy backend response.
  5. If it returns Backend authentication unavailable, set MEDIAROUTER_API_TOKEN or the token matching the signed-in user’s role. If it returns 401, the backend key is expired, disabled, revoked, or not the key represented by the Space bootstrap hash.

Authentication and authorization

MediaRouter uses stateless opaque API keys. There are no passwords, login sessions, cookies, or JWTs. Except for the public endpoints below, every REST and MCP request must send:

Authorization: Bearer mp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys contain at least 256 bits of cryptographically secure entropy and use an environment prefix (mp_live_ or mp_test_). The database stores only a SHA-256 hash, a short display prefix, lifecycle state, scopes, role, limits, timestamps, and operator metadata. A plaintext secret is returned once at creation or rotation and cannot be recovered later.

The only unauthenticated endpoints are GET /, GET /health, GET /version, GET /docs, GET /openapi.json, and GET /redoc. GET /v1/auth/context is protected but requires no product scope, allowing a client to validate a key and retrieve its safe effective authorization context.

Bootstrap and key creation

Generate a first administrator without putting plaintext key material in the server configuration:

python -m app.security.cli generate-bootstrap --environment live

The command prints API_KEY once and hash-only AUTH_BOOTSTRAP_* values. Store API_KEY in a secret manager; configure the hash, prefix, and environment on the server. On first startup, and only when the api_keys table is empty, MediaRouter inserts that bootstrap administrator. Removing the bootstrap variables after the first successful startup is recommended. If direct database access is available, python -m app.security.cli create --name "Recovery Admin" --role admin can create a recovery key and likewise prints its secret once.

The idempotent migration is documented in app/security/migrations/0001_api_key_security.sql. Startup applies equivalent SQLAlchemy metadata for api_keys, audit_logs, and rate_limits, including indexes on key prefix/hash, lifecycle state, expiration, audit request/key timestamps, and rate buckets. Back up the database before changing schema or moving persistent storage.

An administrator can then create a narrower key:

curl -X POST "$MEDIAROUTER_URL/v1/api-keys" \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "name":"Production n8n",
    "environment":"live",
    "role":"operator",
    "expires_in_seconds":2592000,
    "notes":"Media publishing workflow"
  }'

The api_key property in the response is the only copy of the new secret. The lifecycle API is:

Method Endpoint Purpose
GET /v1/api-keys List safe key metadata
POST /v1/api-keys Create a key and return its secret once
GET / PATCH / DELETE /v1/api-keys/{id} Read, rename/update, or irrevocably revoke
POST /v1/api-keys/{id}/rotate Create a replacement; optional grace period up to 24 hours
POST /v1/api-keys/{id}/disable Temporarily reject an active key
POST /v1/api-keys/{id}/enable Re-enable a disabled, non-revoked key
GET /v1/api-keys/capabilities Discover scopes, configured roles, and limit defaults
GET /v1/audit-logs Read authenticated request audit records

Revocation is immediate and irreversible without restarting the service. Rotation creates a distinct record and secret. With {"grace_period_seconds":300}, both keys work for five minutes; without a grace period, the old key is rejected immediately. Expired keys always fail, including disabled keys that are later enabled.

Scopes and roles

Scopes are enforced before route execution. Explicit scopes are combined with the selected role; admin grants all scopes.

Domain Scopes
Templates templates:read, templates:run
Operations operations:read, operations:execute
Jobs jobs:read, jobs:create, jobs:cancel
Assets assets:read, assets:write, assets:delete
MCP mcp:read, mcp:execute
Generation foundation generation:providers:read, generation:requests:read, generation:requests:create, generation:jobs:cancel
System system:read
Administration admin

Built-in roles are admin (all access), developer (read/run/execute, job control, and asset writes), operator (read/run/execute and job control), and viewer (read-only). Override or add roles with an AUTH_ROLE_SCOPES JSON object, for example {"publisher":["templates:read","templates:run","assets:read"]}. Unknown scopes are rejected at startup or key validation rather than silently ignored.

Limits, auditing, and error contracts

Each key has independently configurable requests per minute, concurrent processing jobs, uploads per hour, and processing bytes per UTC day. Defaults are 100, 10, 20, and 100 GiB. A limit failure returns 429 Too Many Requests with a Retry-After header. Hugging Face Spaces should run one Uvicorn process, which also prevents duplicate in-memory concurrency and Whisper state.

Every authenticated HTTP request records its request ID, key ID/name, trusted client IP, user agent, method, endpoint, status, processing time, and upload/download byte counts. Standalone stdio MCP calls generate equivalent tool audit records. Logs never contain plaintext API keys.

Authentication failures are intentionally indistinguishable:

{"error":"Unauthorized","message":"Invalid or expired API key."}

Missing scopes return 403 with {"error":"Forbidden","message":"Missing required scope."}. Limit failures return 429 with {"error":"Rate limit exceeded","message":"Retry later."}. Clients must never use response differences to infer whether a key ID or hash exists.

REST, SDK, and n8n clients

All SDKs use the same bearer header. A Python SDK constructor can expose MediaPlatform(base_url="...", api_key="mp_live_..."); a JavaScript SDK should accept the same two values. Neither needs a token refresh flow.

import os

import httpx

api_key = os.environ["MEDIAROUTER_API_KEY"]
client = httpx.Client(
    base_url="https://OWNER-SPACE.hf.space",
    headers={"Authorization": f"Bearer {api_key}"},
)
templates = client.get("/v1/templates").raise_for_status().json()
const { MEDIAROUTER_URL: baseUrl, MEDIAROUTER_API_KEY: apiKey } = process.env;
const response = await fetch(`${baseUrl}/v1/templates`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) throw new Error(`MediaRouter returned ${response.status}`);
const templates = await response.json();

In n8n, create a reusable Header Auth credential with header name Authorization and value Bearer mp_live_.... Each HTTP Request node then needs only the Base URL/path and that credential. The future official MediaRouter node uses two credential fields—Base URL and API Key—and sends this header automatically.

Security recommendations: give each automation its own least-privilege key; use mp_test_ outside production; prefer short expirations for temporary agents; rotate on a schedule; revoke on suspected exposure; never put keys in URLs, client logs, Git, NEXT_PUBLIC_* variables, or media metadata; and treat frontend validation only as UX because the backend remains authoritative.

Official SDKs

Publish-ready clients live under sdk/:

  • @mediarouter/media-platform — strict TypeScript with ESM and CommonJS builds, typed resources, retries, uploads, downloads, polling, and MCP helpers.
  • media-platform — typed Python resource client with stdlib-only runtime dependencies, streamed downloads, multipart uploads, retries, polling, and typed exceptions.

Both clients use the existing /openapi.json as their discovery source and keep FFmpeg, Whisper, yt-dlp, and all business logic on the backend. Run the SDK workflow for type checks, mocked transport tests, Python tests, and package builds before publishing a semantic-versioned release.

Model Context Protocol (MCP)

MCP is an open protocol that lets an AI client discover and call typed tools, read application resources, and use reusable prompts. This project uses the official Python MCP SDK and exposes the same processing implementation through both interfaces:

FastAPI REST routes ─┐
                     ├─ InputResolver → MediaProcessor → shared services/operations
MCP tools ───────────┘                         ├─ FFmpeg / FFprobe
                                               ├─ yt-dlp
                                               └─ faster-whisper

No FFmpeg command, download implementation, transcription implementation, input parser, logger, cleanup implementation, or authentication implementation is duplicated in app/mcp. Mounted and standalone Streamable HTTP pass through the same API-key middleware as REST. The transport-neutral registry also enforces MCP scopes, limits, and audit logging for stdio calls.

MCP transports

The normal Docker command starts FastAPI on port 7860 and serves both REST and MCP:

  • REST and OpenAPI: https://<owner>-<space>.hf.space/v1/* and /docs
  • Streamable HTTP MCP: https://<owner>-<space>.hf.space/mcp/

The trailing slash on /mcp/ is recommended. Streamable HTTP is stateless and returns JSON responses while remaining compliant with the MCP transport. The application owns the MCP session-manager lifespan, so it works correctly even though the MCP ASGI application is mounted under FastAPI.

For a local stdio client, run from the project directory:

MCP_STDIO_API_KEY="$MEDIAROUTER_API_KEY" python -m app.mcp.server

Structured logs are sent to stderr in stdio mode so JSON-RPC messages on stdout are never corrupted. A standalone MCP-only HTTP process is also available for development:

python -m app.mcp.server --transport streamable-http

Use the normal uvicorn main:app ... command in Hugging Face because it exposes both interfaces together.

MCP client configuration

Claude Desktop and other clients that accept the conventional mcpServers JSON can launch stdio directly. Replace the paths with absolute paths on the client machine:

{
  "mcpServers": {
    "enterprise-media": {
      "command": "/absolute/path/media-api/.venv/bin/python",
      "args": ["-m", "app.mcp.server"],
      "cwd": "/absolute/path/media-api",
      "env": {
        "MCP_STDIO_API_KEY": "mp_live_REPLACE_WITH_KEY",
        "TEMP_DIR": "/absolute/path/media-api/temp",
        "OUTPUT_DIR": "/absolute/path/media-api/outputs",
        "WHISPER_MODEL": "small",
        "MAX_WORKERS": "2"
      }
    }
  }
}

Clients that support remote Streamable HTTP can use:

{
  "mcpServers": {
    "enterprise-media": {
      "url": "https://OWNER-SPACE.hf.space/mcp/",
      "headers": {
        "Authorization": "Bearer mp_live_REPLACE_WITH_KEY"
      }
    }
  }
}

Common client locations and connection choices are:

Client Configuration
Claude Desktop Add the stdio mcpServers entry to claude_desktop_config.json, then restart Claude.
ChatGPT Add a remote custom connector in developer/connector settings with the Space /mcp/ URL. The Space must be reachable from ChatGPT.
Cursor Put the remote mcpServers entry in .cursor/mcp.json, or use the stdio entry for local files.
VS Code Add an HTTP server under servers in .vscode/mcp.json: {"type":"http","url":"https://OWNER-SPACE.hf.space/mcp/"}.
Continue Add an MCP server in Continue configuration using the Streamable HTTP URL or the stdio command above.
Cline Open MCP Servers → Configure and add the mcpServers JSON entry.
Windsurf Add the same entry in Windsurf MCP settings (mcp_config.json).

Client configuration keys can vary between releases; select Streamable HTTP, not legacy SSE, and configure an Authorization: Bearer <api_key> header. A client without custom-header support cannot connect to a protected remote MCP endpoint; use its stdio mode with MCP_STDIO_API_KEY instead.

For a Docker-based stdio client, override the image command and keep stdin open:

docker run --rm -i \
  -v "$PWD/temp:/app/temp" \
  -v "$PWD/outputs:/app/outputs" \
  -e MCP_STDIO_API_KEY="$MEDIAROUTER_API_KEY" \
  media-api python -m app.mcp.server

In stdio mode, consume the returned output_file locally. In Streamable HTTP mode, set BASE_URL=https://<owner>-<space>.hf.space so download_url is absolute.

MCP media input

Every MCP media tool delegates to the existing InputResolver. A MediaInput accepts exactly one source:

{"url": "https://cdn.example.com/video.mp4"}
{
  "base64": "data:audio/wav;base64,UklGR...",
  "filename": "speech.wav",
  "mime_type": "audio/wav"
}
{
  "binary": {
    "data": "AAAAHGZ0eXBpc29t...",
    "fileName": "clip.mp4",
    "mimeType": "video/mp4"
  }
}
{"temp_path": "/app/outputs/<request-uuid>/converted.mp4"}

The binary object accepts n8n properties such as data, file, video, or audio. temp_path is copied into the new request workspace and is accepted only when it resolves below configured TEMP_DIR or OUTPUT_DIR; arbitrary host paths and traversal are rejected. URLs use the same SSRF protection and automatic yt-dlp detection as REST.

MCP tool arguments are JSON by protocol, so a client file attachment must be represented as a URL, Base64/data URI, n8n binary object, or a managed temp_path. Native multipart/form-data and streamed application/octet-stream remain available on every corresponding REST endpoint and enter the same resolver and service layer.

Single-input tools use an input argument. Multi-input tools use inputs; composition tools use descriptive arguments such as video, audio, watermark, overlay, or subtitles. A successful file-producing call returns both the managed server output_file and normal REST download_url:

{
  "success": true,
  "request_id": "43b32630-9b32-4b27-b038-a39fbfd4b803",
  "processing_time": 1.52,
  "output_file": "/app/outputs/43b32630-9b32-4b27-b038-a39fbfd4b803/compressed.mp4",
  "download_url": "https://OWNER-SPACE.hf.space/v1/media/43b32630-4b27-b038-a39fbfd4b803/compressed.mp4",
  "metadata": {}
}

Errors use the same safe application codes and never expose raw Python exceptions:

{
  "success": false,
  "request_id": "43b32630-9b32-4b27-b038-a39fbfd4b803",
  "processing_time": 0.01,
  "error": {
    "code": "INVALID_INPUT",
    "message": "The managed temporary file does not exist",
    "details": null
  }
}

Available MCP tools

All tools return the structured envelope above. Defaults shown here match the callable schemas advertised to MCP clients.

Video tool Purpose and principal options
compress_video CPU-optimized compression; crf=28, preset=medium, format=mp4, optional max_width and bitrate.
resize_video Resize using width, height, fit (contain, cover, fill), and format.
crop_video Crop using required width/height and optional x/y.
trim_video Trim with start plus duration or end.
convert_video Convert to a supported video format.
merge_videos Normalize and merge inputs; optional output width/height.
concat_videos Concatenate inputs; optional stream_copy for already-compatible streams.
watermark_video Watermark video with an image; position, opacity, and watermark_scale.
overlay_video Overlay media on video; position, opacity, and watermark_scale.
extract_audio Extract a video audio stream to format (default MP3).
replace_audio Replace the audio in video with the supplied audio.
remove_audio Remove all audio streams.
generate_thumbnail Create a JPEG at timestamp; optional FFmpeg quality.
extract_frames Extract at fps, optionally bounded by max_frames, and return a ZIP.
burn_subtitles Burn SRT, VTT, ASS, or SSA subtitles into video; optional ASS style.
Audio tool Purpose and principal options
convert_audio Convert to MP3, WAV, AAC, M4A, FLAC, OGG, or Opus.
normalize_audio EBU loudness normalization with target_lufs=-16.
trim_audio Trim with start plus duration or end.
merge_audio Normalize and merge multiple inputs.
remove_silence Remove silence using a threshold such as -45dB.
Image tool Purpose and principal options
image_to_video Turn one still image into H.264 video; duration and fps.
slideshow Create a video from inputs; duration_per_image, width, height, and fps.
watermark_image Watermark an image; position, opacity, watermark_scale, and output format.
resize_image Resize with width, height, fit, and output format.
Whisper tool Purpose and principal options
transcribe Transcribe speech; optional model, language, output_format, beam_size, and vad_filter.
translate Translate speech to English with the same options.
detect_language Detect spoken language using an optional model.
generate_srt Generate SRT with optional model, language, and task.
generate_vtt Generate WebVTT with optional model, language, and task.
generate_json Generate JSON transcript segments with optional model, language, and task.
yt-dlp tool Purpose and principal options
download_video Download best video; optional yt-dlp format_selector.
download_audio Download/convert audio; audio_format=mp3 and optional format_selector.
video_metadata Extract platform metadata without downloading media.
playlist_metadata Extract flat playlist metadata, bounded by max_entries (1–1000).
list_formats List normalized available audio/video formats.
FFprobe tool Purpose
probe_media Complete normalized metadata.
probe_video_metadata Video codec, dimensions, FPS, rotation, duration, and streams.
audio_metadata Audio streams, duration, and bitrate.
stream_info Video, audio, and subtitle stream summaries.
container_info Container, tags, creation date, size, bitrate, and duration.

MCP tool names must be unique. The yt-dlp metadata tool retains the requested video_metadata name; the FFprobe view is exposed as probe_video_metadata to disambiguate it.

Utility tool Purpose
health Application and dependency health without loading Whisper.
cleanup_temp Run the existing expired-workspace cleanup pass.
disk_usage Capacity and use for temp and output storage.
system_info CPU, memory, Python, platform, and process details.
supported_operations Tool names grouped by domain.
supported_formats Media, transcription, model, and yt-dlp formats.
ffmpeg_version Installed FFmpeg version banner.
whisper_models Allowed/default models and CPU compute configuration.
yt_dlp_version Installed yt-dlp package version.
Template tool Purpose
list_templates Discover all dynamically loaded template versions, optionally filtered by category.
template_details Return complete metadata and pipeline documentation for id, id@version, or id@latest.
run_template Resolve one or more media inputs and execute a versioned YAML workflow.
template_categories Return categories discovered from loaded YAML templates.

Available MCP resources

Resources return structured JSON, including a safe success indicator:

Resource URI Contents
media://operations Registered operations grouped by video, audio, image, Whisper, yt-dlp, probe, templates, and system.
media://formats Supported containers, transcription formats, models, and audio download formats.
media://codecs Installed FFmpeg codecs and encode/decode capabilities.
media://health Application version and dependency availability.
media://configuration Non-secret runtime limits and paths.
media://version Application, MCP SDK, yt-dlp, and faster-whisper versions.

Available MCP prompts

Prompt Workflow
compress_for_social_media Probe, resize as appropriate, and call compress_video.
youtube_to_mp3 Call download_audio as MP3.
download_and_transcribe Download audio, then call transcribe on its managed output.
generate_subtitles Call generate_srt or generate_vtt.
extract_audio Call the extract_audio tool in the requested format.
make_thumbnail Call generate_thumbnail at a timestamp.
probe_media Call probe_media and summarize streams/container data.
instagram_reel Probe, resize to 1080×1920, then compress.
tiktok_video Resize vertically and compress for TikTok.
podcast_audio Normalize to -16 LUFS and optionally convert.

Example MCP calls

AI clients construct the JSON-RPC envelope automatically. The tool argument payloads are:

Compress a remote video:

{
  "name": "compress_video",
  "arguments": {
    "input": {"url": "https://cdn.example.com/input.mp4"},
    "crf": 28,
    "preset": "veryfast",
    "format": "mp4",
    "max_width": 1280
  }
}

Transcribe Base64 audio:

{
  "name": "transcribe",
  "arguments": {
    "input": {
      "base64": "data:audio/mpeg;base64,SUQz...",
      "filename": "meeting.mp3",
      "mime_type": "audio/mpeg"
    },
    "model": "small",
    "output_format": "json"
  }
}

Download a YouTube video:

{
  "name": "download_video",
  "arguments": {
    "url": "https://www.youtube.com/watch?v=VIDEO_ID"
  }
}

Generate subtitles:

{
  "name": "generate_srt",
  "arguments": {
    "input": {"url": "https://cdn.example.com/interview.mp4"},
    "language": "en",
    "task": "transcribe"
  }
}

Probe media:

{
  "name": "probe_media",
  "arguments": {
    "input": {"url": "https://cdn.example.com/input.mp4"}
  }
}

Enterprise Template Engine

The Template Engine is a versioned orchestration layer above the existing media operations. At startup, TemplateLoader recursively scans TEMPLATE_DIR for .yaml and .yml files, parses them with yaml.safe_load_all, validates every definition, and builds an immutable TemplateRegistry. REST and MCP resolve media through the same InputResolver, then TemplateExecutor chains allow-listed operation functions through OperationExecutor:

REST /v1/templates/run ─┐
                       ├─ InputResolver → Template Registry/Validator
MCP run_template ──────┘                         │
                                                ▼
                                 parameter substitution + pipeline
                                                │
                                                ▼
                              existing operation functions and services

Intermediate artifacts stay inside TEMP_DIR/<request-id>/outputs; only the final result is published. Every generated media artifact is FFprobed before the next step when applicable. Logs include the template ID/version, resolved parameters, request ID, operations, wall and CPU time, memory, and output bytes. Existing FFmpeg logging still records commands and bounded stdout/stderr.

Template folders and built-ins

app/templates/
├── __init__.py
├── loader.py
├── registry.py
├── executor.py
├── schema.py
├── validator.py
├── models.py
└── categories/
    ├── social/
    ├── faceless/
    ├── motivation/
    ├── lyrics/
    ├── podcast/
    ├── subtitles/
    ├── youtube/
    ├── conversion/
    ├── branding/
    ├── utility/
    └── custom/

The distribution contains 71 versioned workflows:

Category Templates
Social youtube_shorts, tiktok_hd, facebook_reel, instagram_reel, linkedin_video, twitter_video, whatsapp_status
Faceless reddit_story, ai_story, movie_recap, history_short, true_crime, did_you_know, top10_video, facts_video, book_summary, finance_short, crypto_news, tech_news
Motivation motivational_video, morning_motivation, business_motivation, gym_motivation, success_quotes, stoic_quotes, daily_quotes, affirmations
Lyrics lyrics_basic, karaoke, spotify_style, cinematic_lyrics, neon_lyrics, music_video
Podcast podcast_video, podcast_short, audiogram, waveform_video
Subtitles/AI auto_subtitles, translate_video, transcribe, transcribe_srt, transcribe_vtt, transcribe_json, youtube_to_transcript
YouTube youtube_to_mp3, youtube_to_audio, youtube_to_shorts, youtube_to_podcast, download_only
Branding company_branding, creator_branding, watermark, intro_outro, logo_animation
Conversion mp4, mov, avi, webm, gif, mp3, wav, aac, flac
Utility thumbnail_pack, extract_frames, extract_audio, merge_videos, concat_videos, compress_max, compress_balanced, compress_mobile

Faceless and motivation templates format already assembled source media; they do not pretend to generate narration, images, or copyrighted source content. Lyrics templates consume supplied timed subtitle files. waveform_video consumes a pre-rendered waveform artwork image plus audio, reusing the existing image/video and audio replacement operations.

Template schema and parameters

Each template includes the required metadata (id, name, description, category, author, version, tags, estimated_runtime, supported_inputs, and supported_outputs), parameter documentation, at least one pipeline operation, an output contract, and examples:

id: instagram_reel_custom
name: Instagram Reel Custom
category: custom
description: Resize and compress video for a vertical Instagram Reel.
author: Your Team
version: 1
tags: [instagram, vertical]
estimated_runtime: medium
supported_inputs: [video, url, ytdlp]
supported_outputs: [mp4]

parameters:
  crf:
    type: integer
    default: 23
    minimum: 0
    maximum: 51
  width:
    type: integer
    default: 1080
    minimum: 2

pipeline:
  - operation: resize
    width: "{{ width }}"
    height: 1920
    fit: cover
  - operation: fps
    value: 30
  - operation: compress
    crf: "{{ crf }}"
    preset: veryfast
    format: mp4

output:
  format: mp4

examples:
  - input: {url: "https://example.com/input.mp4"}
    parameters: {crf: 23, width: 1080}

Parameter types are string, integer, number, boolean, array, and object. Definitions can use required, default, enum, minimum, maximum, min_length, and max_length. Unknown parameters and type coercion are rejected: the string "23" is not accepted for an integer parameter.

Variables use only {{ parameter_name }} syntax—expressions and executable template code are not supported. When a YAML value consists only of a variable, substitution preserves the declared type, so an integer remains an integer and a boolean can control when:

parameters:
  add_logo: {type: boolean, default: false}
pipeline:
  - operation: watermark_video
    when: "{{ add_logo }}"
    inputs: [current, original:1]

Multiple-input and multi-artifact workflows can select current, original, original:N, originals, or artifact:name. A step can save its result for a later step:

pipeline:
  - operation: transcribe
    output_format: srt
    save_as: captions
  - operation: burn_subtitles
    inputs: [original:0, artifact:captions]

Supported YAML operation names are allow-listed aliases over existing implementations. They cover video compression/resizing/cropping/trimming/rotation/merging/concatenation/conversion/overlays/subtitles/effects, audio extraction/conversion/normalization/trimming/merging/filters, image conversion/composition, Whisper transcribe/translate, and resolver-backed download. No YAML value is used as a command name or shell string.

Validation and versioning

The application refuses to start when a configured YAML file has invalid syntax, missing metadata, duplicate ID/version, invalid defaults, undeclared variables, unsafe selectors, unsupported operations, or an output format outside supported_outputs. Runtime parameters are validated again before any operation runs.

References support stable version selection:

  • youtube_shorts resolves the highest installed version.
  • youtube_shorts@latest explicitly resolves the highest installed version.
  • youtube_shorts@1 remains pinned to version 1 when version 2 is added.

Keep old YAML documents when introducing a new version so existing automations remain reproducible. A YAML file may contain one template, multiple --- documents, or a top-level templates list.

Template REST API

Method Endpoint Purpose
GET /v1/templates List all versions with parameters, metadata, and examples; optional ?category=social.
GET /v1/templates/categories List categories discovered from YAML.
GET /v1/templates/{reference} Get full details and pipeline for an ID/version reference.
POST /v1/templates/run Resolve media and execute a template.

List and inspect:

curl -H "Authorization: Bearer $MEDIAROUTER_API_KEY" http://localhost:7860/v1/templates
curl -H "Authorization: Bearer $MEDIAROUTER_API_KEY" http://localhost:7860/v1/templates/instagram_reel@1

Run with a direct or yt-dlp-supported URL:

curl -X POST http://localhost:7860/v1/templates/run \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "template":"instagram_reel@latest",
    "input":{"url":"https://example.com/input.mp4"},
    "parameters":{"crf":21}
  }'

Run with multipart media; parameters is a JSON form field:

curl -X POST http://localhost:7860/v1/templates/run \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -F 'file=@input.mp4' \
  -F 'template=youtube_shorts@1' \
  -F 'parameters={"crf":23,"max_duration":60}'

Multiple-input templates collect uploaded files in multipart order or use an inputs JSON array. For company_branding, input 0 is video and input 1 is the logo; for intro_outro, send intro, main, and outro in that order.

Template MCP examples

Discover templates:

{"name":"list_templates","arguments":{"category":"social"}}

Inspect a pinned version:

{"name":"template_details","arguments":{"template":"youtube_shorts@1"}}

Execute a URL template:

{
  "name": "run_template",
  "arguments": {
    "template": "youtube_to_mp3@latest",
    "input": {"url": "https://www.youtube.com/watch?v=VIDEO_ID"},
    "parameters": {}
  }
}

For multiple inputs, omit input and provide inputs. MCP returns the same structured request ID, output_file, download_url, metadata, and safe error contract as the other tools.

Template n8n examples

For an n8n URL workflow, configure an HTTP Request node with POST, JSON body, and /v1/templates/run:

{
  "template": "compress_mobile@1",
  "input": {"url": "{{$json.media_url}}"},
  "parameters": {"crf": 28}
}

For n8n binary JSON input:

{
  "template": "mp3@1",
  "input": {
    "binary": {
      "data": "{{$binary.audio.data}}",
      "fileName": "{{$binary.audio.fileName}}",
      "mimeType": "{{$binary.audio.mimeType}}"
    }
  },
  "parameters": {}
}

For large n8n files, send the binary property as multipart under any file field, plus text fields template and JSON parameters. The existing resolver supports binary.data, binary.file, binary.video, binary.audio, Base64/data URIs, raw bytes, direct HTTP(S), and automatic yt-dlp selection for templates exactly as it does for normal operations.

Adding custom templates

  1. Add a .yaml or .yml file below app/templates/categories/custom/ or a mounted TEMPLATE_DIR.
  2. Choose a unique id and positive integer version; never replace an old version used by automation.
  3. Declare strict parameter types/defaults and complete metadata.
  4. Compose allow-listed existing operations. Use save_as and input selectors for branched workflows.
  5. Restart the process. Startup scanning automatically validates, registers, exposes, and documents the template through REST and MCP—no Python registration change is required.
  6. If a genuinely new media primitive is needed, implement it once in app/operations, validate its arguments, add it to OPERATION_BINDINGS, and then reference it from any number of YAML workflows.

Set TEMPLATE_DIR to an external mounted directory to operate a private workflow catalog without modifying application code. Invalid catalogs fail closed and are never partially executed.

Input contract

All media-processing and probe endpoints use the same resolver. Operation parameters can be top-level JSON fields, multipart text fields, an options JSON object, or query parameters. Body/form values take precedence over query parameters.

Multipart file

curl -X POST http://localhost:7860/v1/video/compress \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -F 'file=@input.mp4' \
  -F 'crf=28' \
  -F 'preset=veryfast'

Multiple-input operations accept repeated or differently named file fields; every uploaded file part is collected in form order.

curl -X POST http://localhost:7860/v1/video/watermark \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -F 'video=@input.mp4' \
  -F 'watermark=@logo.png' \
  -F 'position=bottom-right' \
  -F 'opacity=0.7'

JSON URL

curl -X POST http://localhost:7860/v1/video/resize \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://cdn.example.com/video.mp4","width":1280,"height":720,"fit":"contain"}'

URLs supported by a non-generic yt-dlp extractor automatically use yt-dlp. This covers YouTube, TikTok, Instagram, Facebook, X/Twitter, Reddit, Vimeo, SoundCloud, and the other sites supported by the installed yt-dlp release. Ordinary direct HTTP(S) media is downloaded in bounded chunks. Redirect targets are revalidated, credentials in URLs are rejected, and private/loopback/link-local addresses are blocked unless ALLOW_PRIVATE_URLS=true.

For multiple remote inputs:

{
  "inputs": [
    {"url": "https://cdn.example.com/part-1.mp4"},
    {"url": "https://cdn.example.com/part-2.mp4"}
  ],
  "width": 1280,
  "height": 720
}

JSON Base64 and n8n binary

{
  "base64": "data:audio/wav;base64,UklGR...",
  "filename": "speech.wav",
  "format": "mp3"
}

The resolver recognizes n8n properties named binary.data, binary.file, binary.video, binary.audio, and any other key under binary:

{
  "binary": {
    "video": {
      "data": "AAAAHGZ0eXBpc29t...",
      "fileName": "clip.mp4",
      "mimeType": "video/mp4"
    }
  },
  "crf": 26
}

Raw streamed bytes

curl -X POST 'http://localhost:7860/v1/probe' \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -H 'Content-Type: application/octet-stream' \
  -H 'X-Filename: input.mp4' \
  --data-binary '@input.mp4'

Raw and multipart uploads are written to disk in 1 MiB chunks. Downloads are also streamed; Base64 is size-checked before and after decoding.

Endpoints

All protected examples below assume MEDIAROUTER_API_KEY is set and include Authorization: Bearer $MEDIAROUTER_API_KEY. Processing routes use POST. Health and output downloads use GET.

Group Endpoints
Public /, /health, /version, /docs, /openapi.json, /redoc
Authentication /v1/auth/context, /v1/api-keys/*, /v1/audit-logs
System /v1/probe, /v1/media/{request_id}/{filename}
Video basics /v1/video/compress, resize, crop, trim, rotate, reverse, convert, merge, concat
Video composition /v1/video/overlay, watermark, replace-audio, subtitles/burn, subtitles/soft
Video outputs /v1/video/frames, gif, thumbnail, remove-audio, mute
Video timing/quality /v1/video/speed, speed-up, slow-motion, fps, bitrate, scale, pad, blur, sharpen, denoise, normalize
Audio /v1/audio/extract, convert, normalize, trim, merge, concat, fade, volume, remove-silence, noise-reduction
Image /v1/image/resize, crop, convert, slideshow, sequence, video, watermark, overlay
yt-dlp /v1/ytdlp/download
Whisper /v1/whisper/transcribe, subtitles, detect-language
Templates GET /v1/templates, GET /v1/templates/categories, GET /v1/templates/{reference}, POST /v1/templates/run

Common video parameters include format, width, height, fit, crf, preset, start, end, duration, fps, bitrate, factor, position, and opacity. Swagger lists every route; the defaults and bounds are enforced by the operation that consumes each field.

Probe metadata

POST /v1/probe returns normalized duration, resolution, FPS, bitrate, primary codec, video/audio/subtitle stream summaries, rotation, container, creation date, size, and tags. The same probe data is included under metadata.inputs for processing responses.

curl -X POST http://localhost:7860/v1/probe \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -F 'file=@input.mp4'

yt-dlp

mode is video, audio, thumbnail, or metadata. format accepts a yt-dlp format selector. Audio supports mp3, m4a, wav, opus, and flac.

curl -X POST http://localhost:7860/v1/ytdlp/download \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://www.youtube.com/watch?v=VIDEO_ID","mode":"audio","audio_format":"mp3"}'
curl -X POST http://localhost:7860/v1/ytdlp/download \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://vimeo.com/VIDEO_ID","mode":"metadata"}'

faster-whisper

Models: tiny, base, small, medium, and large-v3. The default is small. Models load only on their first request and always use device="cpu" and compute_type="int8". Tasks are transcribe and translate; omit language for automatic language detection. Formats are txt, srt, vtt, json, and tsv.

curl -X POST http://localhost:7860/v1/whisper/transcribe \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -F 'file=@meeting.mp3' \
  -F 'model=small' \
  -F 'task=transcribe' \
  -F 'output_format=json' \
  -F 'vad_filter=true'
curl -X POST http://localhost:7860/v1/whisper/subtitles \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -F 'file=@interview.mp4' \
  -F 'task=translate' \
  -F 'output_format=vtt'

Responses and downloads

Processing responses are JSON:

{
  "success": true,
  "request_id": "43b32630-9b32-4b27-b038-a39fbfd4b803",
  "processing_time": 1.42,
  "download_url": "/v1/media/43b32630-9b32-4b27-b038-a39fbfd4b803/compressed.mp4",
  "metadata": {}
}

Failures never include Python exceptions or tracebacks:

{
  "success": false,
  "request_id": "43b32630-9b32-4b27-b038-a39fbfd4b803",
  "error": {
    "code": "INVALID_INPUT",
    "message": "The uploaded media is empty",
    "details": null
  }
}

Fetch download_url before CLEANUP_MINUTES expires. The download endpoint uses StreamingResponse, reads in 1 MiB chunks, and supplies Content-Length and Content-Disposition; output files are never loaded into RAM. Streaming file bodies are necessarily binary rather than JSON envelopes.

Set BASE_URL=https://<owner>-<space>.hf.space when clients require absolute download URLs. If it is empty, URLs are relative, which works well in n8n when joined to the request host.

n8n recipes

Multipart upload

In an HTTP Request node:

  • Method: POST
  • URL: https://<space>.hf.space/v1/video/compress
  • Authentication: reusable Header Auth credential, Authorization = Bearer <api_key>
  • Send Body: on
  • Body Content Type: Form-Data
  • Add a n8n Binary File parameter named file, selecting the incoming binary property (usually data)
  • Add text parameters crf=28 and preset=veryfast

Use a second HTTP Request node with {{$json.download_url}}, enable Download, and store its response as binary.

JSON URL

Set Body Content Type to JSON:

{
  "url": "{{$json.media_url}}",
  "width": 1280,
  "height": 720,
  "fit": "contain"
}

n8n binary property as JSON Base64

When the upstream binary property is data, use an expression body:

{
  "binary": {
    "data": {
      "data": "{{$binary.data.data}}",
      "fileName": "{{$binary.data.fileName}}",
      "mimeType": "{{$binary.data.mimeType}}"
    }
  },
  "format": "mp3"
}

For large files, prefer n8n's multipart binary option or raw binary body; JSON Base64 temporarily expands data by roughly 33%.

Social Automation: YouTube Phase 2

MediaRouter now publishes YouTube videos through the shared SocialService → SocialPublisher → YouTubeProvider pipeline. REST, frontend, MCP, TypeScript/Python SDKs, and the n8n Social node use the same typed post, durable job, OAuth/token, validation, retry, and status-reconciliation implementation; none contains Google publishing logic or receives Google credentials.

YouTube is implemented with the official YouTube Data API v3. It supports OAuth + PKCE, stable channel discovery, encrypted TokenService credentials, registered MediaRouter video outputs, typed YouTube metadata including the required audience declaration, resumable chunked upload, crash-safe external-video reconciliation, status polling, deletion, and supported video statistics. GET /v1/social/providers reports YouTube as implemented and reports the other requested providers as registered/unimplemented. Apply all social migrations in numeric order through 0004_youtube_media_assets.sql before enabling production writes.

Google setup, exact redirect URI, scope, output registration, metadata, scheduling, retries, quotas, analytics limits, and troubleshooting are documented in docs/social-youtube.md. The implementation report and live-test status are in docs/social-youtube-production-readiness.md. No staging credentials were supplied, so live Google verification is NOT VERIFIED.

Social Automation: TikTok Phase 4

TikTok Direct Post video publishing now uses the same SocialService, durable SocialJob, TokenService, scheduler, retry, idempotency, and media-variant pipeline. It calls only the official TikTok Content Posting API: creator info, video initialization, sequential FILE_UPLOAD chunks, and publish-status reconciliation. TikTok Direct Post is fail-closed behind TIKTOK_DIRECT_POST_ENABLED=false and requires an explicit OAuth reconnect with the approved video.publish scope. MediaRouter scheduling is supported; TikTok-native scheduling and deletion are not advertised. See docs/social-tiktok-publishing.md for approval, media requirements, metadata, retries, idempotency, and integration usage. Live TikTok verification requires a dedicated approved app and test creator and is not part of normal CI.

Phase 4C adds explicit video.list authorization and official Display API video analytics, tenant/security hardening, credential redaction, optional live integration coverage, and a classified readiness audit. See docs/social-tiktok-production-readiness.md. Live TikTok, Postgres RLS, and Docker verification remain environment-dependent and must not be inferred from configuration alone.

Social Automation: X Phase 5

X account connection uses the official X API v2 OAuth 2.0 authorization-code flow with mandatory S256 PKCE and confidential-client authentication. The foundation requests only tweet.read users.read offline.access, discovers the authorized identity through GET /2/users/me, and stores credentials only through TokenService. Phase 5B adds typed text/reply creation, bounded image upload, streamed chunked GIF/video upload, official processing and post-status reconciliation, MediaRouter UTC scheduling, crash-safe idempotency recovery, and confirmed deletion. Publishing scopes (tweet.write media.write) are requested only through explicit publishing authorization. See docs/social-x-foundation.md for Developer Console and OAuth setup and docs/social-x-publishing.md for access tiers, media limits, workflows, scheduling, retries, and idempotency. Publishing is fail-closed behind X_PUBLISHING_ENABLED=false until an operator confirms current write/media entitlement. Quote posts remain unavailable on self-serve X tiers and X-native scheduling is not advertised.

Phase 5C adds official public post analytics (impression_count, likes, replies, reposts, and returned media views), credential and tenant hardening, optional live integration coverage, and a classified production audit. It does not request additional analytics scopes: the implementation uses the existing tweet.read grant and does not request restricted non-public/ads metrics. See docs/social-x-production-readiness.md. Live X, non-owner PostgreSQL RLS, target-runtime backend, and Docker validation remain explicitly NOT VERIFIED until they run in the deployment environment.

Social Automation: LinkedIn Phase 6

LinkedIn connection uses the official OAuth/OIDC and versioned Marketing APIs for member identity plus explicitly authorized organization discovery. Phase 6B adds typed member/organization text, single-image, video, and link/article publishing through the current Posts, Images, and Videos APIs. Uploads stream from workspace-owned assets, video parts preserve the provider byte ranges and ETags in encrypted job state, scheduling remains MediaRouter-native, and Posts deletion uses the persisted URL-encoded URN. Publishing is fail-closed behind LINKEDIN_PUBLISHING_ENABLED=false until Community Management access is approved. Member (w_member_social) and organization (w_organization_social) elevation are requested separately. See docs/social-linkedin-foundation.md and docs/social-linkedin-publishing.md for setup, permissions, media rules, retries, idempotency, and official API limitations.

Phase 6C adds official account-specific analytics. Member post statistics require explicit r_member_postAnalytics; organization share statistics use the separately authorized organization administrator grant. Normalized snapshots are stored in social_post_metrics, while safe provider values remain in raw_metrics. Security/tenant hardening, credential-safe client coverage, an opt-in live suite, and the classified audit are documented in docs/social-linkedin-production-readiness.md. Live LinkedIn, PostgreSQL RLS, the stripped-host backend runtime, and Docker remain NOT VERIFIED until exercised in the deployment environment.

Social quick discovery

curl -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  https://your-space.hf.space/v1/social/providers

Register a completed MediaRouter output, then create a typed YouTube draft with a one-time idempotency key:

curl -X POST https://your-space.hf.space/v1/social/posts \
  -H "Authorization: Bearer $MEDIAROUTER_API_KEY" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{"media_asset_id":"<registered_social_asset_id>","publish_mode":"draft","targets":[{"social_account_id":"<youtube_account_id>","youtube":{"title":"Example","description":"YouTube copy","privacy_status":"private","made_for_kids":false}}]}'

Configuration

Variable Default Purpose
APP_NAME MediaRouter OpenAPI/application name
APP_VERSION 1.0.0 Runtime and health version
APP_ENVIRONMENT development (production in Docker) Enables fail-closed production configuration validation
HOST 0.0.0.0 Documented bind host; Docker command binds explicitly to this address
PORT 7860 Hugging Face application port
CORS_ALLOWED_ORIGINS empty Comma-separated exact frontend origins; production requires HTTPS and rejects wildcard CORS
TEMP_DIR ./temp Request workspaces and in-progress files
OUTPUT_DIR ./outputs Published files served by download URLs
TEMPLATE_DIR built-in app/templates/categories Recursively scanned YAML workflow catalog
MAX_UPLOAD_SIZE 1073741824 Maximum bytes for each upload/download
WHISPER_MODEL small Default faster-whisper model
CLEANUP_MINUTES 60 TTL for request workspaces and outputs
CLEANUP_INTERVAL_SECONDS 60 Cleanup scan interval
MAX_WORKERS 2 Shared per-service CPU process/task concurrency
LOG_LEVEL INFO Structured log threshold
MAX_DURATION_SECONDS 21600 Maximum probed media duration
MAX_RESOLUTION_PIXELS 33177600 Maximum width × height (8K default)
DOWNLOAD_TIMEOUT_SECONDS 300 Remote download timeout; FFmpeg gets 4× this value
ALLOW_PRIVATE_URLS false Permit private/loopback URL downloads (normally unsafe)
BASE_URL empty Optional public origin for absolute download URLs
FFMPEG_BINARY ffmpeg FFmpeg executable name/path
FFPROBE_BINARY ffprobe FFprobe executable name/path
AUTH_ENABLED true Fail-closed API-key enforcement; disable only for isolated development/tests
DATABASE_URL sqlite+aiosqlite:///./data/mediarouter.db Backend-only security/tenant/asset store; bare postgresql:// is normalized to postgresql+asyncpg://
SECURITY_AUTO_MIGRATE false Permit metadata creation for controlled local/testing use; PostgreSQL production must apply app/security/migrations/
SECURITY_DATABASE_ROLE empty Required with PostgreSQL RLS; backend-only role for authoritative tenancy and canonical asset administration
SECURITY_ENFORCE_RLS true Fail startup if the security-store role cannot administer its forced-RLS tables
AUTH_ROLE_SCOPES {} JSON custom role-to-scope mappings
AUTH_BOOTSTRAP_KEY_HASH empty SHA-256 hash for first-start administrator
AUTH_BOOTSTRAP_KEY_PREFIX empty Safe display prefix matching the bootstrap key
AUTH_BOOTSTRAP_KEY_NAME Bootstrap Administrator Bootstrap record label
AUTH_BOOTSTRAP_ENVIRONMENT live live or test; must match the prefix
AUTH_LAST_USED_UPDATE_SECONDS 60 Throttle for database last-used writes
AUTH_DEFAULT_REQUESTS_PER_MINUTE 100 Default per-key request window
AUTH_DEFAULT_CONCURRENT_JOBS 10 Default processing request concurrency
AUTH_DEFAULT_UPLOADS_PER_HOUR 20 Default upload request window
AUTH_DEFAULT_PROCESSING_BYTES_PER_DAY 107374182400 Default daily uploaded processing bytes
AUTH_TRUST_PROXY_HEADERS true Use first X-Forwarded-For address for audits behind HF/Vercel
MCP_STDIO_API_KEY empty Existing API key required by authenticated standalone stdio MCP
GENERATION_ENABLED true Enable the additive durable generation domain; it does not configure a model by itself
GENERATION_JOB_RETRY_LIMIT 3 Bounded durable submission retry count; the initial submission is separate
AI_WORKER_CONNECT_TIMEOUT_SECONDS 10 Remote generation-worker connection timeout
AI_WORKER_REQUEST_TIMEOUT_SECONDS 60 Remote generation-worker request/write timeout
AI_WORKER_READ_TIMEOUT_SECONDS 300 Remote generation-worker response/read timeout
AI_WORKER_MAX_RETRIES 3 Bounded transport retries for safe idempotent worker operations
AI_WORKER_RETRY_BACKOFF_SECONDS 0.5 Base exponential backoff for worker transport retries
WAN_SPACE_URL empty Trusted backend-only WAN worker origin; requires WAN_SPACE_TOKEN and is never client-selectable
WAN_SPACE_TOKEN empty Backend-only Bearer token for the WAN worker; never return, log, or store in browser/MCP/n8n/SDK output
FLUX_SPACE_URL empty Trusted backend-only FLUX worker origin; requires FLUX_SPACE_TOKEN and is never client-selectable
FLUX_SPACE_TOKEN empty Backend-only Bearer token for FLUX; never return, log, or store in browser/MCP/n8n/SDK output
GENERATION_WORKER_ENABLED true Run durable generation dispatch, polling, and output-ingestion worker
GENERATION_WORKER_INTERVAL_SECONDS 5 Generation worker dispatch/reconciliation interval
GENERATION_WORKER_POLL_BACKOFF_SECONDS 2 Base bounded reconciliation poll backoff after a transient worker error
GENERATION_WORKER_BATCH_SIZE 8 Maximum generation jobs claimed per dispatch/reconciliation cycle
GENERATION_JOB_STALE_AFTER_SECONDS 900 Submission ambiguity threshold and durable reconciliation lease duration
SOCIAL_ENABLED true Enable the additive social domain; existing media APIs remain independent
SOCIAL_DATABASE_URL DATABASE_URL Async SQLAlchemy URL; use Supabase/Postgres in production
SOCIAL_TENANT_DATABASE_ROLE empty Required with PostgreSQL RLS; non-owner/non-BYPASSRLS role for API tenant sessions
SOCIAL_WORKER_DATABASE_URL empty Backend-only trusted worker connection; required for PostgreSQL scheduler/Vault usage
SOCIAL_WORKER_DATABASE_ROLE empty Expected BYPASSRLS role for the worker URL; checked at social startup
SOCIAL_ENFORCE_RLS true Fail startup if PostgreSQL API/worker role separation cannot be verified
SOCIAL_AUTO_MIGRATE false Local/test metadata creation only; never use for production migration management
SOCIAL_WORKER_ENABLED true Run durable scheduler/publisher claim loop when schema is ready
SOCIAL_SCHEDULER_INTERVAL_SECONDS 30 Scheduler polling interval
SOCIAL_JOB_STALE_AFTER_SECONDS 900 Recover an interrupted active job after this worker-lease period
SOCIAL_PUBLISH_RETRY_LIMIT 5 Maximum provider publishing attempts
SOCIAL_OAUTH_ENCRYPTION_KEY empty Required secret for PKCE state and local encrypted token fallback
SOCIAL_OAUTH_REDIRECT_BASE_URL empty Public backend origin used to build exact provider callback URLs
SUPABASE_VAULT_ENABLED false Store provider tokens as Supabase Vault secret references
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET empty Backend-only YouTube OAuth application credentials
YOUTUBE_UPLOAD_CHUNK_BYTES 8388608 Resumable upload chunk size; must be a multiple of 256 KiB
YOUTUBE_MAX_CONCURRENT_UPLOADS 2 Conservative per-process YouTube upload concurrency
YOUTUBE_REQUEST_TIMEOUT_SECONDS 60 Per-request YouTube Data API timeout
YOUTUBE_PROCESSING_POLL_SECONDS 30 Delay between server-side processing reconciliation polls
META_CLIENT_ID, META_CLIENT_SECRET empty Backend-only Facebook/Instagram OAuth application credentials
TIKTOK_CLIENT_KEY, TIKTOK_CLIENT_SECRET empty Backend-only TikTok Login Kit application credentials
TIKTOK_REDIRECT_URI empty Exact backend-owned TikTok callback registered in Login Kit (https://<api-host>/v1/social/accounts/tiktok/callback)
TIKTOK_DIRECT_POST_ENABLED false Fail-closed Direct Post gate; enable only after TikTok Content Posting approval
TIKTOK_UPLOAD_CHUNK_BYTES 10000000 Sequential FILE_UPLOAD chunk target (5–64 MB; final chunk may be up to 128 MB)
TIKTOK_REQUEST_TIMEOUT_SECONDS 60 TikTok OAuth, Content Posting, and upload request timeout
TIKTOK_PROCESSING_POLL_SECONDS 30 Delay between official publish-status reconciliation polls
LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET empty Backend-only LinkedIn OAuth credentials
LINKEDIN_REDIRECT_URI empty Exact backend-owned LinkedIn callback (https://<api-host>/v1/social/accounts/linkedin/callback)
LINKEDIN_PUBLISHING_ENABLED false Fail-closed publishing gate; enable only after LinkedIn Community Management approval
LINKEDIN_REQUEST_TIMEOUT_SECONDS 60 Per-request LinkedIn OAuth, Posts, Images, Videos, and upload timeout
LINKEDIN_MEDIA_PROCESSING_POLL_SECONDS 5 Poll delay when an explicitly granted read scope permits video status checks
LINKEDIN_MEDIA_PROCESSING_TIMEOUT_SECONDS 600 Bounded LinkedIn video-processing wait before durable retry
RUN_LINKEDIN_INTEGRATION_TESTS false Opt-in destructive LinkedIn integration suite; dedicated credentials and explicit publish/delete consent are additionally required
X_CLIENT_ID, X_CLIENT_SECRET empty Backend-only X OAuth 2.0 confidential Web App credentials
X_REDIRECT_URI empty Exact X callback (https://<api-host>/v1/social/accounts/x/callback) registered in the Developer Console
X_PUBLISHING_ENABLED false Fail-closed publishing gate; enable only after verifying current X write/media entitlement and credits
X_UPLOAD_CHUNK_BYTES 5242880 Bounded chunk size for official X GIF/video upload
X_REQUEST_TIMEOUT_SECONDS 60 Per-request X OAuth, media, and post API timeout
X_MEDIA_PROCESSING_POLL_SECONDS 5 Fallback official media-processing poll interval
X_MEDIA_PROCESSING_TIMEOUT_SECONDS 300 Bounded wait for X GIF/video processing before durable retry
TELEGRAM_BOT_TOKEN empty Backend-only Telegram bot credential foundation
WHATSAPP_CLIENT_ID, WHATSAPP_CLIENT_SECRET empty Backend-only WhatsApp Business credentials

Logging and safety

Logs are one JSON object per line. Request completion and operation records include UUID, API key ID/name (never the secret), operation, input/output bytes, wall time, CPU percentage, RSS memory, status, and errors. FFmpeg records include the exact argument array, bounded stdout/stderr, exit status, and duration. Structured audit rows preserve authenticated request metadata independently of application logs.

Safety controls include filename normalization, path containment checks, extension/MIME/size validation, duration and pixel limits, HTTP scheme and redirect validation, SSRF address filtering, bounded concurrency, bounded output capture, and subprocess arrays with shell=False semantics. User filenames never select output paths.

Testing and quality checks

pip install -r requirements.txt -r requirements-dev.txt
pytest -q
ruff check app tests main.py
black --check app tests main.py

The suite covers health, Base64/n8n resolution, streamed URL download, real FFprobe metadata, a real FFmpeg conversion, lazy mocked Whisper transcription, MCP registration and authorization, key entropy/hash-only storage, invalid/expired/revoked keys, scopes, rotation grace, disable/enable, request and concurrent limits, audit persistence, middleware contracts, YAML loading and versioning, parameter substitution, template execution, cleanup behavior, and safe error envelopes. Binary integration tests skip only when the respective system executable is absent.

Extending the API

  1. Add an async operation to the most relevant module in app/operations. Its inputs are FFmpegService, Sequence[InputMedia], parsed parameters, and the request output directory; return an OperationResult.
  2. Validate every parameter before building arguments. Pass an argument list to FFmpegService; never use a shell or concatenate a command string.
  3. Register the operation in the appropriate API router with operation_route. The shared executor supplies input resolution, FFprobe validation, publication, metrics, errors, and cleanup.
  4. To expose it over MCP, add a thin typed function to the matching app/mcp/tools/ registration module and call MCPRegistry.run_operation with the same operation function. Do not build FFmpeg arguments in the MCP module.
  5. Add unit coverage plus a small FFmpeg integration test when the operation changes media bytes, and update the expected MCP tool set when applicable.

This contract keeps new operations independent of multipart, URLs, Base64, n8n, storage, and response handling.

Troubleshooting

  • Space is out of memory: use tiny, base, or small; set MAX_WORKERS=1; do not launch multiple Uvicorn workers. medium and large-v3 require substantially more RAM even with int8.
  • First transcription is slow: the model is downloaded and initialized lazily. Use persistent Space storage or pre-warm with a short request after deployment.
  • FFmpeg reports incompatible streams during concat: leave stream_copy false (the default), which normalizes dimensions and re-encodes. Enable it only for files with identical stream layout, codec, time base, and parameters.
  • Remote URL is rejected: private and non-global addresses are blocked intentionally. Set ALLOW_PRIVATE_URLS=true only in a trusted network and never on a public Space.
  • Download URL returns 404: the request TTL expired, the Space restarted without persistent storage, or the filename/request ID was changed. Download results promptly.
  • Upload receives 413/422: increase MAX_UPLOAD_SIZE only after checking Space disk and RAM. Prefer multipart/raw streaming over Base64.
  • ImageMagick policy error: media operations use FFmpeg for image transforms; ImageMagick is installed for extension use but is not required by the built-in image routes.
  • No audio after merging videos: if any input lacks an audio stream, the merge deliberately emits video-only output instead of failing the whole request. Add silent audio before merging if a continuous audio track is required.
  • Every protected request returns 401 after first deployment: the database has no administrator. Generate a bootstrap key, configure its matching hash/prefix/environment secrets, and restart once. Ensure persistent storage contains the expected database.
  • Hugging Face restarted and keys disappeared: the default relative SQLite file was on ephemeral storage. Attach persistent storage and use sqlite+aiosqlite:////data/mediarouter.db, then create/rotate keys again.
  • MCP client cannot connect: use the trailing-slash URL /mcp/, select Streamable HTTP rather than legacy SSE, include the bearer header, and verify /health first. Stdio clients must set MCP_STDIO_API_KEY and launch with the project directory as their working directory.
  • MCP output URL is relative: set BASE_URL to the public Space origin. A managed output_file can also be passed directly into a later MCP call as temp_path before cleanup expires.