- Change localhost:3003 to localhost:3002 in demo pages - Add video playback modal to page-api-demo-query.php - Enhance search results with clickable video chunks - Update search functions to support identity and universal search
334 lines
12 KiB
PHP
334 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Template Name: API Demo - 展示 (Display)
|
|
* Description: Demonstrates file detail display, identity visualization, and classification results
|
|
*/
|
|
get_header();
|
|
?>
|
|
|
|
<div class="site-content">
|
|
<h1>Momentry API Demo - 展示</h1>
|
|
<p class="page-description">示範檔案詳情展示、身份視覺化、分類結果呈現等展示類 API 的操作</p>
|
|
|
|
<div class="api-nav">
|
|
<a href="/api-demo-query/" class="nav-item">查詢</a>
|
|
<a href="/api-demo-display/" class="nav-item active">展示</a>
|
|
<a href="/api-demo-operation/" class="nav-item">操作</a>
|
|
<a href="/api-demo-application/" class="nav-item">應用</a>
|
|
</div>
|
|
|
|
<!-- 展示 1: 檔案詳情儀表板 -->
|
|
<div class="api-section">
|
|
<h2>1. 檔案詳情儀表板</h2>
|
|
<p>整合展示檔案的元數據、處理進度、分類標籤等完整資訊。</p>
|
|
|
|
<div class="input-group">
|
|
<label for="dashboard_uuid">File UUID:</label>
|
|
<input type="text" id="dashboard_uuid" placeholder="輸入 file_uuid">
|
|
<button class="btn btn-primary" onclick="loadDashboard()">載入</button>
|
|
</div>
|
|
|
|
<div id="dashboard_container" class="dashboard-grid" style="display:none;">
|
|
<div class="card card-metadata">
|
|
<h3>基本資訊</h3>
|
|
<div id="metadata_content"></div>
|
|
</div>
|
|
<div class="card card-status">
|
|
<h3>處理狀態</h3>
|
|
<div id="status_content"></div>
|
|
</div>
|
|
<div class="card class="card-classification">
|
|
<h3>分類標籤</h3>
|
|
<div id="classification_content"></div>
|
|
</div>
|
|
<div class="card card-identity">
|
|
<h3>關聯身份</h3>
|
|
<div id="identity_content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 展示 2: 身份視覺化 -->
|
|
<div class="api-section">
|
|
<h2>2. 身份視覺化</h2>
|
|
<p>展示身份的跨檔案關聯、臉部檢測統計、品質分數等。</p>
|
|
|
|
<div class="input-group">
|
|
<label for="identity_viz_uuid">Identity UUID:</label>
|
|
<input type="text" id="identity_viz_uuid" placeholder="輸入 identity_uuid">
|
|
<button class="btn btn-primary" onclick="visualizeIdentity()">視覺化</button>
|
|
</div>
|
|
|
|
<div id="identity_viz_container" class="identity-visualization" style="display:none;">
|
|
<div class="viz-header">
|
|
<h3 id="identity_name"></h3>
|
|
<div class="quality-score" id="quality_score"></div>
|
|
</div>
|
|
<div class="viz-grid">
|
|
<div class="viz-card">
|
|
<h4>關聯檔案</h4>
|
|
<ul id="linked_files"></ul>
|
|
</div>
|
|
<div class="viz-card">
|
|
<h4>臉部統計</h4>
|
|
<div id="face_stats"></div>
|
|
</div>
|
|
<div class="viz-card">
|
|
<h4>角度覆蓋</h4>
|
|
<div id="angle_coverage"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 展示 3: 片段展示 -->
|
|
<div class="api-section">
|
|
<h2>3. 影片片段展示</h2>
|
|
<p>展示影片的語義片段、說話者分段、鏡頭切換等分類結果。</p>
|
|
|
|
<div class="input-group">
|
|
<label for="chunks_uuid">File UUID:</label>
|
|
<input type="text" id="chunks_uuid" placeholder="輸入 file_uuid">
|
|
<select id="chunk_type">
|
|
<option value="sentence">語義片段</option>
|
|
<option value="cut">鏡頭切換</option>
|
|
<option value="time">時間片段</option>
|
|
</select>
|
|
<button class="btn btn-primary" onclick="loadChunks()">載入片段</button>
|
|
</div>
|
|
|
|
<div id="chunks_container" class="chunks-timeline" style="display:none;">
|
|
<div id="chunks_list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 展示 4: 分類結果 -->
|
|
<div class="api-section">
|
|
<h2>4. 分類結果展示</h2>
|
|
<p>展示 YOLO 檢測、姿勢估計、動作識別等視覺分類結果。</p>
|
|
|
|
<div class="input-group">
|
|
<label for="classify_uuid">File UUID:</label>
|
|
<input type="text" id="classify_uuid" placeholder="輸入 file_uuid">
|
|
<select id="processor_type">
|
|
<option value="yolo">YOLO 物件檢測</option>
|
|
<option value="pose">姿勢估計</option>
|
|
<option value="face">臉部檢測</option>
|
|
<option value="ocr">OCR 文字識別</option>
|
|
</select>
|
|
<button class="btn btn-primary" onclick="loadClassification()">載入結果</button>
|
|
</div>
|
|
|
|
<div id="classification_container" class="classification-results" style="display:none;">
|
|
<div id="classification_content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.dashboard-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
margin-top: 1rem;
|
|
}
|
|
.card {
|
|
background: var(--card-background);
|
|
border-radius: var(--border-radius);
|
|
padding: 1rem;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.card h3 {
|
|
margin-top: 0;
|
|
color: var(--primary-color);
|
|
border-bottom: 1px solid #e2e8f0;
|
|
padding-bottom: 0.5rem;
|
|
}
|
|
.identity-visualization {
|
|
margin-top: 1rem;
|
|
}
|
|
.viz-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.viz-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 1rem;
|
|
}
|
|
.viz-card {
|
|
background: #f8fafc;
|
|
border-radius: 4px;
|
|
padding: 1rem;
|
|
}
|
|
.viz-card h4 {
|
|
margin-top: 0;
|
|
color: var(--secondary-color);
|
|
}
|
|
.chunks-timeline {
|
|
margin-top: 1rem;
|
|
}
|
|
.chunk-item {
|
|
padding: 0.75rem;
|
|
background: #f1f5f9;
|
|
border-radius: 4px;
|
|
margin-bottom: 0.5rem;
|
|
border-left: 3px solid var(--primary-color);
|
|
}
|
|
.chunk-time {
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
font-family: monospace;
|
|
}
|
|
.classification-results {
|
|
margin-top: 1rem;
|
|
}
|
|
.classification-item {
|
|
display: flex;
|
|
gap: 1rem;
|
|
padding: 0.75rem;
|
|
background: #f8fafc;
|
|
border-radius: 4px;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.confidence-bar {
|
|
width: 100px;
|
|
height: 8px;
|
|
background: #e2e8f0;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.confidence-fill {
|
|
height: 100%;
|
|
background: var(--success-color);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
const API_BASE = 'http://localhost:3002/api/v1';
|
|
|
|
async function loadDashboard() {
|
|
const uuid = document.getElementById('dashboard_uuid').value.trim();
|
|
if (!uuid) return alert('請輸入 file_uuid');
|
|
|
|
try {
|
|
const [fileRes, jobRes] = await Promise.all([
|
|
fetch(`${API_BASE}/files/${uuid}`),
|
|
fetch(`${API_BASE}/jobs/${uuid}/status`)
|
|
]);
|
|
const fileData = await fileRes.json();
|
|
const jobData = await jobRes.json();
|
|
|
|
document.getElementById('dashboard_container').style.display = 'grid';
|
|
|
|
document.getElementById('metadata_content').innerHTML = `
|
|
<p><strong>檔案名稱:</strong> ${fileData.file_name || '-'}</p>
|
|
<p><strong>檔案類型:</strong> ${fileData.file_type || '-'}</p>
|
|
<p><strong>時長:</strong> ${fileData.duration ? fileData.duration.toFixed(1) + 's' : '-'}</p>
|
|
<p><strong>解析度:</strong> ${fileData.width}x${fileData.height}</p>
|
|
<p><strong>幀率:</strong> ${fileData.fps ? fileData.fps.toFixed(1) : '-'} fps</p>
|
|
`;
|
|
|
|
document.getElementById('status_content').innerHTML = `
|
|
<p><strong>狀態:</strong> <span class="badge-status ${fileData.status}">${fileData.status || 'unknown'}</span></p>
|
|
<p><strong>處理進度:</strong> ${jobData.progress || 0}%</p>
|
|
<p><strong>已完成處理器:</strong> ${(jobData.completed_processors || []).join(', ') || '無'}</p>
|
|
`;
|
|
|
|
document.getElementById('classification_content').innerHTML = `
|
|
<p><strong>分類標籤:</strong> ${(fileData.tags || []).join(', ') || '無'}</p>
|
|
<p><strong>語義標籤:</strong> ${(fileData.semantic_tags || []).join(', ') || '無'}</p>
|
|
`;
|
|
|
|
document.getElementById('identity_content').innerHTML = `
|
|
<p><strong>檢測到身份:</strong> ${fileData.identities_count || 0}</p>
|
|
<p><strong>主要身份:</strong> ${fileData.primary_identity || '無'}</p>
|
|
`;
|
|
} catch (e) {
|
|
alert('載入失敗: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function visualizeIdentity() {
|
|
const uuid = document.getElementById('identity_viz_uuid').value.trim();
|
|
if (!uuid) return alert('請輸入 identity_uuid');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/identities/${uuid}`);
|
|
const data = await res.json();
|
|
|
|
document.getElementById('identity_viz_container').style.display = 'block';
|
|
document.getElementById('identity_name').textContent = data.name || '未命名身份';
|
|
document.getElementById('quality_score').innerHTML = `
|
|
品質分數: <strong>${(data.quality_score || 0).toFixed(1)}</strong>/100
|
|
`;
|
|
|
|
document.getElementById('linked_files').innerHTML = (data.linked_files || [])
|
|
.map(f => `<li>${f.file_name} <span class="uuid">${f.file_uuid}</span></li>`)
|
|
.join('') || '<li>無關聯檔案</li>';
|
|
|
|
document.getElementById('face_stats').innerHTML = `
|
|
<p>檢測次數: ${data.detection_count || 0}</p>
|
|
<p>平均品質: ${(data.avg_quality || 0).toFixed(1)}</p>
|
|
`;
|
|
|
|
document.getElementById('angle_coverage').innerHTML = (data.angle_coverage || {})
|
|
.map(a => `<span class="badge ${a.status}">${a.angle}</span>`)
|
|
.join('') || '無資料';
|
|
} catch (e) {
|
|
alert('載入失敗: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function loadChunks() {
|
|
const uuid = document.getElementById('chunks_uuid').value.trim();
|
|
const type = document.getElementById('chunk_type').value;
|
|
if (!uuid) return alert('請輸入 file_uuid');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/files/${uuid}/chunks?type=${type}`);
|
|
const data = await res.json();
|
|
|
|
document.getElementById('chunks_container').style.display = 'block';
|
|
document.getElementById('chunks_list').innerHTML = (data.chunks || [])
|
|
.map(c => `
|
|
<div class="chunk-item">
|
|
<div class="chunk-time">${c.start_time?.toFixed(1) || 0}s - ${c.end_time?.toFixed(1) || 0}s</div>
|
|
<div>${c.text_content || c.description || '無內容'}</div>
|
|
</div>
|
|
`).join('') || '<p>沒有片段</p>';
|
|
} catch (e) {
|
|
alert('載入失敗: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function loadClassification() {
|
|
const uuid = document.getElementById('classify_uuid').value.trim();
|
|
const processor = document.getElementById('processor_type').value;
|
|
if (!uuid) return alert('請輸入 file_uuid');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/files/${uuid}/processors/${processor}`);
|
|
const data = await res.json();
|
|
|
|
document.getElementById('classification_container').style.display = 'block';
|
|
document.getElementById('classification_content').innerHTML = (data.results || [])
|
|
.map(r => `
|
|
<div class="classification-item">
|
|
<div>
|
|
<strong>${r.label || r.class}</strong>
|
|
<div class="confidence-bar"><div class="confidence-fill" style="width:${(r.confidence || 0) * 100}%"></div></div>
|
|
</div>
|
|
<div>${r.bbox ? `[${r.bbox.join(',')}]` : ''}</div>
|
|
</div>
|
|
`).join('') || '<p>沒有檢測結果</p>';
|
|
} catch (e) {
|
|
alert('載入失敗: ' + e.message);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<?php get_footer(); ?>
|