You've already forked lubo_comment_query
OCR
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 视频右侧栏 OCR 结果 canvas 复刻绘制。
|
||||
* 数据源:CDN 上的分P全量 JSON({space:[w,h], recs:[[ts,[[x1,y1,x2,y2,text],...]],...]})
|
||||
* 或服务器注入的单帧快照({ts, lines:[[x1,y1,x2,y2,text],...]})。
|
||||
*/
|
||||
(function () {
|
||||
// 按框坐标 1:1 复刻:黑底、黄框、白字,字号按框高自适应
|
||||
function drawFrame(canvas, space, lines) {
|
||||
const sw = space[0], sh = space[1];
|
||||
const cssWidth = canvas.clientWidth || 200;
|
||||
const scale = cssWidth / sw;
|
||||
const cssHeight = Math.max(1, Math.round(sh * scale));
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = Math.round(cssWidth * dpr);
|
||||
canvas.height = Math.round(cssHeight * dpr);
|
||||
canvas.style.height = cssHeight + "px";
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.setTransform(dpr * scale, 0, 0, dpr * scale, 0, 0);
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillRect(0, 0, sw, sh);
|
||||
ctx.strokeStyle = "#eab308";
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.textBaseline = "middle";
|
||||
lines.forEach(function (line) {
|
||||
const x1 = line[0], y1 = line[1], x2 = line[2], y2 = line[3], text = line[4];
|
||||
ctx.lineWidth = 2 / scale;
|
||||
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
|
||||
if (text) {
|
||||
ctx.font = Math.max(4, (y2 - y1) * 0.75) + "px sans-serif";
|
||||
ctx.fillText(text, x1 + 2 / scale, (y1 + y2) / 2, x2 - x1 - 4 / scale);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatHms(seconds) {
|
||||
seconds = Math.max(0, Math.floor(seconds));
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
return h + ":" + pad(m) + ":" + pad(s);
|
||||
}
|
||||
|
||||
// 单帧 space:按本帧最大框坐标向上取整到 16 的倍数(与导出端一致)
|
||||
function frameSpace(lines) {
|
||||
let maxX = 0, maxY = 0;
|
||||
lines.forEach(function (line) {
|
||||
maxX = Math.max(maxX, line[2]);
|
||||
maxY = Math.max(maxY, line[3]);
|
||||
});
|
||||
return [Math.max(16, Math.ceil(maxX / 16) * 16), Math.max(16, Math.ceil(maxY / 16) * 16)];
|
||||
}
|
||||
|
||||
// 稿件详情页:CDN 全量 + 进度条联动(展示 ts<=t 最近一条)
|
||||
function initPlayer(panel) {
|
||||
const urls = JSON.parse(panel.dataset.urls || "{}");
|
||||
const canvas = panel.querySelector("canvas");
|
||||
const slider = panel.querySelector("input[type=range]");
|
||||
const timeLabel = panel.querySelector("[data-ocr-time]");
|
||||
const status = panel.querySelector("[data-ocr-status]");
|
||||
const tabs = panel.querySelectorAll("[data-ocr-part]");
|
||||
let data = null;
|
||||
let loadSeq = 0;
|
||||
|
||||
function showStatus(text) {
|
||||
if (status) {
|
||||
status.textContent = text;
|
||||
status.style.display = text ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
function currentIndex(t) {
|
||||
// 二分:ts<=t 的最后一条;t 早于首条时展示首条
|
||||
const recs = data.recs;
|
||||
let lo = 0, hi = recs.length - 1, ans = 0;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (recs[mid][0] <= t) { ans = mid; lo = mid + 1; } else { hi = mid - 1; }
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!data || data.recs.length === 0) {
|
||||
return;
|
||||
}
|
||||
const t = parseFloat(slider.value);
|
||||
const idx = currentIndex(t);
|
||||
timeLabel.textContent = formatHms(t) + "(OCR @" + formatHms(data.recs[idx][0]) + ")";
|
||||
drawFrame(canvas, data.space, data.recs[idx][1]);
|
||||
}
|
||||
|
||||
function load(partNum) {
|
||||
const seq = ++loadSeq;
|
||||
data = null;
|
||||
showStatus("加载中…");
|
||||
tabs.forEach(function (tab) {
|
||||
tab.classList.toggle("font-bold", tab.dataset.ocrPart === String(partNum));
|
||||
});
|
||||
fetch(urls[partNum])
|
||||
.then((response) => {
|
||||
if (!response.ok) { throw new Error("HTTP " + response.status); }
|
||||
return response.json();
|
||||
})
|
||||
.then((payload) => {
|
||||
if (seq !== loadSeq) { return; }
|
||||
data = payload;
|
||||
slider.max = payload.recs.length ? Math.floor(payload.recs[payload.recs.length - 1][0]) : 0;
|
||||
showStatus("");
|
||||
render();
|
||||
})
|
||||
.catch(() => {
|
||||
if (seq !== loadSeq) { return; }
|
||||
showStatus("OCR 数据加载失败");
|
||||
});
|
||||
}
|
||||
|
||||
slider.addEventListener("input", render);
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener("click", function () {
|
||||
slider.value = 0;
|
||||
load(tab.dataset.ocrPart);
|
||||
});
|
||||
});
|
||||
const firstPart = Object.keys(urls)[0];
|
||||
if (firstPart !== undefined) {
|
||||
load(firstPart);
|
||||
}
|
||||
}
|
||||
|
||||
// 节目详情页:服务器注入的单帧快照
|
||||
function initSnapshots(scope) {
|
||||
(scope || document).querySelectorAll("canvas[data-ocr-frame]").forEach(function (canvas) {
|
||||
const frame = JSON.parse(canvas.dataset.ocrFrame);
|
||||
drawFrame(canvas, frameSpace(frame.lines), frame.lines);
|
||||
});
|
||||
}
|
||||
|
||||
window.OcrCanvas = {
|
||||
drawFrame: drawFrame,
|
||||
initPlayer: initPlayer,
|
||||
initSnapshots: initSnapshots,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user