feat(asr-pipeline): merge PR #1 webauth/CSRF/locking hardening and web_password fallback
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user