dedlepexa commited on
Commit
54bfe01
·
verified ·
1 Parent(s): f8b3e0a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -35
app.py CHANGED
@@ -152,11 +152,14 @@ import asyncio
152
 
153
  app = FastAPI()
154
 
 
155
  DISP_W = 96
156
  DISP_H = 96
157
- GRID_SIZE = 2 # 4 части 48x48
158
- PART_W = DISP_W // GRID_SIZE
159
- PART_H = DISP_H // GRID_SIZE
 
 
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
- parts = {}
168
- full_text = None
 
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
- parts_line = []
180
- prev_color = None
181
- run = 0
182
- for x in range(w):
183
- r, g, b = tile_rgb[y, x]
184
- current = (r, g, b)
185
- if current == prev_color:
186
- run += 1
187
- else:
188
- if prev_color is not None and run > 0:
189
- parts_line.append(rgb_to_hex_amp(*prev_color))
190
- parts_line.append(PIXEL_CHAR * run)
191
- prev_color = current
192
- run = 1
193
- if prev_color is not None and run > 0:
194
- parts_line.append(rgb_to_hex_amp(*prev_color))
195
- parts_line.append(PIXEL_CHAR * run)
196
- lines.append("".join(parts_line))
 
 
 
 
 
 
 
 
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 parts, full_text
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
- new_parts = {}
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
- compressed = compress_tile(tile)
298
- new_parts[(row, col)] = compressed
299
 
300
- full_compressed = compress_tile(frame_rgb)
 
301
 
302
  async with text_lock:
303
- parts = new_parts
304
  full_text = full_compressed
305
 
306
- return {"ok": True, "parts": len(new_parts), "full_length": len(full_compressed)}
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
- text = parts.get((row, col))
324
- if text is None:
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
  # =========================