Files
m5wp.momentry.ddns.net/themes/momentry/page-api-demo-query.php

521 lines
17 KiB
PHP

<?php
/**
* Template Name: API Demo - 查詢 (Query)
* Description: Demonstrates file and identity query 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 active">查詢</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">應用</a>
</div>
<!-- 查詢 1: 檔案查詢 -->
<div class="api-section">
<h2>1. 檔案查詢 (GET /api/v1/files/:uuid)</h2>
<p>透過 file_uuid 查詢檔案的完整資訊,包含元數據、處理狀態、分類標籤等。</p>
<div class="api-tester">
<div class="input-group">
<label for="file_uuid">File UUID:</label>
<input type="text" id="file_uuid" placeholder="輸入 file_uuid 或從下方列表選擇">
<button class="btn btn-primary" onclick="queryFile()">查詢</button>
</div>
<div class="response-panel" id="file_query_result"></div>
</div>
<div class="file-list-panel">
<h3>最近註冊的檔案</h3>
<button class="btn btn-secondary" onclick="loadRecentFiles()">載入列表</button>
<div id="recent_files_list"></div>
</div>
</div>
<!-- 查詢 2: 身份查詢 -->
<div class="api-section">
<h2>2. 身份查詢 (GET /api/v1/identities/:uuid)</h2>
<p>查詢跨檔案的全域身份資訊,包含關聯的檔案、臉部特徵、品質分數等。</p>
<div class="api-tester">
<div class="input-group">
<label for="identity_uuid">Identity UUID:</label>
<input type="text" id="identity_uuid" placeholder="輸入 identity_uuid">
<button class="btn btn-primary" onclick="queryIdentity()">查詢</button>
</div>
<div class="response-panel" id="identity_query_result"></div>
</div>
</div>
<!-- 查詢 3: 處理狀態 -->
<div class="api-section">
<h2>3. 處理狀態查詢 (GET /api/v1/jobs/:uuid/status)</h2>
<p>查詢檔案的處理進度與各處理器狀態 (ASR, YOLO, Face, OCR 等)。</p>
<div class="api-tester">
<div class="input-group">
<label for="job_uuid">File UUID (Job):</label>
<input type="text" id="job_uuid" placeholder="輸入 file_uuid 查詢處理狀態">
<button class="btn btn-primary" onclick="queryJobStatus()">查詢</button>
</div>
<div class="response-panel" id="job_status_result"></div>
</div>
</div>
<!-- 查詢 4: 遷移歷史 -->
<div class="api-section">
<h2>4. 檔案遷移歷史 (GET /api/v1/files/:uuid/history)</h2>
<p>查詢檔案因移動而產生的身份變更鏈,追蹤 parent_uuid 關聯。</p>
<div class="api-tester">
<div class="input-group">
<label for="history_uuid">File UUID:</label>
<input type="text" id="history_uuid" placeholder="輸入 file_uuid 查詢歷史">
<button class="btn btn-primary" onclick="queryFileHistory()">查詢</button>
</div>
<div class="response-panel" id="file_history_result"></div>
</div>
</div>
<!-- 查詢 5: 語義搜尋 -->
<div class="api-section">
<h2>5. 語義搜尋 (POST /api/v1/search)</h2>
<p>使用自然語言搜尋相關的影片片段或身份。</p>
<div class="api-tester">
<div class="input-group">
<label for="search_query">搜尋查詢:</label>
<input type="text" id="search_query" placeholder="例如: 戴眼鏡的男性在說話">
<select id="search_type">
<option value="chunk">片段搜尋</option>
<option value="identity">身份搜尋</option>
<option value="face">臉部搜尋</option>
</select>
<button class="btn btn-primary" onclick="semanticSearch()">搜尋</button>
</div>
<div class="response-panel" id="search_result"></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>
.api-nav {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
padding: 1rem;
background: var(--card-background);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.api-nav .nav-item {
padding: 0.5rem 1rem;
border-radius: 4px;
text-decoration: none;
color: var(--text-secondary);
font-weight: 500;
}
.api-nav .nav-item.active {
background: var(--primary-color);
color: white;
}
.api-section {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: var(--shadow);
}
.api-section h2 {
margin-top: 0;
color: var(--primary-color);
}
.api-tester {
margin: 1rem 0;
}
.input-group {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.input-group label {
font-weight: 500;
min-width: 120px;
}
.input-group input,
.input-group select {
padding: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 0.875rem;
flex: 1;
min-width: 200px;
}
.btn-primary {
background: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-secondary {
background: var(--secondary-color);
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.response-panel {
margin-top: 1rem;
padding: 1rem;
background: #f1f5f9;
border-radius: 4px;
font-family: monospace;
font-size: 0.75rem;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
display: none;
}
.response-panel.has-content {
display: block;
}
.file-list-panel {
margin-top: 1rem;
}
.file-item {
padding: 0.75rem;
background: #f8fafc;
border-radius: 4px;
margin-bottom: 0.5rem;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-item:hover {
background: #e2e8f0;
}
.file-item .uuid {
font-family: monospace;
font-size: 0.75rem;
color: var(--primary-color);
}
.file-item .name {
font-weight: 500;
}
.badge-status {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
}
.badge-status.pending { background: #fef3c7; color: #92400e; }
.badge-status.processing { background: #dbeafe; color: #1e40af; }
.badge-status.completed { background: #d1fae5; color: #065f46; }
.badge-status.error { background: #fee2e2; color: #991b1b; }
/* 视频播放模态窗口 */
.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;
padding: 0.75rem;
background: #f8fafc;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.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';
const URL_WITH_KEY = `${API_BASE}?api_key=${API_KEY}`;
const URL_WITHOUT_KEY = API_BASE;
function showResult(elementId, data) {
const el = document.getElementById(elementId);
el.textContent = JSON.stringify(data, null, 2);
el.classList.add('has-content');
}
function showError(elementId, message) {
const el = document.getElementById(elementId);
el.textContent = 'Error: ' + message;
el.classList.add('has-content');
el.style.color = '#ef4444';
}
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_result').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);
}
});
});
async function queryFile() {
const uuid = document.getElementById('file_uuid').value.trim();
if (!uuid) return alert('請輸入 file_uuid');
try {
const res = await fetch(`${API_BASE}/files/${uuid}`);
const data = await res.json();
showResult('file_query_result', data);
} catch (e) {
showError('file_query_result', e.message);
}
}
async function loadRecentFiles() {
const el = document.getElementById('recent_files_list');
el.innerHTML = '<p>載入中...</p>';
try {
const res = await fetch(`${API_BASE}/files?limit=10&sort=created_at&order=desc`);
const data = await res.json();
if (data.files && data.files.length > 0) {
el.innerHTML = data.files.map(f => `
<div class="file-item" onclick="document.getElementById('file_uuid').value='${f.file_uuid}'; queryFile();">
<span class="name">${f.file_name}</span>
<span class="uuid">${f.file_uuid}</span>
<span class="badge-status ${f.status}">${f.status || 'unknown'}</span>
</div>
`).join('');
} else {
el.innerHTML = '<p>沒有找到檔案</p>';
}
} catch (e) {
el.innerHTML = '<p style="color:#ef4444">載入失敗: ' + e.message + '</p>';
}
}
async function queryIdentity() {
const uuid = document.getElementById('identity_uuid').value.trim();
if (!uuid) return alert('請輸入 identity_uuid');
try {
const res = await fetch(`${API_BASE}/identities/${uuid}`);
const data = await res.json();
showResult('identity_query_result', data);
} catch (e) {
showError('identity_query_result', e.message);
}
}
async function queryJobStatus() {
const uuid = document.getElementById('job_uuid').value.trim();
if (!uuid) return alert('請輸入 file_uuid');
try {
const res = await fetch(`${API_BASE}/jobs/${uuid}/status`);
const data = await res.json();
showResult('job_status_result', data);
} catch (e) {
showError('job_status_result', e.message);
}
}
async function queryFileHistory() {
const uuid = document.getElementById('history_uuid').value.trim();
if (!uuid) return alert('請輸入 file_uuid');
try {
const res = await fetch(`${API_BASE}/files/${uuid}/history`);
const data = await res.json();
showResult('file_history_result', data);
} catch (e) {
showError('file_history_result', e.message);
}
}
async function semanticSearch() {
const query = document.getElementById('search_query').value.trim();
const type = document.getElementById('search_type').value;
if (!query) return alert('請輸入搜尋查詞');
const resultEl = document.getElementById('search_result');
resultEl.innerHTML = '<p style="padding: 1rem;">搜尋中,請稍候...(身份搜尋可能需要較長時間)</p>';
resultEl.classList.add('has-content');
try {
let resultsHtml = '';
if (type === 'identity') {
const res = await fetch(`${API_BASE}/identities/search?q=${encodeURIComponent(query)}&api_key=${API_KEY}`);
const data = await res.json();
const results = data.results || [];
resultsHtml = results.map(i => {
const textAttr = (i.text_content || '').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const startTime = i.start_time || (i.start_frame && i.fps ? i.start_frame / i.fps : 0);
const endTime = i.end_time || (i.end_frame && i.fps ? i.end_frame / i.fps : startTime + 10);
return `<div class="chunk-item clickable"
data-file-uuid="${i.file_uuid}"
data-start-time="${startTime}"
data-end-time="${endTime}"
data-text="${textAttr}">
<div style="font-weight: 600; color: #1e40af;">${i.name} (${i.source || 'unknown'})</div>
<div class="chunk-time">${startTime.toFixed(1)}s - ${endTime.toFixed(1)}s${i.chunk_id ? ` | ${i.chunk_id}` : ''}</div>
<div style="margin-top: 4px;font-size:13px;color:#555;">${i.text_content || ''}</div>
<button class="play-btn" style="margin-top:6px;">▶ 播放片段</button>
</div>`;
}).join('');
resultEl.innerHTML = `<p style="padding: 0.5rem; background: #d1fae5; border-radius: 4px; margin-bottom: 0.5rem;">找到 ${data.total || 0} 個身份相關片段</p>${resultsHtml || '<p>沒有結果</p>'}`;
} else {
const typesParam = type === 'face' ? ['frame'] : ['chunk'];
const res = await fetch(`${API_BASE}/search/universal?api_key=${API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, types: typesParam })
});
const data = await res.json();
const results = data.results || [];
resultsHtml = results.map(r => {
const textAttr = (r.text || '').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const startTime = r.start_time || (r.start_frame && r.fps ? r.start_frame / r.fps : 0);
const endTime = r.end_time || (r.end_frame && r.fps ? r.end_frame / r.fps : startTime + 10);
return `<div class="chunk-item clickable"
data-file-uuid="${r.file_uuid}"
data-start-time="${startTime}"
data-end-time="${endTime}"
data-text="${textAttr}">
<div class="chunk-time">${startTime.toFixed(1)}s - ${endTime.toFixed(1)}s${r.chunk_id ? ` | ${r.chunk_id}` : ''}</div>
<div style="margin-top: 4px;font-size:13px;color:#555;">${r.text || ''}</div>
<div class="badge-status completed" style="margin-top: 4px;">相似度: ${(r.score * 100).toFixed(1)}%</div>
<button class="play-btn" style="margin-top:6px;">▶ 播放片段</button>
</div>`;
}).join('');
resultEl.innerHTML = `<p style="padding: 0.5rem; background: #d1fae5; border-radius: 4px; margin-bottom: 0.5rem;">找到 ${data.total || 0} 個片段</p>${resultsHtml || '<p>沒有結果</p>'}`;
}
} catch (e) {
resultEl.innerHTML = '<p style="color:#ef4444; padding: 1rem;">錯誤: ' + e.message + '</p>';
}
}
</script>
<?php get_footer(); ?>