feat(render): real progress %, ETA, and frequent preview during AE renders

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>
This commit is contained in:
soroush.asadi
2026-06-18 01:18:54 +03:30
parent 23d1fd8fb1
commit 6d79ddb8d1
5 changed files with 116 additions and 29 deletions
@@ -29,6 +29,7 @@ interface StatusResponse {
progressMessage?: string | null;
errorMessage?: string | null;
previewB64?: string | null;
etaSeconds?: number | null;
}
interface ActiveRender {
@@ -180,8 +181,11 @@ export default function RenderPage() {
setProgressMessage(data.progressMessage ?? `Rendering… ${p}%`);
if (data.previewB64) setPreviewB64(data.previewB64);
// ETA from the observed progress rate.
if (p > 0 && p < 100) {
// ETA: prefer the server's estimate (elapsed vs. progress); otherwise fall
// back to the client-observed progress rate.
if (typeof data.etaSeconds === "number" && data.etaSeconds >= 0 && p < 100) {
setEtaSec(data.etaSeconds);
} else if (p > 0 && p < 100) {
const now = Date.now();
const base = etaBaseRef.current;
if (!base || p < base.p) {
@@ -33,5 +33,6 @@ export async function GET(_request: Request, context: RouteContext) {
progressMessage: job.progress_message,
errorMessage: job.error_message,
previewB64: job.preview_b64 ?? null,
etaSeconds: job.eta_seconds ?? null,
});
}