|
|
|
|
@@ -473,15 +473,24 @@ async function semanticSearch() {
|
|
|
|
|
const textAttr = (i.text_content || '').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
|
|
|
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);
|
|
|
|
|
const thumbFrame = Math.round(startTime * 24);
|
|
|
|
|
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">${i.file_uuid} | ${startTime.toFixed(1)}s - ${endTime.toFixed(1)}s</div>
|
|
|
|
|
<div style="margin-top: 4px;">${i.text_content || '無內容'}</div>
|
|
|
|
|
<button class="play-btn">▶ 播放片段</button>
|
|
|
|
|
<div style="display:flex;gap:10px;">
|
|
|
|
|
<img class="search-thumb" loading="lazy"
|
|
|
|
|
data-src="${API_BASE}/file/${i.file_uuid}/thumbnail?api_key=${API_KEY}&frame=${thumbFrame}"
|
|
|
|
|
style="width:120px;height:68px;object-fit:cover;border-radius:4px;flex-shrink:0;background:#e2e8f0;"
|
|
|
|
|
alt="">
|
|
|
|
|
<div style="flex:1;">
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
</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>'}`;
|
|
|
|
|
@@ -498,23 +507,72 @@ async function semanticSearch() {
|
|
|
|
|
const textAttr = (r.text || '').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
|
|
|
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);
|
|
|
|
|
const thumbFrame = Math.round(startTime * 24);
|
|
|
|
|
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">${r.file_uuid} | ${startTime.toFixed(1)}s - ${endTime.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 style="display:flex;gap:10px;">
|
|
|
|
|
<img class="search-thumb" loading="lazy"
|
|
|
|
|
data-src="${API_BASE}/file/${r.file_uuid}/thumbnail?api_key=${API_KEY}&frame=${thumbFrame}"
|
|
|
|
|
style="width:120px;height:68px;object-fit:cover;border-radius:4px;flex-shrink:0;background:#e2e8f0;"
|
|
|
|
|
alt="">
|
|
|
|
|
<div style="flex:1;">
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
</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>'}`;
|
|
|
|
|
}
|
|
|
|
|
loadSearchThumbnails();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
resultEl.innerHTML = '<p style="color:#ef4444; padding: 1rem;">錯誤: ' + e.message + '</p>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadSearchThumbnails(){
|
|
|
|
|
const imgs = Array.from(document.querySelectorAll('img.search-thumb[data-src]'));
|
|
|
|
|
const MAX = 4;
|
|
|
|
|
let queue = [];
|
|
|
|
|
let inflight = 0;
|
|
|
|
|
|
|
|
|
|
function next(){
|
|
|
|
|
while (inflight < MAX && queue.length) {
|
|
|
|
|
const img = queue.shift();
|
|
|
|
|
inflight++;
|
|
|
|
|
img.onload = function(){ inflight--; next(); };
|
|
|
|
|
img.onerror = function(){ inflight--; next(); };
|
|
|
|
|
img.src = img.dataset.src;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('IntersectionObserver' in window) {
|
|
|
|
|
const obs = new IntersectionObserver(function(entries){
|
|
|
|
|
entries.forEach(function(e){
|
|
|
|
|
if (!e.isIntersecting) return;
|
|
|
|
|
var img = e.target;
|
|
|
|
|
if (img.dataset.loaded) return;
|
|
|
|
|
img.dataset.loaded = '1';
|
|
|
|
|
queue.push(img);
|
|
|
|
|
obs.unobserve(img);
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
}, { rootMargin: '200px 0px' });
|
|
|
|
|
imgs.forEach(function(img){ obs.observe(img); });
|
|
|
|
|
} else {
|
|
|
|
|
imgs.forEach(function(img){
|
|
|
|
|
if (img.dataset.loaded) return;
|
|
|
|
|
img.dataset.loaded = '1';
|
|
|
|
|
queue.push(img);
|
|
|
|
|
});
|
|
|
|
|
next();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<?php get_footer(); ?>
|
|
|
|
|
|