Spaces:
Configuration error
Configuration error
| /* | |
| This version adds: | |
| * Editable steps, guidance scale, shift, and seed | |
| * Step presets: 4 and 20 | |
| * Deterministic comparisons using the same fixture tensors | |
| * Run history showing parameters, generation time, hashes, Gaussian count, and pass/fail | |
| * Download PLY and Download .splat | |
| * Persistent OPFS preference where supported | |
| * Storage usage and persistence reporting | |
| * Image dimension and alpha-channel reporting | |
| * Package-owned production worker rather than the workspace source worker | |
| * Current run cancellation | |
| * No separate HTML changes because the additional controls are created dynamically | |
| */import { createElement } from 'react' | |
| import { flushSync } from 'react-dom' | |
| import { createRoot, type Root } from 'react-dom/client' | |
| import { TripoSplatWebGPU } from '../packages/triposplat-webgpu/dist/index.js' | |
| import type { | |
| CacheBackend, | |
| GenerationProgress, | |
| GaussianScene, | |
| LoadProgress, | |
| } from '../packages/triposplat-webgpu/dist/index.js' | |
| import { | |
| qualifyGaussianSceneStructure, | |
| qualifyGaussianViewer, | |
| type GaussianSceneStructuralQualification, | |
| type GaussianViewerObservation, | |
| type GaussianViewerQualification, | |
| } from './e2eLabQualification' | |
| import { | |
| SplatPreview, | |
| type SplatPreviewStatus, | |
| } from './components/SplatPreview' | |
| const FLOAT_COUNTS = { | |
| vaeNoise: 1 * 32 * 128 * 128, | |
| latentNoise: 1 * 8192 * 16, | |
| cameraNoise: 1 * 1 * 5, | |
| } as const | |
| const DEFAULT_GENERATION_SETTINGS = { | |
| steps: 4, | |
| guidanceScale: 3, | |
| shift: 3, | |
| seed: 42, | |
| } as const | |
| const STEP_PRESETS = [4, 20] as const | |
| const PLY_FLOATS_PER_GAUSSIAN = 17 | |
| const SPLAT_BYTES_PER_GAUSSIAN = 32 | |
| const VIEWER_LOAD_TIMEOUT_MS = 90_000 | |
| const MAX_PROGRESS_RECORDS = 5_000 | |
| const MAX_RUN_HISTORY = 20 | |
| interface GenerationParameters { | |
| steps: 4 | 20 | |
| guidanceScale: number | |
| shift: number | |
| seed: number | |
| } | |
| interface PreparedImageMetadata { | |
| mimeType: string | |
| byteLength: number | |
| width: number | |
| height: number | |
| hasAlpha: boolean | |
| } | |
| interface ProgressRecord { | |
| phase: 'load' | 'generate' | |
| elapsedMs: number | |
| stage: string | |
| message: string | |
| progress?: number | |
| loadedBytes?: number | |
| totalBytes?: number | |
| graph?: string | |
| step?: number | |
| totalSteps?: number | |
| invocation?: number | |
| totalInvocations?: number | |
| } | |
| interface ExportQualification { | |
| passed: boolean | |
| ply: { | |
| byteLength: number | |
| sha256: string | |
| headerBytes: number | |
| expectedByteLength: number | |
| binaryLittleEndian: boolean | |
| vertexCountMatches: boolean | |
| byteLengthMatches: boolean | |
| } | |
| splat: { | |
| byteLength: number | |
| sha256: string | |
| expectedByteLength: number | |
| byteLengthMatches: boolean | |
| } | |
| } | |
| interface StorageQualification { | |
| persistenceRequested: boolean | |
| persisted?: boolean | |
| usageBytes?: number | |
| quotaBytes?: number | |
| } | |
| interface E2ELabSuccess { | |
| passed: boolean | |
| completed: true | |
| qualificationScope: 'end-to-end-execution-structure-export-and-viewer-sanity' | |
| numericalParityClaimed: false | |
| model: { | |
| baseUrl: string | |
| manifestVersion?: string | |
| modelRevision?: string | |
| declaredModelBytes?: number | |
| configuredGraphs: readonly string[] | |
| cache: CacheBackend | |
| } | |
| fixtures: { | |
| preparedImage: string | |
| preparedImageMetadata: PreparedImageMetadata | |
| vaeNoise: string | |
| latentNoise: string | |
| cameraNoise: string | |
| explicitDeterministicNoise: true | |
| } | |
| generation: GenerationParameters | |
| scene: GaussianSceneStructuralQualification | |
| gaussianCount: number | |
| exports: ExportQualification | |
| viewer: GaussianViewerQualification | |
| timingsMs: { | |
| fixtureFetch: number | |
| load: number | |
| generate: number | |
| plyExportAndHash: number | |
| splatExportAndHash: number | |
| viewerLoad: number | |
| total: number | |
| } | |
| pipelineMeasuredTimingsMs?: unknown | |
| progress: ProgressRecord[] | |
| lifecycle: { | |
| sceneDisposed: boolean | |
| modelDisposed: boolean | |
| } | |
| storage?: StorageQualification | |
| environment: { | |
| userAgent: string | |
| crossOriginIsolated: boolean | |
| webgpu: boolean | |
| } | |
| } | |
| interface E2ELabFailure { | |
| passed: false | |
| completed: false | |
| qualificationScope: 'end-to-end-execution-structure-export-and-viewer-sanity' | |
| numericalParityClaimed: false | |
| error: SerializedError | |
| progress: ProgressRecord[] | |
| timingsMs: { | |
| total: number | |
| } | |
| environment: { | |
| userAgent: string | |
| crossOriginIsolated: boolean | |
| webgpu: boolean | |
| } | |
| } | |
| interface SerializedError { | |
| name: string | |
| message: string | |
| stack?: string | |
| code?: string | |
| stage?: string | |
| diagnostics?: unknown | |
| cause?: SerializedError | |
| } | |
| interface RunHistoryRecord { | |
| runNumber: number | |
| timestamp: string | |
| passed: boolean | |
| steps: number | |
| guidanceScale: number | |
| shift: number | |
| seed: number | |
| gaussianCount: number | |
| fixtureFetchMs: number | |
| loadMs: number | |
| generateMs: number | |
| totalMs: number | |
| plyBytes: number | |
| splatBytes: number | |
| plySha256: string | |
| splatSha256: string | |
| } | |
| interface InjectedControls { | |
| stepsInput: HTMLInputElement | |
| guidanceInput: HTMLInputElement | |
| shiftInput: HTMLInputElement | |
| seedInput: HTMLInputElement | |
| downloadPlyButton: HTMLButtonElement | |
| downloadSplatButton: HTMLButtonElement | |
| clearHistoryButton: HTMLButtonElement | |
| storageStatus: HTMLElement | |
| imageStatus: HTMLElement | |
| historyBody: HTMLTableSectionElement | |
| } | |
| type E2ELabResult = E2ELabSuccess | E2ELabFailure | |
| declare global { | |
| interface Window { | |
| __TRIPOSPLAT_E2E_RESULT__?: E2ELabResult | |
| __TRIPOSPLAT_E2E_HISTORY__?: RunHistoryRecord[] | |
| } | |
| } | |
| function requiredElement<T extends Element>(selector: string): T { | |
| const element = document.querySelector<T>(selector) | |
| if (element === null) { | |
| throw new Error(`Required E2E lab element '${selector}' was not found.`) | |
| } | |
| return element | |
| } | |
| const modelBaseInput = requiredElement<HTMLInputElement>('#model-base') | |
| const cacheInput = requiredElement<HTMLSelectElement>('#cache') | |
| const preparedImageInput = requiredElement<HTMLInputElement>('#prepared-image') | |
| const vaeNoiseInput = requiredElement<HTMLInputElement>('#vae-noise') | |
| const flowFixtureInput = requiredElement<HTMLInputElement>('#flow-fixture') | |
| const runButton = requiredElement<HTMLButtonElement>('#run') | |
| const cancelButton = requiredElement<HTMLButtonElement>('#cancel') | |
| const statusElement = requiredElement<HTMLElement>('[data-testid="e2e-status"]') | |
| const errorElement = requiredElement<HTMLPreElement>('[data-testid="e2e-error"]') | |
| const resultElement = requiredElement<HTMLPreElement>('[data-testid="e2e-result"]') | |
| const previewMount = requiredElement<HTMLElement>('#e2e-preview-root') | |
| const previewStatusElement = requiredElement<HTMLElement>( | |
| '[data-testid="e2e-viewer-status"]', | |
| ) | |
| const previewRoot: Root = createRoot(previewMount) | |
| const injectedControls = createInjectedControls() | |
| let activeController: AbortController | undefined | |
| let activeModel: TripoSplatWebGPU | undefined | |
| let activePlyUrl: string | undefined | |
| let downloadablePly: Blob | undefined | |
| let downloadableSplat: Blob | undefined | |
| let previewGenerationKey = 0 | |
| let runCounter = 0 | |
| let busy = false | |
| let pageClosing = false | |
| const runHistory: RunHistoryRecord[] = [] | |
| window.__TRIPOSPLAT_E2E_HISTORY__ = runHistory | |
| function createInjectedControls(): InjectedControls { | |
| const panel = document.createElement('section') | |
| panel.id = 'e2e-generation-controls' | |
| panel.setAttribute('aria-label', 'Generation comparison controls') | |
| panel.style.display = 'grid' | |
| panel.style.gap = '12px' | |
| panel.style.margin = '16px 0' | |
| panel.style.padding = '16px' | |
| panel.style.border = '1px solid currentColor' | |
| panel.style.borderRadius = '8px' | |
| const heading = document.createElement('h2') | |
| heading.textContent = 'Generation comparison' | |
| heading.style.margin = '0' | |
| heading.style.fontSize = '1rem' | |
| panel.appendChild(heading) | |
| const fields = document.createElement('div') | |
| fields.style.display = 'grid' | |
| fields.style.gridTemplateColumns = 'repeat(auto-fit, minmax(140px, 1fr))' | |
| fields.style.gap = '12px' | |
| const stepsInput = createNumberInput( | |
| fields, | |
| 'Sampling steps', | |
| 'e2e-steps', | |
| DEFAULT_GENERATION_SETTINGS.steps, | |
| 4, | |
| 20, | |
| 1, | |
| ) | |
| const guidanceInput = createNumberInput( | |
| fields, | |
| 'Guidance scale', | |
| 'e2e-guidance-scale', | |
| DEFAULT_GENERATION_SETTINGS.guidanceScale, | |
| 0, | |
| 30, | |
| 0.1, | |
| ) | |
| const shiftInput = createNumberInput( | |
| fields, | |
| 'Shift', | |
| 'e2e-shift', | |
| DEFAULT_GENERATION_SETTINGS.shift, | |
| 0, | |
| 30, | |
| 0.1, | |
| ) | |
| const seedInput = createNumberInput( | |
| fields, | |
| 'Seed', | |
| 'e2e-seed', | |
| DEFAULT_GENERATION_SETTINGS.seed, | |
| 0, | |
| 2_147_483_647, | |
| 1, | |
| ) | |
| panel.appendChild(fields) | |
| const presets = document.createElement('div') | |
| presets.style.display = 'flex' | |
| presets.style.flexWrap = 'wrap' | |
| presets.style.gap = '8px' | |
| const presetLabel = document.createElement('span') | |
| presetLabel.textContent = 'Step presets:' | |
| presets.appendChild(presetLabel) | |
| for (const steps of STEP_PRESETS) { | |
| const button = document.createElement('button') | |
| button.type = 'button' | |
| button.textContent = String(steps) | |
| button.dataset.stepsPreset = String(steps) | |
| button.addEventListener('click', () => { | |
| stepsInput.value = String(steps) | |
| }) | |
| presets.appendChild(button) | |
| } | |
| panel.appendChild(presets) | |
| const actions = document.createElement('div') | |
| actions.style.display = 'flex' | |
| actions.style.flexWrap = 'wrap' | |
| actions.style.gap = '8px' | |
| const downloadPlyButton = document.createElement('button') | |
| downloadPlyButton.type = 'button' | |
| downloadPlyButton.textContent = 'Download current PLY' | |
| downloadPlyButton.disabled = true | |
| const downloadSplatButton = document.createElement('button') | |
| downloadSplatButton.type = 'button' | |
| downloadSplatButton.textContent = 'Download current .splat' | |
| downloadSplatButton.disabled = true | |
| const clearHistoryButton = document.createElement('button') | |
| clearHistoryButton.type = 'button' | |
| clearHistoryButton.textContent = 'Clear comparison history' | |
| actions.append( | |
| downloadPlyButton, | |
| downloadSplatButton, | |
| clearHistoryButton, | |
| ) | |
| panel.appendChild(actions) | |
| const storageStatus = document.createElement('div') | |
| storageStatus.dataset.testid = 'e2e-storage-status' | |
| storageStatus.textContent = 'Storage status has not been measured.' | |
| panel.appendChild(storageStatus) | |
| const imageStatus = document.createElement('div') | |
| imageStatus.dataset.testid = 'e2e-image-status' | |
| imageStatus.textContent = 'Prepared image has not been inspected.' | |
| panel.appendChild(imageStatus) | |
| const historyContainer = document.createElement('div') | |
| historyContainer.style.overflowX = 'auto' | |
| const historyTable = document.createElement('table') | |
| historyTable.style.width = '100%' | |
| historyTable.style.borderCollapse = 'collapse' | |
| historyTable.dataset.testid = 'e2e-run-history' | |
| const historyHead = document.createElement('thead') | |
| historyHead.innerHTML = ` | |
| <tr> | |
| <th>Run</th> | |
| <th>Result</th> | |
| <th>Steps</th> | |
| <th>Guidance</th> | |
| <th>Shift</th> | |
| <th>Seed</th> | |
| <th>Gaussians</th> | |
| <th>Generate</th> | |
| <th>Total</th> | |
| <th>PLY hash</th> | |
| <th>.splat hash</th> | |
| </tr> | |
| ` | |
| const historyBody = document.createElement('tbody') | |
| historyTable.append(historyHead, historyBody) | |
| historyContainer.appendChild(historyTable) | |
| panel.appendChild(historyContainer) | |
| const insertionParent = runButton.parentElement ?? runButton | |
| insertionParent.insertAdjacentElement('afterend', panel) | |
| downloadPlyButton.addEventListener('click', () => { | |
| if (downloadablePly !== undefined) { | |
| downloadBlob( | |
| downloadablePly, | |
| buildExportFilename('ply'), | |
| ) | |
| } | |
| }) | |
| downloadSplatButton.addEventListener('click', () => { | |
| if (downloadableSplat !== undefined) { | |
| downloadBlob( | |
| downloadableSplat, | |
| buildExportFilename('splat'), | |
| ) | |
| } | |
| }) | |
| clearHistoryButton.addEventListener('click', () => { | |
| runHistory.splice(0, runHistory.length) | |
| window.__TRIPOSPLAT_E2E_HISTORY__ = runHistory | |
| renderRunHistory(historyBody) | |
| }) | |
| return { | |
| stepsInput, | |
| guidanceInput, | |
| shiftInput, | |
| seedInput, | |
| downloadPlyButton, | |
| downloadSplatButton, | |
| clearHistoryButton, | |
| storageStatus, | |
| imageStatus, | |
| historyBody, | |
| } | |
| } | |
| function createNumberInput( | |
| parent: HTMLElement, | |
| labelText: string, | |
| id: string, | |
| value: number, | |
| min: number, | |
| max: number, | |
| step: number, | |
| ): HTMLInputElement { | |
| const label = document.createElement('label') | |
| label.htmlFor = id | |
| label.style.display = 'grid' | |
| label.style.gap = '4px' | |
| const labelTextElement = document.createElement('span') | |
| labelTextElement.textContent = labelText | |
| const input = document.createElement('input') | |
| input.id = id | |
| input.type = 'number' | |
| input.value = String(value) | |
| input.min = String(min) | |
| input.max = String(max) | |
| input.step = String(step) | |
| label.append(labelTextElement, input) | |
| parent.appendChild(label) | |
| return input | |
| } | |
| function environment(): E2ELabResult['environment'] { | |
| return { | |
| userAgent: navigator.userAgent, | |
| crossOriginIsolated: self.crossOriginIsolated, | |
| webgpu: 'gpu' in navigator, | |
| } | |
| } | |
| function normalizeDirectory(value: string): string { | |
| return value.replace(/\/+$/, '') | |
| } | |
| function requiredInputValue( | |
| input: HTMLInputElement | HTMLSelectElement, | |
| label: string, | |
| ): string { | |
| const value = input.value.trim() | |
| if (value.length === 0) { | |
| throw new Error(`${label} must not be empty.`) | |
| } | |
| return value | |
| } | |
| function parseFiniteNumber( | |
| input: HTMLInputElement, | |
| label: string, | |
| minimum: number, | |
| maximum: number, | |
| ): number { | |
| const value = Number(input.value) | |
| if (!Number.isFinite(value)) { | |
| throw new Error(`${label} must be a finite number.`) | |
| } | |
| if (value < minimum || value > maximum) { | |
| throw new Error( | |
| `${label} must be between ${minimum} and ${maximum}.`, | |
| ) | |
| } | |
| return value | |
| } | |
| function parseInteger( | |
| input: HTMLInputElement, | |
| label: string, | |
| minimum: number, | |
| maximum: number, | |
| ): number { | |
| const value = parseFiniteNumber( | |
| input, | |
| label, | |
| minimum, | |
| maximum, | |
| ) | |
| if (!Number.isInteger(value)) { | |
| throw new Error(`${label} must be an integer.`) | |
| } | |
| return value | |
| } | |
| function parseSamplingSteps(input: HTMLInputElement): 4 | 20 { | |
| const value = parseInteger( | |
| input, | |
| 'Sampling steps', | |
| 4, | |
| 20, | |
| ) | |
| if (value !== 4 && value !== 20) { | |
| throw new Error('Sampling steps must use the supported 4-step or 20-step schedule.') | |
| } | |
| return value | |
| } | |
| function selectedGenerationParameters(): GenerationParameters { | |
| return { | |
| steps: parseSamplingSteps(injectedControls.stepsInput), | |
| guidanceScale: parseFiniteNumber( | |
| injectedControls.guidanceInput, | |
| 'Guidance scale', | |
| 0, | |
| 30, | |
| ), | |
| shift: parseFiniteNumber( | |
| injectedControls.shiftInput, | |
| 'Shift', | |
| 0, | |
| 30, | |
| ), | |
| seed: parseInteger( | |
| injectedControls.seedInput, | |
| 'Seed', | |
| 0, | |
| 2_147_483_647, | |
| ), | |
| } | |
| } | |
| function selectedCache(): CacheBackend { | |
| const selected = cacheInput.value | |
| if ( | |
| selected === 'none' | |
| || selected === 'opfs' | |
| || selected === 'cache-api' | |
| ) { | |
| return selected | |
| } | |
| throw new Error(`Unsupported cache selection '${selected}'.`) | |
| } | |
| function configureDefaultCache(): void { | |
| const hasOpfs = | |
| navigator.storage !== undefined | |
| && typeof navigator.storage.getDirectory === 'function' | |
| if (hasOpfs) { | |
| cacheInput.value = 'opfs' | |
| return | |
| } | |
| if ('caches' in window) { | |
| cacheInput.value = 'cache-api' | |
| return | |
| } | |
| cacheInput.value = 'none' | |
| } | |
| async function prepareStorage( | |
| cache: CacheBackend, | |
| ): Promise<StorageQualification | undefined> { | |
| if (cache !== 'opfs' || navigator.storage === undefined) { | |
| injectedControls.storageStatus.textContent = | |
| `Cache backend: ${cache}. OPFS persistence was not requested.` | |
| return undefined | |
| } | |
| const result: StorageQualification = { | |
| persistenceRequested: false, | |
| } | |
| if (typeof navigator.storage.persisted === 'function') { | |
| result.persisted = await navigator.storage.persisted() | |
| .catch(() => false) | |
| } | |
| if ( | |
| result.persisted !== true | |
| && typeof navigator.storage.persist === 'function' | |
| ) { | |
| result.persistenceRequested = true | |
| result.persisted = await navigator.storage.persist() | |
| .catch(() => false) | |
| } | |
| await updateStorageEstimate(result) | |
| renderStorageStatus(result) | |
| return result | |
| } | |
| async function updateStorageEstimate( | |
| storage: StorageQualification, | |
| ): Promise<void> { | |
| if ( | |
| navigator.storage === undefined | |
| || typeof navigator.storage.estimate !== 'function' | |
| ) { | |
| return | |
| } | |
| const estimate = await navigator.storage.estimate() | |
| .catch(() => undefined) | |
| if (estimate?.usage !== undefined) { | |
| storage.usageBytes = estimate.usage | |
| } | |
| if (estimate?.quota !== undefined) { | |
| storage.quotaBytes = estimate.quota | |
| } | |
| } | |
| function renderStorageStatus( | |
| storage: StorageQualification, | |
| ): void { | |
| const persistence = | |
| storage.persisted === true | |
| ? 'persistent' | |
| : storage.persisted === false | |
| ? 'not guaranteed persistent' | |
| : 'persistence unknown' | |
| const usage = | |
| storage.usageBytes === undefined | |
| ? 'usage unknown' | |
| : `${formatBytes(storage.usageBytes)} used` | |
| const quota = | |
| storage.quotaBytes === undefined | |
| ? 'quota unknown' | |
| : `${formatBytes(storage.quotaBytes)} quota` | |
| injectedControls.storageStatus.textContent = | |
| `OPFS cache: ${persistence}; ${usage}; ${quota}.` | |
| } | |
| async function fetchRequired( | |
| url: string, | |
| signal: AbortSignal, | |
| ): Promise<Response> { | |
| if (url.length === 0) { | |
| throw new Error('Attempted to fetch an empty URL.') | |
| } | |
| const response = await fetch(url, { | |
| signal, | |
| cache: 'no-cache', | |
| }) | |
| if (!response.ok) { | |
| throw new Error( | |
| `Could not fetch ${url}: HTTP ${response.status} ${response.statusText}.`, | |
| ) | |
| } | |
| return response | |
| } | |
| async function fetchFloat32( | |
| url: string, | |
| expectedElements: number, | |
| signal: AbortSignal, | |
| ): Promise<Float32Array> { | |
| const response = await fetchRequired(url, signal) | |
| const bytes = await response.arrayBuffer() | |
| const expectedBytes = | |
| expectedElements * Float32Array.BYTES_PER_ELEMENT | |
| if (bytes.byteLength !== expectedBytes) { | |
| throw new Error( | |
| `${url} has ${bytes.byteLength.toLocaleString()} bytes; ` | |
| + `expected ${expectedBytes.toLocaleString()} bytes ` | |
| + `(${expectedElements.toLocaleString()} Float32 values).`, | |
| ) | |
| } | |
| return new Float32Array(bytes) | |
| } | |
| async function inspectPreparedImage( | |
| blob: Blob, | |
| url: string, | |
| ): Promise<PreparedImageMetadata> { | |
| if (blob.size === 0) { | |
| throw new Error(`Prepared image fixture '${url}' is empty.`) | |
| } | |
| if (blob.type.length > 0 && !blob.type.startsWith('image/')) { | |
| throw new Error( | |
| `Prepared image fixture '${url}' has unexpected MIME type '${blob.type}'.`, | |
| ) | |
| } | |
| const bitmap = await createImageBitmap(blob) | |
| try { | |
| if (bitmap.width === 0 || bitmap.height === 0) { | |
| throw new Error( | |
| `Prepared image fixture '${url}' decoded with zero dimensions.`, | |
| ) | |
| } | |
| const canvas = document.createElement('canvas') | |
| canvas.width = bitmap.width | |
| canvas.height = bitmap.height | |
| const context = canvas.getContext('2d', { | |
| willReadFrequently: true, | |
| }) | |
| if (context === null) { | |
| throw new Error( | |
| 'Could not create a 2D canvas context to inspect the prepared image.', | |
| ) | |
| } | |
| context.drawImage(bitmap, 0, 0) | |
| const pixels = context.getImageData( | |
| 0, | |
| 0, | |
| bitmap.width, | |
| bitmap.height, | |
| ).data | |
| let hasAlpha = false | |
| for (let index = 3; index < pixels.length; index += 4) { | |
| if (pixels[index] !== 255) { | |
| hasAlpha = true | |
| break | |
| } | |
| } | |
| const metadata: PreparedImageMetadata = { | |
| mimeType: blob.type || 'unknown', | |
| byteLength: blob.size, | |
| width: bitmap.width, | |
| height: bitmap.height, | |
| hasAlpha, | |
| } | |
| injectedControls.imageStatus.textContent = | |
| `Prepared image: ${metadata.width}×${metadata.height}, ` | |
| + `${metadata.mimeType}, ${formatBytes(metadata.byteLength)}, ` | |
| + `${metadata.hasAlpha ? 'contains transparency' : 'fully opaque'}.` | |
| return metadata | |
| } finally { | |
| bitmap.close() | |
| } | |
| } | |
| async function sha256(blob: Blob): Promise<string> { | |
| const digest = await crypto.subtle.digest( | |
| 'SHA-256', | |
| await blob.arrayBuffer(), | |
| ) | |
| return Array.from( | |
| new Uint8Array(digest), | |
| (value) => value.toString(16).padStart(2, '0'), | |
| ).join('') | |
| } | |
| function formatBytes(bytes: number): string { | |
| if (bytes < 1_024) { | |
| return `${bytes} B` | |
| } | |
| if (bytes < 1_024 * 1_024) { | |
| return `${(bytes / 1_024).toFixed(1)} KiB` | |
| } | |
| if (bytes < 1_024 * 1_024 * 1_024) { | |
| return `${(bytes / (1_024 * 1_024)).toFixed(1)} MiB` | |
| } | |
| return `${(bytes / (1_024 * 1_024 * 1_024)).toFixed(2)} GiB` | |
| } | |
| function formatDuration(milliseconds: number): string { | |
| if (milliseconds < 1_000) { | |
| return `${milliseconds.toFixed(0)} ms` | |
| } | |
| return `${(milliseconds / 1_000).toFixed(2)} s` | |
| } | |
| function shortHash(hash: string): string { | |
| return hash.slice(0, 12) | |
| } | |
| function buildExportFilename( | |
| extension: 'ply' | 'splat', | |
| ): string { | |
| const parameters = selectedGenerationParameters() | |
| return [ | |
| 'triposplat', | |
| `${parameters.steps}steps`, | |
| `g${parameters.guidanceScale}`, | |
| `shift${parameters.shift}`, | |
| `seed${parameters.seed}`, | |
| ].join('-') + `.${extension}` | |
| } | |
| function downloadBlob( | |
| blob: Blob, | |
| filename: string, | |
| ): void { | |
| const url = URL.createObjectURL(blob) | |
| const anchor = document.createElement('a') | |
| anchor.href = url | |
| anchor.download = filename | |
| anchor.style.display = 'none' | |
| document.body.appendChild(anchor) | |
| anchor.click() | |
| anchor.remove() | |
| window.setTimeout(() => { | |
| URL.revokeObjectURL(url) | |
| }, 0) | |
| } | |
| function setDownloadableExports( | |
| ply: Blob | undefined, | |
| splat: Blob | undefined, | |
| ): void { | |
| downloadablePly = ply | |
| downloadableSplat = splat | |
| injectedControls.downloadPlyButton.disabled = | |
| ply === undefined | |
| injectedControls.downloadSplatButton.disabled = | |
| splat === undefined | |
| } | |
| function setControlsDisabled(disabled: boolean): void { | |
| injectedControls.stepsInput.disabled = disabled | |
| injectedControls.guidanceInput.disabled = disabled | |
| injectedControls.shiftInput.disabled = disabled | |
| injectedControls.seedInput.disabled = disabled | |
| for (const button of document.querySelectorAll<HTMLButtonElement>( | |
| '[data-steps-preset]', | |
| )) { | |
| button.disabled = disabled | |
| } | |
| injectedControls.clearHistoryButton.disabled = disabled | |
| } | |
| function setPreviewStatus( | |
| state: 'waiting' | 'loading' | 'ready' | 'failed', | |
| message: string, | |
| ): void { | |
| previewStatusElement.dataset.viewerState = state | |
| previewStatusElement.textContent = message | |
| } | |
| function releaseActivePreview(): void { | |
| if (!pageClosing) { | |
| flushSync(() => { | |
| previewRoot.render(null) | |
| }) | |
| } | |
| if (activePlyUrl !== undefined) { | |
| URL.revokeObjectURL(activePlyUrl) | |
| activePlyUrl = undefined | |
| } | |
| } | |
| function resetPreview(): void { | |
| releaseActivePreview() | |
| setPreviewStatus( | |
| 'waiting', | |
| 'Viewer is waiting for an exported PLY.', | |
| ) | |
| } | |
| function signalError(signal: AbortSignal): Error { | |
| return signal.reason instanceof Error | |
| ? signal.reason | |
| : new DOMException( | |
| 'The operation was aborted.', | |
| 'AbortError', | |
| ) | |
| } | |
| function observeViewerCanvas(): GaussianViewerObservation['canvas'] { | |
| const canvas = previewMount.querySelector<HTMLCanvasElement>('canvas') | |
| return { | |
| present: canvas !== null, | |
| width: canvas?.width ?? 0, | |
| height: canvas?.height ?? 0, | |
| clientWidth: canvas?.clientWidth ?? 0, | |
| clientHeight: canvas?.clientHeight ?? 0, | |
| } | |
| } | |
| async function loadPlyInViewer( | |
| ply: Blob, | |
| signal: AbortSignal, | |
| ): Promise<GaussianViewerQualification> { | |
| if (signal.aborted) { | |
| throw signalError(signal) | |
| } | |
| resetPreview() | |
| activePlyUrl = URL.createObjectURL(ply) | |
| previewGenerationKey += 1 | |
| const plyUrl = activePlyUrl | |
| const generationKey = previewGenerationKey | |
| const startedAt = performance.now() | |
| const observation = await new Promise<GaussianViewerObservation>( | |
| (resolve, reject) => { | |
| let settled = false | |
| let layoutFrame: number | undefined | |
| const cleanup = () => { | |
| window.clearTimeout(timeout) | |
| signal.removeEventListener('abort', onAbort) | |
| if (layoutFrame !== undefined) { | |
| cancelAnimationFrame(layoutFrame) | |
| layoutFrame = undefined | |
| } | |
| } | |
| const complete = ( | |
| status: GaussianViewerObservation['status'], | |
| message: string, | |
| afterLayout = false, | |
| ) => { | |
| if (settled) { | |
| return | |
| } | |
| settled = true | |
| cleanup() | |
| setPreviewStatus(status, message) | |
| const finish = () => { | |
| layoutFrame = undefined | |
| if (signal.aborted) { | |
| reject(signalError(signal)) | |
| return | |
| } | |
| resolve({ | |
| status, | |
| message, | |
| loadMs: performance.now() - startedAt, | |
| timeoutMs: VIEWER_LOAD_TIMEOUT_MS, | |
| canvas: observeViewerCanvas(), | |
| }) | |
| } | |
| if (afterLayout) { | |
| layoutFrame = requestAnimationFrame(finish) | |
| } else { | |
| finish() | |
| } | |
| } | |
| const onStatus = (status: SplatPreviewStatus) => { | |
| if (settled) { | |
| return | |
| } | |
| setPreviewStatus(status.state, status.message) | |
| if (status.state === 'ready') { | |
| complete('ready', status.message, true) | |
| } else if (status.state === 'failed') { | |
| complete('failed', status.message) | |
| } | |
| } | |
| const onAbort = () => { | |
| if (settled) { | |
| return | |
| } | |
| settled = true | |
| cleanup() | |
| setPreviewStatus('failed', 'Viewer load aborted.') | |
| reject(signalError(signal)) | |
| } | |
| const timeout = window.setTimeout(() => { | |
| complete( | |
| 'failed', | |
| 'Preview failed: viewer did not reach ready or failed within ' | |
| + `${VIEWER_LOAD_TIMEOUT_MS} ms.`, | |
| ) | |
| }, VIEWER_LOAD_TIMEOUT_MS) | |
| signal.addEventListener('abort', onAbort, { | |
| once: true, | |
| }) | |
| try { | |
| previewRoot.render( | |
| createElement(SplatPreview, { | |
| plyUrl, | |
| generationKey, | |
| bgColor: '#000000', | |
| fov: 45, | |
| autoRotate: false, | |
| maxScreenSize: 1024, | |
| splatPosition: [0, 0, 0], | |
| splatRotation: [0, 0, 0], | |
| splatFlip: [false, false, false], | |
| onViewerStateChange: onStatus, | |
| }), | |
| ) | |
| } catch (error) { | |
| complete( | |
| 'failed', | |
| `Preview failed: ${ | |
| error instanceof Error | |
| ? error.message | |
| : String(error) | |
| }`, | |
| ) | |
| } | |
| }, | |
| ) | |
| return qualifyGaussianViewer(observation) | |
| } | |
| async function qualifyExports( | |
| ply: Blob, | |
| splat: Blob, | |
| count: number, | |
| plyHash: string, | |
| splatHash: string, | |
| ): Promise<ExportQualification> { | |
| const headerPrefix = await ply.slice(0, 4096).text() | |
| const marker = 'end_header\n' | |
| const markerIndex = headerPrefix.indexOf(marker) | |
| const markerEnd = | |
| markerIndex >= 0 | |
| ? markerIndex + marker.length | |
| : -1 | |
| const headerBytes = | |
| markerEnd >= 0 | |
| ? new TextEncoder() | |
| .encode(headerPrefix.slice(0, markerEnd)) | |
| .byteLength | |
| : 0 | |
| const expectedPlyBytes = | |
| headerBytes | |
| + count | |
| * PLY_FLOATS_PER_GAUSSIAN | |
| * Float32Array.BYTES_PER_ELEMENT | |
| const expectedSplatBytes = | |
| count * SPLAT_BYTES_PER_GAUSSIAN | |
| const plySummary = { | |
| byteLength: ply.size, | |
| sha256: plyHash, | |
| headerBytes, | |
| expectedByteLength: expectedPlyBytes, | |
| binaryLittleEndian: headerPrefix.startsWith( | |
| 'ply\nformat binary_little_endian 1.0\n', | |
| ), | |
| vertexCountMatches: headerPrefix.includes( | |
| `element vertex ${count}\n`, | |
| ), | |
| byteLengthMatches: | |
| headerBytes > 0 | |
| && ply.size === expectedPlyBytes, | |
| } | |
| const splatSummary = { | |
| byteLength: splat.size, | |
| sha256: splatHash, | |
| expectedByteLength: expectedSplatBytes, | |
| byteLengthMatches: | |
| splat.size === expectedSplatBytes, | |
| } | |
| return { | |
| passed: | |
| plySummary.binaryLittleEndian | |
| && plySummary.vertexCountMatches | |
| && plySummary.byteLengthMatches | |
| && splatSummary.byteLengthMatches, | |
| ply: plySummary, | |
| splat: splatSummary, | |
| } | |
| } | |
| function createProgressRecorder(startedAt: number): { | |
| records: ProgressRecord[] | |
| load: (progress: LoadProgress) => void | |
| generate: (progress: GenerationProgress) => void | |
| } { | |
| const records: ProgressRecord[] = [] | |
| let previousKey = '' | |
| const record = ( | |
| phase: ProgressRecord['phase'], | |
| update: LoadProgress | GenerationProgress, | |
| ) => { | |
| const bucket = | |
| update.progress === undefined | |
| ? '' | |
| : Math.floor(update.progress * 20) | |
| const generation = update as GenerationProgress | |
| const load = update as LoadProgress | |
| const key = [ | |
| phase, | |
| update.stage, | |
| load.graph ?? '', | |
| generation.invocation ?? '', | |
| generation.step ?? '', | |
| bucket, | |
| ].join(':') | |
| const next: ProgressRecord = { | |
| phase, | |
| elapsedMs: performance.now() - startedAt, | |
| stage: update.stage, | |
| message: update.message, | |
| } | |
| if (update.progress !== undefined) { | |
| next.progress = update.progress | |
| } | |
| if (update.loadedBytes !== undefined) { | |
| next.loadedBytes = update.loadedBytes | |
| } | |
| if (update.totalBytes !== undefined) { | |
| next.totalBytes = update.totalBytes | |
| } | |
| if (load.graph !== undefined) { | |
| next.graph = load.graph | |
| } | |
| if (generation.step !== undefined) { | |
| next.step = generation.step | |
| } | |
| if (generation.totalSteps !== undefined) { | |
| next.totalSteps = generation.totalSteps | |
| } | |
| if (generation.invocation !== undefined) { | |
| next.invocation = generation.invocation | |
| } | |
| if (generation.totalInvocations !== undefined) { | |
| next.totalInvocations = generation.totalInvocations | |
| } | |
| if (key === previousKey && records.length > 0) { | |
| records[records.length - 1] = next | |
| } else if (records.length < MAX_PROGRESS_RECORDS) { | |
| records.push(next) | |
| } | |
| previousKey = key | |
| statusElement.textContent = update.message | |
| } | |
| return { | |
| records, | |
| load: (progress) => record('load', progress), | |
| generate: (progress) => record('generate', progress), | |
| } | |
| } | |
| function publish(result: E2ELabResult): void { | |
| window.__TRIPOSPLAT_E2E_RESULT__ = result | |
| resultElement.textContent = JSON.stringify(result, null, 2) | |
| resultElement.hidden = false | |
| if (result.completed) { | |
| if (result.passed) { | |
| statusElement.textContent = | |
| 'PASS: the browser-local pipeline produced ' | |
| + `${result.gaussianCount.toLocaleString()} finite Gaussians ` | |
| + `using ${result.generation.steps} steps, valid-size exports, ` | |
| + 'and a ready Gaussian viewer canvas.' | |
| } else { | |
| statusElement.textContent = | |
| 'FAIL: end-to-end execution completed, but a scene, export, ' | |
| + 'or viewer sanity check failed.' | |
| } | |
| return | |
| } | |
| statusElement.textContent = | |
| `E2E run did not complete: ${result.error.message}` | |
| } | |
| function serializeError( | |
| value: unknown, | |
| depth = 0, | |
| ): SerializedError { | |
| const baseError = | |
| value instanceof Error | |
| ? value | |
| : new Error(String(value)) | |
| const error = errorWithDetails(baseError) | |
| const serialized: SerializedError = { | |
| name: error.name, | |
| message: error.message, | |
| } | |
| if (error.stack !== undefined) { | |
| serialized.stack = error.stack | |
| } | |
| if (typeof error.code === 'string') { | |
| serialized.code = error.code | |
| } | |
| if (typeof error.stage === 'string') { | |
| serialized.stage = error.stage | |
| } | |
| if (error.diagnostics !== undefined) { | |
| serialized.diagnostics = error.diagnostics | |
| } | |
| if (error.cause !== undefined && depth < 6) { | |
| serialized.cause = serializeError( | |
| error.cause, | |
| depth + 1, | |
| ) | |
| } | |
| return serialized | |
| } | |
| function errorWithDetails(value: Error): Error & { | |
| code?: unknown | |
| stage?: unknown | |
| diagnostics?: unknown | |
| cause?: unknown | |
| } { | |
| return value | |
| } | |
| async function disposeModel( | |
| model: TripoSplatWebGPU | undefined, | |
| ): Promise<boolean> { | |
| if (model === undefined) { | |
| return true | |
| } | |
| await model.dispose() | |
| return true | |
| } | |
| function addRunHistoryRecord( | |
| result: E2ELabSuccess, | |
| ): void { | |
| runCounter += 1 | |
| runHistory.unshift({ | |
| runNumber: runCounter, | |
| timestamp: new Date().toISOString(), | |
| passed: result.passed, | |
| steps: result.generation.steps, | |
| guidanceScale: result.generation.guidanceScale, | |
| shift: result.generation.shift, | |
| seed: result.generation.seed, | |
| gaussianCount: result.gaussianCount, | |
| fixtureFetchMs: result.timingsMs.fixtureFetch, | |
| loadMs: result.timingsMs.load, | |
| generateMs: result.timingsMs.generate, | |
| totalMs: result.timingsMs.total, | |
| plyBytes: result.exports.ply.byteLength, | |
| splatBytes: result.exports.splat.byteLength, | |
| plySha256: result.exports.ply.sha256, | |
| splatSha256: result.exports.splat.sha256, | |
| }) | |
| if (runHistory.length > MAX_RUN_HISTORY) { | |
| runHistory.length = MAX_RUN_HISTORY | |
| } | |
| window.__TRIPOSPLAT_E2E_HISTORY__ = runHistory | |
| renderRunHistory(injectedControls.historyBody) | |
| } | |
| function renderRunHistory( | |
| body: HTMLTableSectionElement, | |
| ): void { | |
| body.replaceChildren() | |
| for (const record of runHistory) { | |
| const row = document.createElement('tr') | |
| appendHistoryCell(row, String(record.runNumber)) | |
| appendHistoryCell(row, record.passed ? 'PASS' : 'FAIL') | |
| appendHistoryCell(row, String(record.steps)) | |
| appendHistoryCell(row, String(record.guidanceScale)) | |
| appendHistoryCell(row, String(record.shift)) | |
| appendHistoryCell(row, String(record.seed)) | |
| appendHistoryCell( | |
| row, | |
| record.gaussianCount.toLocaleString(), | |
| ) | |
| appendHistoryCell( | |
| row, | |
| formatDuration(record.generateMs), | |
| ) | |
| appendHistoryCell( | |
| row, | |
| formatDuration(record.totalMs), | |
| ) | |
| appendHistoryCell( | |
| row, | |
| shortHash(record.plySha256), | |
| record.plySha256, | |
| ) | |
| appendHistoryCell( | |
| row, | |
| shortHash(record.splatSha256), | |
| record.splatSha256, | |
| ) | |
| body.appendChild(row) | |
| } | |
| } | |
| function appendHistoryCell( | |
| row: HTMLTableRowElement, | |
| text: string, | |
| title?: string, | |
| ): void { | |
| const cell = document.createElement('td') | |
| cell.textContent = text | |
| cell.style.padding = '6px' | |
| cell.style.borderTop = '1px solid currentColor' | |
| if (title !== undefined) { | |
| cell.title = title | |
| } | |
| row.appendChild(cell) | |
| } | |
| async function run(): Promise<void> { | |
| if (busy) { | |
| return | |
| } | |
| busy = true | |
| runButton.disabled = true | |
| cancelButton.disabled = false | |
| errorElement.hidden = true | |
| resultElement.hidden = true | |
| delete window.__TRIPOSPLAT_E2E_RESULT__ | |
| setControlsDisabled(true) | |
| setDownloadableExports(undefined, undefined) | |
| resetPreview() | |
| const startedAt = performance.now() | |
| const controller = new AbortController() | |
| activeController = controller | |
| const progress = createProgressRecorder(startedAt) | |
| let model: TripoSplatWebGPU | undefined | |
| let scene: GaussianScene | undefined | |
| let sceneDisposed = false | |
| let modelDisposed = false | |
| try { | |
| if (!('gpu' in navigator)) { | |
| throw new Error( | |
| 'WebGPU is unavailable in this browser.', | |
| ) | |
| } | |
| const generation = selectedGenerationParameters() | |
| const modelBaseUrl = normalizeDirectory( | |
| requiredInputValue( | |
| modelBaseInput, | |
| 'Model base URL', | |
| ), | |
| ) | |
| const preparedImageUrl = requiredInputValue( | |
| preparedImageInput, | |
| 'Prepared image URL', | |
| ) | |
| const vaeNoiseUrl = requiredInputValue( | |
| vaeNoiseInput, | |
| 'VAE noise URL', | |
| ) | |
| const flowBase = normalizeDirectory( | |
| requiredInputValue( | |
| flowFixtureInput, | |
| 'Flow fixture directory', | |
| ), | |
| ) | |
| const latentNoiseUrl = `${flowBase}/latent.f32` | |
| const cameraNoiseUrl = `${flowBase}/camera.f32` | |
| const cache = selectedCache() | |
| statusElement.textContent = | |
| cache === 'opfs' | |
| ? 'Preparing persistent OPFS model cache…' | |
| : `Preparing ${cache} model cache…` | |
| const storage = await prepareStorage(cache) | |
| const fixtureStartedAt = performance.now() | |
| statusElement.textContent = | |
| 'Fetching prepared image and deterministic noise fixtures…' | |
| const [ | |
| imageResponse, | |
| vaeNoise, | |
| latentNoise, | |
| cameraNoise, | |
| ] = await Promise.all([ | |
| fetchRequired( | |
| preparedImageUrl, | |
| controller.signal, | |
| ), | |
| fetchFloat32( | |
| vaeNoiseUrl, | |
| FLOAT_COUNTS.vaeNoise, | |
| controller.signal, | |
| ), | |
| fetchFloat32( | |
| latentNoiseUrl, | |
| FLOAT_COUNTS.latentNoise, | |
| controller.signal, | |
| ), | |
| fetchFloat32( | |
| cameraNoiseUrl, | |
| FLOAT_COUNTS.cameraNoise, | |
| controller.signal, | |
| ), | |
| ]) | |
| const preparedImage = await imageResponse.blob() | |
| const preparedImageMetadata = | |
| await inspectPreparedImage( | |
| preparedImage, | |
| preparedImageUrl, | |
| ) | |
| const fixtureFetchMs = | |
| performance.now() - fixtureStartedAt | |
| /* | |
| * The package dist entry owns production worker creation. | |
| * | |
| * Do not point this lab at: | |
| * ../packages/triposplat-webgpu/src/worker.ts | |
| * | |
| * Doing that would test the workspace source worker instead of the | |
| * packaged worker shipped by the installed distribution. | |
| */ | |
| model = new TripoSplatWebGPU({ | |
| modelBaseUrl, | |
| executionProviders: ['webgpu'], | |
| cache, | |
| logLevel: 'info', | |
| wasmPaths: { | |
| mjs: '/ort/ort-wasm-simd-threaded.asyncify.mjs', | |
| wasm: '/ort/ort-wasm-simd-threaded.asyncify.wasm', | |
| }, | |
| }) | |
| activeModel = model | |
| const loadStartedAt = performance.now() | |
| await model.load({ | |
| signal: controller.signal, | |
| onProgress: progress.load, | |
| }) | |
| const loadMs = | |
| performance.now() - loadStartedAt | |
| const generationStartedAt = performance.now() | |
| scene = await model.generate(preparedImage, { | |
| steps: generation.steps, | |
| guidanceScale: generation.guidanceScale, | |
| shift: generation.shift, | |
| seed: generation.seed, | |
| inputIsPrepared: true, | |
| vaeNoise, | |
| latentNoise, | |
| cameraNoise, | |
| signal: controller.signal, | |
| onProgress: progress.generate, | |
| }) | |
| const generateMs = | |
| performance.now() - generationStartedAt | |
| const gaussianCount = scene.count | |
| const sceneQualification = | |
| qualifyGaussianSceneStructure(scene) | |
| statusElement.textContent = | |
| 'Encoding and hashing binary PLY…' | |
| const plyStartedAt = performance.now() | |
| const ply = await scene.exportPLY() | |
| const plyHash = await sha256(ply) | |
| const plyExportAndHashMs = | |
| performance.now() - plyStartedAt | |
| statusElement.textContent = | |
| 'Encoding and hashing browser .splat…' | |
| const splatStartedAt = performance.now() | |
| const splat = await scene.exportSplat() | |
| const splatHash = await sha256(splat) | |
| const splatExportAndHashMs = | |
| performance.now() - splatStartedAt | |
| const exportsQualification = await qualifyExports( | |
| ply, | |
| splat, | |
| gaussianCount, | |
| plyHash, | |
| splatHash, | |
| ) | |
| setDownloadableExports(ply, splat) | |
| statusElement.textContent = | |
| 'Loading the exported PLY in the browser Gaussian viewer…' | |
| const viewerQualification = await loadPlyInViewer( | |
| ply, | |
| controller.signal, | |
| ) | |
| const measuredGenerationSettings = | |
| scene.metadata.generationSettings | |
| const manifest = model.manifest | |
| const configuredGraphs = | |
| model.capabilities.configuredGraphs | |
| scene.dispose() | |
| sceneDisposed = scene.isDisposed | |
| modelDisposed = await disposeModel(model) | |
| activeModel = undefined | |
| model = undefined | |
| if (storage !== undefined) { | |
| await updateStorageEstimate(storage) | |
| renderStorageStatus(storage) | |
| } | |
| const result: E2ELabSuccess = { | |
| passed: | |
| sceneQualification.passed | |
| && exportsQualification.passed | |
| && viewerQualification.passed, | |
| completed: true, | |
| qualificationScope: | |
| 'end-to-end-execution-structure-export-and-viewer-sanity', | |
| numericalParityClaimed: false, | |
| model: { | |
| baseUrl: modelBaseUrl, | |
| configuredGraphs, | |
| cache, | |
| }, | |
| fixtures: { | |
| preparedImage: preparedImageUrl, | |
| preparedImageMetadata, | |
| vaeNoise: vaeNoiseUrl, | |
| latentNoise: latentNoiseUrl, | |
| cameraNoise: cameraNoiseUrl, | |
| explicitDeterministicNoise: true, | |
| }, | |
| generation, | |
| scene: sceneQualification, | |
| gaussianCount, | |
| exports: exportsQualification, | |
| viewer: viewerQualification, | |
| timingsMs: { | |
| fixtureFetch: fixtureFetchMs, | |
| load: loadMs, | |
| generate: generateMs, | |
| plyExportAndHash: | |
| plyExportAndHashMs, | |
| splatExportAndHash: | |
| splatExportAndHashMs, | |
| viewerLoad: | |
| viewerQualification.loadMs, | |
| total: | |
| performance.now() - startedAt, | |
| }, | |
| pipelineMeasuredTimingsMs: | |
| measuredGenerationSettings.measuredTimingsMs, | |
| progress: progress.records, | |
| lifecycle: { | |
| sceneDisposed, | |
| modelDisposed, | |
| }, | |
| storage, | |
| environment: environment(), | |
| } | |
| if (manifest !== undefined) { | |
| result.model.manifestVersion = | |
| manifest.version | |
| result.model.modelRevision = | |
| manifest.modelRevision | |
| if ( | |
| manifest.estimatedModelBytes !== undefined | |
| ) { | |
| result.model.declaredModelBytes = | |
| manifest.estimatedModelBytes | |
| } | |
| } | |
| addRunHistoryRecord(result) | |
| publish(result) | |
| } catch (caught) { | |
| if (scene !== undefined && !scene.isDisposed) { | |
| scene.dispose() | |
| sceneDisposed = scene.isDisposed | |
| } | |
| if (model !== undefined) { | |
| modelDisposed = await disposeModel(model) | |
| .catch(() => false) | |
| } | |
| activeModel = undefined | |
| const error = | |
| caught instanceof Error | |
| ? caught | |
| : new Error(String(caught)) | |
| releaseActivePreview() | |
| setPreviewStatus( | |
| 'failed', | |
| 'Viewer unavailable because the E2E run stopped: ' | |
| + error.message, | |
| ) | |
| const serialized = serializeError(error) | |
| const result: E2ELabFailure = { | |
| passed: false, | |
| completed: false, | |
| qualificationScope: | |
| 'end-to-end-execution-structure-export-and-viewer-sanity', | |
| numericalParityClaimed: false, | |
| error: serialized, | |
| progress: progress.records, | |
| timingsMs: { | |
| total: | |
| performance.now() - startedAt, | |
| }, | |
| environment: environment(), | |
| } | |
| errorElement.textContent = | |
| `${error.name}: ${error.message}` | |
| errorElement.hidden = false | |
| publish(result) | |
| void sceneDisposed | |
| void modelDisposed | |
| } finally { | |
| if (activeController === controller) { | |
| activeController = undefined | |
| } | |
| busy = false | |
| runButton.disabled = false | |
| cancelButton.disabled = true | |
| setControlsDisabled(false) | |
| } | |
| } | |
| configureDefaultCache() | |
| renderRunHistory(injectedControls.historyBody) | |
| runButton.addEventListener('click', () => { | |
| void run() | |
| }) | |
| cancelButton.addEventListener('click', () => { | |
| cancelButton.disabled = true | |
| statusElement.textContent = | |
| 'Cancelling after the active ONNX invocation returns…' | |
| activeController?.abort( | |
| new DOMException( | |
| 'Cancelled from the E2E lab.', | |
| 'AbortError', | |
| ), | |
| ) | |
| }) | |
| document.documentElement.dataset.e2eBootstrap = 'ready' | |
| window.addEventListener('beforeunload', () => { | |
| pageClosing = true | |
| activeController?.abort( | |
| new DOMException( | |
| 'E2E lab page is closing.', | |
| 'AbortError', | |
| ), | |
| ) | |
| void activeModel?.dispose() | |
| previewRoot.unmount() | |
| if (activePlyUrl !== undefined) { | |
| URL.revokeObjectURL(activePlyUrl) | |
| activePlyUrl = undefined | |
| } | |
| downloadablePly = undefined | |
| downloadableSplat = undefined | |
| }) |