v1.0.10 两件大事:
1. 界面中英双语(英文界面无中文残留)
- 新增 web/i18n.js(约 4200 条词条),零依赖零构建
- 后端下发中文数据走 bt4() 查表翻译;独立模块(upstream/netdetail/
realtime/update)纳入翻译并注册切语言重绘钩子
- 后端「状态/聚合」接口只回稳定枚举+原始数值,不下发中文文案
- 顺带修一个真实 XSS:顶层 const 不挂 window,独立脚本 esc 静默退化成零转义
2. 修掉「文字竖着排/错位」(多分辨率反复暴露)
- 单元格 min-content 只有 1 字宽 → word-break:keep-all + 表头 nowrap
- 依赖卡片正文被标签/按钮挤成 27px → .dep-body{flex:1 1 200px} + flex-wrap
- 裸 flex:1(下限为0)给正文 → 一律改 flex:1 1 <px>
- .dep-item 横向容器直接塞块级子项 → 退回块级
- 关键陷阱:auto-fill 布局屏越宽列数越多单卡越窄,窄屏反而测不出来,
真机审计推进到「逐视图点按钮 + 1920/2560 宽视口 + 中英双语」
- 守卫 t-layout.py 扩到 35 条,12 处反向验证全红
3. 修真缺陷:_link_since() 重启检测被误改成 if False:(注释还写着「已修」),
uptime 归零后相减得负数 → 恢复比对 boot_id
附带:新增 25 个检查器(t-*.py)、排版审计工具链(shot-*/ocr-*/live-*)、
版本号兜底值同步到 1.0.10、.gitignore 收纳 i18n/排版排查的一次性脚本。
104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""用 Windows 内置 OCR(WinRT)读截图,输出「文字 + 像素包围盒」。
|
||
|
||
为什么需要:模型看不了图,但排版问题的判据其实是**几何** ——
|
||
「一列一个字」的文字块包围盒一定又高又窄。OCR 给出 bbox 就能机器判定。
|
||
|
||
大图先按 TILE_H 切片(OcrEngine 对单张图有尺寸上限),坐标换算回原图。
|
||
切片用 Pillow 做(WinRT 的 BitmapBounds 字段在 PyWinRT 里是只读的)。
|
||
|
||
用法:python _dev/shot-ocr.py <图片> [最小可疑比例] [输出tsv]
|
||
"""
|
||
import asyncio
|
||
import io
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
|
||
from PIL import Image
|
||
from winrt.windows.graphics.imaging import BitmapDecoder
|
||
from winrt.windows.media.ocr import OcrEngine
|
||
from winrt.windows.storage import FileAccessMode, StorageFile
|
||
|
||
Image.MAX_IMAGE_PIXELS = None
|
||
TILE_H = 1100
|
||
TILE_OVERLAP = 60
|
||
|
||
|
||
def winpath(p):
|
||
"""WinRT 的 StorageFile 只吃反斜杠 Win32 路径,给正斜杠会报
|
||
UNABLE_TO_MASK_PATH(路径过长 32831528 那种鬼错误)。"""
|
||
return os.path.abspath(p).replace('/', '\\')
|
||
|
||
|
||
async def ocr_file(engine, path):
|
||
f = await StorageFile.get_file_from_path_async(winpath(path))
|
||
s = await f.open_async(FileAccessMode.READ)
|
||
dec = await BitmapDecoder.create_async(s)
|
||
bmp = await dec.get_software_bitmap_async()
|
||
res = await engine.recognize_async(bmp)
|
||
out = []
|
||
for ln in res.lines:
|
||
xs, ys, xe, ye = [], [], [], []
|
||
for w in ln.words:
|
||
r = w.bounding_rect
|
||
xs.append(r.x)
|
||
ys.append(r.y)
|
||
xe.append(r.x + r.width)
|
||
ye.append(r.y + r.height)
|
||
if not xs:
|
||
continue
|
||
out.append({'t': ln.text, 'x': int(min(xs)), 'y': int(min(ys)),
|
||
'w': int(max(xe) - min(xs)), 'h': int(max(ye) - min(ys))})
|
||
return out
|
||
|
||
|
||
async def main():
|
||
path = sys.argv[1]
|
||
ratio_min = float(sys.argv[2]) if len(sys.argv) > 2 else 1.6
|
||
tsv = sys.argv[3] if len(sys.argv) > 3 else ''
|
||
engine = OcrEngine.try_create_from_user_profile_languages()
|
||
print('engine lang = %s' % engine.recognizer_language.language_tag)
|
||
|
||
im = Image.open(path)
|
||
W, H = im.size
|
||
print('=== %s %dx%d' % (os.path.basename(path), W, H))
|
||
|
||
tmp = os.path.join(tempfile.gettempdir(), '_ocr_tile.png')
|
||
all_lines = []
|
||
y = 0
|
||
while y < H:
|
||
y1 = min(y + TILE_H, H)
|
||
im.crop((0, y, W, y1)).save(tmp, 'PNG')
|
||
for ln in await ocr_file(engine, tmp):
|
||
ln['y'] += y
|
||
dup = any(abs(ln['y'] - o['y']) < 6 and ln['t'] == o['t']
|
||
for o in all_lines)
|
||
if not dup:
|
||
all_lines.append(ln)
|
||
y += TILE_H - TILE_OVERLAP
|
||
|
||
all_lines.sort(key=lambda r: (r['y'], r['x']))
|
||
print('识别到 %d 行文字' % len(all_lines))
|
||
sus = []
|
||
for r in all_lines:
|
||
r['ratio'] = r['h'] / float(max(r['w'], 1))
|
||
tag = ''
|
||
if r['ratio'] >= ratio_min and len(r['t']) >= 2:
|
||
tag = ' <== 又高又窄(疑似竖排)'
|
||
sus.append(r)
|
||
print(' y=%-6d x=%-5d w=%-5d h=%-4d r=%.2f | %s%s'
|
||
% (r['y'], r['x'], r['w'], r['h'], r['ratio'], r['t'][:60], tag))
|
||
print('可疑(高/宽 >= %.1f 且 >=2 字)共 %d 行' % (ratio_min, len(sus)))
|
||
if tsv:
|
||
with io.open(tsv, 'w', encoding='utf-8', newline='\n') as fh:
|
||
fh.write('y\tx\tw\th\tratio\ttext\n')
|
||
for r in all_lines:
|
||
fh.write('%d\t%d\t%d\t%d\t%.2f\t%s\n'
|
||
% (r['y'], r['x'], r['w'], r['h'], r['ratio'], r['t']))
|
||
print('TSV -> %s' % tsv)
|
||
|
||
|
||
asyncio.run(main())
|