flowocr 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowocr/__init__.py +15 -0
- flowocr/analyze/__init__.py +8 -0
- flowocr/analyze/align.py +151 -0
- flowocr/analyze/build_tracks.py +2886 -0
- flowocr/analyze/cluster_layers.py +155 -0
- flowocr/analyze/game_align.py +652 -0
- flowocr/analyze/gamescript.py +1220 -0
- flowocr/analyze/gtdbundle.py +306 -0
- flowocr/analyze/match.py +67 -0
- flowocr/analyze/matchers/__init__.py +5 -0
- flowocr/analyze/matchers/gametext.py +70 -0
- flowocr/analyze/merge_nameplate.py +178 -0
- flowocr/analyze/models/slot_pair.json +148 -0
- flowocr/analyze/nameplate.py +148 -0
- flowocr/analyze/pair_features.py +168 -0
- flowocr/analyze/pair_model.py +127 -0
- flowocr/analyze/refine_boundaries.py +409 -0
- flowocr/analyze/script_align.py +641 -0
- flowocr/analyze/scriptmatch.py +1018 -0
- flowocr/analyze/slot_learned.py +306 -0
- flowocr/analyze/slot_lines.py +251 -0
- flowocr/analyze/slot_modes.py +143 -0
- flowocr/analyze/slot_pairs.py +562 -0
- flowocr/analyze/slot_veto.py +104 -0
- flowocr/analyze/uigate.py +351 -0
- flowocr/artifacts/__init__.py +5 -0
- flowocr/artifacts/evalkit.py +123 -0
- flowocr/artifacts/matchedio.py +75 -0
- flowocr/artifacts/srtio.py +166 -0
- flowocr/artifacts/tracksio.py +345 -0
- flowocr/extensions.py +77 -0
- flowocr/extract/__init__.py +8 -0
- flowocr/extract/childproc.py +118 -0
- flowocr/extract/decode_proc.py +511 -0
- flowocr/extract/decode_shards.py +574 -0
- flowocr/extract/detpost.py +215 -0
- flowocr/extract/edge_proc.py +314 -0
- flowocr/extract/edge_refine.py +598 -0
- flowocr/extract/fast_det.py +214 -0
- flowocr/extract/ffcheck.py +87 -0
- flowocr/extract/framegrid.py +265 -0
- flowocr/extract/framesource.py +1264 -0
- flowocr/extract/ocr_args.py +754 -0
- flowocr/extract/ocr_complete.py +231 -0
- flowocr/extract/ocr_parallel.py +208 -0
- flowocr/extract/ort_server.py +632 -0
- flowocr/extract/ortclient.py +363 -0
- flowocr/extract/ptsclock.py +150 -0
- flowocr/extract/recdecode.py +78 -0
- flowocr/extract/recort.py +162 -0
- flowocr/extract/recpack.py +94 -0
- flowocr/extract/recpool.py +206 -0
- flowocr/extract/recprep.py +71 -0
- flowocr/extract/refine_video.py +271 -0
- flowocr/extract/regions.py +387 -0
- flowocr/extract/reuse_v2.py +593 -0
- flowocr/extract/run_groups.py +247 -0
- flowocr/extract/run_ocr2.py +1605 -0
- flowocr/extract/supervisor.py +261 -0
- flowocr/extract/timeline.py +84 -0
- flowocr/extract/typewriter_fuse.py +394 -0
- flowocr/models.py +263 -0
- flowocr/output/__init__.py +3 -0
- flowocr/output/export.py +205 -0
- flowocr/output/layout.py +210 -0
- flowocr/output/presets/__init__.py +6 -0
- flowocr/output/presets/_overlay.py +40 -0
- flowocr/output/presets/default.py +21 -0
- flowocr/output/presets/default_all.py +20 -0
- flowocr/output/presets/dev.py +21 -0
- flowocr/output/presets/matched_srt.py +32 -0
- flowocr/output/presets/script.py +20 -0
- flowocr/output/presets/srt_main.py +33 -0
- flowocr/output/render.py +82 -0
- flowocr/output/run_srt.py +83 -0
- flowocr/output/script.py +541 -0
- flowocr/paths.py +168 -0
- flowocr/provenance.py +119 -0
- flowocr/typeset/__init__.py +9 -0
- flowocr/typeset/__main__.py +38 -0
- flowocr/typeset/assfile.py +216 -0
- flowocr/typeset/core.py +821 -0
- flowocr/typeset/fx/__init__.py +11 -0
- flowocr-0.1.0.dist-info/METADATA +109 -0
- flowocr-0.1.0.dist-info/RECORD +91 -0
- flowocr-0.1.0.dist-info/WHEEL +5 -0
- flowocr-0.1.0.dist-info/entry_points.txt +10 -0
- flowocr-0.1.0.dist-info/licenses/LICENSE +674 -0
- flowocr-0.1.0.dist-info/licenses/LICENSES/Apache-2.0.txt +201 -0
- flowocr-0.1.0.dist-info/licenses/LICENSES/PP-OCRv6-NOTICE.md +18 -0
- flowocr-0.1.0.dist-info/top_level.txt +1 -0
flowocr/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""flowocr —— 从视频里抽全部屏幕文字、带起止时间、按区域聚类。
|
|
2
|
+
|
|
3
|
+
正式包(project-structure 计划)。
|
|
4
|
+
迁移期间 `tools/` 里的模块逐个搬进来(2026-09-22 搬完、shim 删光;调用方一律包 import),
|
|
5
|
+
三个阶段的子包(extract / analyze / output / artifacts)**有真代码才建目录**,
|
|
6
|
+
这里不预先摆空壳。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = _version("flowocr") # 唯一来源是 pyproject.toml 的 version,经包元数据读
|
|
14
|
+
except PackageNotFoundError: # 没装成包、只把 src/ 放进 PYTHONPATH 时
|
|
15
|
+
__version__ = "0+unknown"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""阶段 2:聚类与匹配(时间合并、默认空间聚类、UI / 名牌 / 噪音判断、可选的参考文本匹配)。
|
|
2
|
+
|
|
3
|
+
project-structure 计划。2026-09-22 第一刀把 `tools/` 里这些模块**原样**搬进来:
|
|
4
|
+
`build_tracks`(仍然连写 SRT 的部分一起,拆到 output 是下一步)、`uigate`、`nameplate`、`cluster_layers`、
|
|
5
|
+
`slot_lines` / `slot_modes` / `slot_pairs` / `slot_veto` / `slot_learned`(模型 `models/slot_pair.json` 随代码走)、
|
|
6
|
+
`pair_features` / `pair_model`、`game_patches`、`gamescript`、`game_align`、`script_align`、
|
|
7
|
+
`scriptmatch`(含 ASS 渲染,同样待拆)、`merge_nameplate`。`tools/` 原位留 shim。
|
|
8
|
+
"""
|
flowocr/analyze/align.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""区域的**对齐方式**(左 / 居中 / 右):阶段 2 判好写进 `regions[].align`,叠加导出只照着锚边。
|
|
2
|
+
|
|
3
|
+
方案:overlay-style 计划的「对齐」一节(owner 2026-09-24:叠加默认居中,原文多行左对齐时靠左)。
|
|
4
|
+
|
|
5
|
+
两级判据,**区域永远有值**:
|
|
6
|
+
|
|
7
|
+
1. **区域统计**:区域里可用事件的左沿 / 中心 / 右沿,各自的四分位距除以中位行高。
|
|
8
|
+
某一个 ≤ `TOL` 行高、而且 ≤ 另两者较小值的 `DOMINANCE` 倍,就判成它。
|
|
9
|
+
为什么用区域统计当主判据:左对齐对话框里的**单行台词**,只看邻居的话,旁边正好挂着名牌时才判得出。
|
|
10
|
+
判不出的情况:行宽几乎不变(三个量一样稳)、样本太少、聚类误合并把统计搅散。
|
|
11
|
+
2. **成对投票**(owner 说的"上下附近有左边界一致的其他字幕"):同时在屏、上下相邻的两行,
|
|
12
|
+
左沿齐而中心不齐 → 一票 `left`,右沿对称;中心齐而两沿都不齐 → 一票 `center`。
|
|
13
|
+
赢家要 ≥ `MIN_VOTES` 票且严格多于别的;平局或票不够 → `center`。
|
|
14
|
+
|
|
15
|
+
可用事件 = 不在动(`moving`:框跟着字走,对齐没有意义)、不是 `scrolling`(它的框是整条滚动的并集)、
|
|
16
|
+
不是名牌(短、位置固定,会把统计拉偏)、没被判成 UI。UI 用 `tracksio.ui_flagged`:
|
|
17
|
+
这里的作用域正是**区域轨**自己,它上面的 `ui_filtered` 就是这条轨的判决。
|
|
18
|
+
名牌不进统计,但可以当投票的邻居(名牌 + 正文左沿齐,正是左对齐对话框的样子)。
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import statistics
|
|
23
|
+
from collections import Counter
|
|
24
|
+
|
|
25
|
+
from flowocr.artifacts import tracksio
|
|
26
|
+
|
|
27
|
+
TOL = 0.5
|
|
28
|
+
"""离散度 / 位置差的容差,单位是中位行高。"""
|
|
29
|
+
DOMINANCE = 0.5
|
|
30
|
+
"""区域统计的赢家要 ≤ 另两者较小值的这么多倍,才算"明显更稳"。"""
|
|
31
|
+
MIN_EVENTS = 5
|
|
32
|
+
"""区域统计至少要这么多个可用事件。"""
|
|
33
|
+
WIDTH_MIN_IQR = 1.0
|
|
34
|
+
"""行宽的四分位距不到这么多行高:三条边一样稳,统计分不出,交给投票。"""
|
|
35
|
+
NEIGHBOR_GAP = 1.5
|
|
36
|
+
"""投票的邻居:上下两框的垂直间隙不超过这么多行高。"""
|
|
37
|
+
MIN_VOTES = 2
|
|
38
|
+
"""投票的赢家至少要这么多票(1 票太容易是一对误读的框)。"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def usable(ev: dict) -> bool:
|
|
42
|
+
flags = set(ev.get("flags") or ())
|
|
43
|
+
b = ev["box"]
|
|
44
|
+
return (not flags & {"moving", "scrolling", "nameplate"} and not tracksio.ui_flagged(ev)
|
|
45
|
+
and b[3] > b[1] and b[2] > b[0])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def iqr(xs: list[float]) -> float:
|
|
49
|
+
q = statistics.quantiles(xs, n=4, method="inclusive")
|
|
50
|
+
return q[2] - q[0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def by_stats(evs: list[dict]) -> tuple[str | None, dict | None]:
|
|
54
|
+
"""区域统计。返回 (判定或 None, 三个离散度);样本不够时离散度也是 None。"""
|
|
55
|
+
if len(evs) < MIN_EVENTS:
|
|
56
|
+
return None, None
|
|
57
|
+
h = statistics.median(e["box"][3] - e["box"][1] for e in evs)
|
|
58
|
+
x0 = [e["box"][0] for e in evs]
|
|
59
|
+
x1 = [e["box"][2] for e in evs]
|
|
60
|
+
cx = [(a + b) / 2 for a, b in zip(x0, x1)]
|
|
61
|
+
spread = {"left": iqr(x0) / h, "center": iqr(cx) / h, "right": iqr(x1) / h}
|
|
62
|
+
rounded = {k: round(v, 3) for k, v in spread.items()}
|
|
63
|
+
if iqr([b - a for a, b in zip(x0, x1)]) / h < WIDTH_MIN_IQR:
|
|
64
|
+
return None, rounded
|
|
65
|
+
best = min(spread, key=spread.get)
|
|
66
|
+
others = min(v for k, v in spread.items() if k != best)
|
|
67
|
+
# 严格小于:两条边一样稳(比如都是 0)时分不出,别按 dict 顺序挑一个
|
|
68
|
+
if spread[best] <= TOL and spread[best] < DOMINANCE * others:
|
|
69
|
+
return best, rounded
|
|
70
|
+
return None, rounded
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def pair_vote(a: list[float], b: list[float]) -> str | None:
|
|
74
|
+
"""上下相邻的两行投一票:左沿齐而中心不齐 → left;右沿对称;中心齐而两沿都不齐 → center;其余不投。"""
|
|
75
|
+
tol = TOL * (a[3] - a[1] + b[3] - b[1]) / 2
|
|
76
|
+
d0, d1 = abs(a[0] - b[0]), abs(a[2] - b[2])
|
|
77
|
+
dc = abs((a[0] + a[2]) / 2 - (b[0] + b[2]) / 2)
|
|
78
|
+
if dc > tol and d0 <= tol < d1:
|
|
79
|
+
return "left"
|
|
80
|
+
if dc > tol and d1 <= tol < d0:
|
|
81
|
+
return "right"
|
|
82
|
+
if dc <= tol and d0 > tol and d1 > tol:
|
|
83
|
+
return "center"
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def adjacent(a: list[float], b: list[float]) -> bool:
|
|
88
|
+
"""上下相邻:左右有重叠,垂直间隙不超过 `NEIGHBOR_GAP` 行高。"""
|
|
89
|
+
if min(a[2], b[2]) <= max(a[0], b[0]):
|
|
90
|
+
return False
|
|
91
|
+
gap = max(a[1], b[1]) - min(a[3], b[3])
|
|
92
|
+
return 0 <= gap <= NEIGHBOR_GAP * (a[3] - a[1] + b[3] - b[1]) / 2
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def votes(own: list[dict], names: list[dict]) -> Counter:
|
|
96
|
+
"""区域里同时在屏、上下相邻的每一对投一票(每对只投一次)。至少一方是本区域的事件;
|
|
97
|
+
另一方可以是本区域的,也可以是名牌。按 `t_start` 扫,只比时间有交叠的。"""
|
|
98
|
+
own_ids = {e["id"] for e in own}
|
|
99
|
+
evs = sorted({e["id"]: e for e in [*own, *names]}.values(), key=lambda e: e["t_start"])
|
|
100
|
+
out: Counter = Counter()
|
|
101
|
+
active: list[dict] = []
|
|
102
|
+
for e in evs:
|
|
103
|
+
active = [a for a in active if a["t_end"] > e["t_start"]]
|
|
104
|
+
for a in active:
|
|
105
|
+
if (a["id"] in own_ids or e["id"] in own_ids) and adjacent(a["box"], e["box"]):
|
|
106
|
+
v = pair_vote(a["box"], e["box"])
|
|
107
|
+
if v:
|
|
108
|
+
out[v] += 1
|
|
109
|
+
active.append(e)
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def judge(own: list[dict], names: list[dict]) -> tuple[str, dict]:
|
|
114
|
+
"""一个区域的对齐方式和依据(`align_how`)。"""
|
|
115
|
+
got, spread = by_stats(own)
|
|
116
|
+
how = {"by": "stats", "n": len(own), "spread": spread, "votes": {}}
|
|
117
|
+
if got:
|
|
118
|
+
return got, how
|
|
119
|
+
tally = votes(own, names)
|
|
120
|
+
how.update(by="vote", votes=dict(sorted(tally.items())))
|
|
121
|
+
ranked = tally.most_common()
|
|
122
|
+
if ranked and ranked[0][1] >= MIN_VOTES and (len(ranked) == 1 or ranked[0][1] > ranked[1][1]):
|
|
123
|
+
return ranked[0][0], how
|
|
124
|
+
how["by"] = "none"
|
|
125
|
+
return "center", how
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def annotate(doc: dict) -> Counter:
|
|
129
|
+
"""给每个区域写 `align` / `align_how`(原地改 `doc`),返回各种判定的区域数。"""
|
|
130
|
+
by_region: dict[int, list[dict]] = {}
|
|
131
|
+
names = []
|
|
132
|
+
for ev in doc["events"]:
|
|
133
|
+
flags = set(ev.get("flags") or ())
|
|
134
|
+
if "nameplate" in flags and not tracksio.ui_flagged(ev) and "moving" not in flags:
|
|
135
|
+
names.append(ev)
|
|
136
|
+
if usable(ev):
|
|
137
|
+
by_region.setdefault(ev["region"], []).append(ev)
|
|
138
|
+
n = Counter()
|
|
139
|
+
for r in doc["regions"]:
|
|
140
|
+
own = by_region.get(r["index"], [])
|
|
141
|
+
near = []
|
|
142
|
+
if own:
|
|
143
|
+
# 只带区域外接框附近的名牌进来:名牌是全片的,每个区域都扫一遍全体太浪费
|
|
144
|
+
h = max(e["box"][3] - e["box"][1] for e in own)
|
|
145
|
+
x0, x1 = min(e["box"][0] for e in own), max(e["box"][2] for e in own)
|
|
146
|
+
y0, y1 = min(e["box"][1] for e in own) - 2 * h, max(e["box"][3] for e in own) + 2 * h
|
|
147
|
+
near = [e for e in names if e["box"][2] > x0 and e["box"][0] < x1
|
|
148
|
+
and e["box"][3] > y0 and e["box"][1] < y1]
|
|
149
|
+
r["align"], r["align_how"] = judge(own, near)
|
|
150
|
+
n[(r["align"], r["align_how"]["by"])] += 1
|
|
151
|
+
return n
|