Skip to content

模型加载进度

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:onModelLoadingProgressModelLoadingProgressEventvisualizationOptions.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'})`);
  }
}

推荐的加载覆盖层策略

  1. isLoading 初始为 true,缓存命中也会短暂显示。
  2. onModelLoadingProgress 更新进度;stage.type === 'ready' 显示 “Initializing...”。
  3. 仅在 onInitialized 关闭覆盖层。
  4. 首次事件前显示默认文案(如 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>

了解多模块加载

事件会在各模块交错触发,百分比仅代表该模块:

  1. 人脸检测开始下载:{ percentage: 30, stage: { type: 'downloading', filename: 'face_landmarker.task' }, module: 'mp-vision-face-mesh' }
  2. 年龄估计同步下载:{ percentage: 30, stage: { type: 'downloading', filename: 'ssd_mobilenetv1_model-weights_manifest.json' }, module: 'face-api' }
  3. 人脸检测完成:{ percentage: 100, stage: { type: 'ready', fromCache: false }, module: 'mp-vision-face-mesh' }
  4. 年龄估计完成:{ 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'})`);
  }
}