Compare commits

...

7 Commits

Author SHA1 Message Date
OpenCode
72a4e8f3b1 feat: update search cards with chunk_id display; sync snippet db 2026-06-01 21:58:33 +08:00
OpenCode
ccfdfb2c20 Fix video playback: calculate time from frame/fps when start_time/end_time not available
- Identity search returns start_frame/end_frame/fps instead of start_time/end_time
- Calculate start_time = start_frame / fps, end_time = end_frame / fps
- Apply same fix to universal search results
2026-05-30 19:37:00 +08:00
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
OpenCode
48ef06a07a Add WordPress database backup (split into 50MB parts)
- 563MB MySQL dump split into 12 parts
- wp_snippets, wp_posts, wp_options, wp_postmeta
- Use: cat database/wordpress_tables_part_* | mysql -u wp_user -p wordpress
2026-05-29 19:16:21 +08:00
OpenCode
7ca909179f Add WordPress database backup (561MB) with git-lfs
- Full MySQL dump of wordpress database
- Includes wp_snippets (all API proxies)
- Includes wp_posts, wp_options, wp_postmeta
- Uses git-lfs for large file handling
2026-05-29 19:13:32 +08:00
OpenCode
3d008235be Add WordPress database backup with git-lfs
- Full MySQL dump: 561MB
- Using git-lfs for large file storage
- Includes all wp_* tables
2026-05-29 19:12:20 +08:00
OpenCode
f2a0347dab Add WordPress database backup (split into 50MB parts)
- Total: 561MB MySQL dump
- Split into ~12 parts for git compatibility
- Includes wp_snippets, wp_posts, wp_options, etc.
2026-05-29 19:11:43 +08:00
19 changed files with 35624 additions and 72 deletions

2
.gitignore vendored
View File

@@ -11,8 +11,8 @@ debug.log
error_log
# Generated files
*.sql
*.log
database/wordpress_tables_orig.sql
# OS files
.DS_Store

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,25 +0,0 @@
<IfModule mod_authz_core.c>
<FilesMatch ".*">
Require all denied
</FilesMatch>
<FilesMatch "\.log$">
Require all granted
</FilesMatch>
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
<FilesMatch "\.log$">
Order allow,deny
Allow from all
</FilesMatch>
</IfModule>
<IfModule mod_mime.c>
AddType text/plain .log
</IfModule>
<IfModule mod_dir.c>
DirectoryIndex index.php
</IfModule>
<IfModule mod_autoindex.c>
Options -Indexes
</IfModule>

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<security>
<authorization>
<deny users="*" />
</authorization>
</security>
<requestFiltering>
<fileExtensions allowUnlisted="false">
<add fileExtension=".log" allowed="true" />
</fileExtensions>
</requestFiltering>
<staticContent>
<mimeMap fileExtension=".log" mimeType="text/plain" />
</staticContent>
<defaultDocument>
<files>
<add value="index.php" />
</files>
</defaultDocument>
<directoryBrowse enabled="false" />
</system.webServer>
</configuration>

View File

@@ -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">&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;
@@ -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, '&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);
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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">&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;
@@ -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,62 @@ 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, '&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) {
showError('search_result', e.message);
resultEl.innerHTML = '<p style="color:#ef4444; padding: 1rem;">錯誤: ' + e.message + '</p>';
}
}
</script>