Property Video Ads

Generate 15-second vertical Reels-shaped video ads for a property from its photos, with AI camera motion and branded overlays. Powered by Replicate + FFmpeg + Meta Marketing API.

Admin only — Video ad generation is currently exposed in the admin property detail page. Per-organization self-serve will follow once the cluster concurrency cap is comfortable at higher volume.

Overview

A property video ad is a 15-second vertical (1080×1920) MP4 built from your top three property photos. The pipeline:

  1. Pick 3 photos — featured-first, then by display order. Duplicates are skipped. The property needs at least 3 distinct photos or the render is rejected.
  2. Generate 3× 5-second clips via Replicate (prunaai/p-video) with pre-defined camera-motion prompts: slow push-in, slow orbit, lateral pan.
  3. Composite with FFmpeg — center-crop each clip to 1080×1920, xfade 0.5s crossfade transitions, append a 1s clone-hold on the final frame, overlay two branded Canvacord PNG badges (location + price) with alpha-expression fade-in/fade-out.
  4. Upload to DigitalOcean Spaces at ads/video/<adCreativeId>/final.mp4 plus a thumbnail frame from t=7.5s.
  5. (Optional) push to Meta — uploads via POST /act_{id}/advideos, polls until status: ready, then creates an AdCreative using object_story_spec.video_data.

No audio is added on our side. Meta's "Add music" toggle handles the soundtrack at publish time.

Cost

Roughly $0.75 per video in Replicate spend — three 5-second clips at ~$0.05/second of generated video. Compositing, Spaces storage, and Meta upload are negligible.

Generating a video

  1. Open Admin → Property Listings and click into a property that has at least 3 photos.
  2. In the Details tab, find the Video Ad card.
  3. Click Generate video ad. The render typically takes 60–120 seconds end-to-end — most of that wall time is waiting on Replicate.
  4. The card polls the render job every 3 seconds. When status === succeeded, the embedded <video> preview appears with Open MP4 and Open thumbnail links.

If the cluster is already running 2 video renders, your job lands in the queued state. The video-render-queue-drain cronjob (every 60s) promotes it to pending and spawns the K8s Job as soon as capacity frees.

Pushing to Meta

After a render succeeds, push it to a Meta campaign:

POST /admin/ads/push-video-to-meta/:adCreativeId { "campaignId": "1234567890", "pageId": "9876543210", "websiteUrl": "https://yourbrand.com/properties/<slug>", "language": "en-US", "instantFormId": "1122334455", "dailyBudget": 10, "status": "PAUSED" }

This uploads the MP4 to Meta by URL (no buffer roundtrip), polls until the video is ready, and creates the AdCreative + Ad in a single request. If you omit adSetId, the service reuses an existing AdSet on the campaign matching the targeting language (same rule as carousel ads), or creates a new lead-gen AdSet if none exists.

Troubleshooting

SymptomCauseFix
Property must have at least 3 distinct photosProperty has fewer than 3 image URLs.Upload more photos. Duplicates are not counted.
Render status stuck at pending for >2 minK8s Job didn't schedule (cluster full or image pull issue).kubectl get jobs -l kind=video-render to inspect; the queue drain will mark it failed after 30 min.
Render failed: Meta video processing failedMeta rejected the MP4 (corrupt, wrong codec, expired URL).Re-render — the Spaces URL has a 30-day TTL so this is almost always a transient transcode issue on Meta's side.
aspect_ratio is ignored warning in logsExpected. prunaai/p-video ignores aspect_ratio when an input image is provided; FFmpeg center-crops to vertical in compositing.None — informational only.

How retention works

  • final.mp4 and thumbnail.jpg: 30-day TTL hint on Spaces (kept public).
  • Intermediate clips (clip-0.mp4 through clip-2.mp4) and overlay PNGs: cleaned up by the next files-cleanup pass after 24 hours.

If you need to re-render the same property, generate a new video — the old AdCreative row is left intact so historical campaigns keep working.

Hard rules baked into the pipeline

  • Replicate only for clip generation. No in-cluster diffusion.
  • FFmpeg only for compositing. No Remotion / Chromium.
  • K8s Jobs only for rendering. One Job per render, never a long-lived worker pod.
  • No audio from us. Every FFmpeg invocation passes -an.

For the architecture details, see docs/plans/2026-05-19-property-video-ads.md in the monorepo.

Fondaro Help

Docs & support

Hi there, how can we help?

Browse popular articles or ask a question below.

Popular articles

Or ask a question