Skip to content

Commit 339c00f

Browse files
committed
feat(engine): fps 强制自测(忽略外部传入,从 decoder 元数据读)+ extract() 通用文本提取入口(ExtractionResult/ExtractedSegment dataclass);建仓评估脚本(引擎依赖 7 模块零应用泄漏,ocr_engine.py 不随引擎)
1 parent de55287 commit 339c00f

4 files changed

Lines changed: 142 additions & 4 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""列出引擎 FieldExtractor 方法、__init__ 参数、extract 状态。"""
2+
import ast
3+
from pathlib import Path
4+
5+
root = Path(__file__).resolve().parent.parent.parent
6+
src = (root / "video_ocr_engine" / "extractor.py").read_text(encoding="utf-8")
7+
t = ast.parse(src)
8+
cls = next(n for n in t.body if isinstance(n, ast.ClassDef)
9+
and n.name == "FieldExtractor")
10+
methods = [n for n in cls.body if isinstance(n, ast.FunctionDef)]
11+
12+
print("=== FieldExtractor 方法 ===")
13+
for m in methods:
14+
decs = " ".join(ast.unparse(d) for d in m.decorator_list)
15+
print(f" {decs or '-':28} def {m.name}")
16+
doc = ast.get_docstring(m)
17+
if doc:
18+
first = doc.strip().splitlines()[0][:70]
19+
print(f" {first}")
20+
21+
init = next(m for m in methods if m.name == "__init__")
22+
pos = [a.arg for a in init.args.args if a.arg != "self"]
23+
kw = [a.arg for a in init.args.kwonlyargs]
24+
print("\n=== __init__ 参数 ===")
25+
print(" 位置参数:", ", ".join(pos))
26+
print(" 仅关键字:", ", ".join(kw))
27+
28+
print("\n=== extract() 是否已实现 ===")
29+
ext = next((m for m in methods if m.name == "extract"), None)
30+
print(" 存在:", ext is not None)
31+
if ext:
32+
body = ast.unparse(ext.body[0]) if ext.body else "(空)"
33+
print(" 首个语句:", body)
34+
35+
# SegmentPipeline 怎么调它
36+
print("\n=== SegmentPipeline.__init__ 调 super() 的参数 ===")
37+
sf_src = (root / "segment_flow.py").read_text(encoding="utf-8")
38+
if "super().__init__(" in sf_src:
39+
i = sf_src.index("super().__init__(")
40+
j = sf_src.index(")", i)
41+
print(sf_src[i:j + 1])
42+
else:
43+
print(" 未发现 super().__init__ 调用")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""建独立仓库评估:引擎依赖的 8 个模块各自的 import(找应用层泄漏)。
2+
3+
目标:确认 video_ocr_engine 独立仓库需要哪些文件、哪些模块需净化。
4+
"""
5+
import ast
6+
import json
7+
from pathlib import Path
8+
9+
ROOT = Path(__file__).resolve().parent.parent.parent
10+
CANDIDATES = ["engine_config", "segmentation", "hybrid_decode", "ocr_native",
11+
"ocr_trt", "ocr_engine", "video_utils", "constants"]
12+
# 应用层(不随引擎走)
13+
APP_MODULES = {"config", "segment_flow", "seg_correction", "ocr_text",
14+
"csv_io", "gui", "signals", "monitor", "headless",
15+
"export_controller", "analysis"}
16+
17+
report = {}
18+
for name in CANDIDATES:
19+
p = ROOT / f"{name}.py"
20+
if not p.exists():
21+
report[name] = {"error": "missing"}
22+
continue
23+
tree = ast.parse(p.read_text(encoding="utf-8"))
24+
imports = []
25+
for n in ast.walk(tree):
26+
if isinstance(n, ast.ImportFrom) and n.module:
27+
imports.append(n.module.split(".")[0])
28+
elif isinstance(n, ast.Import):
29+
imports.append("")
30+
leaks = sorted(m for m in imports if m in APP_MODULES)
31+
report[name] = {
32+
"top_imports": sorted(m for m in imports if m),
33+
"app_leaks": leaks,
34+
"size_kb": round(p.stat().st_size / 1024, 1),
35+
}
36+
37+
for name, r in report.items():
38+
print(f"\n=== {name} ({r.get('size_kb')}KB) ===")
39+
print(f" 顶层 import: {r.get('top_imports')}")
40+
leaks = r.get("app_leaks")
41+
print(f" 应用泄漏: {leaks if leaks else '无'}")

video_ocr_engine/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
for seg in result.segments:
1212
print(seg.text, seg.confidence)
1313
"""
14-
from video_ocr_engine.extractor import FieldExtractor # noqa: F401
14+
from video_ocr_engine.extractor import ( # noqa: F401
15+
FieldExtractor, ExtractedSegment, ExtractionResult,
16+
)
1517
from video_ocr_engine import _version # noqa: F401
1618

1719
__version__ = _version.__version__

video_ocr_engine/extractor.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import os as _os
1212
import threading
1313
import time
14+
from dataclasses import dataclass, field
1415
from pathlib import Path
16+
from typing import Any, Optional
1517

1618
import numpy as np
1719

@@ -37,6 +39,30 @@ def _ocr_batch_size() -> int:
3739
return config.OCR_BATCH_SIZE
3840

3941

42+
@dataclass
43+
class ExtractedSegment:
44+
"""引擎输出的单个文本字段段(原有字段区间 + 代表帧 + 原始文本)。"""
45+
46+
start: int # 段首帧号
47+
end: int # 段末帧号
48+
frames: tuple = () # 段内帧号序列
49+
rep_frame: int = -1 # 代表帧号(段内最清晰帧)
50+
text: Optional[str] = None # OCR 原始文本(None=未读出)
51+
confidence: float = 0.0 # OCR 置信度 0-1
52+
rep_crop: Any = None # 代表帧 ROI 图像(YUV420 或 RGB)
53+
54+
55+
@dataclass
56+
class ExtractionResult:
57+
"""引擎通用提取结果(无领域语义)。"""
58+
59+
segments: list = field(default_factory=list) # list[ExtractedSegment]
60+
frames: list = field(default_factory=list) # 全部采样帧号
61+
fps: float = 0.0 # 自测帧率
62+
timing: dict = field(default_factory=dict) # 各阶段耗时
63+
meta: dict = field(default_factory=dict) # backend/codec/引擎版本等
64+
65+
4066
class FieldExtractor:
4167
"""从视频固定区域提取文本的通用引擎(识别链:解码∥分段∥OCR)。
4268
@@ -55,7 +81,10 @@ def __init__(self, video_path: str, roi: tuple, *, frame_start=None,
5581
yuv_output: bool = False):
5682
self._video_path = Path(video_path)
5783
self._roi = tuple(roi)
58-
self._fps = fps # 外部给定时直接用(truth 头),否则识别链推导
84+
# fps 强制自测:open decoder 后从 get_avg_fps/get_fps 读,忽略外部
85+
# 传入(truth 头的 fps 可能与视频实际帧率偏离;自测无额外解码开销,
86+
# 只在打开时读一次元数据)。fps 参数保留仅为 API 兼容(已废弃)。
87+
self._fps = None
5988
self._frame_start = frame_start or 0
6089
self._frame_end = frame_end
6190
self._force_aspect = force_aspect
@@ -100,8 +129,31 @@ def __init__(self, video_path: str, roi: tuple, *, frame_start=None,
100129
# 后处理参数由子类(SegmentPipeline)在构造时设置;引擎识别链不读。
101130

102131
def extract(self):
103-
"""通用文本提取入口(待精修:解码∥分段∥OCR → 每段 text/conf 结果)。"""
104-
raise NotImplementedError
132+
"""通用文本提取:解码∥分段∥OCR → 结构化结果(每段原始文本+置信度)。
133+
134+
引擎的正式通用入口(无任何领域语义)。返回 ExtractionResult:
135+
- segments: list[ExtractedSegment](start/end/rep_frame/text/confidence/
136+
rep_crop)
137+
- frames / fps / timing / meta
138+
识别层不解析文本含义(速度/数值由上层应用处理)。ffis 强制自测。
139+
"""
140+
frames, segs, texts, confs, rep_frames = self._run_pipelined()
141+
segments = [
142+
ExtractedSegment(
143+
start=seg[0], end=seg[-1], frames=tuple(seg),
144+
rep_frame=rep_frames[i],
145+
text=texts[i] if i < len(texts) else None,
146+
confidence=confs[i] if i < len(confs) else 0.0,
147+
rep_crop=self.crops.get(rep_frames[i]))
148+
for i, seg in enumerate(segs)
149+
]
150+
return ExtractionResult(
151+
segments=segments, frames=frames, fps=self._fps or 0.0,
152+
timing=dict(self.timing),
153+
meta={"backend": self._backend,
154+
"ocr_backend": self._ocr_backend_used,
155+
"codec": self._codec,
156+
"n_segments": len(segments)})
105157

106158
@property
107159
def frames(self) -> list:

0 commit comments

Comments
 (0)