"""Public, read-only aggregate reports from the mounted statistics bucket. The module name is retained for compatibility with existing UI fixtures. """ import json import logging from html import escape from datetime import datetime, timedelta from pathlib import Path import gradio as gr from generation_stats import JST LOGGER = logging.getLogger(__name__) MAX_BYTES = 10_000_000 def make_report(raw, registered, now=None, show_past=False): now = (now or datetime.now(JST)).astimezone(JST) today = now.date() yesterday = today - timedelta(days=1) current_month = today.strftime("%Y-%m") previous_month = (today.replace(day=1) - timedelta(days=1)).strftime("%Y-%m") if (not isinstance(raw, dict) or raw.get("version") != 1 or raw.get("timezone") != "Asia/Tokyo" or not isinstance(raw.get("counts"), dict)): raise ValueError("invalid statistics schema") counts = raw["counts"] current = {(family, name) for family, names in registered.items() for name in names} known = set(current) monthly = {} for day, families in counts.items(): parsed = datetime.strptime(day, "%Y-%m-%d").date() if parsed.isoformat() != day or parsed > today or not isinstance(families, dict): raise ValueError("invalid statistics day") month = day[:7] for family, models in families.items(): if family not in ("anima", "illustrious") or not isinstance(models, dict): raise ValueError("invalid statistics family") for name, count in models.items(): if (not isinstance(name, str) or not name or len(name) > 256 or type(count) is not int or not 0 <= count <= 10**12): raise ValueError("invalid statistics count") known.add((family, name)) key = (month, family, name) monthly[key] = monthly.get(key, 0) + count labels = {"anima": "Anima", "illustrious": "Illustrious"} models = sorted(known if show_past else current) daily_rows = [[labels[family], name, counts.get(yesterday.isoformat(), {}).get(family, {}).get(name, 0), counts.get(today.isoformat(), {}).get(family, {}).get(name, 0)] for family, name in models] daily_rows.append(["表示中のモデル合計", "", sum(row[2] for row in daily_rows), sum(row[3] for row in daily_rows)]) month_rows = [[labels[family], name, monthly.get((previous_month, family, name), 0), monthly.get((current_month, family, name), 0)] for family, name in models] month_rows.append(["表示中のモデル合計", "", sum(row[2] for row in month_rows), sum(row[3] for row in month_rows)]) return { "daily_headers": ["モード", "モデル", f"昨日 {yesterday.isoformat()}", f"今日 {today.isoformat()}"], "monthly_headers": ["モード", "モデル", f"前月合計 {previous_month}", f"当月合計 {current_month}"], "daily": daily_rows, "monthly": month_rows, "fetched_at": now.strftime("%Y-%m-%d %H:%M:%S JST"), } class AdminStatistics: def __init__(self, path, registered): self.path = Path(path) if path is not None else None self.registered = registered def fetch(self, show_past=False): # Public aggregates only. No flush, GPU call or Hub upload. if self.path is None: raise ValueError("GENERATION_STATS_DIR is not configured") stat = self.path.stat() if stat.st_size > MAX_BYTES: raise ValueError("statistics file too large") contents = self.path.read_bytes() if len(contents) > MAX_BYTES: raise ValueError("statistics file too large") report = make_report(json.loads(contents), self.registered, show_past=show_past) report["saved_at"] = datetime.fromtimestamp(stat.st_mtime, JST).strftime("%Y-%m-%d %H:%M:%S JST") return report TABLE_CSS = """ width: 100%; max-height: none; overflow: visible; color: var(--body-text-color, #111); background: var(--block-background-fill, #fff); table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: .88rem; } th, td { padding: .45rem; border: 1px solid var(--border-color-primary, #aaa); overflow-wrap: anywhere; vertical-align: top; } tbody td { background: var(--block-background-fill, #fff); } th { text-align: left; } thead th, tfoot th, tfoot td { background: var(--background-fill-secondary, #eee); } td:nth-child(n+3), th:nth-child(n+3) { text-align: right; font-variant-numeric: tabular-nums; } col:first-child { width: 17%; } col:nth-child(2) { width: 45%; } col:nth-child(n+3) { width: 19%; } tfoot td { text-align: right; font-weight: bold; font-variant-numeric: tabular-nums; } @media(max-width: 640px) { th, td { padding: .3rem; } table { font-size: .78rem; } } """ def render_table(headers, rows, caption): """Escape all bucket-derived text; full-height table without inner scrolling.""" head = ''.join(f'