Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import time
|
| 3 |
+
import math
|
| 4 |
+
from collections import deque, defaultdict
|
| 5 |
+
|
| 6 |
+
# --- 設定參數 ---
|
| 7 |
+
MAX_RECENT_EVENTS = 1000 # 暫存區大小 (達到此數量觸發壓縮)
|
| 8 |
+
GRID_PRECISION = 4 # 網格精度 (4小數點約=11公尺)
|
| 9 |
+
MAX_GRID_INTENSITY = 20.0 # 單一網格最大亮度 (避免單點過曝)
|
| 10 |
+
|
| 11 |
+
# --- 資料結構 ---
|
| 12 |
+
# 1. 近期事件 (Delta Queue): 用於快速同步
|
| 13 |
+
# 格式: {'t': time, 'u': user_id, 'l': [lat, lng], 's': speed}
|
| 14 |
+
recent_events = deque()
|
| 15 |
+
|
| 16 |
+
# 2. 歷史網格 (Snapshot Grid): 儲存壓縮後的長期記憶
|
| 17 |
+
# Key: (lat_round, lng_round), Value: intensity
|
| 18 |
+
archived_grid = defaultdict(float)
|
| 19 |
+
|
| 20 |
+
# 3. 狀態版本控制
|
| 21 |
+
snapshot_version = 0
|
| 22 |
+
last_snapshot_time = time.time()
|
| 23 |
+
|
| 24 |
+
def get_intensity_by_speed(speed_kmh):
|
| 25 |
+
"""
|
| 26 |
+
根據速度決定迷霧驅散強度 (亮度)
|
| 27 |
+
< 5 km/h (步行/停留): 強度 1.0
|
| 28 |
+
< 20 km/h (跑步/腳踏車): 強度 0.5
|
| 29 |
+
> 20 km/h (車輛): 強度 0.1
|
| 30 |
+
"""
|
| 31 |
+
if speed_kmh is None: return 0.5
|
| 32 |
+
if speed_kmh < 5: return 1.0
|
| 33 |
+
if speed_kmh < 20: return 0.5
|
| 34 |
+
return 0.1
|
| 35 |
+
|
| 36 |
+
def compress_events_to_grid(events_list):
|
| 37 |
+
"""將事件列表壓縮進網格"""
|
| 38 |
+
global archived_grid
|
| 39 |
+
for event in events_list:
|
| 40 |
+
lat, lng = event['l']
|
| 41 |
+
speed = event['s']
|
| 42 |
+
|
| 43 |
+
# 網格化座標
|
| 44 |
+
grid_key = (round(lat, GRID_PRECISION), round(lng, GRID_PRECISION))
|
| 45 |
+
|
| 46 |
+
# 累加亮度
|
| 47 |
+
intensity = get_intensity_by_speed(speed)
|
| 48 |
+
archived_grid[grid_key] += intensity
|
| 49 |
+
|
| 50 |
+
# 限制最大亮度 (防止單點無限疊加)
|
| 51 |
+
if archived_grid[grid_key] > MAX_GRID_INTENSITY:
|
| 52 |
+
archived_grid[grid_key] = MAX_GRID_INTENSITY
|
| 53 |
+
|
| 54 |
+
def trigger_snapshot():
|
| 55 |
+
"""檢查並執行快照壓縮"""
|
| 56 |
+
global recent_events, snapshot_version, last_snapshot_time
|
| 57 |
+
|
| 58 |
+
# 如果暫存區超過閾值,將最舊的一半資料壓縮進歷史網格
|
| 59 |
+
if len(recent_events) > MAX_RECENT_EVENTS:
|
| 60 |
+
events_to_archive = []
|
| 61 |
+
count_to_remove = int(MAX_RECENT_EVENTS / 2)
|
| 62 |
+
|
| 63 |
+
for _ in range(count_to_remove):
|
| 64 |
+
events_to_archive.append(recent_events.popleft())
|
| 65 |
+
|
| 66 |
+
compress_events_to_grid(events_to_archive)
|
| 67 |
+
|
| 68 |
+
snapshot_version += 1
|
| 69 |
+
last_snapshot_time = time.time()
|
| 70 |
+
print(f"[Server] Snapshot Updated. Ver: {snapshot_version}, Grid Points: {len(archived_grid)}")
|
| 71 |
+
|
| 72 |
+
def sync_game_state(client_last_time, client_ver, user_id, lat, lng, speed):
|
| 73 |
+
"""
|
| 74 |
+
主 API: 處理客戶端同步請求
|
| 75 |
+
"""
|
| 76 |
+
current_time = time.time()
|
| 77 |
+
|
| 78 |
+
# 1. 記錄用戶上傳的位置 (如果有)
|
| 79 |
+
if lat is not None and lng is not None:
|
| 80 |
+
# 過濾異常數據 (例如 GPS 飄移瞬間移動 > 150km/h)
|
| 81 |
+
if speed is None or speed < 150:
|
| 82 |
+
new_event = {
|
| 83 |
+
"t": current_time,
|
| 84 |
+
"u": user_id,
|
| 85 |
+
"l": [lat, lng],
|
| 86 |
+
"s": speed
|
| 87 |
+
}
|
| 88 |
+
recent_events.append(new_event)
|
| 89 |
+
|
| 90 |
+
# 2. 檢查是否需要觸發壓縮
|
| 91 |
+
trigger_snapshot()
|
| 92 |
+
|
| 93 |
+
# 3. 準備回傳資料
|
| 94 |
+
response = {
|
| 95 |
+
"server_time": current_time,
|
| 96 |
+
"snapshot_version": snapshot_version,
|
| 97 |
+
"sync_type": "delta",
|
| 98 |
+
"payload": []
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
# 判斷同步策略
|
| 102 |
+
# 如果客戶端版本落後 (需要全量更新) 或 客戶端剛加入 (ver = -1)
|
| 103 |
+
# if client_ver < snapshot_version:
|
| 104 |
+
if client_ver < 0:
|
| 105 |
+
response["sync_type"] = "snapshot"
|
| 106 |
+
# 回傳壓縮後的網格數據: [[lat, lng, intensity], ...]
|
| 107 |
+
response["payload"] = [[k[0], k[1], v] for k, v in archived_grid.items()]
|
| 108 |
+
|
| 109 |
+
# 同時附上還沒被壓縮的近期事件,確保數據不出現斷層
|
| 110 |
+
response["recent_events"] = list(recent_events)
|
| 111 |
+
else:
|
| 112 |
+
# 增量更新: 只回傳 client_last_time 之後發生的事件
|
| 113 |
+
# 注意: 這裡回傳的是未壓縮的原始事件
|
| 114 |
+
delta = [e for e in recent_events if e['t'] > client_last_time]
|
| 115 |
+
response["payload"] = delta
|
| 116 |
+
|
| 117 |
+
return response
|
| 118 |
+
|
| 119 |
+
# --- Gradio 介面設定 ---
|
| 120 |
+
with gr.Blocks() as demo:
|
| 121 |
+
gr.Markdown("## City Scout Game API Server")
|
| 122 |
+
gr.Markdown("此 Space 為遊戲後端,請使用前端 HTML 連接。")
|
| 123 |
+
|
| 124 |
+
with gr.Row(visible=False): # 隱藏 UI,僅作為 API 使用
|
| 125 |
+
# Inputs
|
| 126 |
+
in_time = gr.Number(label="Client Last Time")
|
| 127 |
+
in_ver = gr.Number(label="Client Snapshot Version")
|
| 128 |
+
in_uid = gr.Textbox(label="User ID")
|
| 129 |
+
in_lat = gr.Number(label="Lat")
|
| 130 |
+
in_lng = gr.Number(label="Lng")
|
| 131 |
+
in_speed = gr.Number(label="Speed (km/h)")
|
| 132 |
+
|
| 133 |
+
# Outputs
|
| 134 |
+
out_json = gr.JSON(label="Sync Response")
|
| 135 |
+
|
| 136 |
+
# API 按鈕
|
| 137 |
+
btn_sync = gr.Button("Sync")
|
| 138 |
+
btn_sync.click(
|
| 139 |
+
fn=sync_game_state,
|
| 140 |
+
inputs=[in_time, in_ver, in_uid, in_lat, in_lng, in_speed],
|
| 141 |
+
outputs=out_json,
|
| 142 |
+
api_name="sync" # 重要: 前端 client.predict("/sync") 用這個名字
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# 啟動並允許跨域 (CORS)
|
| 146 |
+
demo.queue(max_size=50).launch(cors_allowed_origins=["*"])
|