Render engine
- Add Remotion (code-based) as a 2nd render engine alongside After Effects.
node-agent dispatches on Job.Engine; RunRemotion maps bindings -> --props,
renders native then ffmpeg-scales to the quality tier (aspect-preserving).
- content.projects.render_engine + render_remotion_comp (migration 32);
render-svc claim resolves engine and routes (skips .aep for Remotion).
- Admin TemplatesAdmin gains an engine picker + Remotion composition id field.
Template pack (services/remotion)
- 16 branded, Persian (Vazirmatn), color- and text-editable templates, each in
3 aspects (16:9 / 1:1 / 9:16): LogoMotion, Opener, InstaPromo, YouTubeIntro,
Slideshow, HappyBirthday, SalePromo, QuoteCard, EventInvite, Countdown,
GlitterReveal (editable logo image), NowruzGreeting (animated characters),
and 4 cinematic 3D templates via @remotion/three (Hero3D, Nowruz3D,
Birthday3D, Promo3D) with reflections + bloom/DOF/vignette.
- scripts/seed_remotion_templates.py seeds containers/projects/scenes/colors.
Pricing
- Rewrite /pricing to the seconds-based model (charge = length x resolution),
data-driven from /v1/plans, Toman, broker checkout.
Coming-soon
- Persian experimental-build overlay on all pages (launch date + countdown).
Fixes
- middleware matcher bypasses all static asset paths; catalog mapping passes
cover image + preview video so real thumbnails render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The render page already displayed progress/ETA/preview — but the node agent never
fed real data: aeRender used fake +5%/10s increments, discarded aerender stdout,
and pushed a preview only every 30s. (Plus the deployed agent predated even the
progress-reporting wiring.)
node-agent (aeRender):
- Capture aerender stdout; parse "(N):" current frame + "N frames"/"to N" total.
- Real percentage when total is known (5–90%, headroom for transcode/upload),
else a smooth time-asymptotic estimate that never sticks — message shows the
live frame number either way.
- Push a preview frame ~every 8s (was 30s) so the box fills in quickly.
render-svc:
- GET /v1/renders/:id/progress now computes eta_seconds from started_at + progress
(linear extrapolation) instead of returning null.
frontend:
- Thread eta_seconds → status route → render page; page prefers the server ETA and
falls back to the client-observed rate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
r_height was hardcoded 1080. render-svc now derives r_height from the resolution
(ResolutionHeight) on job create; node-agent ffmpeg downscales to the tier height
(scale=-2:H). Quality picker now actually changes output size.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Edits previously never reached the MP4 (the node rendered template defaults). Now:
- render-svc claim includes the saved input values as bindings (GetRenderBindings →
saved_scene_contents with non-empty value).
- node-agent: new binder.go emits a JSON bind-spec + downloads media locally, runs the
pre-existing data-driven bind.jsx via afterfx (sets text layers' Source Text, replaces
media footage), saves a bound.aep next to the template, then aerender renders THAT.
- 12-min timeout + fresh-AE + done-marker polling (mirrors scan). Non-fatal: on bind
failure the job still renders template defaults.
Verified binding data flows (edited frl_c1t1/frl_c1t2 → claim bindings). Live MP4
verification needs the updated node-agent.exe re-run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
THE bug behind "AEPFilePath is required for real AE render": CreateJob inserted
original_project_id = saved_project_id (VALUES $3,$3), so the claim looked for the
render bundle at templates/{saved_project_id}/ — which never exists. The bundle
lives at templates/{TEMPLATE_id}/. Now original_project_id is resolved from
studio.saved_projects.original_project_id (the template the project was built from).
(Direct-SQL test renders masked this by setting the template id explicitly.)
Also harden the node-agent: Run() falls back to mock render when AEPFilePath is
empty even if AE is installed (previously hard-errored), so a missing/un-promoted
template degrades gracefully instead of failing the job.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aerender can't reliably write H.264 directly in modern AE — it renders the
project's output module (Lossless AVI/MOV) and ignores the .mp4 extension,
producing a multi-GB .avi the agent then failed to find/upload.
- findRenderedOutput(): locate the file aerender actually wrote (output.avi/.mov/.mp4)
- transcodeToMP4(): ffmpeg → H.264 yuv420p + AAC + faststart; drops the lossless
intermediate. ffmpeg located via $FFMPEG_PATH, beside the agent exe, or PATH.
- Graceful fallback: if ffmpeg is missing/fails, upload the raw render so the job
still delivers a (large but valid) file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three bugs surfaced bringing up a real After Effects node (verified: AE 2026
claimed + ran, but produced no usable output):
1. aerender got no -comp/-rqindex → "output argument ignored", nothing rendered.
- Claim now returns comp_name from content.projects.render_aep_comp (e.g. "frfinal")
via new Store.GetTemplateCompName; threaded through ClaimedJob → runner.Job →
aerender args (`-comp <name>`, or `-rqindex 1` fallback when unknown).
2. CreateExportForJob INSERT passed render_quality as a bare param into an enum
column → 500 ("output-upload-url HTTP 500"), so completed renders had no export.
- Cast $8::render.render_quality (+ explicit casts for file_type/create_type enums).
3. flatrender-exports bucket didn't exist → uploads would fail anyway.
- render-svc now MakeBucket(exports, templates) idempotently at startup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
render-svc:
- db.UpdateJobPreview(): writes base64 PNG to render_jobs.image_preview_b64
(only on active jobs; Done/Failed/Cancelled rows ignored)
- POST /v1/internal/render/jobs/:job_id/preview — node agent endpoint
- Route registered under /v1/internal (nodeAuth)
node-agent:
- runner.PreviewFn callback type alongside ProgressFn
- runner.preview.go: GeneratePreviewB64(percent, quality, resolution)
— pure stdlib (image/png + encoding/base64), no external deps
— 320×180 dark frame with animated progress bar + colored indicator dots
- mock render: pushes a preview frame at every step (5→95%)
- real AE render: pushes a preview frame every 30s
- client.UpdatePreview(): POST /v1/internal/render/jobs/:job_id/preview
- main.go: onPreview callback wires client.UpdatePreview() into runner.Run()
frontend:
- render-jobs.ts: RenderJobRow.preview_b64 field; read from progress endpoint
- status/route.ts: previewB64 included in JSON response
- RenderModal: aspect-ratio preview pane during polling — shows spinner until
first frame arrives, then live-updates with each poll (every 3s);
step label overlaid as badge bottom-right
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>