предварительный коммит shotmap

This commit is contained in:
2025-11-17 18:34:55 +03:00
parent 7ebfc4eeaa
commit 0ed7b5f06d
2 changed files with 179 additions and 7 deletions

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@
/TestJson /TestJson
/logs/* /logs/*
*.venv *.venv
*.env *.env
/shotmaps/*

View File

@@ -16,6 +16,10 @@ import nasio
import io, os, platform, time import io, os, platform, time
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import re import re
from PIL import Image, ImageDraw, ImageFont
import base64
from io import BytesIO
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -87,6 +91,18 @@ SYNO_URL = os.getenv("SYNO_URL")
SYNO_USERNAME = os.getenv("SYNO_USERNAME") SYNO_USERNAME = os.getenv("SYNO_USERNAME")
SYNO_PASSWORD = os.getenv("SYNO_PASSWORD") SYNO_PASSWORD = os.getenv("SYNO_PASSWORD")
SYNO_PATH_VMIX = os.getenv("SYNO_PATH_VMIX") SYNO_PATH_VMIX = os.getenv("SYNO_PATH_VMIX")
SYNO_FONT_PATH = os.getenv("SYNO_FONT_PATH")
SHOTMAP_SUBDIR = "shotmaps"
# SHOTMAP_DIR = (
# os.path.join(SYNO_PATH_VMIX, SHOTMAP_SUBDIR)
# if SYNO_PATH_VMIX
# else os.path.join(os.getcwd(), SHOTMAP_SUBDIR)
# )
# os.makedirs(SHOTMAP_DIR, exist_ok=True)
SHOTMAP_DIR = os.path.join(os.getcwd(), "shotmaps")
os.makedirs(SHOTMAP_DIR, exist_ok=True)
CALENDAR = None CALENDAR = None
@@ -602,14 +618,10 @@ def results_consumer():
and GAME_START_DT.date() == datetime.now().date() and GAME_START_DT.date() == datetime.now().date()
): ):
globals()["STATUS"] = "finished_wait" globals()["STATUS"] = "finished_wait"
globals()[ globals()["CLEAR_OUTPUT_FOR_VMIX"] = False
"CLEAR_OUTPUT_FOR_VMIX"
] = False # 👈 включаем режим "пустых" данных
else: else:
globals()["STATUS"] = "finished_wait" globals()["STATUS"] = "finished_wait"
globals()[ globals()["CLEAR_OUTPUT_FOR_VMIX"] = False
"CLEAR_OUTPUT_FOR_VMIX"
] = False # 👈 включаем режим "пустых" данных
human_time = datetime.fromtimestamp(switch_at).strftime( human_time = datetime.fromtimestamp(switch_at).strftime(
"%H:%M:%S" "%H:%M:%S"
@@ -2238,6 +2250,8 @@ async def team(who: str):
"Career_TPlayedTime": format_time(car_T_sec), "Career_TPlayedTime": format_time(car_T_sec),
"Career_TGameCount": car_T_gms, "Career_TGameCount": car_T_gms,
"Career_TStartCount": car_T_starts, "Career_TStartCount": car_T_starts,
"startNum": stats.get("startNum"),
"photoShotMapGFX": "",
} }
team_rows.append(row) team_rows.append(row)
@@ -2295,6 +2309,59 @@ async def team(who: str):
key=lambda x: role_priority.get(x.get("startRole", 99), 99), key=lambda x: role_priority.get(x.get("startRole", 99), 99),
) )
# --- 👇 ДОБАВЛЯЕМ КАРТЫ БРОСКОВ ПО startNum --- #
# Берём play-by-play из текущего game, если он есть
plays = result.get("plays") or []
# Собираем множество startNum текущей команды, чтобы не ловить чужих игроков
team_startnums = {
p.get("startNum")
for p in payload.get("starts", [])
if p.get("startRole") == "Player"
}
# startNum -> список (x, y, is_made)
shots_by_startnum: dict[int, list[tuple[float, float, bool]]] = {}
for ev in plays:
play_code = ev.get("play")
if play_code not in (2, 3, 5, 6):
continue
sn = ev.get("startNum")
if sn not in team_startnums:
continue
x = ev.get("x")
y = ev.get("y")
if x is None or y is None:
continue
sec = ev.get("sec")
period = ev.get("period")
is_made = play_code in (2, 3) # 2,3 — точные, 5,6 — промахи
shots_by_startnum.setdefault(sn, []).append((x, y, is_made, sec, period))
# Рендерим по картинке на каждого startNum
shotmap_paths: dict[int, str] = {}
for sn, points in shots_by_startnum.items():
# timestamp/версия — просто количество бросков этого игрока.
# Увеличилось кол-во бросков → поменялся путь → vMix не возьмёт картинку из кеша.
version = str(len(points))
bib = str(sn) # можно заменить на номер игрока, если нужно
path = get_image(points, bib, version)
shotmap_paths[sn] = path
# Присоединяем пути к игрокам по startNum
for row in sorted_team:
sn = row.get("startNum")
if sn in shotmap_paths:
row["photoShotMapGFX"] = f"{shotmap_paths[sn]}"
else:
# если бросков не было — оставляем пустую строку
row["photoShotMapGFX"] = ""
return sorted_team return sorted_team
@@ -3245,6 +3312,110 @@ async def games_online():
return todays_games return todays_games
def get_image(points, bib, count_point):
"""
points: список кортежей (x, y, is_made)
x, y — координаты из API, где (0,0) = центр кольца
is_made — True (play 2,3) или False (play 5,6)
bib: startNum/номер игрока
count_point: строка-версия (анти-кэш vMix)
"""
if not points:
return ""
# # --- подложка ---
# try:
# base_image = Image.open(COURT_IMAGE_PATH).convert("RGBA")
# except Exception as e:
# logger.warning(
# f"[shotmap] не удалось открыть COURT_IMAGE_PATH={COURT_IMAGE_PATH}: {e}"
# )
base_image = Image.new("RGBA", (1500, 2800), (0, 0, 0, 0))
width, height = base_image.size # ожидаем 1500 x 2800
draw = ImageDraw.Draw(base_image)
# === ДИАПАЗОН КООРДИНАТ ИЗ API ===
COURT_WIDTH_UNITS = 150.0 # по X
COURT_LENGTH_UNITS = 280.0 # по Y
# масштаб: пиксели на 1 API-юнит
scale_x = height / COURT_LENGTH_UNITS
scale_y = width / COURT_WIDTH_UNITS
# === ЦЕНТР КОЛЬЦА В PILLOW-КООРДИНАТАХ (ОТ ВЕРХНЕГО ЛЕВОГО УГЛА) ===
HOOP_X_PX = 750
HOOP_Y_PX = 157.5
def to_px(x, y):
"""
(0,0) из API → (HOOP_X_PX, HOOP_Y_PX) на картинке.
x > 0 — вправо, y > 0 — вниз (вглубь площадки).
Если нужно, чтобы y шёл вверх — поменяй + на -.
"""
px = int(HOOP_X_PX - x * scale_x)
py = int(HOOP_Y_PX + y * scale_y)
return px, py
point_radius = 10
# try:
nasio_font = nasio.load_bio(
user=SYNO_USERNAME,
password=SYNO_PASSWORD,
nas_ip=SYNO_URL,
nas_port="443",
path=SYNO_FONT_PATH,
)
if nasio_font:
font = ImageFont.truetype(nasio_font, 18)
# except Exception:
# font = ImageFont.load_default()
for x_raw, y_raw, is_made, sec, period in points:
try:
x = float(x_raw)
y = float(y_raw)
except (TypeError, ValueError):
continue
px, py = to_px(x, y)
# кружок
bbox = (
px - point_radius,
py - point_radius,
px + point_radius,
py + point_radius,
)
color = (0, 255, 0, 255) if is_made else (255, 0, 0, 255)
draw.ellipse(bbox, fill=color)
# подпись координат (для отладки)
label = f"{x:.2f}; {y:.2f}"
text_x = px + point_radius + 4
text_y = py - point_radius - 4
label = f"Q{period}"
# тень
draw.text((text_x + 1, text_y + 1), label, font=font, fill=(0, 0, 0, 255))
# текст
draw.text((text_x, text_y), label, font=font, fill=(255, 255, 255, 255))
filename = f"shots_{bib}_{count_point}.png"
path = os.path.join(SHOTMAP_DIR, filename)
try:
base_image.save(path, "PNG")
except Exception as e:
logger.warning(f"[shotmap] не удалось сохранить {path}: {e}")
return ""
return path
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run( uvicorn.run(
"get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="debug" "get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="debug"