v1.4.0 全局搜索+水印5风格+对比列表管理+排行列可配:搜索框拆分(网页顶部居中=选中跳详情; 排行内搜索=添加速度项到本区域查看,可✕移除); 水印预置5种风格(右下角/全图斜纹/底部横条/四角/对角大字)后台可选; 运行速度对比图要对比模型列表可叉掉/清空一键去除/+对比添加; 图表改深色主题适配深色背景; 排行榜新增推理框架列,首字ms默认隐藏,平均解码+最佳解码合并,硬件+推理框架合并,列显示/合并后台⚙️设置可配
This commit is contained in:
@@ -31,6 +31,11 @@ def _watermark_text():
|
||||
return db.get_setting("watermark_text", config.DEFAULT_WATERMARK)
|
||||
|
||||
|
||||
def _watermark_style():
|
||||
st = db.get_setting("watermark_style", config.DEFAULT_WATERMARK_STYLE)
|
||||
return st if st in config.WATERMARK_STYLES else config.DEFAULT_WATERMARK_STYLE
|
||||
|
||||
|
||||
def _top_n():
|
||||
try:
|
||||
return max(1, int(db.get_setting("top_n", str(config.DEFAULT_TOP_N))))
|
||||
@@ -38,10 +43,22 @@ def _top_n():
|
||||
return config.DEFAULT_TOP_N
|
||||
|
||||
|
||||
def apply_watermark(img_bytes: bytes, text: str) -> bytes:
|
||||
"""给图片叠加简单水印:全图浅淡斜纹 + 右下角标注。颜色自适应当前背景(浅底深字/深底浅字)"""
|
||||
def _column_config():
|
||||
out = {}
|
||||
for k, default in config.COLUMN_DEFAULTS.items():
|
||||
v = db.get_setting(k, default)
|
||||
out[k] = 1 if str(v).strip() in ("1", "true", "on") else 0
|
||||
return out
|
||||
|
||||
|
||||
def apply_watermark(img_bytes: bytes, text: str, style: str = "") -> bytes:
|
||||
"""给图片叠加水印,5 种风格可选:corner右下角标注 / diagonal全图斜纹 / bottom底部横条 / corners四角标注 / diag-big对角大字
|
||||
颜色自适应:浅底深字 / 深底浅字"""
|
||||
if not text:
|
||||
return img_bytes
|
||||
style = style or config.DEFAULT_WATERMARK_STYLE
|
||||
if style not in config.WATERMARK_STYLES:
|
||||
style = config.DEFAULT_WATERMARK_STYLE
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
except Exception:
|
||||
@@ -51,34 +68,59 @@ def apply_watermark(img_bytes: bytes, text: str) -> bytes:
|
||||
except Exception:
|
||||
return img_bytes
|
||||
w, h = img.size
|
||||
# 自适应水印颜色:取样左上角判断背景深浅
|
||||
bg = img.getpixel((4, 4))[:3]
|
||||
bg_luma = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2]
|
||||
dark_bg = bg_luma < 140
|
||||
fill = (50, 50, 60) if not dark_bg else (255, 255, 255)
|
||||
diag_alpha = 30 if not dark_bg else 26
|
||||
corner_alpha = 120 if not dark_bg else 128
|
||||
a_diag = 30 if not dark_bg else 26
|
||||
a_main = 130 if not dark_bg else 135
|
||||
|
||||
overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(overlay)
|
||||
try:
|
||||
font = ImageFont.truetype(_WATERMARK_FONT, 16)
|
||||
font_big = ImageFont.truetype(_WATERMARK_FONT, 22)
|
||||
font_big = ImageFont.truetype(_WATERMARK_FONT, 24)
|
||||
font_huge = ImageFont.truetype(_WATERMARK_FONT, 60)
|
||||
except Exception:
|
||||
font = ImageFont.load_default()
|
||||
font_big = ImageFont.load_default()
|
||||
font_huge = ImageFont.load_default()
|
||||
|
||||
# 全图浅淡斜纹水印(45° 重复)
|
||||
step = 170
|
||||
for y in range(-h, h, step):
|
||||
for x in range(-w, w, step):
|
||||
d.text((x + y * 0.4, y), text, font=font, fill=fill + (diag_alpha,))
|
||||
def _corner_label():
|
||||
box = d.textbbox((0, 0), text, font=font_big)
|
||||
tw = box[2] - box[0]; th = box[3] - box[1]
|
||||
d.text((w - tw - 16, h - th - 12), text, font=font_big, fill=fill + (a_main,))
|
||||
|
||||
# 右下角较清晰标注
|
||||
box = d.textbbox((0, 0), text, font=font_big)
|
||||
tw = box[2] - box[0]
|
||||
th = box[3] - box[1]
|
||||
d.text((w - tw - 16, h - th - 12), text, font=font_big, fill=fill + (corner_alpha,))
|
||||
if style == "corner":
|
||||
_corner_label()
|
||||
elif style == "diagonal":
|
||||
step = 170
|
||||
for y in range(-h, h, step):
|
||||
for x in range(-w, w, step):
|
||||
d.text((x + y * 0.4, y), text, font=font, fill=fill + (a_diag,))
|
||||
elif style == "bottom":
|
||||
# 底部半透明横条
|
||||
box = d.textbbox((0, 0), text, font=font_big)
|
||||
tw = box[2] - box[0]; th = box[3] - box[1]
|
||||
bh = th + 24
|
||||
d.rectangle([0, h - bh, w, h], fill=(0, 0, 0, 90) if not dark_bg else (255, 255, 255, 70))
|
||||
d.text((16, h - bh + 10), text, font=font_big, fill=fill + (a_main,))
|
||||
# 底部中央小字
|
||||
cx = (w - tw) // 2
|
||||
d.text((cx, h - 20), text, font=font_big, fill=fill + (110,))
|
||||
elif style == "corners":
|
||||
box = d.textbbox((0, 0), text, font=font_big)
|
||||
tw = box[2] - box[0]; th = box[3] - box[1]
|
||||
for px, py in [(12, 12), (w - tw - 12, 12), (12, h - th - 12), (w - tw - 12, h - th - 12)]:
|
||||
d.text((px, py), text, font=font_big, fill=fill + (a_main,))
|
||||
elif style == "diag-big":
|
||||
box = d.textbbox((0, 0), text, font=font_huge)
|
||||
tw = box[2] - box[0]; th = box[3] - box[1]
|
||||
for i in range(-1, 2):
|
||||
d.text(((w - tw) // 2 + i * 14, (h - th) // 2 + i * 28), text, font=font_huge, fill=fill + (38,))
|
||||
d.text(((w - tw) // 2, (h - th) // 2), text, font=font_huge, fill=fill + (80,))
|
||||
else:
|
||||
_corner_label()
|
||||
|
||||
out = Image.alpha_composite(img, overlay)
|
||||
buf = io.BytesIO()
|
||||
@@ -209,8 +251,10 @@ def leaderboard():
|
||||
|
||||
chart_rows = rows[: int(request.args.get("chart_top", 30) or 30)]
|
||||
chart_csv = _speed_csv(chart_rows)
|
||||
cols = _column_config()
|
||||
return jsonify({"ok": True, "rows": rows, "chart_csv": chart_csv, "top_n": _top_n(),
|
||||
"watermark": _watermark_text()})
|
||||
"watermark": _watermark_text(), "watermark_style": _watermark_style(),
|
||||
"columns": cols})
|
||||
|
||||
|
||||
def _speed_csv(chart_rows):
|
||||
@@ -271,7 +315,7 @@ def model_detail():
|
||||
line_payload = {
|
||||
"data": line_csv, "chartType": "line",
|
||||
"title": "%s %s · 解码速度随上下文长度变化" % (provider or "", model),
|
||||
"theme": "default", "showLegend": True, "showGrid": True, "showLabel": False,
|
||||
"theme": "dark", "showLegend": True, "showGrid": True, "showLabel": False,
|
||||
"smoothLine": True, "dualYAxis": True,
|
||||
"leftAxisName": "预填充速度(tok/s)", "rightAxisName": "解码速度(tok/s)",
|
||||
"seriesTypes": ["line", "line"], "seriesAxis": [0, 1],
|
||||
@@ -341,7 +385,7 @@ def submission_chart(sid):
|
||||
payload = {
|
||||
"data": "\n".join(csv_lines), "chartType": "line",
|
||||
"title": "%s %s · 速度随上下文长度(提交#%d)" % (s["provider"], s["model"], sid),
|
||||
"theme": "default", "showLegend": True, "showGrid": True, "showLabel": False,
|
||||
"theme": "dark", "showLegend": True, "showGrid": True, "showLabel": False,
|
||||
"smoothLine": True, "dualYAxis": True,
|
||||
"leftAxisName": "预填充速度(tok/s)", "rightAxisName": "解码速度(tok/s)",
|
||||
"seriesTypes": ["line", "line"], "seriesAxis": [0, 1],
|
||||
@@ -389,7 +433,9 @@ def account_delete(aid):
|
||||
|
||||
@app.route("/api/settings")
|
||||
def public_settings():
|
||||
return jsonify({"ok": True, "watermark": _watermark_text(), "top_n": _top_n()})
|
||||
return jsonify({"ok": True, "watermark": _watermark_text(),
|
||||
"watermark_style": _watermark_style(),
|
||||
"top_n": _top_n(), "columns": _column_config()})
|
||||
|
||||
|
||||
@app.route("/api/admin/speed-items")
|
||||
@@ -437,14 +483,15 @@ def admin_speed_item_sync():
|
||||
@admin_required
|
||||
def admin_settings_get():
|
||||
return jsonify({"ok": True, "settings": db.get_all_settings(),
|
||||
"watermark": _watermark_text(), "top_n": _top_n()})
|
||||
"watermark": _watermark_text(), "watermark_style": _watermark_style(),
|
||||
"top_n": _top_n(), "columns": _column_config()})
|
||||
|
||||
|
||||
@app.route("/api/admin/settings", methods=["PUT"])
|
||||
@admin_required
|
||||
def admin_settings_put():
|
||||
body = request.get_json(force=True) or {}
|
||||
allow = {"watermark_text", "top_n"}
|
||||
allow = {"watermark_text", "watermark_style", "top_n"} | set(config.COLUMN_DEFAULTS.keys())
|
||||
for k, v in body.items():
|
||||
if k in allow:
|
||||
db.set_setting(k, v)
|
||||
@@ -500,8 +547,8 @@ def _chart_proxy(payload):
|
||||
return jsonify({"ok": False, "error": "图表服务不可用: %s" % e}), 502
|
||||
if resp.status_code != 200:
|
||||
return jsonify({"ok": False, "error": "图表生成失败(%d): %s" % (resp.status_code, resp.text[:300])}), 502
|
||||
# 叠加水印(默认首页网址,后台可配置)
|
||||
data = apply_watermark(resp.content, _watermark_text())
|
||||
# 叠加水印(默认首页网址,后台可配置文字+5种风格)
|
||||
data = apply_watermark(resp.content, _watermark_text(), _watermark_style())
|
||||
return send_file(io.BytesIO(data), mimetype="image/png")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user