Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -152,11 +152,14 @@ import asyncio
|
|
| 152 |
|
| 153 |
app = FastAPI()
|
| 154 |
|
|
|
|
| 155 |
DISP_W = 96
|
| 156 |
DISP_H = 96
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
| 160 |
ROWS = GRID_SIZE
|
| 161 |
COLS = GRID_SIZE
|
| 162 |
|
|
@@ -164,39 +167,55 @@ PIXEL_CHAR = "█"
|
|
| 164 |
ADMIN_KEY = "supersecret123"
|
| 165 |
current_streamer = None
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
|
|
|
| 169 |
text_lock = asyncio.Lock()
|
| 170 |
|
| 171 |
# ----------------------------------------------------------------
|
| 172 |
def rgb_to_hex_amp(r, g, b):
|
|
|
|
| 173 |
return f"&#{r:02X}{g:02X}{b:02X}"
|
| 174 |
|
| 175 |
-
def compress_tile(tile_rgb):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
h, w, _ = tile_rgb.shape
|
| 177 |
lines = []
|
| 178 |
for y in range(h):
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
return "\n".join(lines)
|
| 198 |
|
| 199 |
# ----------------------------------------------------------------
|
|
|
|
| 200 |
@app.get("/", response_class=HTMLResponse)
|
| 201 |
def home(nick: str = "Unknown"):
|
| 202 |
return f"""
|
|
@@ -279,31 +298,31 @@ def force_stop(key: str):
|
|
| 279 |
# ----------------------------------------------------------------
|
| 280 |
@app.post("/frame_upload")
|
| 281 |
async def frame_upload(file: UploadFile):
|
| 282 |
-
global
|
| 283 |
data = await file.read()
|
| 284 |
nparr = np.frombuffer(data, np.uint8)
|
| 285 |
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 286 |
frame = cv2.resize(frame, (DISP_W, DISP_H))
|
| 287 |
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 288 |
|
| 289 |
-
|
| 290 |
for row in range(ROWS):
|
| 291 |
for col in range(COLS):
|
| 292 |
y1 = row * PART_H
|
| 293 |
y2 = y1 + PART_H
|
| 294 |
x1 = col * PART_W
|
| 295 |
x2 = x1 + PART_W
|
| 296 |
-
tile = frame_rgb[y1:y2, x1:x2]
|
| 297 |
-
|
| 298 |
-
new_parts[(row, col)] = compressed
|
| 299 |
|
| 300 |
-
|
|
|
|
| 301 |
|
| 302 |
async with text_lock:
|
| 303 |
-
|
| 304 |
full_text = full_compressed
|
| 305 |
|
| 306 |
-
return {"ok": True, "parts": len(
|
| 307 |
|
| 308 |
@app.get("/info")
|
| 309 |
async def get_info():
|
|
@@ -318,12 +337,22 @@ async def get_info():
|
|
| 318 |
}
|
| 319 |
|
| 320 |
@app.get("/pixels_part")
|
| 321 |
-
async def get_pixels_part(row: int, col: int, format: str = "text"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
async with text_lock:
|
| 323 |
-
|
| 324 |
-
if
|
| 325 |
return JSONResponse({"error": "Part not found"}, status_code=404)
|
| 326 |
|
|
|
|
|
|
|
|
|
|
| 327 |
if format == "json":
|
| 328 |
return {
|
| 329 |
"row": row,
|
|
@@ -336,6 +365,7 @@ async def get_pixels_part(row: int, col: int, format: str = "text"):
|
|
| 336 |
|
| 337 |
@app.get("/pixels_full")
|
| 338 |
async def get_pixels_full():
|
|
|
|
| 339 |
async with text_lock:
|
| 340 |
if full_text is None:
|
| 341 |
return JSONResponse({"error": "Нет кадра"}, status_code=404)
|
|
@@ -357,6 +387,7 @@ async def get_pixels_full():
|
|
| 357 |
|
| 358 |
|
| 359 |
|
|
|
|
| 360 |
# =========================
|
| 361 |
# 🚀 RUN
|
| 362 |
# =========================
|
|
|
|
| 152 |
|
| 153 |
app = FastAPI()
|
| 154 |
|
| 155 |
+
# Размеры дисплея
|
| 156 |
DISP_W = 96
|
| 157 |
DISP_H = 96
|
| 158 |
+
|
| 159 |
+
# Разбиение на 4 части (2x2)
|
| 160 |
+
GRID_SIZE = 2
|
| 161 |
+
PART_W = DISP_W // GRID_SIZE # 48
|
| 162 |
+
PART_H = DISP_H // GRID_SIZE # 48
|
| 163 |
ROWS = GRID_SIZE
|
| 164 |
COLS = GRID_SIZE
|
| 165 |
|
|
|
|
| 167 |
ADMIN_KEY = "supersecret123"
|
| 168 |
current_streamer = None
|
| 169 |
|
| 170 |
+
# Хранилище: для каждой части сохраняем сырой numpy-массив (48,48,3)
|
| 171 |
+
parts_raw = {}
|
| 172 |
+
full_text = None # весь кадр (опционально)
|
| 173 |
text_lock = asyncio.Lock()
|
| 174 |
|
| 175 |
# ----------------------------------------------------------------
|
| 176 |
def rgb_to_hex_amp(r, g, b):
|
| 177 |
+
"""RGB (0-255) → &#RRGGBB"""
|
| 178 |
return f"&#{r:02X}{g:02X}{b:02X}"
|
| 179 |
|
| 180 |
+
def compress_tile(tile_rgb, use_rle=False):
|
| 181 |
+
"""
|
| 182 |
+
Преобразует прямоугольный фрагмент в строку.
|
| 183 |
+
use_rle=False → каждый пиксель отдельно (фиксированная длина строки).
|
| 184 |
+
use_rle=True → сжатие RLE (длина плавает).
|
| 185 |
+
"""
|
| 186 |
h, w, _ = tile_rgb.shape
|
| 187 |
lines = []
|
| 188 |
for y in range(h):
|
| 189 |
+
if not use_rle:
|
| 190 |
+
# Каждый пиксель: &#RRGGBB + символ, ровно 10 символов
|
| 191 |
+
row_str = "".join(
|
| 192 |
+
rgb_to_hex_amp(tile_rgb[y, x, 0], tile_rgb[y, x, 1], tile_rgb[y, x, 2]) + PIXEL_CHAR
|
| 193 |
+
for x in range(w)
|
| 194 |
+
)
|
| 195 |
+
lines.append(row_str)
|
| 196 |
+
else:
|
| 197 |
+
parts_line = []
|
| 198 |
+
prev_color = None
|
| 199 |
+
run = 0
|
| 200 |
+
for x in range(w):
|
| 201 |
+
r, g, b = tile_rgb[y, x]
|
| 202 |
+
current = (r, g, b)
|
| 203 |
+
if current == prev_color:
|
| 204 |
+
run += 1
|
| 205 |
+
else:
|
| 206 |
+
if prev_color is not None and run > 0:
|
| 207 |
+
parts_line.append(rgb_to_hex_amp(*prev_color))
|
| 208 |
+
parts_line.append(PIXEL_CHAR * run)
|
| 209 |
+
prev_color = current
|
| 210 |
+
run = 1
|
| 211 |
+
if prev_color is not None and run > 0:
|
| 212 |
+
parts_line.append(rgb_to_hex_amp(*prev_color))
|
| 213 |
+
parts_line.append(PIXEL_CHAR * run)
|
| 214 |
+
lines.append("".join(parts_line))
|
| 215 |
return "\n".join(lines)
|
| 216 |
|
| 217 |
# ----------------------------------------------------------------
|
| 218 |
+
# HTML-интерфейс стримера (без изменений)
|
| 219 |
@app.get("/", response_class=HTMLResponse)
|
| 220 |
def home(nick: str = "Unknown"):
|
| 221 |
return f"""
|
|
|
|
| 298 |
# ----------------------------------------------------------------
|
| 299 |
@app.post("/frame_upload")
|
| 300 |
async def frame_upload(file: UploadFile):
|
| 301 |
+
global parts_raw, full_text
|
| 302 |
data = await file.read()
|
| 303 |
nparr = np.frombuffer(data, np.uint8)
|
| 304 |
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 305 |
frame = cv2.resize(frame, (DISP_W, DISP_H))
|
| 306 |
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 307 |
|
| 308 |
+
new_parts_raw = {}
|
| 309 |
for row in range(ROWS):
|
| 310 |
for col in range(COLS):
|
| 311 |
y1 = row * PART_H
|
| 312 |
y2 = y1 + PART_H
|
| 313 |
x1 = col * PART_W
|
| 314 |
x2 = x1 + PART_W
|
| 315 |
+
tile = frame_rgb[y1:y2, x1:x2].copy()
|
| 316 |
+
new_parts_raw[(row, col)] = tile
|
|
|
|
| 317 |
|
| 318 |
+
# Полный кадр сохраняем в сжатом виде (для /pixels_full)
|
| 319 |
+
full_compressed = compress_tile(frame_rgb, use_rle=False)
|
| 320 |
|
| 321 |
async with text_lock:
|
| 322 |
+
parts_raw = new_parts_raw
|
| 323 |
full_text = full_compressed
|
| 324 |
|
| 325 |
+
return {"ok": True, "parts": len(new_parts_raw), "full_length": len(full_compressed)}
|
| 326 |
|
| 327 |
@app.get("/info")
|
| 328 |
async def get_info():
|
|
|
|
| 337 |
}
|
| 338 |
|
| 339 |
@app.get("/pixels_part")
|
| 340 |
+
async def get_pixels_part(row: int, col: int, rle: int = 0, format: str = "text"):
|
| 341 |
+
"""
|
| 342 |
+
Возвращает часть изображения.
|
| 343 |
+
rle=0 (по умолчанию) – фиксированная длина строки, без сжатия.
|
| 344 |
+
rle=1 – RLE-сжатие (длина строки плавает).
|
| 345 |
+
format=json – JSON с длиной и текстом.
|
| 346 |
+
format=text (по умолчанию) – чистый текст.
|
| 347 |
+
"""
|
| 348 |
async with text_lock:
|
| 349 |
+
tile = parts_raw.get((row, col))
|
| 350 |
+
if tile is None:
|
| 351 |
return JSONResponse({"error": "Part not found"}, status_code=404)
|
| 352 |
|
| 353 |
+
# Кодируем на лету в зависимости от rle
|
| 354 |
+
text = compress_tile(tile, use_rle=bool(rle))
|
| 355 |
+
|
| 356 |
if format == "json":
|
| 357 |
return {
|
| 358 |
"row": row,
|
|
|
|
| 365 |
|
| 366 |
@app.get("/pixels_full")
|
| 367 |
async def get_pixels_full():
|
| 368 |
+
"""Весь кадр одной строкой (может быть большим, используй с осторожностью)"""
|
| 369 |
async with text_lock:
|
| 370 |
if full_text is None:
|
| 371 |
return JSONResponse({"error": "Нет кадра"}, status_code=404)
|
|
|
|
| 387 |
|
| 388 |
|
| 389 |
|
| 390 |
+
|
| 391 |
# =========================
|
| 392 |
# 🚀 RUN
|
| 393 |
# =========================
|