Download admin_stats.py from yamiuchi/qm-image: direct link, hf CLI and curl.
- Browser
- Download file 8.94 kB
-
https://huggingface.co/spaces/yamiuchi/qm-image/resolve/main/admin_stats.py
- Command line
-
hf download hf://spaces/yamiuchi/qm-image/admin_stats.py
-
curl -L -o admin_stats.py https://huggingface.co/spaces/yamiuchi/qm-image/resolve/main/admin_stats.py
8.94 kB
| """Public, read-only aggregate reports from this app's isolated statistics file.""" | |
| 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 ("qwen", "moss") 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 = {"qwen": "Qwen Image 2.1", "moss": "MOSS Image"} | |
| 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'<th scope="col">{escape(str(h))}</th>' for h in headers) | |
| body = ''.join('<tr>' + ''.join(f'<td>{escape(format(v, ",") if type(v) is int else str(v))}</td>' | |
| for v in row) + '</tr>' for row in rows[:-1]) | |
| total = rows[-1] | |
| foot = (f'<tr><th scope="row" colspan="2">{escape(total[0])}</th>' | |
| f'<td>{total[2]:,}</td><td>{total[3]:,}</td></tr>') | |
| return (f'<table aria-label="{escape(caption, quote=True)}"><colgroup><col><col><col><col></colgroup>' | |
| f'<thead><tr>{head}</tr></thead><tbody>{body}</tbody><tfoot>{foot}</tfoot></table>') | |
| def build_admin_statistics(blocks, counter, registered): | |
| service = AdminStatistics(counter.path, registered) | |
| with gr.Accordion("利用状況・生成統計", open=False, elem_classes=["generation-statistics"]): | |
| gr.Markdown("モデル入れ替えの参考として、モデル別の生成件数を公開しています。ログイン不要で閲覧できます。プロンプト・画像・利用者情報は含みません。") | |
| with gr.Column(elem_classes=["statistics-panel"]) as panel: | |
| show_past = gr.Checkbox(label="過去のモデルも表示", value=False) | |
| status = gr.Markdown() | |
| gr.Markdown("### 昨日・今日のモデル別生成件数") | |
| daily = gr.HTML(value="", css_template=TABLE_CSS, apply_default_css=False, elem_classes=["statistics-table", "statistics-daily"]) | |
| gr.Markdown("### 月別合計(前月・当月)") | |
| monthly = gr.HTML(value="", css_template=TABLE_CSS, apply_default_css=False, elem_classes=["statistics-table", "statistics-monthly"]) | |
| refresh = gr.Button("生成統計を更新") | |
| def refresh_statistics(include_past=False): | |
| try: | |
| report = service.fetch(show_past=include_past) | |
| except Exception: | |
| LOGGER.warning("Public statistics read failed", exc_info=True) | |
| # Do not render exception details, filesystem paths or stale counts. | |
| return gr.update(visible=True), "保存済み統計を読み込めません。バケットの接続・保存状態を確認して更新してください。", "", "" | |
| text = (f"取得日時:{report['fetched_at']} \n統計ファイル更新日時:{report['saved_at']}\n\n" | |
| "日本時間・全利用者のWebUI/API/MCP経由の成功した生成を集計しています。" | |
| "接続済みの統計バケットを読み取っています。通常約5分間隔で保存されるため、未保存分は含みません。" | |
| "保存障害時は遅れが大きくなる場合があります。月別合計は前月・当月の記録済み件数で、当月は途中経過です。" | |
| "記録のない日・モデルは0として表示します。" | |
| "初期表示は現行Checkpointのみです。過去の記録は削除せず、チェックで表示を切り替えられます。" | |
| "合計は表示中のモデルだけを対象とし、旧モデル名の件数を現行名へ統合しません。") | |
| return (gr.update(visible=True), text, | |
| render_table(report["daily_headers"], report["daily"], "昨日・今日のモデル別生成件数"), | |
| render_table(report["monthly_headers"], report["monthly"], "前月・当月のモデル別生成件数")) | |
| outputs = [panel, status, daily, monthly] | |
| refresh.click(refresh_statistics, inputs=show_past, outputs=outputs, | |
| api_visibility="private", queue=False, show_progress="hidden") | |
| show_past.change(refresh_statistics, inputs=show_past, outputs=outputs, | |
| api_visibility="private", queue=False, show_progress="hidden") | |
| blocks.load(refresh_statistics, inputs=show_past, outputs=outputs, | |
| api_visibility="private", queue=False, show_progress="hidden") | |