模型加载进度
INFO
注意: 模型加载进度目前适用于 Web SDK(JavaScript、React、Vue),会显示下载与缓存状态。
Vitals™ SDK 初始化会加载多个 AI 模块(人脸检测、特征点、年龄估计、实时估测等)。首次加载可能需要数秒,透过进度可给用户明确反馈。
概览
每个模块会下载多个模型文件,每个文件都会经历:
- downloading:从 CDN 下载
- caching:写入浏览器缓存
- ready:该模块下载/缓存完成;相机可能仍在预热,请等待
onInitialized
主要组件
| 组件 | 作用 |
|---|---|
| 内建可视化 | 自动在视频上方显示进度(可开关) |
| 进度回调 | 取得事件自行做 UI 或监控 |
关键优势
| 优势 | 说明 |
|---|---|
| 用户反馈 | 告知初始化状态 |
| 网络监控 | 观察不同装置/网络的实际加载时间 |
| 自定义界面 | 依产品风格客制覆盖层 |
| 模块区分 | 对人脸检测与年龄估计显示不同文案 |
| 错误处理 | 下载/缓存失败可提示重试 |
| 性能观察 | 找出最耗时的模块或文件 |
进度事件结构
typescript
interface ModelLoadingProgressEvent {
percentage: number; // 0-100,单一模块的百分比
loaded: number; // 该模块已下载的字节(downloading)
total: number; // 该模块的总字节(downloading)
stage: {
type: 'downloading' | 'caching' | 'ready';
fromCache: boolean;
filename?: string;
error?: Error;
};
module?: string; // 如 face-api、mp-vision-face-mesh
}百分比与阶段说明
- 事件以模块为单位,不做跨模块加总。
- downloading:约 30% 起跳,按模块下载字节推进到 100%;无 content-length 时用文件数估算。
- caching:
loaded/total × 100,针对正在缓存的单一文件,数值可能低于上一笔下载百分比。 - ready:永远 100%,代表该模块完成下载/缓存;相机预热仍需等
onInitialized。 - 某些模块会先发 10% 起始事件,确保 UI 立即显示。
模块字段
face-api:年龄估计/人脸分析mp-vision-face-mesh:MediaPipe 人脸检测/特征点face-mesh:另一套人脸检测实现
阶段类型
- downloading:该模块的模型文件正在下载,
filename指示文件名 - caching:该模块的模型文件正在写入缓存
- ready:该模块文件已完成下载/缓存;需等
onInitialized才表示相机就绪
多文件、多模块
事件会对每个文件、每个模块分别触发;percentage 仅代表当前模块进度,不是全域百分比。
目标 SDK
JavaScript
React
Vue
TIP
参考示例代码和API 参考。核心 API:onModelLoadingProgress、ModelLoadingProgressEvent、visualizationOptions.modelLoadingProgress。
JavaScript SDK 提供内建覆盖层与回调两种方式。
使用内建可视化
typescript
import { createVitalSignCamera } from 'ts-vital-sign-camera';
const video = document.querySelector('video')!;
const camera = createVitalSignCamera({
isActive: true,
userInfo: { age: 30, gender: 'male' }
});
camera.bind(video);
camera.visualizationOptions = {
modelLoadingProgress: { enabled: true }
};使用进度回调(推荐覆盖层策略)
typescript
let isLoading = true;
let progress = null;
const overlayEl = document.getElementById('loading-container');
const barEl = document.getElementById('loading-progress');
const textEl = document.getElementById('loading-text');
const camera = createVitalSignCamera({
isActive: true,
userInfo: { age: 30, gender: 'male' },
onModelLoadingProgress: (evt) => {
progress = evt;
if (barEl) barEl.style.width = `${evt.percentage}%`;
if (textEl) textEl.textContent = evt.stage.type === 'ready' ? 'Initializing...' : `${evt.percentage}%`;
if (overlayEl) overlayEl.style.display = 'flex';
},
onInitialized: () => {
isLoading = false;
if (overlayEl) overlayEl.style.display = 'none';
}
});
camera.bind(video);覆盖层最佳实践
- 启动时就显示覆盖层,缓存命中也能短暂呈现。
ready阶段显示 “Initializing...”,真正关闭在onInitialized。- 第一笔事件前显示默认文字(如 Starting)。
自定义内建覆盖层
typescript
camera.visualizationOptions = {
modelLoadingProgress: {
enabled: true,
backgroundColor: 'rgba(0,0,0,0.7)',
progressColor: '#4CAF50',
textColor: '#4CAF50',
showBytes: true
}
};文件层级追踪示例
typescript
onModelLoadingProgress: (p) => {
if (p.stage.filename) {
console.log(`Downloading ${p.stage.filename} (${p.module || 'unknown'})`);
}
}推荐的加载覆盖层策略
isLoading初始为 true,缓存命中也会短暂显示。- 在
onModelLoadingProgress更新进度;stage.type === 'ready'显示 “Initializing...”。 - 仅在
onInitialized关闭覆盖层。 - 首次事件前显示默认文案(如 Starting)。
typescript
let isLoading = true;
let progress = null;
const overlayEl = document.getElementById('loading-overlay');
const barEl = document.getElementById('loading-bar');
const textEl = document.getElementById('loading-text');
const camera = createVitalSignCamera({
isActive: true,
userInfo: { age: 30, gender: 'male' },
onModelLoadingProgress: (evt) => {
progress = evt;
if (barEl) barEl.style.width = `${evt.percentage}%`;
if (textEl) textEl.textContent = evt.stage.type === 'ready' ? 'Initializing...' : `${evt.percentage}%`;
if (overlayEl) overlayEl.style.display = 'flex';
},
onInitialized: () => {
isLoading = false;
if (overlayEl) overlayEl.style.display = 'none';
}
});
camera.bind(document.querySelector('video'));typescript
import { useState, useCallback } from 'react';
import { VitalSignCamera, Gender } from 'react-vital-sign-camera';
import type { ModelLoadingProgressEvent } from 'react-vital-sign-camera';
export function CameraWithOverlay() {
const [isLoading, setIsLoading] = useState(true);
const [progress, setProgress] = useState<ModelLoadingProgressEvent | null>(null);
const handleProgress = useCallback((evt: ModelLoadingProgressEvent) => {
setProgress(evt);
}, []);
const handleInitialized = useCallback(() => {
setIsLoading(false);
}, []);
return (
<>
{isLoading && (
<div className="overlay">
<div className="bar" style={{ width: `${progress?.percentage || 0}%` }} />
<div className="text">
{progress?.stage.type === 'ready' ? 'Initializing...' : `${progress?.percentage || 0}%`}
</div>
</div>
)}
<VitalSignCamera
isActive={true}
userInfo={{ age: 30, gender: Gender.Male }}
onModelLoadingProgress={handleProgress}
onInitialized={handleInitialized}
/>
</>
);
}vue
<script setup lang="ts">
import { ref } from 'vue'
import { VitalSignCamera, Gender } from 'vue-vital-sign-camera'
import type { ModelLoadingProgressEvent } from 'vue-vital-sign-camera'
const isLoading = ref(true)
const progress = ref<ModelLoadingProgressEvent | null>(null)
const handleProgress = (evt: ModelLoadingProgressEvent) => {
progress.value = evt
}
const handleInitialized = () => {
isLoading.value = false
}
</script>
<template>
<div>
<div v-if="isLoading" class="overlay">
<div class="bar" :style="{ width: `${progress?.percentage || 0}%` }"></div>
<div class="text">
<span v-if="progress?.stage.type === 'ready'">Initializing...</span>
<span v-else>{{ progress?.percentage || 0 }}%</span>
</div>
</div>
<VitalSignCamera
:is-active="true"
:user-info="{ age: 30, gender: Gender.Male }"
@onModelLoadingProgress="handleProgress"
@onInitialized="handleInitialized"
/>
</div>
</template>了解多模块加载
事件会在各模块交错触发,百分比仅代表该模块:
- 人脸检测开始下载:
{ percentage: 30, stage: { type: 'downloading', filename: 'face_landmarker.task' }, module: 'mp-vision-face-mesh' } - 年龄估计同步下载:
{ percentage: 30, stage: { type: 'downloading', filename: 'ssd_mobilenetv1_model-weights_manifest.json' }, module: 'face-api' } - 人脸检测完成:
{ percentage: 100, stage: { type: 'ready', fromCache: false }, module: 'mp-vision-face-mesh' } - 年龄估计完成:
{ percentage: 100, stage: { type: 'ready', fromCache: false }, module: 'face-api' }
不要将不同模块的百分比当成同一根进度条。
模块内的进度计算
- downloading:基础 ~30% + 模块下载字节占比 70%;没有 content-length 时用文件数估算。
- caching:针对单一文件的
loaded/total × 100,可能比上一笔下载百分比更低。 - ready:该模块永远 100%;相机预热仍要等
onInitialized。
模块别追踪
typescript
const moduleProgress = { 'face-api': 0, 'mp-vision-face-mesh': 0 };
const camera = createVitalSignCamera({
isActive: true,
userInfo: { age: 30, gender: 'male' },
onModelLoadingProgress: (p) => {
if (p.module) moduleProgress[p.module] = p.percentage;
console.log(`Overall (last event): ${p.percentage}%`);
}
});文件层级追踪
typescript
onModelLoadingProgress: (p) => {
if (p.stage.filename) {
console.log(`Downloading ${p.stage.filename} (${p.module || 'unknown'})`);
}
}