Skip to content

模型載入進度

INFO

注意: 模型載入進度目前適用於網頁版 SDK(JavaScript、React、Vue)。它會即時顯示 AI 模型的下載與快取狀態。

Vitals™ SDK 初始化時會載入多個 AI 模組(臉部偵測、臉部特徵點、年齡估計、即時估測等)。依網路與裝置效能不同,首次載入可能需要數秒。透過模型載入進度,你可以回饋給使用者正在發生的事。

概覽

SDK 會依模組分批載入多個模型檔案,每個檔案都會經過以下階段:

  • 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,僅針對正在快取的檔案,數值可能比前一個 downloading 百分比低。
  • 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'})`);
  }
}