From d7a74daa96300d76f5a8a2acb68ed6f0b1171e8b Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sat, 6 Jun 2026 23:04:32 +0330 Subject: [PATCH] =?UTF-8?q?feat(render):=205=20quality=20tiers=20(360p?= =?UTF-8?q?=E2=80=934K)=20+=20ETA=20on=20render=20page;=2024h=20session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Render page: resolution picker now 360p/540p/720p/1080p/4K; live ETA ('تقریباً … باقی مانده') computed from progress rate; preview+progress bar already wired. - render-schemas: resolution enum + RESOLUTION_DIMENSIONS add 360p/540p. - render-jobs.mapQuality: 5-tier → render_quality (Low/Medium/High/Full). - Session: Jwt__AccessTokenMinutes=1440 (24h) via compose so logins persist (refresh middleware + 30d refresh token back it up). (Real per-tier output height still pending: render-svc r_height is hardcoded 1080 → node ffmpeg scale — next.) Co-Authored-By: Claude Opus 4.8 --- docker-compose.v2.yml | 1 + .../studio/render/[projectId]/page.tsx | 43 +++++++++++++++++-- src/lib/render-jobs.ts | 13 ++++-- src/lib/render-schemas.ts | 4 +- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/docker-compose.v2.yml b/docker-compose.v2.yml index d916db3..f547d20 100644 --- a/docker-compose.v2.yml +++ b/docker-compose.v2.yml @@ -72,6 +72,7 @@ services: Jwt__Secret: "${JWT_SECRET}" Jwt__Issuer: "flatrender-identity" Jwt__Audience: "flatrender" + Jwt__AccessTokenMinutes: "${JWT_ACCESS_MINUTES:-1440}" ServiceToken: "${SERVICE_TOKEN:-internal-service-secret}" ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}" ZarinPal__CallbackUrl: "${ZARINPAL_CALLBACK_URL:-http://localhost:8080/v1/payments/callback/zarinpal}" diff --git a/src/app/[locale]/studio/render/[projectId]/page.tsx b/src/app/[locale]/studio/render/[projectId]/page.tsx index 3866aeb..cebc005 100644 --- a/src/app/[locale]/studio/render/[projectId]/page.tsx +++ b/src/app/[locale]/studio/render/[projectId]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import { @@ -35,9 +35,21 @@ interface ActiveRender { render_progress: number; } -const RESOLUTIONS: RenderSettings["resolution"][] = ["720p", "1080p", "4K"]; +const RESOLUTIONS: RenderSettings["resolution"][] = ["360p", "540p", "720p", "1080p", "4K"]; const FPS_OPTIONS: RenderSettings["fps"][] = [24, 30, 60]; +/** Format seconds as "M:SS" / "Hh Mm" for the ETA label. */ +function formatEta(sec: number): string { + if (!isFinite(sec) || sec <= 0) return "—"; + const s = Math.round(sec); + if (s < 60) return `${s} ثانیه`; + const m = Math.floor(s / 60); + const r = s % 60; + if (m < 60) return `${m}:${String(r).padStart(2, "0")} دقیقه`; + const h = Math.floor(m / 60); + return `${h} ساعت و ${m % 60} دقیقه`; +} + export default function RenderPage() { const router = useRouter(); const params = useParams<{ projectId: string }>(); @@ -56,6 +68,9 @@ export default function RenderPage() { const [errorMessage, setErrorMessage] = useState(null); // An active render that belongs to a DIFFERENT project (blocks starting a new one). const [blockingJobId, setBlockingJobId] = useState(null); + // ETA: estimate remaining time from the progress rate (first sample where progress > 0). + const [etaSec, setEtaSec] = useState(null); + const etaBaseRef = useRef<{ t: number; p: number } | null>(null); // Apply preset from the query (?preset=full) useEffect(() => { @@ -105,10 +120,25 @@ export default function RenderPage() { setErrorMessage("Could not fetch render status."); return; } - setProgress(data.progress ?? 0); - setProgressMessage(data.progressMessage ?? `Rendering… ${data.progress}%`); + const p = data.progress ?? 0; + setProgress(p); + setProgressMessage(data.progressMessage ?? `Rendering… ${p}%`); if (data.previewB64) setPreviewB64(data.previewB64); + // ETA from the observed progress rate. + if (p > 0 && p < 100) { + const now = Date.now(); + const base = etaBaseRef.current; + if (!base || p < base.p) { + etaBaseRef.current = { t: now, p }; + } else if (p > base.p) { + const rate = (p - base.p) / ((now - base.t) / 1000); // %/sec + if (rate > 0) setEtaSec((100 - p) / rate); + } + } else { + setEtaSec(null); + } + if (data.status === "completed") { if (data.outputUrl) setOutputUrl(data.outputUrl); setProgress(100); @@ -214,6 +244,11 @@ export default function RenderPage() { style={{ width: `${progress}%` }} /> + {etaSec != null && ( +

+ تقریباً {formatEta(etaSec)} باقی مانده +

+ )} )} diff --git a/src/lib/render-jobs.ts b/src/lib/render-jobs.ts index 79e1362..b7305ec 100644 --- a/src/lib/render-jobs.ts +++ b/src/lib/render-jobs.ts @@ -37,11 +37,16 @@ export interface RenderJobRow { // ── Helpers ────────────────────────────────────────────────────────────────── -/** Map frontend resolution string to V2 quality enum. */ +/** Map frontend resolution string to V2 render_quality enum (Low/Medium/High/Full/Lossless). */ function mapQuality(resolution: string): string { - if (resolution === "4K") return "Full"; - if (resolution === "720p") return "Medium"; - return "High"; // 1080p default + switch (resolution) { + case "360p": return "Low"; + case "540p": return "Medium"; + case "720p": return "Medium"; + case "1080p": return "High"; + case "4K": return "Full"; + default: return "High"; + } } /** Map V2 step → legacy status for frontend compatibility. */ diff --git a/src/lib/render-schemas.ts b/src/lib/render-schemas.ts index ff06738..b9edb24 100644 --- a/src/lib/render-schemas.ts +++ b/src/lib/render-schemas.ts @@ -22,7 +22,7 @@ export const sceneSchema = z.object({ }); export const renderSettingsSchema = z.object({ - resolution: z.enum(["720p", "1080p", "4K"]), + resolution: z.enum(["360p", "540p", "720p", "1080p", "4K"]), format: z.literal("mp4").default("mp4"), fps: z.union([z.literal(24), z.literal(30), z.literal(60)]), }); @@ -52,6 +52,8 @@ export const RESOLUTION_DIMENSIONS: Record< RenderSettings["resolution"], { width: number; height: number } > = { + "360p": { width: 640, height: 360 }, + "540p": { width: 960, height: 540 }, "720p": { width: 1280, height: 720 }, "1080p": { width: 1920, height: 1080 }, "4K": { width: 3840, height: 2160 },