wip: snapshot full local workspace state
Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
This commit is contained in:
@@ -1,104 +1,104 @@
|
||||
"""ElevenLabs TTS 语音合成适配器。"""
|
||||
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.services.adapters.base import AdapterConfig, BaseAdapter
|
||||
from app.services.adapters.registry import AdapterRegistry
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
ELEVENLABS_API_BASE = "https://api.elevenlabs.io/v1"
|
||||
DEFAULT_VOICE_ID = "21m00Tcm4TlvDq8ikWAM" # Rachel
|
||||
|
||||
|
||||
@AdapterRegistry.register("tts", "elevenlabs")
|
||||
class ElevenLabsTtsAdapter(BaseAdapter[bytes]):
|
||||
"""ElevenLabs TTS 语音合成适配器,返回 MP3 bytes。"""
|
||||
|
||||
adapter_type = "tts"
|
||||
adapter_name = "elevenlabs"
|
||||
|
||||
def __init__(self, config: AdapterConfig):
|
||||
super().__init__(config)
|
||||
self.api_base = config.api_base or ELEVENLABS_API_BASE
|
||||
|
||||
async def execute(self, text: str, **kwargs) -> bytes:
|
||||
"""将文本转换为语音 MP3 bytes。"""
|
||||
start_time = time.time()
|
||||
logger.info("elevenlabs_tts_start", text_length=len(text))
|
||||
|
||||
voice_id = kwargs.get("voice_id") or DEFAULT_VOICE_ID
|
||||
model_id = kwargs.get("model") or self.config.model or "eleven_multilingual_v2"
|
||||
stability = kwargs.get("stability", 0.5)
|
||||
similarity_boost = kwargs.get("similarity_boost", 0.75)
|
||||
|
||||
url = f"{self.api_base}/text-to-speech/{voice_id}"
|
||||
|
||||
payload = {
|
||||
"text": text,
|
||||
"model_id": model_id,
|
||||
"voice_settings": {
|
||||
"stability": stability,
|
||||
"similarity_boost": similarity_boost,
|
||||
},
|
||||
}
|
||||
|
||||
audio_bytes = await self._call_api(url, payload)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(
|
||||
"elevenlabs_tts_success",
|
||||
elapsed_seconds=round(elapsed, 2),
|
||||
audio_size_bytes=len(audio_bytes),
|
||||
)
|
||||
|
||||
return audio_bytes
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""检查 ElevenLabs API 是否可用。"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.get(
|
||||
f"{self.api_base}/voices",
|
||||
headers={"xi-api-key": self.config.api_key},
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@property
|
||||
def estimated_cost(self) -> float:
|
||||
"""预估每千字符成本 (USD)。"""
|
||||
return 0.03
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=10),
|
||||
retry=retry_if_exception_type((httpx.HTTPError, httpx.TimeoutException)),
|
||||
reraise=True,
|
||||
)
|
||||
async def _call_api(self, url: str, payload: dict) -> bytes:
|
||||
"""调用 ElevenLabs API,带重试机制。"""
|
||||
timeout = self.config.timeout_ms / 1000
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={
|
||||
"xi-api-key": self.config.api_key,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "audio/mpeg",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
"""ElevenLabs TTS 语音合成适配器。"""
|
||||
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.services.adapters.base import AdapterConfig, BaseAdapter
|
||||
from app.services.adapters.registry import AdapterRegistry
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
ELEVENLABS_API_BASE = "https://api.elevenlabs.io/v1"
|
||||
DEFAULT_VOICE_ID = "21m00Tcm4TlvDq8ikWAM" # Rachel
|
||||
|
||||
|
||||
@AdapterRegistry.register("tts", "elevenlabs")
|
||||
class ElevenLabsTtsAdapter(BaseAdapter[bytes]):
|
||||
"""ElevenLabs TTS 语音合成适配器,返回 MP3 bytes。"""
|
||||
|
||||
adapter_type = "tts"
|
||||
adapter_name = "elevenlabs"
|
||||
|
||||
def __init__(self, config: AdapterConfig):
|
||||
super().__init__(config)
|
||||
self.api_base = config.api_base or ELEVENLABS_API_BASE
|
||||
|
||||
async def execute(self, text: str, **kwargs) -> bytes:
|
||||
"""将文本转换为语音 MP3 bytes。"""
|
||||
start_time = time.time()
|
||||
logger.info("elevenlabs_tts_start", text_length=len(text))
|
||||
|
||||
voice_id = kwargs.get("voice_id") or DEFAULT_VOICE_ID
|
||||
model_id = kwargs.get("model") or self.config.model or "eleven_multilingual_v2"
|
||||
stability = kwargs.get("stability", 0.5)
|
||||
similarity_boost = kwargs.get("similarity_boost", 0.75)
|
||||
|
||||
url = f"{self.api_base}/text-to-speech/{voice_id}"
|
||||
|
||||
payload = {
|
||||
"text": text,
|
||||
"model_id": model_id,
|
||||
"voice_settings": {
|
||||
"stability": stability,
|
||||
"similarity_boost": similarity_boost,
|
||||
},
|
||||
}
|
||||
|
||||
audio_bytes = await self._call_api(url, payload)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(
|
||||
"elevenlabs_tts_success",
|
||||
elapsed_seconds=round(elapsed, 2),
|
||||
audio_size_bytes=len(audio_bytes),
|
||||
)
|
||||
|
||||
return audio_bytes
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""检查 ElevenLabs API 是否可用。"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.get(
|
||||
f"{self.api_base}/voices",
|
||||
headers={"xi-api-key": self.config.api_key},
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@property
|
||||
def estimated_cost(self) -> float:
|
||||
"""预估每千字符成本 (USD)。"""
|
||||
return 0.03
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=10),
|
||||
retry=retry_if_exception_type((httpx.HTTPError, httpx.TimeoutException)),
|
||||
reraise=True,
|
||||
)
|
||||
async def _call_api(self, url: str, payload: dict) -> bytes:
|
||||
"""调用 ElevenLabs API,带重试机制。"""
|
||||
timeout = self.config.timeout_ms / 1000
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={
|
||||
"xi-api-key": self.config.api_key,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "audio/mpeg",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
|
||||
Reference in New Issue
Block a user