Bring-your-own API key. Operators configure their own provider key (Google or fal.ai) on the API Keys settings page and pick a video provider + model on the AI settings page; per-clip generation costs are billed by the provider, not by WebProCMS. No membership required.
WebProCMS lets editors generate short video clips from a text prompt directly inside the Media Library. A clip renders server-side via Google Veo or fal.ai, lands as a reusable video item with an auto-generated poster frame, and is then picked into any self-hosted x-dl.video spot through the normal media picker — the same way an uploaded clip would be. Crucially, the whole thing works without a queue worker, so it runs on the same shared hosting every other WebProCMS install targets.
The problem
Video generation is fundamentally different from image generation in two ways that break the existing AI-image pattern:
- It's slow and asynchronous. Every video API (Veo, Sora, Kling, etc.) is a long-running operation: you submit a prompt, get back an operation handle, and the clip renders for ~30–120 seconds before you can download it. The synchronous "request → bytes in the same response" flow the image generator uses simply doesn't apply.
- Most installs have no queue worker. WebProCMS is built to run on ordinary hosting where
QUEUE_CONNECTION=syncand there's nophp artisan queue:workdaemon. A naive "dispatch a job that polls for two minutes" approach either blocks the web request (timing out PHP-FPM) or never runs at all.
The off-the-shelf alternative — generate in a third-party tool, download, re-upload, fill in a poster by hand — crosses three tools and leaves the editor managing files manually.
The fix
WebProCMS models video generation as a chunked poll state machine driven entirely by short web requests — no worker, no long-held connection:
- Submit. The editor enters a prompt; the server POSTs it to the provider, gets an operation handle, and stores a
processingrow. The request returns in a second or two. - Poll. A conditional
wire:pollon the Media Library page advances the oldest in-flight generation once every few seconds. Each tick is a sub-second request — "still rendering?" — so nothing ever blocks. - Finalize. The moment the provider reports done, that same tick streams the finished clip to disk, creates a video
MediaItem, and triggers client-side poster extraction. The clip appears in the grid.
Because progress lives in the database and advances across normal requests, the feature needs zero infrastructure beyond the web server. The editor can close the modal and keep working — the clip still lands when ready.
Two providers, one workflow
The active provider is configured at Dashboard → Settings → AI → Video Generation (ai.video_provider), and its key on the API Keys page. The Media Library UI is identical regardless of which provider is active; only the request-side details change.
| Provider | Default model | Notes |
|---|---|---|
| Google Veo | veo-3.1-generate-preview |
Native Veo via the Gemini API host — high-fidelity text-to-video with audio. Submits to …:predictLongRunning, polls the returned operation name, downloads the file with the API key attached. Reuses the same host/header as Gemini image generation. |
| fal.ai | fal-ai/veo3.1 |
One key, many models. fal hosts Veo 3.1 plus Kling, Seedance, Wan and others — switch model in settings without a code change. Submits to queue.fal.run, polls the status URL, fetches the result URL. Already the same key used for fal image generation. |
Why only these two. Of the six AI keys WebProCMS supports, four can technically do video, but only Google and fal are worth shipping. OpenAI's Sora API was announced for shutdown (Sept 2026), and Stability's video model is weak (~2-second image-to-video only). The provider abstraction leaves room to add others behind the same poll loop later.
Provider switching requires no code changes and no migration — the poll loop reads Setting::get('ai.video_provider') at request time and dispatches to the matching method. Model IDs are editable dropdowns, not hardcoded lists, because Veo's preview model IDs change frequently; operators can paste a new ID the day a provider ships it.
Generating a clip
The Generate Video button appears in the Media Library toolbar (next to Generate Image) only when a video provider is configured and its key is set. It opens a modal with:
- Prompt — free text describing the clip.
- Aspect ratio — Landscape 16:9 / Portrait 9:16 / Square 1:1.
- Duration — 4 / 6 / 8 seconds.
- A cost-and-time note — "Rendering takes ~30–120s and is billed per second of output by your provider" — so editors aren't surprised by either the wait or the bill.
On submit, the modal shows a live "Generating your video…" state. The clip is saved to the currently-selected media category (or the default category), and the editor sees a "Video saved to your library" confirmation. From there it's a normal video MediaItem — pick it into a self-hosted x-dl.video field, a section background video, or anywhere the media picker is used.
The no-worker async loop in detail
State for each generation lives in the ai_video_generations table — one row per clip, queryable, with full history. The loop:
| Step | What happens | Where |
|---|---|---|
| Submit | AiVideoGenerator::submit() POSTs the prompt, returns {provider, model, handle}. A processing row is inserted. |
submitVideoGeneration() on the Media Library component |
| Poll | A wire:poll.5s — rendered on the page root only while a generation is processing — calls pollVideoGeneration(), which polls the oldest in-flight row. |
AiVideoGenerator::poll($provider, $handle) |
| Finalize | On done, the clip is streamed to the media disk under YYYY/MM/, a video MediaItem is created, the row is marked done with its media_item_id, and a media-video-generated browser event fires. |
pollVideoGeneration() → MediaItem::createForStoragePath() |
The poll attribute disappears the moment nothing is processing, so there's no idle polling and no background churn. A generation stuck past 15 minutes is defensively marked failed so the loop and UI never spin forever.
This is a deliberate departure from the defer() pattern WebProCMS uses for CMS updates and backups. Those are single indivisible operations (git pull, composer install) that have to run to completion in one shot. Video generation is naturally chunkable — submit, then poll, then download — so splitting it across short requests avoids FPM timeouts entirely and works identically on sync, database, or a real queue driver.
Memory-safe download
Unlike image providers (which return base64 inside a JSON body), video providers return a download URL. The finished clip — potentially tens of megabytes — is streamed straight to disk via Guzzle's sink option, so it's never buffered in PHP memory. Google's file endpoint requires the API key, so it's re-attached on the download request for that provider.
Auto-generated poster
WebProCMS generates video posters client-side (a first-frame canvas grab — there's no server-side ffmpeg). A server-downloaded AI clip has no browser involved at creation time, so the poster is attached the moment it shows up in the grid:
- Finalize dispatches
media-video-generatedwith the new item's id and URL. - An Alpine listener on the Media Library calls
window.attachGeneratedVideoPoster(id, url, …). - That reuses the exact same client-side frame-grab the manual "Regenerate from video" button uses (
regeneratePosterForVideo→extractFirstFrameFromUrl→ theregenerate-posterendpoint), then refreshes the component so the poster appears.
Poster extraction is non-fatal: if the browser can't decode a frame, the clip is still saved — it just shows the "No poster" badge until the editor regenerates it manually. Once a poster is attached, every self-hosted-video consumer (x-dl.video, section background video) emits poster="…" for it automatically via the query-free MediaItem::posterUrlForPath() cache — the same path uploaded videos use.
Image-to-video (capability)
The generator service supports an optional start frame for image-to-video: Google receives it as base64 inline image bytes, fal as a data-URI image_url. The capability is built and tested; the Media Library modal currently exposes text-to-video only. A future iteration (or an in-editor "generate video for this spot" flow) can pass a start image without touching the provider layer.
What lives where
| Path | Purpose |
|---|---|
app/Support/Ai/AiVideoGenerator.php |
The shared service. submit() / poll() / download() for Google Veo and fal.ai. Three discrete steps so the same class works from the poll loop (and a future job). |
app/Models/AiVideoGeneration.php |
One in-flight (or finished) generation — status (processing / done / failed), provider, model, prompt, handle, linked media_item_id, error. |
resources/views/pages/dashboard/media-library/⚡index.blade.php |
Hosts openGenerateVideoModal, submitVideoGeneration, pollVideoGeneration, the hasProcessingVideo computed that gates the root wire:poll, the Generate Video button, and the modal. |
resources/js/manager.js |
window.attachGeneratedVideoPoster — auto-attaches a first-frame poster on completion, reusing the existing client-side extraction. |
resources/views/pages/dashboard/settings/⚡ai.blade.php |
The Video Generation settings section — provider radios (Off / Google / fal) and editable per-provider model dropdowns. |
app/Models/MediaItem.php |
createForStoragePath() registers the downloaded clip; kind='video' is auto-detected from the file's mime. |
Settings reference
| Key | Default | Notes |
|---|---|---|
ai.video_provider |
'' |
One of google, fal. Empty means video generation is off — the Generate Video button is hidden. |
ai.google_video_model |
veo-3.1-generate-preview |
Active Veo model when Google is the provider. Editable dropdown — paste any model ID the provider ships. |
ai.fal_video_model |
fal-ai/veo3.1 |
Active fal.ai model. Switch to Kling, Seedance, etc. without a code change. |
ai.google_key |
'' |
Google AI API key — the same key powers Gemini image and text/vision calls. |
ai.fal_key |
'' |
fal.ai API key — the same key powers fal image generation. |
Relationship to other features
- Media Library — generated clips are ordinary video
MediaItems; everything the library does (categories, rename/move, replace, usage warnings, posters) applies to them. - AI Image Generation — the sibling synchronous flow. Video deliberately uses a different (chunked-poll) architecture because of its long render time and the no-worker constraint.
- Lazy Cron — the same "do background-ish work without a daemon" philosophy WebProCMS applies elsewhere; video generation advances on user-driven
wire:pollticks rather than scheduled cron. - The self-hosted video player, poster ownership, and the
x-dl.videocomponent are documented alongside the media library and design library; generated clips slot into all of them unchanged.