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
This commit is contained in:
@@ -165,6 +165,24 @@ get_header();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频播放模态窗口 -->
|
||||
<div id="video-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<span class="close-btn">×</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;
|
||||
@@ -337,10 +355,68 @@ get_header();
|
||||
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:3003/api/v1';
|
||||
const API_BASE = 'http://localhost:3002/api/v1';
|
||||
const API_KEY = 'muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69';
|
||||
let workflowFileUuid = null;
|
||||
|
||||
function showResult(elementId, data) {
|
||||
@@ -349,6 +425,60 @@ function showResult(elementId, data) {
|
||||
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`);
|
||||
@@ -569,10 +699,10 @@ async function searchWorkflow() {
|
||||
if (!query) return alert('請輸入搜尋查詢');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/search`, {
|
||||
const res = await fetch(`${API_BASE}/search/universal?api_key=${API_KEY}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, type: 'chunk' })
|
||||
body: JSON.stringify({ query, types: ['chunk'] })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
@@ -582,13 +712,19 @@ async function searchWorkflow() {
|
||||
<p>涵蓋 ${data.files_count || 0} 個檔案</p>
|
||||
`;
|
||||
document.getElementById('search_details_content').innerHTML = (data.results || [])
|
||||
.map(r => `
|
||||
<div class="chunk-item">
|
||||
<div class="chunk-time">${r.file_name} | ${r.start_time?.toFixed(1)}s - ${r.end_time?.toFixed(1)}s</div>
|
||||
<div>${r.text_content || r.description || '無內容'}</div>
|
||||
<div class="badge">相似度: ${(r.similarity * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
`).join('') || '<p>沒有結果</p>';
|
||||
.map(r => {
|
||||
const textAttr = (r.text || '').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ get_header();
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:3003/api/v1';
|
||||
const API_BASE = 'http://localhost:3002/api/v1';
|
||||
|
||||
async function loadDashboard() {
|
||||
const uuid = document.getElementById('dashboard_uuid').value.trim();
|
||||
|
||||
@@ -171,7 +171,7 @@ get_header();
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:3003/api/v1';
|
||||
const API_BASE = 'http://localhost:3002/api/v1';
|
||||
|
||||
function showResult(elementId, data) {
|
||||
const el = document.getElementById(elementId);
|
||||
|
||||
@@ -104,6 +104,24 @@ get_header();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频播放模态窗口 -->
|
||||
<div id="video-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<span class="close-btn">×</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;
|
||||
@@ -223,10 +241,75 @@ get_header();
|
||||
.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);
|
||||
@@ -241,6 +324,60 @@ function showError(elementId, message) {
|
||||
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');
|
||||
@@ -320,18 +457,58 @@ async function queryFileHistory() {
|
||||
async function semanticSearch() {
|
||||
const query = document.getElementById('search_query').value.trim();
|
||||
const type = document.getElementById('search_type').value;
|
||||
if (!query) return alert('請輸入搜尋查詢');
|
||||
|
||||
if (!query) return alert('請輸入搜尋查詞');
|
||||
|
||||
const resultEl = document.getElementById('search_result');
|
||||
resultEl.innerHTML = '<p style="padding: 1rem;">搜尋中,請稍候...(身份搜尋可能需要較長時間)</p>';
|
||||
resultEl.classList.add('has-content');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, type })
|
||||
});
|
||||
const data = await res.json();
|
||||
showResult('search_result', data);
|
||||
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, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
return `<div class="chunk-item clickable"
|
||||
data-file-uuid="${i.file_uuid}"
|
||||
data-start-time="${i.start_time || 0}"
|
||||
data-end-time="${i.end_time || (i.start_time || 0) + 10}"
|
||||
data-text="${textAttr}">
|
||||
<div style="font-weight: 600; color: #1e40af;">${i.name} (${i.source || 'unknown'})</div>
|
||||
<div class="chunk-time">${i.file_uuid} | ${(i.start_time || 0).toFixed(1)}s - ${(i.end_time || (i.start_time || 0) + 10).toFixed(1)}s</div>
|
||||
<div style="margin-top: 4px;">${i.text_content || '無內容'}</div>
|
||||
<button class="play-btn">▶ 播放片段</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, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
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 style="margin-top: 4px;">${r.text || '無內容'}</div>
|
||||
<div class="badge-status completed" style="margin-top: 4px;">相似度: ${(r.score * 100).toFixed(1)}%</div>
|
||||
<button class="play-btn">▶ 播放片段</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) {
|
||||
showError('search_result', e.message);
|
||||
resultEl.innerHTML = '<p style="color:#ef4444; padding: 1rem;">錯誤: ' + e.message + '</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user