From 82026beb420897d22d0f4ca2ffbc8c5b1319c37a Mon Sep 17 00:00:00 2001 From: sed-i Date: Wed, 7 Jan 2026 19:22:32 +0800 Subject: [PATCH] chore(model-driver-mediapipe): streamline asset prep with retries (#902) --------- Co-authored-by: Neko --- packages/model-driver-mediapipe/package.json | 1 + .../tasks/prepare-tasks.ts | 98 +++++++++++++------ pnpm-lock.yaml | 3 + 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/packages/model-driver-mediapipe/package.json b/packages/model-driver-mediapipe/package.json index 083f0f097..e66b5799a 100644 --- a/packages/model-driver-mediapipe/package.json +++ b/packages/model-driver-mediapipe/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@mediapipe/tasks-vision": "^0.10.0", + "@moeru/std": "catalog:", "@pixiv/three-vrm": "^3.4.4", "es-toolkit": "catalog:", "three": "^0.182.0" diff --git a/packages/model-driver-mediapipe/tasks/prepare-tasks.ts b/packages/model-driver-mediapipe/tasks/prepare-tasks.ts index ce86479c2..7416fb1f2 100644 --- a/packages/model-driver-mediapipe/tasks/prepare-tasks.ts +++ b/packages/model-driver-mediapipe/tasks/prepare-tasks.ts @@ -8,6 +8,8 @@ import fs from 'node:fs/promises' import { Buffer } from 'node:buffer' import { fileURLToPath } from 'node:url' +import { withRetry } from '@moeru/std' +import { attemptAsync } from 'es-toolkit' import { ofetch } from 'ofetch' import { visionTaskAssets } from './tasks' @@ -18,39 +20,79 @@ const taskSources: Record = { face: 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task', } -await fs.mkdir(fileURLToPath(new URL('./assets', import.meta.url)), { recursive: true }) - -await Promise.all(Object.entries(taskSources).map( - async ([key, url]) => { - console.log(`Downloading MediaPipe vision task asset for ${key} from ${url}...`) - const res = await ofetch(url, { responseType: 'arrayBuffer' }) - const outputPath = fileURLToPath(visionTaskAssets[key as keyof VisionTaskAssets]) - await fs.writeFile(outputPath, Buffer.from(res)) - console.log(`MediaPipe vision task asset for ${key} saved to ${outputPath}`) - }, -)) - +const assetsRoot = fileURLToPath(new URL('./assets', import.meta.url)) const wasmSourceDir = fileURLToPath(new URL('../node_modules/@mediapipe/tasks-vision/wasm', import.meta.url)) const wasmOutputDir = fileURLToPath(new URL('./assets/wasm', import.meta.url)) +const taskTargets = Object.entries(taskSources).map(([key, source]) => ({ + key: key as keyof VisionTaskAssets, + source, + outputPath: fileURLToPath(visionTaskAssets[key as keyof VisionTaskAssets]), +})) + +async function isUsableFile(path: string) { + try { + const stat = await fs.stat(path) + return stat.isFile() && stat.size > 0 + } + catch { + return false + } +} + +async function downloadAsset(key: string, url: string, outputPath: string) { + const tempPath = `${outputPath}.download` + let attempt = 0 + + const downloadWithRetry = withRetry(async () => { + attempt += 1 + console.log(`Downloading MediaPipe vision task asset for ${key} from ${url} (attempt ${attempt})...`) + + try { + const [fetchError, response] = await attemptAsync(() => ofetch(url, { responseType: 'arrayBuffer' })) + + if (fetchError || !response) + throw fetchError ?? new Error(`Missing response while downloading MediaPipe vision task asset for ${key}`) + + await fs.writeFile(tempPath, Buffer.from(response)) + await fs.rename(tempPath, outputPath) + console.log(`MediaPipe vision task asset for ${key} saved to ${outputPath}`) + } + finally { + await fs.rm(tempPath, { force: true }) + } + }, { + onError: (error) => { + const message = error instanceof Error ? error.message : String(error) + console.warn(`Failed to download MediaPipe vision task asset for ${key} (attempt ${attempt}): ${message}`) + }, + }) + + try { + await downloadWithRetry() + } + catch (error) { + throw new Error(`Failed to download MediaPipe vision task asset for ${key} after ${attempt} attempts`, { + cause: error, + }) + } +} + +await fs.mkdir(assetsRoot, { recursive: true }) + +for (const { key, source, outputPath } of taskTargets) { + if (await isUsableFile(outputPath)) { + console.log(`MediaPipe vision task asset for ${key} already exists at ${outputPath}, skipping download.`) + continue + } + await downloadAsset(key, source, outputPath) + + if (!await isUsableFile(outputPath)) + throw new Error(`Failed to ensure MediaPipe vision task asset for ${key}: missing or empty file at ${outputPath}`) +} + await fs.mkdir(wasmOutputDir, { recursive: true }) await fs.cp(wasmSourceDir, wasmOutputDir, { recursive: true, force: true }) -await Promise.all(Object.entries(visionTaskAssets).map( - async ([key, url]) => { - const path = fileURLToPath(url) - try { - await fs.access(path, fs.constants.R_OK) - } - catch (err) { - throw new Error(`Failed to ensure MediaPipe vision task asset for ${key}: ${err}`) - } - const stat = await fs.stat(path) - if (!stat.isFile()) { - throw new Error(`Failed to ensure MediaPipe vision task asset for ${key}: not a file: ${path}`) - } - }, -)) - const wasmEntries = await fs.readdir(wasmOutputDir) if (!wasmEntries.length) throw new Error(`Failed to ensure MediaPipe WASM assets: ${wasmOutputDir} is empty`) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5e811135..da051fbb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1808,6 +1808,9 @@ importers: '@mediapipe/tasks-vision': specifier: ^0.10.0 version: 0.10.21 + '@moeru/std': + specifier: 'catalog:' + version: 0.1.0-beta.14 '@pixiv/three-vrm': specifier: ^3.4.4 version: 3.4.4(three@0.182.0)