Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| import json | |
| import random | |
| from datetime import datetime, timedelta, timezone | |
| from pathlib import Path | |
| from huggingface_hub import CommitScheduler | |
| # --- ① 統計データ保存用の設定 --- | |
| # 自分のリポジトリ名に合わせて変更、または環境変数から取得 | |
| DATASET_REPO_ID = "Nyanpre/hasunosora-stats" | |
| DATA_FILE = Path("data") / "stats.json" | |
| DATA_FILE.parent.mkdir(parents=True, exist_ok=True) | |
| # 初回起動時にファイルがない場合は空のデータを作成 | |
| if not DATA_FILE.exists(): | |
| with open(DATA_FILE, "w", encoding="utf-8") as f: | |
| json.dump({"total": 0, "history": []}, f) | |
| # Hugging Face Datasetへ自動保存するスケジューラー | |
| # ※SpaceのSettingsでCP_ORACLE_TOKEN (write権限) の設定が必要です | |
| scheduler = CommitScheduler( | |
| repo_id=DATASET_REPO_ID, | |
| repo_type="dataset", | |
| folder_path=DATA_FILE.parent, | |
| path_in_repo="stats.json", | |
| token=os.getenv("CP_ORACLE_TOKEN") | |
| ) | |
| def load_stats(): | |
| if DATA_FILE.exists(): | |
| try: | |
| with open(DATA_FILE, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except: | |
| pass | |
| return {"total": 0, "history": []} | |
| # --- ② 既存ロジックと統計の統合 --- | |
| members = ["かほ", "さや", "こず", "るり", "めぐ", "つづ", "ぎん", "すず", "ひめ", "せら", "いず", "さち"] | |
| def get_personal_daily_oracle(device_id): | |
| seed_base = device_id if device_id else "default_fate" | |
| jst = timezone(timedelta(hours=9)) | |
| now = datetime.now(jst) | |
| today_str = now.strftime("%Y-%m-%d") | |
| random.seed(f"{seed_base}_{today_str}") | |
| selected = random.sample(members, 2) | |
| random.shuffle(selected) | |
| pair_name = f"{selected[0]}{selected[1]}" | |
| # --- ③ 統計の記録処理 --- | |
| with scheduler.lock: | |
| stats = load_stats() | |
| stats["total"] += 1 | |
| stats["history"].append({"date": today_str, "pair": pair_name}) | |
| with open(DATA_FILE, "w", encoding="utf-8") as f: | |
| json.dump(stats, f, ensure_ascii=False) | |
| oracle_html = ( | |
| f"<div id='pair-raw' style='display:none;'>{pair_name}</div>" | |
| f"<div style='font-size: 38px; font-weight: normal; margin-bottom: 2px;'>本日の神託</div>" | |
| f"<div style='font-size: 52px; font-weight: 900; letter-spacing: 2px;'>{pair_name}</div>" | |
| ) | |
| peace_msg = "これにより、不毛なカップリング論争は終結しました。" | |
| return oracle_html, peace_msg, gr.update(visible=True), gr.update(visible=True), gr.update(visible=True) | |
| # --- 神モード:集計ロジック --- | |
| def update_stats_view(): | |
| stats = load_stats() | |
| hist = stats.get("history", []) | |
| jst = timezone(timedelta(hours=9)) | |
| today_str = datetime.now(jst).strftime("%Y-%m-%d") | |
| counts_all = {} | |
| counts_today = {} | |
| for h in hist: | |
| p = h['pair'] | |
| counts_all[p] = counts_all.get(p, 0) + 1 | |
| if h.get('date') == today_str: | |
| counts_today[p] = counts_today.get(p, 0) + 1 | |
| def gen_rank_html(title, data_dict): | |
| sorted_items = sorted(data_dict.items(), key=lambda x: x[1], reverse=True)[:5] | |
| rows = "".join([f"<li>{i+1}位: <b>{p}</b> ({c}回)</li>" for i, (p, c) in enumerate(sorted_items)]) | |
| return f"<h4>{title}</h4><ul>{rows if rows else '<li>データなし</li>'}</ul>" | |
| html_all = gen_rank_html("全人類神託カップリングTOP5【累計】", counts_all) | |
| html_today = gen_rank_html("全人類神託カップリングTOP5【本日】", counts_today) | |
| return f"<div style='text-align: left;'>{html_all}<hr>{html_today}<p>総神託数: {stats['total']}</p></div>" | |
| # --- JS: 既存のものを維持 --- | |
| js_logic = """ | |
| function(deviceId) { | |
| const id = localStorage.getItem('cp_oracle_device_id') || "guest"; | |
| const lastDraw = localStorage.getItem('lastOracleDate'); | |
| const lastPair = localStorage.getItem('lastPairText'); | |
| const today = new Date().toLocaleDateString('ja-JP'); | |
| if (lastDraw === today && lastPair) { | |
| alert("本日の神託は既に下されています。\\n明日の更新まで、今の思想を維持しなさい。"); | |
| return [id, lastPair, "これにより、不毛なカップリング論争は終結しました。", { "visible": true, "__type__": "update" }, { "visible": true, "__type__": "update" }, { "visible": true, "__type__": "update" }]; | |
| } | |
| localStorage.setItem('lastOracleDate', today); | |
| return [id, null, null, null, null, null]; | |
| } | |
| """ | |
| js_save_result = """ | |
| function(oracleHtml, peaceMsg, bskyBtn, xBtn, resultBox) { | |
| if (oracleHtml && oracleHtml.includes("本日の神託")) { | |
| localStorage.setItem('lastPairText', oracleHtml); | |
| } | |
| } | |
| """ | |
| js_get_share_text = """ | |
| function() { | |
| const pairRawEl = document.getElementById('pair-raw'); | |
| let pairName = ""; | |
| if (pairRawEl) { | |
| pairName = pairRawEl.innerText; | |
| } else { | |
| const lastPair = localStorage.getItem('lastPairText') || ""; | |
| const match = lastPair.match(/>([^<]{4,})<\\/div>$/); | |
| pairName = match ? match[1] : "運命"; | |
| } | |
| const currentUrl = window.location.href; | |
| return `私は「${pairName}」を信仰しています。\\n\\n蓮ノ空聖書正典:\\n${currentUrl}`; | |
| } | |
| """ | |
| js_share_bluesky = f"function() {{ const text = ({js_get_share_text})(); window.open(`https://bsky.app/intent/compose?text=${{encodeURIComponent(text)}}`, '_blank'); }}" | |
| js_share_x = f"function() {{ const text = ({js_get_share_text})(); window.open(`https://twitter.com/intent/tweet?text=${{encodeURIComponent(text)}}`, '_blank'); }}" | |
| # --- CSS --- | |
| custom_css = """ | |
| .gradio-container { max-width: 600px !important; text-align: center !important; } | |
| .center-content { display: flex !important; flex-direction: column !important; align-items: center !important; padding-top: 20px !important; } | |
| h1 { margin-top: 0px !important; margin-bottom: -5px !important; font-size: 32px !important; } | |
| #doctrine { font-size: 1.5em !important; line-height: 1.4 !important; font-weight: bold !important; margin-bottom: 10px !important; text-align: center !important; } | |
| #oracle-box { | |
| color: #000 !important; background: #fff !important; border: 4px solid #000 !important; | |
| padding: 25px 10px !important; line-height: 1.1 !important; min-height: 130px !important; | |
| display: flex !important; flex-direction: column !important; justify-content: center !important; | |
| margin: 10px auto !important; | |
| } | |
| .dark #oracle-box { color: #fff !important; background: #000 !important; border: 4px solid #fff !important; } | |
| #peace-msg { font-size: 20px !important; font-weight: bold !important; color: #d63031 !important; margin-top: 10px !important; } | |
| .action-btn { font-size: 24px !important; font-weight: bold !important; width: 320px !important; border: 2px solid #000 !important; margin-bottom: 10px !important; } | |
| .dark .action-btn { border: 2px solid #fff !important; } | |
| """ | |
| with gr.Blocks(title="蓮ノ空聖書正典", css=custom_css, theme=gr.themes.Monochrome()) as demo: | |
| device_id_storage = gr.Textbox(visible=False) | |
| demo.load(None, None, device_id_storage, js=""" | |
| () => { | |
| let id = localStorage.getItem('cp_oracle_device_id'); | |
| if(!id) { | |
| id = Math.random().toString(36).substring(2, 15); | |
| localStorage.setItem('cp_oracle_device_id', id); | |
| } | |
| return id; | |
| } | |
| """) | |
| gr.Markdown("# ⚖️ 蓮ノ空聖書正典") | |
| with gr.Tabs(): | |
| # --- ① 信仰(メイン)タブ --- | |
| with gr.TabItem("神託"): | |
| with gr.Column(elem_classes="center-content"): | |
| gr.Markdown("日付が変わるまであなたの思想は<br>統一されます。", elem_id="doctrine") | |
| result_display = gr.HTML(elem_id="oracle-box", visible=False) | |
| peace_display = gr.Markdown(elem_id="peace-msg") | |
| draw_btn = gr.Button("神託を受ける", variant="primary", elem_id="draw-btn", elem_classes="action-btn") | |
| share_btn_bsky = gr.Button("Blueskyで信仰を広める", variant="secondary", elem_id="share-bsky", elem_classes="action-btn", visible=False) | |
| share_btn_x = gr.Button("Xで信仰を広める", variant="secondary", elem_id="share-x", elem_classes="action-btn", visible=False) | |
| # --- ② 神モードタブ --- | |
| with gr.TabItem("神モード"): | |
| with gr.Column(elem_classes="center-content"): | |
| gr.Markdown("### 全人類の信仰状況") | |
| stats_display = gr.HTML("<p>ボタンを押して読み込み</p>") | |
| refresh_stats = gr.Button("全人類の神託を見る", variant="primary", elem_classes="action-btn") | |
| # --- 既存のクリックイベント --- | |
| draw_btn.click( | |
| fn=get_personal_daily_oracle, | |
| inputs=[device_id_storage], | |
| outputs=[result_display, peace_display, share_btn_bsky, share_btn_x, result_display], | |
| js=js_logic | |
| ).then( | |
| fn=None, | |
| inputs=[result_display, peace_display, share_btn_bsky, share_btn_x, result_display], | |
| outputs=None, | |
| js=js_save_result | |
| ) | |
| share_btn_bsky.click(fn=None, inputs=None, outputs=None, js=js_share_bluesky) | |
| share_btn_x.click(fn=None, inputs=None, outputs=None, js=js_share_x) | |
| # --- 新機能のクリックイベント --- | |
| refresh_stats.click(fn=update_stats_view, outputs=stats_display) | |
| if __name__ == "__main__": | |
| demo.launch() |