模型載入進度
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: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'})`);
}
}