basyx commited on
Commit
19b1d4a
·
verified ·
1 Parent(s): 03f113c

Harden remote diarization adapter

Browse files
Files changed (1) hide show
  1. app/services/diarization_service.py +35 -17
app/services/diarization_service.py CHANGED
@@ -5,20 +5,17 @@ import math
5
  import os
6
  from pathlib import Path
7
  from typing import Any
 
8
 
9
  import httpx
10
  from pydantic import ValidationError
11
 
12
- from app.core.config import Settings
13
  from app.core.exceptions import InputError, ProcessingError
14
  from app.models.transcript import DiarizationSpan
15
 
16
 
17
  class DiarizationService:
18
- """Optional provider-neutral HTTP adapter for speaker diarization."""
19
-
20
- def __init__(self, settings: Settings) -> None:
21
- self.settings = settings
22
 
23
  @property
24
  def url(self) -> str:
@@ -29,10 +26,7 @@ class DiarizationService:
29
  return bool(self.url)
30
 
31
  async def diarize(self, path: Path, mime_type: str, filename: str) -> list[dict[str, Any]]:
32
- if not self.is_configured:
33
- raise ProcessingError(
34
- "Diarization is enabled but no DIARIZATION_URL is configured"
35
- )
36
  if not path.is_file():
37
  raise ProcessingError("Diarization input file is unavailable")
38
 
@@ -52,10 +46,13 @@ class DiarizationService:
52
  token = os.getenv("DIARIZATION_API_KEY", "").strip()
53
  if token:
54
  headers["Authorization"] = f"Bearer {token}"
 
 
55
 
56
  connect_timeout = self._float_env("DIARIZATION_CONNECT_TIMEOUT_SECONDS", 10.0, 0.1)
57
  timeout_seconds = self._float_env("DIARIZATION_TIMEOUT_SECONDS", 300.0, 1.0)
58
  max_response = self._int_env("DIARIZATION_MAX_RESPONSE_BYTES", 1_048_576, 1024)
 
59
 
60
  timeout = httpx.Timeout(
61
  connect=connect_timeout,
@@ -67,18 +64,12 @@ class DiarizationService:
67
  try:
68
  async with httpx.AsyncClient(timeout=timeout, follow_redirects=False) as client:
69
  with path.open("rb") as handle:
70
- response = await client.post(
71
- self.url,
72
- content=handle,
73
- headers=headers,
74
- )
75
  except (OSError, httpx.HTTPError) as exc:
76
  raise ProcessingError("Diarization backend request failed") from exc
77
 
78
  if response.status_code >= 400:
79
- raise ProcessingError(
80
- f"Diarization backend returned HTTP {response.status_code}"
81
- )
82
  if len(response.content) > max_response:
83
  raise ProcessingError("Diarization backend response exceeds the configured limit")
84
 
@@ -91,6 +82,8 @@ class DiarizationService:
91
  payload = payload.get("spans", payload.get("segments"))
92
  if not isinstance(payload, list):
93
  raise ProcessingError("Diarization backend must return a span array")
 
 
94
 
95
  normalized: list[dict[str, Any]] = []
96
  previous_start = -1.0
@@ -110,6 +103,24 @@ class DiarizationService:
110
 
111
  return normalized
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  @staticmethod
114
  def _int_env(name: str, default: int, minimum: int) -> int:
115
  raw = os.getenv(name, "")
@@ -135,3 +146,10 @@ class DiarizationService:
135
  if not math.isfinite(value) or value < minimum:
136
  raise ProcessingError(f"{name} is below its minimum allowed value")
137
  return value
 
 
 
 
 
 
 
 
5
  import os
6
  from pathlib import Path
7
  from typing import Any
8
+ from urllib.parse import urlparse
9
 
10
  import httpx
11
  from pydantic import ValidationError
12
 
 
13
  from app.core.exceptions import InputError, ProcessingError
14
  from app.models.transcript import DiarizationSpan
15
 
16
 
17
  class DiarizationService:
18
+ """Optional authenticated HTTP adapter for speaker diarization."""
 
 
 
19
 
20
  @property
21
  def url(self) -> str:
 
26
  return bool(self.url)
27
 
28
  async def diarize(self, path: Path, mime_type: str, filename: str) -> list[dict[str, Any]]:
29
+ url = self._validated_url()
 
 
 
30
  if not path.is_file():
31
  raise ProcessingError("Diarization input file is unavailable")
32
 
 
46
  token = os.getenv("DIARIZATION_API_KEY", "").strip()
47
  if token:
48
  headers["Authorization"] = f"Bearer {token}"
49
+ elif self._bool_env("DIARIZATION_REQUIRE_AUTH", True):
50
+ raise ProcessingError("Diarization backend authentication is not configured")
51
 
52
  connect_timeout = self._float_env("DIARIZATION_CONNECT_TIMEOUT_SECONDS", 10.0, 0.1)
53
  timeout_seconds = self._float_env("DIARIZATION_TIMEOUT_SECONDS", 300.0, 1.0)
54
  max_response = self._int_env("DIARIZATION_MAX_RESPONSE_BYTES", 1_048_576, 1024)
55
+ max_spans = self._int_env("DIARIZATION_MAX_SPANS", 10_000, 1)
56
 
57
  timeout = httpx.Timeout(
58
  connect=connect_timeout,
 
64
  try:
65
  async with httpx.AsyncClient(timeout=timeout, follow_redirects=False) as client:
66
  with path.open("rb") as handle:
67
+ response = await client.post(url, content=handle, headers=headers)
 
 
 
 
68
  except (OSError, httpx.HTTPError) as exc:
69
  raise ProcessingError("Diarization backend request failed") from exc
70
 
71
  if response.status_code >= 400:
72
+ raise ProcessingError(f"Diarization backend returned HTTP {response.status_code}")
 
 
73
  if len(response.content) > max_response:
74
  raise ProcessingError("Diarization backend response exceeds the configured limit")
75
 
 
82
  payload = payload.get("spans", payload.get("segments"))
83
  if not isinstance(payload, list):
84
  raise ProcessingError("Diarization backend must return a span array")
85
+ if len(payload) > max_spans:
86
+ raise ProcessingError("Diarization backend returned too many spans")
87
 
88
  normalized: list[dict[str, Any]] = []
89
  previous_start = -1.0
 
103
 
104
  return normalized
105
 
106
+ def _validated_url(self) -> str:
107
+ url = self.url
108
+ if not url:
109
+ raise ProcessingError(
110
+ "Diarization is enabled but no DIARIZATION_URL is configured"
111
+ )
112
+ parsed = urlparse(url)
113
+ if parsed.scheme == "https" and parsed.hostname:
114
+ return url
115
+ environment = os.getenv("APP_ENVIRONMENT", "development").strip().lower()
116
+ if (
117
+ environment in {"development", "test"}
118
+ and parsed.scheme == "http"
119
+ and parsed.hostname in {"localhost", "127.0.0.1", "::1"}
120
+ ):
121
+ return url
122
+ raise ProcessingError("DIARIZATION_URL must use HTTPS")
123
+
124
  @staticmethod
125
  def _int_env(name: str, default: int, minimum: int) -> int:
126
  raw = os.getenv(name, "")
 
146
  if not math.isfinite(value) or value < minimum:
147
  raise ProcessingError(f"{name} is below its minimum allowed value")
148
  return value
149
+
150
+ @staticmethod
151
+ def _bool_env(name: str, default: bool) -> bool:
152
+ raw = os.getenv(name, "")
153
+ if not raw:
154
+ return default
155
+ return raw.strip().lower() in {"1", "true", "yes", "on"}