mirror of
https://github.com/calesthio/OpenMontage.git
synced 2026-08-15 13:14:38 +08:00
fix: scope fal providers and Google TTS networking
This commit is contained in:
@@ -192,7 +192,7 @@ The ASR tool (`qwen3-asr-flash-filetrans`) uses an async submit-poll pattern. Au
|
||||
|
||||
> **Broad single-key coverage.** One API key unlocks image and video providers across multiple models.
|
||||
|
||||
**Tools unlocked:** `flux_image`, `recraft_image`, `kling_video`, `veo_video`, `minimax_video`
|
||||
**Tools unlocked:** `flux_image`, `recraft_image`, `kling_video`, `veo_video`, `minimax_video`, `fal_elevenlabs_tts`, `fal_elevenlabs_music`
|
||||
**Env var:** `FAL_KEY`
|
||||
|
||||
#### Setup
|
||||
@@ -225,6 +225,10 @@ No subscription — pure pay-as-you-go, no minimum spend.
|
||||
|
||||
**Free tier:** None — but $0 to start, you only pay for what you use.
|
||||
|
||||
The same key can also access ElevenLabs speech and music through fal.ai. Use
|
||||
`fal_elevenlabs_tts` when direct ElevenLabs credentials are unavailable, or
|
||||
select it through `tts_selector` with `preferred_provider: "fal.ai"`.
|
||||
|
||||
---
|
||||
|
||||
### Kling Official — Direct API
|
||||
|
||||
@@ -37,6 +37,32 @@ def test_registry_discovers_fal_tts(monkeypatch):
|
||||
assert tool.get_status() == ToolStatus.AVAILABLE
|
||||
|
||||
|
||||
def test_tts_selector_routes_to_fal_provider(monkeypatch):
|
||||
from tools.audio.tts_selector import TTSSelector
|
||||
from tools.base_tool import ToolResult
|
||||
|
||||
monkeypatch.setenv("FAL_KEY", "test-key")
|
||||
tool = FalElevenLabsTTS()
|
||||
selector = TTSSelector()
|
||||
monkeypatch.setattr(selector, "_providers", lambda: [tool])
|
||||
monkeypatch.setattr(
|
||||
selector,
|
||||
"_select_best_tool",
|
||||
lambda _inputs, _candidates, _context: (tool, None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
tool,
|
||||
"execute",
|
||||
lambda inputs: ToolResult(success=True, data={"received": inputs}),
|
||||
)
|
||||
result = selector.execute(
|
||||
{"text": "hello", "preferred_provider": "fal.ai", "voice_id": "Rachel"}
|
||||
)
|
||||
assert result.success
|
||||
assert result.data["selected_tool"] == "fal_elevenlabs_tts"
|
||||
assert result.data["selected_provider"] == "fal.ai"
|
||||
|
||||
|
||||
def test_execute_submits_once_and_downloads_audio(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("FAL_KEY", "test-key")
|
||||
output_path = tmp_path / "speech.mp3"
|
||||
|
||||
@@ -34,26 +34,3 @@ def test_tts_key_uses_header_and_is_redacted_from_errors(monkeypatch, tmp_path):
|
||||
assert result.success is False
|
||||
assert secret not in result.error
|
||||
assert "[REDACTED]" in result.error
|
||||
|
||||
|
||||
def test_production_adapter_can_force_google_tts_to_ipv4(monkeypatch, tmp_path):
|
||||
import socket
|
||||
|
||||
import requests
|
||||
import urllib3.util.connection
|
||||
|
||||
monkeypatch.setenv("GOOGLE_TTS_API_KEY", "test-key")
|
||||
monkeypatch.setenv("GOOGLE_TTS_FORCE_IPV4", "1")
|
||||
original = urllib3.util.connection.allowed_gai_family
|
||||
|
||||
def inspect_request(url, **kwargs):
|
||||
assert urllib3.util.connection.allowed_gai_family() == socket.AF_INET
|
||||
raise requests.HTTPError("safe expected failure")
|
||||
|
||||
monkeypatch.setattr(requests, "post", inspect_request)
|
||||
result = GoogleTTS().execute(
|
||||
{"text": "safe test sentence", "output_path": str(tmp_path / "speech.mp3")}
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert urllib3.util.connection.allowed_gai_family is original
|
||||
|
||||
@@ -8,10 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -33,32 +30,6 @@ from tools.google_credentials import (
|
||||
)
|
||||
|
||||
|
||||
_IPV4_REQUEST_LOCK = threading.Lock()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _google_tts_network_family():
|
||||
"""Use IPv4 when the deployment key is restricted to the server's IPv4."""
|
||||
force_ipv4 = os.environ.get("GOOGLE_TTS_FORCE_IPV4", "").lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
}
|
||||
if not force_ipv4:
|
||||
yield
|
||||
return
|
||||
|
||||
import urllib3.util.connection
|
||||
|
||||
with _IPV4_REQUEST_LOCK:
|
||||
original = urllib3.util.connection.allowed_gai_family
|
||||
urllib3.util.connection.allowed_gai_family = lambda: socket.AF_INET
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
urllib3.util.connection.allowed_gai_family = original
|
||||
|
||||
|
||||
class GoogleTTS(BaseTool):
|
||||
name = "google_tts"
|
||||
version = "0.1.0"
|
||||
@@ -76,7 +47,6 @@ class GoogleTTS(BaseTool):
|
||||
" GOOGLE_API_KEY or GEMINI_API_KEY remain supported for broader Google setups.\n"
|
||||
" Google Cloud API key with Text-to-Speech enabled.\n"
|
||||
" Enable the API at https://console.cloud.google.com/apis/library/texttospeech.googleapis.com\n"
|
||||
" Set GOOGLE_TTS_FORCE_IPV4=1 only when the key is restricted to the caller's IPv4.\n"
|
||||
"Auth option B — service account: set GOOGLE_APPLICATION_CREDENTIALS to the\n"
|
||||
" path of a service-account JSON key (needs the 'google-auth' package)."
|
||||
)
|
||||
@@ -309,13 +279,12 @@ class GoogleTTS(BaseTool):
|
||||
elif api_key:
|
||||
headers["x-goog-api-key"] = api_key
|
||||
|
||||
with _google_tts_network_family():
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=120,
|
||||
)
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=120,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
audio_content = base64.b64decode(response.json()["audioContent"])
|
||||
|
||||
Reference in New Issue
Block a user