PLUS: SSE chunked fix
Browse files- plus/sse_proxy.py +54 -34
plus/sse_proxy.py
CHANGED
|
@@ -314,14 +314,17 @@ class Proxy(BaseHTTPRequestHandler):
|
|
| 314 |
stream = content_type.startswith("text/event-stream")
|
| 315 |
for key, value in answer.getheaders():
|
| 316 |
low = key.lower()
|
| 317 |
-
if low in HOP_HEADERS or
|
|
|
|
|
|
|
| 318 |
continue
|
| 319 |
self.send_header(key, value)
|
| 320 |
if stream:
|
| 321 |
-
self.
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
|
|
|
| 325 |
else:
|
| 326 |
data = answer.read()
|
| 327 |
content_type = (answer.getheader("Content-Type") or "").lower()
|
|
@@ -334,12 +337,41 @@ class Proxy(BaseHTTPRequestHandler):
|
|
| 334 |
and data[:4] == b"MThd"):
|
| 335 |
data, stats = apply_velocity(self.last_audio, data)
|
| 336 |
print("velocity: %s" % stats, flush=True)
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
self.end_headers()
|
| 339 |
-
if method != "HEAD":
|
| 340 |
self.wfile.write(data)
|
| 341 |
upstream.close()
|
| 342 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
def refine(self, notes: list[dict], skip_filter: bool = False) -> list[dict]:
|
| 344 |
"""Пересчитать концы нот и артикуляцию по записи."""
|
| 345 |
if not notes or not self.last_audio:
|
|
@@ -416,34 +448,26 @@ class Proxy(BaseHTTPRequestHandler):
|
|
| 416 |
threading.Thread(target=worker, daemon=True).start()
|
| 417 |
self.send_response(200)
|
| 418 |
self.send_header("Content-Type", "text/event-stream")
|
| 419 |
-
self.send_header("Cache-Control", "no-cache")
|
| 420 |
-
self.send_header("X-Accel-Buffering", "no")
|
| 421 |
-
self.end_headers()
|
| 422 |
try:
|
| 423 |
-
self.
|
| 424 |
-
self.wfile.flush()
|
| 425 |
except Exception:
|
| 426 |
return
|
| 427 |
payload = None
|
| 428 |
-
while True:
|
| 429 |
-
try:
|
| 430 |
-
payload = outcome.get(timeout=self.ping_seconds)
|
| 431 |
-
break
|
| 432 |
-
except queue.Empty:
|
| 433 |
-
try:
|
| 434 |
-
self.wfile.write(b": ping\n\n")
|
| 435 |
-
self.wfile.flush()
|
| 436 |
-
except Exception:
|
| 437 |
-
return
|
| 438 |
-
if payload is None:
|
| 439 |
-
payload = fusion.to_sse([])
|
| 440 |
try:
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
except Exception:
|
| 445 |
pass
|
| 446 |
-
|
|
|
|
| 447 |
|
| 448 |
def stream_events(self, answer):
|
| 449 |
"""Переливаем SSE, вставляя ": ping" во время молчания модели."""
|
|
@@ -462,23 +486,19 @@ class Proxy(BaseHTTPRequestHandler):
|
|
| 462 |
lines.put(None)
|
| 463 |
|
| 464 |
threading.Thread(target=pump, daemon=True).start()
|
| 465 |
-
self.wfile.write(b": open\n\n")
|
| 466 |
-
self.wfile.flush()
|
| 467 |
while True:
|
| 468 |
try:
|
| 469 |
line = lines.get(timeout=PING_SECONDS)
|
| 470 |
except queue.Empty:
|
| 471 |
try:
|
| 472 |
-
self.
|
| 473 |
-
self.wfile.flush()
|
| 474 |
except Exception:
|
| 475 |
return
|
| 476 |
continue
|
| 477 |
if line is None:
|
| 478 |
return
|
| 479 |
try:
|
| 480 |
-
self.
|
| 481 |
-
self.wfile.flush()
|
| 482 |
except Exception:
|
| 483 |
return
|
| 484 |
|
|
|
|
| 314 |
stream = content_type.startswith("text/event-stream")
|
| 315 |
for key, value in answer.getheaders():
|
| 316 |
low = key.lower()
|
| 317 |
+
if low in HOP_HEADERS or low in ("server", "date"):
|
| 318 |
+
continue
|
| 319 |
+
if stream and low in ("cache-control", "x-accel-buffering"):
|
| 320 |
continue
|
| 321 |
self.send_header(key, value)
|
| 322 |
if stream:
|
| 323 |
+
self.begin_stream()
|
| 324 |
+
try:
|
| 325 |
+
self.stream_events(answer)
|
| 326 |
+
finally:
|
| 327 |
+
self.end_stream()
|
| 328 |
else:
|
| 329 |
data = answer.read()
|
| 330 |
content_type = (answer.getheader("Content-Type") or "").lower()
|
|
|
|
| 337 |
and data[:4] == b"MThd"):
|
| 338 |
data, stats = apply_velocity(self.last_audio, data)
|
| 339 |
print("velocity: %s" % stats, flush=True)
|
| 340 |
+
if method == "HEAD":
|
| 341 |
+
length = answer.getheader("Content-Length")
|
| 342 |
+
self.send_header("Content-Length", length if length is not None else "0")
|
| 343 |
+
elif answer.status not in (204, 304) and answer.status >= 200:
|
| 344 |
+
self.send_header("Content-Length", str(len(data)))
|
| 345 |
self.end_headers()
|
| 346 |
+
if method != "HEAD" and answer.status not in (204, 304):
|
| 347 |
self.wfile.write(data)
|
| 348 |
upstream.close()
|
| 349 |
|
| 350 |
+
def begin_stream(self) -> None:
|
| 351 |
+
"""Заголовки SSE: chunked + Connection: close, чтобы туннель (HTTP/2)
|
| 352 |
+
видел конец ответа и не рвал поток с ERR_HTTP2_PROTOCOL_ERROR."""
|
| 353 |
+
self.send_header("Cache-Control", "no-cache, no-transform")
|
| 354 |
+
self.send_header("X-Accel-Buffering", "no")
|
| 355 |
+
self.send_header("Transfer-Encoding", "chunked")
|
| 356 |
+
self.send_header("Connection", "close")
|
| 357 |
+
self.end_headers()
|
| 358 |
+
self.close_connection = True
|
| 359 |
+
self.emit(b": open\n\n")
|
| 360 |
+
|
| 361 |
+
def emit(self, data: bytes) -> None:
|
| 362 |
+
if not data:
|
| 363 |
+
return
|
| 364 |
+
self.wfile.write(b"%x\r\n" % len(data) + data + b"\r\n")
|
| 365 |
+
self.wfile.flush()
|
| 366 |
+
|
| 367 |
+
def end_stream(self) -> None:
|
| 368 |
+
self.close_connection = True
|
| 369 |
+
try:
|
| 370 |
+
self.wfile.write(b"0\r\n\r\n")
|
| 371 |
+
self.wfile.flush()
|
| 372 |
+
except Exception:
|
| 373 |
+
pass
|
| 374 |
+
|
| 375 |
def refine(self, notes: list[dict], skip_filter: bool = False) -> list[dict]:
|
| 376 |
"""Пересчитать концы нот и артикуляцию по записи."""
|
| 377 |
if not notes or not self.last_audio:
|
|
|
|
| 448 |
threading.Thread(target=worker, daemon=True).start()
|
| 449 |
self.send_response(200)
|
| 450 |
self.send_header("Content-Type", "text/event-stream")
|
|
|
|
|
|
|
|
|
|
| 451 |
try:
|
| 452 |
+
self.begin_stream()
|
|
|
|
| 453 |
except Exception:
|
| 454 |
return
|
| 455 |
payload = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
try:
|
| 457 |
+
while True:
|
| 458 |
+
try:
|
| 459 |
+
payload = outcome.get(timeout=self.ping_seconds)
|
| 460 |
+
break
|
| 461 |
+
except queue.Empty:
|
| 462 |
+
self.emit(b": ping\n\n")
|
| 463 |
+
if payload is None:
|
| 464 |
+
payload = fusion.to_sse([])
|
| 465 |
+
self.emit(payload)
|
| 466 |
+
self.emit(b": done\n\n")
|
| 467 |
except Exception:
|
| 468 |
pass
|
| 469 |
+
finally:
|
| 470 |
+
self.end_stream()
|
| 471 |
|
| 472 |
def stream_events(self, answer):
|
| 473 |
"""Переливаем SSE, вставляя ": ping" во время молчания модели."""
|
|
|
|
| 486 |
lines.put(None)
|
| 487 |
|
| 488 |
threading.Thread(target=pump, daemon=True).start()
|
|
|
|
|
|
|
| 489 |
while True:
|
| 490 |
try:
|
| 491 |
line = lines.get(timeout=PING_SECONDS)
|
| 492 |
except queue.Empty:
|
| 493 |
try:
|
| 494 |
+
self.emit(b": ping\n\n")
|
|
|
|
| 495 |
except Exception:
|
| 496 |
return
|
| 497 |
continue
|
| 498 |
if line is None:
|
| 499 |
return
|
| 500 |
try:
|
| 501 |
+
self.emit(line)
|
|
|
|
| 502 |
except Exception:
|
| 503 |
return
|
| 504 |
|