Rthur2003 commited on
Commit
61387f5
·
1 Parent(s): 843c1f9

feat: fST external API istemci için AI music detection eklendi

Browse files
Files changed (1) hide show
  1. app/services/fst_client.py +39 -14
app/services/fst_client.py CHANGED
@@ -13,8 +13,11 @@ Gracefully returns unavailable result on timeout or error.
13
 
14
  from __future__ import annotations
15
 
 
16
  import io
 
17
  import tempfile
 
18
  from dataclasses import dataclass
19
  from pathlib import Path
20
  from typing import Optional, Union
@@ -29,7 +32,13 @@ FST_API_URL = f"https://{FST_SPACE_ID.replace('/', '-')}.hf.space"
29
 
30
  # Timeouts
31
  FST_CONNECT_TIMEOUT = 10.0 # seconds
32
- FST_PREDICT_TIMEOUT = 120.0 # seconds (model inference can be slow)
 
 
 
 
 
 
33
 
34
 
35
  @dataclass
@@ -56,9 +65,13 @@ class FSTClientService:
56
  def __init__(self) -> None:
57
  self._client = None
58
  self._available: Optional[bool] = None
 
 
59
 
60
  def _ensure_client(self) -> bool:
61
- """Lazy-initialize Gradio client."""
 
 
62
  if self._available is not None:
63
  return self._available
64
 
@@ -76,15 +89,18 @@ class FSTClientService:
76
  "gradio_client not installed — FST layer disabled"
77
  )
78
  self._available = False
 
79
  return False
80
  except Exception as e:
81
  logger.warning(f"FST client init failed: {e}")
82
  self._available = False
 
83
  return False
84
 
85
  async def predict(
86
  self,
87
  source: Union[Path, bytes, io.BytesIO],
 
88
  ) -> FSTResult:
89
  """
90
  Submit audio to FST Space for AI detection.
@@ -95,29 +111,37 @@ class FSTClientService:
95
  Returns:
96
  FSTResult with detection outcome.
97
  """
98
- if not self._ensure_client():
99
- return FSTResult(
100
- available=False,
101
- error="fst_client_unavailable",
102
- )
103
 
 
104
  try:
105
- audio_path = self._to_file_path(source)
106
- return await self._call_api(audio_path)
 
 
 
 
 
 
 
107
  except Exception as e:
108
  logger.warning(f"FST prediction failed: {e}")
109
  return FSTResult(
110
  available=False,
111
  error=str(e),
112
  )
 
 
 
113
 
114
  async def _call_api(self, audio_path: Path) -> FSTResult:
115
  """Call the FST Gradio API."""
116
- import asyncio
117
-
118
  try:
119
  # Run synchronous Gradio client in executor
120
- loop = asyncio.get_event_loop()
121
  result = await asyncio.wait_for(
122
  loop.run_in_executor(
123
  None,
@@ -251,14 +275,15 @@ class FSTClientService:
251
  @staticmethod
252
  def _to_file_path(
253
  source: Union[Path, bytes, io.BytesIO],
 
254
  ) -> Path:
255
- """Convert source to a file path for Gradio upload."""
256
  if isinstance(source, Path):
257
  return source
258
  if isinstance(source, bytes):
259
  source = io.BytesIO(source)
260
  tmp = tempfile.NamedTemporaryFile(
261
- suffix=".wav", delete=False,
262
  )
263
  tmp.write(source.read())
264
  tmp.flush()
 
13
 
14
  from __future__ import annotations
15
 
16
+ import asyncio
17
  import io
18
+ import os
19
  import tempfile
20
+ import time
21
  from dataclasses import dataclass
22
  from pathlib import Path
23
  from typing import Optional, Union
 
32
 
33
  # Timeouts
34
  FST_CONNECT_TIMEOUT = 10.0 # seconds
35
+ FST_PREDICT_TIMEOUT = float(os.getenv("FST_PREDICT_TIMEOUT", "60"))
36
+ # After a failed connect, try again this much later instead of never.
37
+ FST_RETRY_AFTER = 300.0
38
+ # Calls in flight at once; a timed-out call keeps its thread until the
39
+ # remote answers, so this also bounds how many threads can pile up.
40
+ FST_MAX_CONCURRENT = 2
41
+ FST_ENABLED = os.getenv("FST_ENABLED", "1") != "0"
42
 
43
 
44
  @dataclass
 
65
  def __init__(self) -> None:
66
  self._client = None
67
  self._available: Optional[bool] = None
68
+ self._failed_at = 0.0
69
+ self._gate: Optional[asyncio.Semaphore] = None
70
 
71
  def _ensure_client(self) -> bool:
72
+ """Lazy-initialize Gradio client (blocking: call from a thread)."""
73
+ if self._available is False and time.monotonic() - self._failed_at > FST_RETRY_AFTER:
74
+ self._available = None
75
  if self._available is not None:
76
  return self._available
77
 
 
89
  "gradio_client not installed — FST layer disabled"
90
  )
91
  self._available = False
92
+ self._failed_at = float("inf")
93
  return False
94
  except Exception as e:
95
  logger.warning(f"FST client init failed: {e}")
96
  self._available = False
97
+ self._failed_at = time.monotonic()
98
  return False
99
 
100
  async def predict(
101
  self,
102
  source: Union[Path, bytes, io.BytesIO],
103
+ suffix: str = ".wav",
104
  ) -> FSTResult:
105
  """
106
  Submit audio to FST Space for AI detection.
 
111
  Returns:
112
  FSTResult with detection outcome.
113
  """
114
+ if not FST_ENABLED:
115
+ return FSTResult(available=False, error="fst_disabled")
116
+ if self._gate is None:
117
+ self._gate = asyncio.Semaphore(FST_MAX_CONCURRENT)
 
118
 
119
+ audio_path: Optional[Path] = None
120
  try:
121
+ async with self._gate:
122
+ # Connecting fetches the remote config over the network.
123
+ if not await asyncio.to_thread(self._ensure_client):
124
+ return FSTResult(
125
+ available=False,
126
+ error="fst_client_unavailable",
127
+ )
128
+ audio_path = self._to_file_path(source, suffix)
129
+ return await self._call_api(audio_path)
130
  except Exception as e:
131
  logger.warning(f"FST prediction failed: {e}")
132
  return FSTResult(
133
  available=False,
134
  error=str(e),
135
  )
136
+ finally:
137
+ if audio_path is not None and not isinstance(source, Path):
138
+ audio_path.unlink(missing_ok=True)
139
 
140
  async def _call_api(self, audio_path: Path) -> FSTResult:
141
  """Call the FST Gradio API."""
 
 
142
  try:
143
  # Run synchronous Gradio client in executor
144
+ loop = asyncio.get_running_loop()
145
  result = await asyncio.wait_for(
146
  loop.run_in_executor(
147
  None,
 
275
  @staticmethod
276
  def _to_file_path(
277
  source: Union[Path, bytes, io.BytesIO],
278
+ suffix: str = ".wav",
279
  ) -> Path:
280
+ """Convert source to a file path for Gradio upload (caller deletes it)."""
281
  if isinstance(source, Path):
282
  return source
283
  if isinstance(source, bytes):
284
  source = io.BytesIO(source)
285
  tmp = tempfile.NamedTemporaryFile(
286
+ suffix=suffix or ".wav", delete=False,
287
  )
288
  tmp.write(source.read())
289
  tmp.flush()