Files
m5wp.momentry.ddns.net/themes/momentry/page-api-demo-application.php
OpenCode 39b9fd7da8 Unify API port to 3002 and add video playback to search page
- 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
2026-05-30 19:32:46 +08:00

735 lines
24 KiB
PHP

<?php
/**
* Template Name: API Demo - 應用 (Application)
* Description: Demonstrates practical workflow scenarios combining multiple APIs
*/
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">展示</a>
<a href="/api-demo-operation/" class="nav-item">操作</a>
<a href="/api-demo-application/" class="nav-item active">應用</a>
</div>
<!-- 應用 1: 完整工作流程 -->
<div class="api-section">
<h2>1. 完整工作流程示範</h2>
<p>從檔案註冊到處理完成,展示完整的端到端工作流程。</p>
<div class="workflow-container">
<div class="workflow-step" id="step1">
<div class="step-header">
<span class="step-number">1</span>
<span class="step-title">註冊檔案</span>
<span class="step-status" id="step1_status">等待中</span>
</div>
<div class="step-content">
<input type="text" id="workflow_path" placeholder="輸入影片路徑">
<button class="btn btn-primary" onclick="workflowRegister()">執行</button>
</div>
</div>
<div class="workflow-step" id="step2">
<div class="step-header">
<span class="step-number">2</span>
<span class="step-title">查詢處理狀態</span>
<span class="step-status" id="step2_status">等待中</span>
</div>
<div class="step-content">
<button class="btn btn-secondary" onclick="workflowCheckStatus()" id="btn_step2" disabled>執行</button>
<span id="step2_progress"></span>
</div>
</div>
<div class="workflow-step" id="step3">
<div class="step-header">
<span class="step-number">3</span>
<span class="step-title">查詢檢測結果</span>
<span class="step-status" id="step3_status">等待中</span>
</div>
<div class="step-content">
<button class="btn btn-secondary" onclick="workflowCheckResults()" id="btn_step3" disabled>執行</button>
<div id="step3_results"></div>
</div>
</div>
<div class="workflow-step" id="step4">
<div class="step-header">
<span class="step-number">4</span>
<span class="step-title">搜尋身份</span>
<span class="step-status" id="step4_status">等待中</span>
</div>
<div class="step-content">
<button class="btn btn-secondary" onclick="workflowSearchIdentity()" id="btn_step4" disabled>執行</button>
<div id="step4_results"></div>
</div>
</div>
</div>
</div>
<!-- 應用 2: 身份追蹤 -->
<div class="api-section">
<h2>2. 跨檔案身份追蹤</h2>
<p>追蹤特定身份在所有檔案中的出現情況,建立身份關聯圖。</p>
<div class="input-group">
<label for="track_uuid">Identity UUID:</label>
<input type="text" id="track_uuid" placeholder="輸入要追蹤的 identity_uuid">
<button class="btn btn-primary" onclick="trackIdentity()">開始追蹤</button>
</div>
<div id="tracking_results" class="tracking-results" style="display:none;">
<div class="tracking-header">
<h3 id="tracking_identity_name"></h3>
<span class="badge" id="tracking_file_count"></span>
</div>
<div class="tracking-timeline" id="tracking_timeline"></div>
<div class="tracking-stats" id="tracking_stats"></div>
</div>
</div>
<!-- 應用 3: 檔案遷移示範 -->
<div class="api-section">
<h2>3. 檔案遷移與身份繼承示範</h2>
<p>演示檔案移動後重新註冊,系統如何建立新的 file_uuid 並保留身份綁定。</p>
<div class="migration-demo">
<div class="migration-step">
<h4>步驟 1: 原始註冊</h4>
<input type="text" id="migration_original_path" placeholder="原始路徑 /path/to/original.mp4">
<button class="btn btn-primary" onclick="migrationRegisterOriginal()">註冊原始檔案</button>
<div class="result-box" id="migration_original_result"></div>
</div>
<div class="migration-arrow">↓</div>
<div class="migration-step">
<h4>步驟 2: 模擬移動檔案</h4>
<input type="text" id="migration_new_path" placeholder="新路徑 /path/to/moved.mp4">
<button class="btn btn-secondary" onclick="migrationSimulateMove()">移動並重新註冊</button>
<div class="result-box" id="migration_new_result"></div>
</div>
<div class="migration-arrow">↓</div>
<div class="migration-step">
<h4>步驟 3: 查詢遷移歷史</h4>
<button class="btn btn-primary" onclick="migrationCheckHistory()">查看歷史</button>
<div class="result-box" id="migration_history_result"></div>
</div>
</div>
</div>
<!-- 應用 4: 批次處理 -->
<div class="api-section">
<h2>4. 批次檔案處理</h2>
<p>一次註冊多個檔案,監控批次處理進度。</p>
<div class="batch-container">
<div class="input-group">
<label for="batch_paths">檔案路徑列表 (每行一個):</label>
<textarea id="batch_paths" rows="5" placeholder="/path/to/video1.mp4&#10;/path/to/video2.mp4&#10;/path/to/video3.mp4"></textarea>
<button class="btn btn-primary" onclick="batchRegister()">批次註冊</button>
</div>
<div class="batch-progress" id="batch_progress" style="display:none;">
<div class="progress-bar">
<div class="progress-fill" id="batch_progress_fill"></div>
</div>
<span id="batch_progress_text">0/0</span>
</div>
<div class="batch-results" id="batch_results"></div>
</div>
</div>
<!-- 應用 5: 語義搜尋工作流 -->
<div class="api-section">
<h2>5. 語義搜尋與片段提取工作流</h2>
<p>使用語義搜尋找到相關片段,然後提取詳細資訊。</p>
<div class="search-workflow">
<div class="input-group">
<label for="semantic_query">搜尋查詢:</label>
<input type="text" id="semantic_query" placeholder="描述你要尋找的內容,例如: 戴眼鏡的男性在戶外說話">
<button class="btn btn-primary" onclick="searchWorkflow()">搜尋</button>
</div>
<div id="search_workflow_results" class="search-results-grid" style="display:none;">
<div class="search-summary">
<h3>搜尋結果摘要</h3>
<div id="search_summary_content"></div>
</div>
<div class="search-details">
<h3>詳細片段</h3>
<div id="search_details_content"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 视频播放模态窗口 -->
<div id="video-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<span class="close-btn">&times;</span>
</div>
<div class="modal-body">
<video id="video-player" controls width="100%">
<source src="" type="video/mp4">
</video>
<div class="video-info">
<div class="chunk-time"></div>
<div class="chunk-text"></div>
</div>
</div>
</div>
</div>
<style>
.workflow-container {
margin-top: 1rem;
}
.workflow-step {
background: #f8fafc;
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
border-left: 4px solid #e2e8f0;
}
.workflow-step.active {
border-left-color: var(--primary-color);
background: #eff6ff;
}
.workflow-step.completed {
border-left-color: var(--success-color);
background: #f0fdf4;
}
.step-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.step-number {
width: 24px;
height: 24px;
background: var(--secondary-color);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
}
.workflow-step.completed .step-number {
background: var(--success-color);
}
.step-title {
font-weight: 600;
flex: 1;
}
.step-status {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: #e2e8f0;
}
.step-content {
display: flex;
gap: 0.5rem;
align-items: center;
}
.step-content input {
flex: 1;
padding: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
}
.migration-demo {
margin-top: 1rem;
}
.migration-step {
background: #f8fafc;
padding: 1rem;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.migration-arrow {
text-align: center;
font-size: 1.5rem;
color: var(--secondary-color);
}
.result-box {
margin-top: 0.5rem;
padding: 0.75rem;
background: #1e293b;
color: #e2e8f0;
border-radius: 4px;
font-family: monospace;
font-size: 0.75rem;
max-height: 200px;
overflow-y: auto;
display: none;
}
.result-box.has-content {
display: block;
}
.batch-container {
margin-top: 1rem;
}
.batch-container textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-family: monospace;
font-size: 0.75rem;
resize: vertical;
}
.batch-progress {
margin: 1rem 0;
display: flex;
align-items: center;
gap: 1rem;
}
.progress-bar {
flex: 1;
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--success-color);
width: 0%;
transition: width 0.3s;
}
.batch-results {
margin-top: 1rem;
}
.batch-item {
padding: 0.75rem;
background: #f8fafc;
border-radius: 4px;
margin-bottom: 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.tracking-results {
margin-top: 1rem;
}
.tracking-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.tracking-timeline {
border-left: 2px solid #e2e8f0;
padding-left: 1rem;
margin-bottom: 1rem;
}
.timeline-item {
margin-bottom: 1rem;
position: relative;
}
.timeline-item::before {
content: '';
width: 8px;
height: 8px;
background: var(--primary-color);
border-radius: 50%;
position: absolute;
left: -1.35rem;
top: 0.5rem;
}
.search-results-grid {
margin-top: 1rem;
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1rem;
}
.search-summary, .search-details {
background: #f8fafc;
padding: 1rem;
border-radius: 4px;
}
/* 视频播放模态窗口 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
}
.modal-content {
margin: 5% auto;
width: 80%;
max-width: 1200px;
background: #1a1a1a;
border-radius: 8px;
overflow: hidden;
}
.modal-header {
padding: 15px;
background: #2d2d2d;
display: flex;
justify-content: space-between;
}
.close-btn {
color: #fff;
font-size: 28px;
cursor: pointer;
}
#video-player {
width: 100%;
max-height: 70vh;
}
.video-info {
padding: 15px;
background: #2d2d2d;
color: #fff;
}
.chunk-item.clickable {
cursor: pointer;
transition: background 0.2s;
}
.chunk-item.clickable:hover {
background: #dbeafe;
}
.play-btn {
margin-top: 8px;
padding: 4px 12px;
background: var(--primary-color);
color: white;
border-radius: 4px;
font-size: 0.8rem;
border: none;
cursor: pointer;
}
</style>
<script>
const API_BASE = 'http://localhost:3002/api/v1';
const API_KEY = 'muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69';
let workflowFileUuid = null;
function showResult(elementId, data) {
const el = document.getElementById(elementId);
el.textContent = JSON.stringify(data, null, 2);
el.classList.add('has-content');
}
function playVideoChunk(fileUuid, startTime, endTime, text) {
const videoUrl = `${API_BASE}/file/${fileUuid}/video?api_key=${API_KEY}&start_time=${startTime}&end_time=${endTime}`;
const modal = document.getElementById('video-modal');
const video = document.getElementById('video-player');
video.pause();
video.src = videoUrl;
video.load();
document.querySelector('.chunk-time').textContent =
`${startTime.toFixed(1)}s - ${endTime.toFixed(1)}s`;
document.querySelector('.chunk-text').textContent = text || '';
modal.style.display = 'block';
video.onloadeddata = () => {
video.play().catch(e => console.warn('playVideoChunk: autoplay blocked', e));
};
video.onerror = () => {
console.error('playVideoChunk: video error', video.error);
};
}
function closeVideoModal() {
const video = document.getElementById('video-player');
document.getElementById('video-modal').style.display = 'none';
video.pause();
video.removeAttribute('src');
video.load();
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('#video-modal .close-btn').onclick = closeVideoModal;
window.onclick = (event) => {
if (event.target === document.getElementById('video-modal')) {
closeVideoModal();
}
};
document.getElementById('search_details_content').addEventListener('click', function(e) {
const item = e.target.closest('.chunk-item.clickable');
if (!item) return;
const fu = item.dataset.fileUuid;
const st = parseFloat(item.dataset.startTime);
const et = parseFloat(item.dataset.endTime);
const txt = item.dataset.text || '';
if (fu && !isNaN(st) && !isNaN(et)) {
playVideoChunk(fu, st, et, txt);
}
});
});
function updateStep(step, status, isCompleted = false) {
const stepEl = document.getElementById(`step${step}`);
const statusEl = document.getElementById(`step${step}_status`);
stepEl.classList.remove('active', 'completed');
if (isCompleted) {
stepEl.classList.add('completed');
statusEl.textContent = '完成';
statusEl.style.background = '#d1fae5';
statusEl.style.color = '#065f46';
} else if (status === 'active') {
stepEl.classList.add('active');
statusEl.textContent = '執行中';
statusEl.style.background = '#dbeafe';
statusEl.style.color = '#1e40af';
} else {
statusEl.textContent = status;
}
}
async function workflowRegister() {
const path = document.getElementById('workflow_path').value.trim();
if (!path) return alert('請輸入影片路徑');
updateStep(1, 'active');
try {
const res = await fetch(`${API_BASE}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_path: path })
});
const data = await res.json();
showResult('step1_result', data);
workflowFileUuid = data.file_uuid || data.uuid;
updateStep(1, '完成', true);
document.getElementById('btn_step2').disabled = false;
} catch (e) {
updateStep(1, '失敗');
}
}
async function workflowCheckStatus() {
if (!workflowFileUuid) return;
updateStep(2, 'active');
try {
const res = await fetch(`${API_BASE}/jobs/${workflowFileUuid}/status`);
const data = await res.json();
document.getElementById('step2_progress').textContent = `進度: ${data.progress || 0}%`;
if (data.status === 'completed') {
updateStep(2, '完成', true);
document.getElementById('btn_step3').disabled = false;
} else {
updateStep(2, `狀態: ${data.status || 'processing'}`);
setTimeout(() => workflowCheckStatus(), 3000);
}
} catch (e) {
updateStep(2, '失敗');
}
}
async function workflowCheckResults() {
if (!workflowFileUuid) return;
updateStep(3, 'active');
try {
const [identitiesRes, chunksRes] = await Promise.all([
fetch(`${API_BASE}/files/${workflowFileUuid}/identities`),
fetch(`${API_BASE}/files/${workflowFileUuid}/chunks`)
]);
const identities = await identitiesRes.json();
const chunks = await chunksRes.json();
document.getElementById('step3_results').innerHTML = `
<p>檢測到身份: ${(identities.identities || []).length}</p>
<p>語義片段: ${(chunks.chunks || []).length}</p>
`;
updateStep(3, '完成', true);
document.getElementById('btn_step4').disabled = false;
} catch (e) {
updateStep(3, '失敗');
}
}
async function workflowSearchIdentity() {
if (!workflowFileUuid) return;
updateStep(4, 'active');
try {
const res = await fetch(`${API_BASE}/files/${workflowFileUuid}/identities`);
const data = await res.json();
document.getElementById('step4_results').innerHTML = (data.identities || [])
.map(i => `<div class="file-item"><span>${i.name || '未命名'}</span><span class="uuid">${i.uuid}</span></div>`)
.join('') || '<p>無身份</p>';
updateStep(4, '完成', true);
} catch (e) {
updateStep(4, '失敗');
}
}
async function trackIdentity() {
const uuid = document.getElementById('track_uuid').value.trim();
if (!uuid) return alert('請輸入 identity_uuid');
try {
const res = await fetch(`${API_BASE}/identities/${uuid}/files`);
const data = await res.json();
document.getElementById('tracking_results').style.display = 'block';
document.getElementById('tracking_identity_name').textContent = data.name || '未命名身份';
document.getElementById('tracking_file_count').textContent = `${(data.files || []).length} 個檔案`;
document.getElementById('tracking_timeline').innerHTML = (data.files || [])
.map(f => `
<div class="timeline-item">
<strong>${f.file_name}</strong>
<div class="uuid">${f.file_uuid}</div>
<div class="chunk-time">時間: ${f.start_time?.toFixed(1) || '?'}s - ${f.end_time?.toFixed(1) || '?'}s</div>
</div>
`).join('') || '<p>無關聯檔案</p>';
document.getElementById('tracking_stats').innerHTML = `
<p>總檢測次數: ${data.total_detections || 0}</p>
<p>平均品質: ${(data.avg_quality || 0).toFixed(1)}</p>
<p>覆蓋角度: ${(data.angle_coverage || []).join(', ') || '無資料'}</p>
`;
} catch (e) {
alert('追蹤失敗: ' + e.message);
}
}
async function migrationRegisterOriginal() {
const path = document.getElementById('migration_original_path').value.trim();
if (!path) return alert('請輸入原始路徑');
try {
const res = await fetch(`${API_BASE}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_path: path })
});
const data = await res.json();
showResult('migration_original_result', data);
} catch (e) {
showResult('migration_original_result', { error: e.message });
}
}
async function migrationSimulateMove() {
const path = document.getElementById('migration_new_path').value.trim();
if (!path) return alert('請輸入新路徑');
try {
const res = await fetch(`${API_BASE}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_path: path })
});
const data = await res.json();
showResult('migration_new_result', data);
} catch (e) {
showResult('migration_new_result', { error: e.message });
}
}
async function migrationCheckHistory() {
const path = document.getElementById('migration_new_path').value.trim();
if (!path) return alert('請先輸入新路徑');
try {
const res = await fetch(`${API_BASE}/files/${path}/history`);
const data = await res.json();
showResult('migration_history_result', data);
} catch (e) {
showResult('migration_history_result', { error: e.message });
}
}
async function batchRegister() {
const paths = document.getElementById('batch_paths').value.trim().split('\n').filter(p => p.trim());
if (paths.length === 0) return alert('請輸入至少一個檔案路徑');
document.getElementById('batch_progress').style.display = 'flex';
const resultsEl = document.getElementById('batch_results');
resultsEl.innerHTML = '';
let completed = 0;
for (const path of paths) {
try {
const res = await fetch(`${API_BASE}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_path: path.trim() })
});
const data = await res.json();
resultsEl.innerHTML += `
<div class="batch-item">
<span>${path.trim()}</span>
<span class="badge-status ${data.status || 'pending'}">${data.file_uuid || 'failed'}</span>
</div>
`;
} catch (e) {
resultsEl.innerHTML += `
<div class="batch-item">
<span>${path.trim()}</span>
<span class="badge-status error">失敗: ${e.message}</span>
</div>
`;
}
completed++;
document.getElementById('batch_progress_fill').style.width = `${(completed / paths.length) * 100}%`;
document.getElementById('batch_progress_text').textContent = `${completed}/${paths.length}`;
}
}
async function searchWorkflow() {
const query = document.getElementById('semantic_query').value.trim();
if (!query) return alert('請輸入搜尋查詢');
try {
const res = await fetch(`${API_BASE}/search/universal?api_key=${API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, types: ['chunk'] })
});
const data = await res.json();
document.getElementById('search_workflow_results').style.display = 'grid';
document.getElementById('search_summary_content').innerHTML = `
<p>找到 ${data.total || 0} 個結果</p>
<p>涵蓋 ${data.files_count || 0} 個檔案</p>
`;
document.getElementById('search_details_content').innerHTML = (data.results || [])
.map(r => {
const textAttr = (r.text || '').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return `<div class="chunk-item clickable"
data-file-uuid="${r.file_uuid}"
data-start-time="${r.start_time || 0}"
data-end-time="${r.end_time || (r.start_time || 0) + 10}"
data-text="${textAttr}">
<div class="chunk-time">${r.file_uuid} | ${(r.start_time || 0).toFixed(1)}s - ${(r.end_time || (r.start_time || 0) + 10).toFixed(1)}s</div>
<div>${r.text || '無內容'}</div>
<div class="badge">相似度: ${(r.score * 100).toFixed(1)}%</div>
<button class="play-btn">▶ 播放片段</button>
</div>`;
}).join('') || '<p>沒有結果</p>';
} catch (e) {
alert('搜尋失敗: ' + e.message);
}
}
</script>
<?php get_footer(); ?>