Spaces:
Running
Running
feat: fST external API istemci için AI music detection eklendi
Browse files- 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 99 |
-
return FSTResult(
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
)
|
| 103 |
|
|
|
|
| 104 |
try:
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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()
|