feat(asr-pipeline): merge PR #1 webauth/CSRF/locking hardening and web_password fallback
Build image / build (push) Canceled after 0s
Build ASR Pipeline / image (push) Canceled after 0s

This commit is contained in:
Codex
2026-11-02 06:37:33 -08:00
parent 9debd1c47d
commit 3c70861463
44 changed files with 4354 additions and 333 deletions
@@ -409,28 +409,45 @@ def probe_duration_seconds(audio: Path) -> int:
return 0
def _encode_multipart(
def _multipart_framing(
fields: dict[str, str],
file_field: str,
file_path: Path,
) -> tuple[bytes, str]:
) -> tuple[bytes, bytes, str, int]:
"""Build multipart head/tail without reading the audio into memory."""
boundary = f"----ASRBoundary{uuid.uuid4().hex}"
body = bytearray()
safe_name = file_path.name.replace('"', "").replace("\r", "").replace("\n", "") or "audio"
head = bytearray()
for name, val in fields.items():
body.extend(f"--{boundary}\r\n".encode())
body.extend(
f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
body.extend(str(val).encode("utf-8"))
body.extend(b"\r\n")
mime = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
body.extend(f"--{boundary}\r\n".encode())
body.extend(
head.extend(f"--{boundary}\r\n".encode())
head.extend(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
head.extend(str(val).encode("utf-8"))
head.extend(b"\r\n")
mime = mimetypes.guess_type(safe_name)[0] or "application/octet-stream"
head.extend(f"--{boundary}\r\n".encode())
head.extend(
f'Content-Disposition: form-data; name="{file_field}"; '
f'filename="{file_path.name}"\r\n'.encode())
body.extend(f"Content-Type: {mime}\r\n\r\n".encode())
body.extend(file_path.read_bytes())
body.extend(f"\r\n--{boundary}--\r\n".encode())
return bytes(body), f"multipart/form-data; boundary={boundary}"
f'filename="{safe_name}"\r\n'.encode())
head.extend(f"Content-Type: {mime}\r\n\r\n".encode())
tail = f"\r\n--{boundary}--\r\n".encode()
total = len(head) + file_path.stat().st_size + len(tail)
return bytes(head), tail, f"multipart/form-data; boundary={boundary}", total
def _open_http(url: str, timeout: int) -> tuple[http.client.HTTPConnection, str]:
parsed = urllib.parse.urlsplit(url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise RuntimeError(f"unsupported ASR URL: {url}")
port = parsed.port or (443 if parsed.scheme == "https" else 80)
if parsed.scheme == "https":
conn: http.client.HTTPConnection = http.client.HTTPSConnection(
parsed.hostname, port, timeout=timeout)
else:
conn = http.client.HTTPConnection(parsed.hostname, port, timeout=timeout)
path = parsed.path or "/"
if parsed.query:
path = f"{path}?{parsed.query}"
return conn, path
def _http_multipart(
@@ -443,29 +460,48 @@ def _http_multipart(
timeout: int = 900,
retries: int = 3,
) -> dict[str, Any]:
data, content_type = _encode_multipart(fields, file_field, file_path)
req_headers = {"Content-Type": content_type, **(headers or {})}
"""POST audio as multipart, streaming the file in 1 MiB chunks."""
head, tail, content_type, total = _multipart_framing(fields, file_field, file_path)
last_exc: Exception | None = None
raw = ""
for attempt in range(1, max(1, retries) + 1):
req = urllib.request.Request(url, data=data, headers=req_headers, method="POST")
conn, path = _open_http(url, timeout)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="ignore")
break
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="ignore")[:300]
if exc.code in (502, 503, 504) and attempt < retries:
last_exc = exc
conn.putrequest("POST", path)
conn.putheader("Content-Type", content_type)
conn.putheader("Content-Length", str(total))
for key, value in (headers or {}).items():
conn.putheader(key, value)
conn.endheaders()
conn.send(head)
with file_path.open("rb") as stream:
while True:
chunk = stream.read(1024 * 1024)
if not chunk:
break
conn.send(chunk)
conn.send(tail)
resp = conn.getresponse()
raw = resp.read().decode("utf-8", errors="ignore")
status = resp.status
if status in (502, 503, 504) and attempt < retries:
last_exc = RuntimeError(f"ASR HTTP {status}")
time.sleep(2 * attempt)
continue
raise RuntimeError(f"ASR HTTP {exc.code}: {detail}") from exc
except (urllib.error.URLError, http.client.HTTPException, ConnectionError, TimeoutError, OSError) as exc:
if status >= 400:
raise RuntimeError(f"ASR HTTP {status}: {raw[:300]}")
break
except RuntimeError:
raise
except (http.client.HTTPException, ConnectionError, TimeoutError, OSError) as exc:
last_exc = exc
if attempt < retries:
log(f"multipart upload transient error (attempt {attempt}/{retries}): {exc}")
time.sleep(2 * attempt)
continue
raise RuntimeError(f"ASR upload connection error after {retries} attempts: {exc}") from exc
finally:
conn.close()
else:
raise RuntimeError(f"ASR upload failed: {last_exc}")
try: