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.
Files changed (91) hide show
  1. flowocr/__init__.py +15 -0
  2. flowocr/analyze/__init__.py +8 -0
  3. flowocr/analyze/align.py +151 -0
  4. flowocr/analyze/build_tracks.py +2886 -0
  5. flowocr/analyze/cluster_layers.py +155 -0
  6. flowocr/analyze/game_align.py +652 -0
  7. flowocr/analyze/gamescript.py +1220 -0
  8. flowocr/analyze/gtdbundle.py +306 -0
  9. flowocr/analyze/match.py +67 -0
  10. flowocr/analyze/matchers/__init__.py +5 -0
  11. flowocr/analyze/matchers/gametext.py +70 -0
  12. flowocr/analyze/merge_nameplate.py +178 -0
  13. flowocr/analyze/models/slot_pair.json +148 -0
  14. flowocr/analyze/nameplate.py +148 -0
  15. flowocr/analyze/pair_features.py +168 -0
  16. flowocr/analyze/pair_model.py +127 -0
  17. flowocr/analyze/refine_boundaries.py +409 -0
  18. flowocr/analyze/script_align.py +641 -0
  19. flowocr/analyze/scriptmatch.py +1018 -0
  20. flowocr/analyze/slot_learned.py +306 -0
  21. flowocr/analyze/slot_lines.py +251 -0
  22. flowocr/analyze/slot_modes.py +143 -0
  23. flowocr/analyze/slot_pairs.py +562 -0
  24. flowocr/analyze/slot_veto.py +104 -0
  25. flowocr/analyze/uigate.py +351 -0
  26. flowocr/artifacts/__init__.py +5 -0
  27. flowocr/artifacts/evalkit.py +123 -0
  28. flowocr/artifacts/matchedio.py +75 -0
  29. flowocr/artifacts/srtio.py +166 -0
  30. flowocr/artifacts/tracksio.py +345 -0
  31. flowocr/extensions.py +77 -0
  32. flowocr/extract/__init__.py +8 -0
  33. flowocr/extract/childproc.py +118 -0
  34. flowocr/extract/decode_proc.py +511 -0
  35. flowocr/extract/decode_shards.py +574 -0
  36. flowocr/extract/detpost.py +215 -0
  37. flowocr/extract/edge_proc.py +314 -0
  38. flowocr/extract/edge_refine.py +598 -0
  39. flowocr/extract/fast_det.py +214 -0
  40. flowocr/extract/ffcheck.py +87 -0
  41. flowocr/extract/framegrid.py +265 -0
  42. flowocr/extract/framesource.py +1264 -0
  43. flowocr/extract/ocr_args.py +754 -0
  44. flowocr/extract/ocr_complete.py +231 -0
  45. flowocr/extract/ocr_parallel.py +208 -0
  46. flowocr/extract/ort_server.py +632 -0
  47. flowocr/extract/ortclient.py +363 -0
  48. flowocr/extract/ptsclock.py +150 -0
  49. flowocr/extract/recdecode.py +78 -0
  50. flowocr/extract/recort.py +162 -0
  51. flowocr/extract/recpack.py +94 -0
  52. flowocr/extract/recpool.py +206 -0
  53. flowocr/extract/recprep.py +71 -0
  54. flowocr/extract/refine_video.py +271 -0
  55. flowocr/extract/regions.py +387 -0
  56. flowocr/extract/reuse_v2.py +593 -0
  57. flowocr/extract/run_groups.py +247 -0
  58. flowocr/extract/run_ocr2.py +1605 -0
  59. flowocr/extract/supervisor.py +261 -0
  60. flowocr/extract/timeline.py +84 -0
  61. flowocr/extract/typewriter_fuse.py +394 -0
  62. flowocr/models.py +263 -0
  63. flowocr/output/__init__.py +3 -0
  64. flowocr/output/export.py +205 -0
  65. flowocr/output/layout.py +210 -0
  66. flowocr/output/presets/__init__.py +6 -0
  67. flowocr/output/presets/_overlay.py +40 -0
  68. flowocr/output/presets/default.py +21 -0
  69. flowocr/output/presets/default_all.py +20 -0
  70. flowocr/output/presets/dev.py +21 -0
  71. flowocr/output/presets/matched_srt.py +32 -0
  72. flowocr/output/presets/script.py +20 -0
  73. flowocr/output/presets/srt_main.py +33 -0
  74. flowocr/output/render.py +82 -0
  75. flowocr/output/run_srt.py +83 -0
  76. flowocr/output/script.py +541 -0
  77. flowocr/paths.py +168 -0
  78. flowocr/provenance.py +119 -0
  79. flowocr/typeset/__init__.py +9 -0
  80. flowocr/typeset/__main__.py +38 -0
  81. flowocr/typeset/assfile.py +216 -0
  82. flowocr/typeset/core.py +821 -0
  83. flowocr/typeset/fx/__init__.py +11 -0
  84. flowocr-0.1.0.dist-info/METADATA +109 -0
  85. flowocr-0.1.0.dist-info/RECORD +91 -0
  86. flowocr-0.1.0.dist-info/WHEEL +5 -0
  87. flowocr-0.1.0.dist-info/entry_points.txt +10 -0
  88. flowocr-0.1.0.dist-info/licenses/LICENSE +674 -0
  89. flowocr-0.1.0.dist-info/licenses/LICENSES/Apache-2.0.txt +201 -0
  90. flowocr-0.1.0.dist-info/licenses/LICENSES/PP-OCRv6-NOTICE.md +18 -0
  91. flowocr-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,155 @@
1
+ """只读 obs,重建**缝合之前**的两层(run、窗区域),给聚类这一层的尺和原型共用(next-steps)。
2
+
3
+ 为什么单独一个模块:量聚类要在**同一批 run、同一批窗区域**上换缝合算法(各臂只差 `stitch_slots`
4
+ 那一步),而 `build_runs` 在整片上要几分钟——所以建一次、缓存、各臂共用。
5
+
6
+ **参数只有一个来源**:`build_tracks.apply_matcher_patch()`——管线走的那条(默认值 + 匹配器按游戏给的聚类补丁
7
+ + 调用方给的 `extra`,`extra` 压过补丁)。不在这里抄默认值(`probe_band_layers` 那次漏传 `region_time_gate` 的教训)。
8
+
9
+ 缓存落在**数据根**的 `tmp/cluster/`(槽里没有 tmp/)。层缓存的键 = obs 的大小和 mtime + **这两层吃的参数**(`LAYER_ARGS`)+
10
+ `build_tracks.py` 里**决定这两层的那一段代码**的 hash(`LAYER_END` 之前);缝合结果的缓存另外带整份文件的 hash。对不上就重建。
11
+
12
+ 用法:
13
+ from flowocr.analyze import cluster_layers as CL
14
+ L = CL.load("f5") # 或 CL.load("gi2", ["--region-time-gate", "2"])
15
+ slots = CL.stitch(L) # 当前默认臂
16
+ slots = CL.stitch(L, reassign=False) # 换一条臂:只覆盖 stitch_slots 的关键字
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import pickle
23
+ import sys
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+
27
+ from flowocr.analyze import build_tracks as bt # noqa: E402
28
+ from flowocr import paths # noqa: E402
29
+ from flowocr.provenance import local # noqa: E402
30
+
31
+ FILMS = {f"f{i}": f"yuka-f{i}" for i in range(1, 6)}
32
+ """yuka 五部:tag -> 产物目录(obs 的位置从那份产物的 provenance 取)。别的 tag 走 `out/gamestream/<tag>.jsonl`。"""
33
+
34
+
35
+ def obs_path(tag: str) -> Path:
36
+ root = paths.data_root()
37
+ if tag in FILMS:
38
+ from flowocr.artifacts import tracksio
39
+ doc = tracksio.load(root / "out" / FILMS[tag] / f"{tag}-tracks.json")
40
+ return local(doc["provenance"]["obs"]) # 不猜文件名,从产物的 provenance 取(记的是 portable 路径)
41
+ return root / "out" / "gamestream" / f"{tag}.jsonl"
42
+
43
+
44
+ @dataclass
45
+ class Layers:
46
+ tag: str
47
+ obs: Path
48
+ video: str
49
+ W: int
50
+ H: int
51
+ frame_us: int
52
+ args: dict
53
+ runs: list
54
+ win_regions: list
55
+ key: str = ""
56
+
57
+
58
+ LAYER_ARGS = ("min_conf", "iou", "sim", "gap_frames", "no_typewriter", "text_pick", "tail_max", "vote_independent",
59
+ "prefix_min", "typewriter_min_chars", "window_sec", "mutual_x", "region_time_gate",
60
+ "region_h_ratio", "grow_h_ratio", "rejoin_split")
61
+ """run / 窗区域这两层吃的参数——**只有它们**进层缓存的键。第一版把整份参数和整个 `build_tracks.py` 的 hash 都放进去,
62
+ 于是翻一个缝合层的默认值、甚至改一句帮助文本,七部片子各 7 分钟的缓存就全废(2026-09-21 一天内重建了三遍)。"""
63
+ LAYER_END = "def slot_fits("
64
+ """`build_tracks.py` 里这一行之前的代码决定 run 和窗区域(`build_runs` / `build_tracks` / `build_regions` /
65
+ `cluster_windowed` / `region_geom` 都在它前面);之后是缝合、打标、导出。守卫核着这个假设。"""
66
+
67
+
68
+ def _code(part: str) -> str:
69
+ src = "\n".join(Path(bt.__file__).read_text(encoding="utf-8").splitlines()) # 换行归一
70
+ cut = src.index("\n" + LAYER_END)
71
+ return hashlib.sha1((src[:cut] if part == "layers" else src).encode()).hexdigest()[:12]
72
+
73
+
74
+ def _args(tag: str, extra: list[str]) -> dict:
75
+ a = bt.apply_matcher_patch(bt.make_parser(),
76
+ ["<obs>", "--outdir", "<out>", "--tag", tag, "--matcher", "gametext", *extra])
77
+ return {k: v for k, v in vars(a).items() if k not in ("matcher", "matcher_ctx", "matcher_patch", "matcher_overridden")}
78
+
79
+
80
+ def layer_key(obs: Path, a: dict) -> str:
81
+ """层缓存的键。**守卫量的是这个函数**(第一版的键改了一半没生效、而我只验了旁边的 `_code`,缓存照样天天全废)。"""
82
+ st = obs.stat()
83
+ return hashlib.sha1(json.dumps([str(obs), st.st_size, st.st_mtime_ns, {k: a[k] for k in LAYER_ARGS},
84
+ _code("layers")], sort_keys=True, default=str).encode()).hexdigest()[:12]
85
+
86
+
87
+ def from_cache(d: dict, a: dict, key: str) -> Layers:
88
+ """缓存里的层数据 + **本次**解析的参数。键收窄之后,缝合层参数不同的两次加载命中同一份缓存——
89
+ 缓存里那份 `args` 是**建缓存那一次**的,原样带回来就会让 `stitch(L)` 静默用上一次调用的旋钮
90
+ (Codex 审计 2026-09-21 复现:缓存来自 `pooled / reassign=False`,请求 `median / True`,缝合收到的是前者)。"""
91
+ return Layers(**{**d, "obs": Path(d["obs"]), "key": key, "args": a})
92
+
93
+
94
+ def load(tag: str, extra: list[str] | None = None, verbose: bool = True) -> Layers:
95
+ obs = obs_path(tag)
96
+ a = _args(tag, list(extra or []))
97
+ st = obs.stat()
98
+ key = layer_key(obs, a)
99
+ cache = paths.data_root() / "tmp" / "cluster" / f"{tag}-{key}.pkl"
100
+ if cache.exists():
101
+ with open(cache, "rb") as f: # 存的是字段字典:直接 pickle 数据类会把定义它的模块名
102
+ d = pickle.load(f) # (当脚本跑时是 `__main__`)钉进缓存,别的入口就读不回来
103
+ return from_cache(d, a, key)
104
+ if verbose:
105
+ print(f"[cluster_layers] 重建 {tag}({obs.name},{st.st_size / 1e6:.0f} MB)…", flush=True)
106
+ with open(obs, encoding="utf-8") as f:
107
+ meta = json.loads(f.readline())["_meta"]
108
+ rows = [o for o in (json.loads(x) for x in f if x.strip()) if o["conf"] >= a["min_conf"]]
109
+ W, H = meta.get("width"), meta.get("height")
110
+ if not W:
111
+ W, H = bt.probe_size(meta["video"])
112
+ frame_us = int(round(1e6 / meta["sample_fps"]))
113
+ runs = bt.build_runs(rows, frame_us, a["iou"], a["sim"], a["gap_frames"],
114
+ grow_typewriter=not a["no_typewriter"], text_pick=a["text_pick"],
115
+ tail_max=a["tail_max"], vote_independent=a["vote_independent"],
116
+ prefix_min=a["prefix_min"], typewriter_min_chars=a["typewriter_min_chars"],
117
+ grow_h_ratio=a["grow_h_ratio"], rejoin_split=a["rejoin_split"])
118
+ _, wr = bt.cluster_windowed(runs, W, H, int(a["window_sec"] * 1e6), a["mutual_x"],
119
+ a["region_time_gate"], a["region_h_ratio"])
120
+ L = Layers(tag, obs, meta["video"], W, H, frame_us, a, runs, wr, key)
121
+ cache.parent.mkdir(parents=True, exist_ok=True)
122
+ with open(cache, "wb") as f:
123
+ pickle.dump({**{k: v for k, v in L.__dict__.items() if k != "key"}, "obs": str(obs)}, f,
124
+ protocol=pickle.HIGHEST_PROTOCOL)
125
+ if verbose:
126
+ print(f"[cluster_layers] {tag}: {len(runs)} run、{len(wr)} 个窗区域 -> {cache.name}", flush=True)
127
+ return L
128
+
129
+
130
+ def stitch(L: Layers, **over) -> list[list[int]]:
131
+ """按生效参数缝合;`over` 覆盖 `stitch_slots` 的关键字(开臂用)。"""
132
+ a = L.args
133
+ kw = dict(anchor=not a["no_slot_anchor"], mutual_x=a["slot_mutual_x"],
134
+ span_ratio=a["slot_span_ratio"], geom_mode=a["slot_geom"],
135
+ move_seed_last=a["move_seed_last"], reassign=a["slot_reassign"],
136
+ reassign_iters=a["slot_reassign_iters"], h_ratio=a["slot_h_ratio"])
137
+ kw.update(over)
138
+ # 整片上缝一条臂要一两分钟,而尺要反复量同几条臂——结果按(层缓存键 + 生效关键字)落盘。
139
+ # 键里带整份 build_tracks.py 的内容 hash(层缓存的键只管前半段),所以改了缝合代码不会读到旧结果。
140
+ h = hashlib.sha1(json.dumps([kw, _code("all")], sort_keys=True).encode()).hexdigest()[:10]
141
+ cache = paths.data_root() / "tmp" / "cluster" / f"{L.tag}-{L.key}-stitch-{h}.pkl"
142
+ if L.key and cache.exists():
143
+ with open(cache, "rb") as f:
144
+ return pickle.load(f)
145
+ slots = bt.stitch_slots(L.runs, L.win_regions, L.W, L.H, **kw)
146
+ if L.key:
147
+ with open(cache, "wb") as f:
148
+ pickle.dump(slots, f, protocol=pickle.HIGHEST_PROTOCOL)
149
+ return slots
150
+
151
+
152
+ if __name__ == "__main__":
153
+ for t in sys.argv[1:]:
154
+ L = load(t)
155
+ print(t, len(L.runs), "run", len(L.win_regions), "窗区域", L.W, L.H, L.video)