import type { Page, Request } from "@playwright/test"; /* * Scripted Media Studio backend for the project and review specs. No RPC can * seed an emulator library into a fresh space, so these specs answer the media * procedures they use with synthetic, schema-shaped data and record every call. */ export const PROJECT_ID = "proj-e2e"; export const SHORTS = "youtube-shorts"; export const PAID = "example-paid-video"; export const TIKTOK = "tiktok"; const at = (minute: number) => new Date(Date.UTC(2026, 9, 1, 10, minute)).toISOString(); function clip(id: string, assetId: string, inMs: number, outMs: number, atMs: number) { return { id, assetId, inMs, outMs, atMs, speed: 1, reframe: "auto", stabilize: false, muted: false, }; } export function edl(profileId: string, width: number, height: number) { return { edlVersion: 1, output: { profileId, width, height, fps: 30, durationTargetMs: 12_000, durationHardMinMs: 3_000, durationHardMaxMs: 60_000, }, tracks: { video: [ clip("clip-1", "asset-1", 0, 4_000, 0), clip("clip-2", "asset-2", 0, 4_000, 4_000), clip("clip-3", "asset-3", 1_000, 4_000, 8_000), ], audio: [], }, transitions: [], captions: { mode: "burned", maxCharsPerLine: 32, wordHighlight: false, position: "lower" }, overlays: [{ id: "text-1", kind: "text", atMs: 500, durationMs: 2_000, text: "Wait for it" }], redactions: { faces: [ { id: "face-1", target: { personId: "person-a" }, mode: "partial_eyes", paddingPct: 20, featherPct: 10, minSizePct: 2, }, ], marks: [{ markId: "mark-1", treatment: "cover" }], regions: [], }, metadata: { title: "Teaser", tags: [], aiAltered: false, aiAlterations: [] }, }; } function plan(id: string, profileId: string, version: number, extra: Record = {}) { const [width, height] = profileId === PAID ? [1920, 1080] : [1080, 1920]; return { id, projectId: PROJECT_ID, profileId, version, edl: edl(profileId, width, height), createdBy: "user", rationale: null, createdAt: at(version), ...extra, }; } const report = (profileId: string, issues: unknown[], passed: boolean, autoFixRound = 0) => ({ profileId, passed, checkedAt: at(30), sampledFrames: 48, issues, autoFixRound, }); function render(id: string, extra: Record) { return { id, planId: "plan-shorts-1", projectId: PROJECT_ID, profileId: SHORTS, quality: "draft", status: "done", progress: 1, outputs: [ { kind: "video", artifactId: null, fileName: "video.mp4", sizeBytes: 1_000_000 }, { kind: "poster", artifactId: "art-poster", fileName: "poster.jpg", sizeBytes: 20_000 }, ], gateReport: null, ffprobe: null, error: null, createdAt: at(20), updatedAt: at(21), ...extra, }; } export const BLOCKED_ISSUES = [ { check: "anonymization", severity: "block", code: "mask_coverage", message: "A face of Alex is not fully covered.", atMs: 5_000, endMs: 5_600, personId: "person-a", }, { check: "content", severity: "block", code: "classifier_threshold", message: "Explicit nudity above the profile limit.", atMs: 2_000, category: "explicit_nudity", score: 0.9, }, { check: "consent", severity: "block", code: "consent_missing", message: "Sam has no recorded consent.", atMs: 9_000, personId: "person-b", }, { check: "anonymization", severity: "warn", code: "mark_residual", message: "The tattoo is faintly visible.", atMs: 10_000, endMs: 10_400, markId: "mark-1", }, { check: "text", severity: "warn", code: "hashtags", message: "Fewer hashtags would read better.", }, ]; /** Mutable backend state the mock answers from; specs read `calls` to assert requests. */ export function mediaBackend() { const plans: Record[] = [ plan("plan-shorts-1", SHORTS, 1), plan("plan-paid-1", PAID, 1), plan("plan-paid-2", PAID, 2, { createdBy: "agent", rationale: "Auto-fix 1/3 after the gate blocked (mask_coverage): widened the face mask", }), ]; const renders: Record[] = [ render("render-shorts-draft", { gateReport: report(SHORTS, [], true) }), render("render-paid-draft", { planId: "plan-paid-1", profileId: PAID, status: "blocked", outputs: [], gateReport: report(PAID, BLOCKED_ISSUES, false, 1), }), ]; const people = [ { id: "person-a", displayName: "Alex", isSelf: false, confirmed: true, anonymizeByDefault: "partial_eyes", consentRecordedAt: at(1), consentArtifactId: null, ageVerifiedAt: at(1), ageVerificationMethod: "platform_verified", faceTrackCount: 3, coverThumbAssetId: null, }, { id: "person-b", displayName: "Sam", isSelf: false, confirmed: true, anonymizeByDefault: "blur", consentRecordedAt: null, consentArtifactId: null, ageVerifiedAt: null, ageVerificationMethod: "none", faceTrackCount: 1, coverThumbAssetId: null, }, ]; const marks = [ { id: "mark-1", kind: "tattoo", label: null, personId: "person-a", assetId: "asset-2", prompt: { tMs: 1_000, point: { x: 0.4, y: 0.6 } }, treatment: "inpaint", qualityScore: 0.42, suggestedFallback: "cover", createdAt: at(2), }, ]; const calls: Array<{ procedure: string; input: Record }> = []; /** Submissions answered with these errors first, then accepted. */ const submitErrors: unknown[][] = []; let renderCounter = 0; let pollsUntilDone = 0; const project = { id: PROJECT_ID, title: "Harbor teaser", goal: "teaser", status: "review", brandKitId: null, collectionIds: [], profileIds: [SHORTS, PAID, TIKTOK], createdAt: at(0), updatedAt: at(0), }; function advanceRenders() { for (const item of renders) { if (item.status !== "queued" && item.status !== "rendering") continue; if (pollsUntilDone > 0) { pollsUntilDone -= 1; item.status = "rendering"; item.progress = 0.5; continue; } item.status = "done"; item.progress = 1; item.outputs = [ { kind: "video", artifactId: null, fileName: "video.mp4", sizeBytes: 2_000_000 }, ]; item.gateReport = report(String(item.profileId), [], true); } } const profiles = [ [SHORTS, "YouTube Shorts", "free_social", false], [PAID, "Example paid video", "paid_video", false], [TIKTOK, "TikTok", "free_social", true], ].map(([id, name, kind, unverified]) => ({ id, name, kind, origin: "builtin", unverified, stale: false, aspectRatios: id === PAID ? ["16:9"] : ["9:16"], hardMinMs: 1_000, hardMaxMs: 60_000, recommendedMs: [15_000], })); const handlers: Record) => unknown> = { capabilities: () => ({ configured: true, operations: {}, accelerator: "cpu", hardwareEncoders: [], }), "profiles/list": () => profiles, "projects/get": () => { advanceRenders(); return { project, plans, renders }; }, "projects/autoPlan": () => { const created = plan("plan-tiktok-1", TIKTOK, 1, { createdBy: "agent" }); plans.push(created); return [created]; }, "assets/get": (input) => ({ asset: { id: input.assetId, sourceId: "source-1", externalId: `${String(input.assetId)}.mp4`, kind: "video", storage: "referenced", capturedAt: at(0), durationMs: 6_000, width: 1920, height: 1080, fps: 30, gpsLat: null, gpsLon: null, deviceModel: null, albumNames: [], isFavorite: false, ingestState: "analyzed", hasProxy: true, hasThumbnail: true, qualityScore: 0.8, createdAt: at(0), }, segments: [], faceTracks: [], transcript: null, }), "plans/submit": (input) => { const errors = submitErrors.shift(); if (errors) return { ok: false, errors }; const base = plans.find((item) => item.profileId === (input.edl as { output: { profileId: string } }).output.profileId); const version = plans.filter((item) => item.profileId === base?.profileId).length + 1; const created = { ...plan(`plan-saved-${version}`, String(base?.profileId), version), edl: input.edl, }; plans.push(created); return { ok: true, plan: created }; }, "renders/start": (input) => { const source = plans.find((item) => item.id === input.planId); renderCounter += 1; const created = render(`render-new-${renderCounter}`, { planId: input.planId, profileId: source?.profileId, quality: input.quality, status: "queued", progress: 0, outputs: [], createdAt: at(40 + renderCounter), }); renders.push(created); return created; }, "renders/get": (input) => { advanceRenders(); return renders.find((item) => item.id === input.renderId); }, "renders/thumbnails": () => ["a", "b", "c"].map((letter) => ({ kind: "thumbnail", artifactId: `art-thumb-${letter}`, fileName: `thumb-${letter}.png`, sizeBytes: 10_000, })), "marketing/copy": (input) => ({ profileId: SHORTS, language: input.language, title: "Harbor at dawn", hook: "You have never seen the harbor like this", caption: "Three minutes before sunrise.", hashtags: ["#harbor", "#sunrise"], issues: [], }), "people/list": () => people, "people/update": (input) => { const person = people.find((item) => item.id === input.personId); if (person && typeof input.anonymizeByDefault === "string") { person.anonymizeByDefault = input.anonymizeByDefault; } return person; }, "marks/list": () => marks, "marks/update": (input) => { const mark = marks.find((item) => item.id === input.markId); if (mark && typeof input.treatment === "string") mark.treatment = input.treatment; return mark; }, "marks/create": (input) => { const created = { id: `mark-${marks.length + 1}`, kind: input.kind, label: null, personId: null, assetId: input.assetId, prompt: input.prompt, treatment: input.treatment, qualityScore: 0.9, suggestedFallback: null, createdAt: at(50), }; marks.push(created as (typeof marks)[number]); return created; }, }; return { calls, submitErrors, /** Let a started render report `rendering` for this many polls before it finishes. */ holdRenders(polls: number) { pollsUntilDone = polls; }, async install(page: Page) { await page.route("**/rpc/media/**", async (route) => { const request: Request = route.request(); const procedure = new URL(request.url()).pathname.replace(/^.*\/rpc\/media\//, ""); const handler = handlers[procedure]; const body = (request.postDataJSON() ?? {}) as { json?: Record }; const input = body.json ?? {}; calls.push({ procedure, input }); if (!handler) { await route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ json: { message: `unmocked ${procedure}` } }), }); return; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ json: handler(input) }), }); }); // Byte routes: no synthetic media files are needed for these flows. await page.route("**/api/media/**", (route) => route.fulfill({ status: 404, body: "" })); }, }; }