fixing error stt and tts, empty chunk audio
Browse files- .gitignore +2 -1
- main.py +2 -0
- src/config.py +1 -1
- src/pipeline.py +1 -0
- src/stt/chirp3_client.py +26 -39
- src/tts/cartesia_client.py +1 -1
- src/tts/gemini_client.py +11 -4
.gitignore
CHANGED
|
@@ -27,4 +27,5 @@ API_CONTRACT_VOICE.md
|
|
| 27 |
HIGHLIGHT_VOICE.md
|
| 28 |
HIGHLIGHT_STT_TTS.md
|
| 29 |
|
| 30 |
-
credentials/
|
|
|
|
|
|
| 27 |
HIGHLIGHT_VOICE.md
|
| 28 |
HIGHLIGHT_STT_TTS.md
|
| 29 |
|
| 30 |
+
credentials/
|
| 31 |
+
scripts/
|
main.py
CHANGED
|
@@ -116,6 +116,8 @@ async def text_to_speech(req: TTSRequest) -> StreamingResponse:
|
|
| 116 |
if not req.text.strip():
|
| 117 |
raise HTTPException(status_code=400, detail="text must not be empty.")
|
| 118 |
|
|
|
|
|
|
|
| 119 |
if req.provider == "gemini":
|
| 120 |
if not GOOGLE_API_KEY:
|
| 121 |
raise HTTPException(status_code=503, detail="Gemini TTS not configured.")
|
|
|
|
| 116 |
if not req.text.strip():
|
| 117 |
raise HTTPException(status_code=400, detail="text must not be empty.")
|
| 118 |
|
| 119 |
+
logger.info("TTS request: provider=%s, text_len=%d, text=%r", req.provider, len(req.text), req.text)
|
| 120 |
+
|
| 121 |
if req.provider == "gemini":
|
| 122 |
if not GOOGLE_API_KEY:
|
| 123 |
raise HTTPException(status_code=503, detail="Gemini TTS not configured.")
|
src/config.py
CHANGED
|
@@ -38,5 +38,5 @@ DEEPGRAM_LANGUAGE: str = os.getenv("DEEPGRAM_LANGUAGE", "id")
|
|
| 38 |
DEEPGRAM_ENDPOINTING_MS: int = int(os.getenv("DEEPGRAM_ENDPOINTING_MS", "300"))
|
| 39 |
DEEPGRAM_UTTERANCE_END_MS: int = int(os.getenv("DEEPGRAM_UTTERANCE_END_MS", "2000"))
|
| 40 |
|
| 41 |
-
CHIRP3_REGION: str = os.getenv("CHIRP3_REGION", "us")
|
| 42 |
CHIRP3_LANGUAGE: str = os.getenv("CHIRP3_LANGUAGE", "id-ID")
|
|
|
|
| 38 |
DEEPGRAM_ENDPOINTING_MS: int = int(os.getenv("DEEPGRAM_ENDPOINTING_MS", "300"))
|
| 39 |
DEEPGRAM_UTTERANCE_END_MS: int = int(os.getenv("DEEPGRAM_UTTERANCE_END_MS", "2000"))
|
| 40 |
|
| 41 |
+
CHIRP3_REGION: str = os.getenv("CHIRP3_REGION", "us-central1") # chirp_2 valid regions: us-central1, europe-west4, asia-southeast1
|
| 42 |
CHIRP3_LANGUAGE: str = os.getenv("CHIRP3_LANGUAGE", "id-ID")
|
src/pipeline.py
CHANGED
|
@@ -94,6 +94,7 @@ class VoicePipeline:
|
|
| 94 |
self._tts_task = asyncio.create_task(self._speak(text))
|
| 95 |
|
| 96 |
async def _speak(self, text: str) -> None:
|
|
|
|
| 97 |
async with self._tts_lock:
|
| 98 |
try:
|
| 99 |
stream = (
|
|
|
|
| 94 |
self._tts_task = asyncio.create_task(self._speak(text))
|
| 95 |
|
| 96 |
async def _speak(self, text: str) -> None:
|
| 97 |
+
logger.info("TTS speak: provider=%s, text_len=%d, text=%r", self._tts_provider, len(text), text)
|
| 98 |
async with self._tts_lock:
|
| 99 |
try:
|
| 100 |
stream = (
|
src/stt/chirp3_client.py
CHANGED
|
@@ -19,34 +19,39 @@ OnTranscriptCallback = Callable[[str], Awaitable[None]]
|
|
| 19 |
|
| 20 |
_CHUNK_SIZE = 24 * 1024 # stay under the 25 KB gRPC streaming limit
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
async def transcribe_audio(data: bytes, mimetype: str = "audio/wav") -> dict:
|
| 24 |
"""Transcribes a full audio file using Chirp 3 (Google Cloud Speech-to-Text V2)."""
|
| 25 |
def _run() -> str:
|
| 26 |
-
logger.info("chirp3 transcribe_audio: data=%d bytes,
|
| 27 |
-
client =
|
| 28 |
-
credentials=_credentials,
|
| 29 |
-
client_options=ClientOptions(
|
| 30 |
-
api_endpoint=f"{CHIRP3_REGION}-speech.googleapis.com"
|
| 31 |
-
)
|
| 32 |
-
)
|
| 33 |
chunks = [data[i: i + _CHUNK_SIZE] for i in range(0, len(data), _CHUNK_SIZE)]
|
| 34 |
audio_requests = (
|
| 35 |
cloud_speech_types.StreamingRecognizeRequest(audio=chunk)
|
| 36 |
for chunk in chunks
|
| 37 |
)
|
| 38 |
-
|
| 39 |
-
auto_decoding_config=cloud_speech_types.AutoDetectDecodingConfig(),
|
| 40 |
-
language_codes=[CHIRP3_LANGUAGE],
|
| 41 |
-
model="chirp_3",
|
| 42 |
-
)
|
| 43 |
-
streaming_config = cloud_speech_types.StreamingRecognitionConfig(
|
| 44 |
-
config=recognition_config
|
| 45 |
-
)
|
| 46 |
-
config_request = cloud_speech_types.StreamingRecognizeRequest(
|
| 47 |
-
recognizer=f"projects/{GOOGLE_PROJECT_ID}/locations/{CHIRP3_REGION}/recognizers/_",
|
| 48 |
-
streaming_config=streaming_config,
|
| 49 |
-
)
|
| 50 |
|
| 51 |
def requests():
|
| 52 |
yield config_request
|
|
@@ -118,13 +123,7 @@ class Chirp3STTStreamer:
|
|
| 118 |
logger.exception("Chirp 3 STT transcription failed")
|
| 119 |
|
| 120 |
def _recognize(self, wav_data: bytes) -> str:
|
| 121 |
-
client =
|
| 122 |
-
credentials=_credentials,
|
| 123 |
-
client_options=ClientOptions(
|
| 124 |
-
api_endpoint=f"{CHIRP3_REGION}-speech.googleapis.com"
|
| 125 |
-
)
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
chunks = [
|
| 129 |
wav_data[i : i + _CHUNK_SIZE]
|
| 130 |
for i in range(0, len(wav_data), _CHUNK_SIZE)
|
|
@@ -133,19 +132,7 @@ class Chirp3STTStreamer:
|
|
| 133 |
cloud_speech_types.StreamingRecognizeRequest(audio=chunk)
|
| 134 |
for chunk in chunks
|
| 135 |
)
|
| 136 |
-
|
| 137 |
-
recognition_config = cloud_speech_types.RecognitionConfig(
|
| 138 |
-
auto_decoding_config=cloud_speech_types.AutoDetectDecodingConfig(),
|
| 139 |
-
language_codes=[CHIRP3_LANGUAGE],
|
| 140 |
-
model="chirp_3",
|
| 141 |
-
)
|
| 142 |
-
streaming_config = cloud_speech_types.StreamingRecognitionConfig(
|
| 143 |
-
config=recognition_config
|
| 144 |
-
)
|
| 145 |
-
config_request = cloud_speech_types.StreamingRecognizeRequest(
|
| 146 |
-
recognizer=f"projects/{GOOGLE_PROJECT_ID}/locations/{CHIRP3_REGION}/recognizers/_",
|
| 147 |
-
streaming_config=streaming_config,
|
| 148 |
-
)
|
| 149 |
|
| 150 |
def requests():
|
| 151 |
yield config_request
|
|
|
|
| 19 |
|
| 20 |
_CHUNK_SIZE = 24 * 1024 # stay under the 25 KB gRPC streaming limit
|
| 21 |
|
| 22 |
+
def _make_client() -> SpeechClient:
|
| 23 |
+
return SpeechClient(
|
| 24 |
+
credentials=_credentials,
|
| 25 |
+
client_options=ClientOptions(api_endpoint=f"{CHIRP3_REGION}-speech.googleapis.com"),
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _make_config_request() -> cloud_speech_types.StreamingRecognizeRequest:
|
| 30 |
+
recognition_config = cloud_speech_types.RecognitionConfig(
|
| 31 |
+
auto_decoding_config=cloud_speech_types.AutoDetectDecodingConfig(),
|
| 32 |
+
language_codes=[CHIRP3_LANGUAGE],
|
| 33 |
+
model="chirp_2",
|
| 34 |
+
)
|
| 35 |
+
streaming_config = cloud_speech_types.StreamingRecognitionConfig(
|
| 36 |
+
config=recognition_config
|
| 37 |
+
)
|
| 38 |
+
return cloud_speech_types.StreamingRecognizeRequest(
|
| 39 |
+
recognizer=f"projects/{GOOGLE_PROJECT_ID}/locations/{CHIRP3_REGION}/recognizers/_",
|
| 40 |
+
streaming_config=streaming_config,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
|
| 44 |
async def transcribe_audio(data: bytes, mimetype: str = "audio/wav") -> dict:
|
| 45 |
"""Transcribes a full audio file using Chirp 3 (Google Cloud Speech-to-Text V2)."""
|
| 46 |
def _run() -> str:
|
| 47 |
+
logger.info("chirp3 transcribe_audio: data=%d bytes, project=%s", len(data), GOOGLE_PROJECT_ID)
|
| 48 |
+
client = _make_client()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
chunks = [data[i: i + _CHUNK_SIZE] for i in range(0, len(data), _CHUNK_SIZE)]
|
| 50 |
audio_requests = (
|
| 51 |
cloud_speech_types.StreamingRecognizeRequest(audio=chunk)
|
| 52 |
for chunk in chunks
|
| 53 |
)
|
| 54 |
+
config_request = _make_config_request()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
def requests():
|
| 57 |
yield config_request
|
|
|
|
| 123 |
logger.exception("Chirp 3 STT transcription failed")
|
| 124 |
|
| 125 |
def _recognize(self, wav_data: bytes) -> str:
|
| 126 |
+
client = _make_client()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
chunks = [
|
| 128 |
wav_data[i : i + _CHUNK_SIZE]
|
| 129 |
for i in range(0, len(wav_data), _CHUNK_SIZE)
|
|
|
|
| 132 |
cloud_speech_types.StreamingRecognizeRequest(audio=chunk)
|
| 133 |
for chunk in chunks
|
| 134 |
)
|
| 135 |
+
config_request = _make_config_request()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
def requests():
|
| 138 |
yield config_request
|
src/tts/cartesia_client.py
CHANGED
|
@@ -32,7 +32,7 @@ async def synthesize_stream(text: str) -> AsyncIterator[bytes]:
|
|
| 32 |
"Content-Type": "application/json",
|
| 33 |
}
|
| 34 |
|
| 35 |
-
logger.info("Cartesia TTS:
|
| 36 |
async with httpx.AsyncClient(timeout=30) as client:
|
| 37 |
async with client.stream("POST", CARTESIA_TTS_URL, json=payload, headers=headers) as response:
|
| 38 |
if response.status_code >= 400:
|
|
|
|
| 32 |
"Content-Type": "application/json",
|
| 33 |
}
|
| 34 |
|
| 35 |
+
logger.info("Cartesia TTS: text_len=%d, text=%r", len(text), text)
|
| 36 |
async with httpx.AsyncClient(timeout=30) as client:
|
| 37 |
async with client.stream("POST", CARTESIA_TTS_URL, json=payload, headers=headers) as response:
|
| 38 |
if response.status_code >= 400:
|
src/tts/gemini_client.py
CHANGED
|
@@ -44,7 +44,7 @@ async def synthesize_stream(text: str) -> AsyncIterator[bytes]:
|
|
| 44 |
last_exc: Exception | None = None
|
| 45 |
|
| 46 |
for model in models_to_try:
|
| 47 |
-
logger.info("Gemini TTS [%s]:
|
| 48 |
|
| 49 |
def _collect(model_name: str = model) -> list[bytes]:
|
| 50 |
client = texttospeech.TextToSpeechClient(credentials=_credentials)
|
|
@@ -64,10 +64,17 @@ async def synthesize_stream(text: str) -> AsyncIterator[bytes]:
|
|
| 64 |
input=texttospeech.StreamingSynthesisInput(text=text)
|
| 65 |
)
|
| 66 |
|
| 67 |
-
|
|
|
|
|
|
|
| 68 |
logger.info(
|
| 69 |
-
"Gemini TTS [%s]: received %d chunks, %d bytes total",
|
| 70 |
-
model_name,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
)
|
| 72 |
return chunks
|
| 73 |
|
|
|
|
| 44 |
last_exc: Exception | None = None
|
| 45 |
|
| 46 |
for model in models_to_try:
|
| 47 |
+
logger.info("Gemini TTS [%s]: text_len=%d, text=%r", model, len(text), text)
|
| 48 |
|
| 49 |
def _collect(model_name: str = model) -> list[bytes]:
|
| 50 |
client = texttospeech.TextToSpeechClient(credentials=_credentials)
|
|
|
|
| 64 |
input=texttospeech.StreamingSynthesisInput(text=text)
|
| 65 |
)
|
| 66 |
|
| 67 |
+
all_chunks = [r.audio_content for r in client.streaming_synthesize(_gen())]
|
| 68 |
+
chunks = [c for c in all_chunks if c]
|
| 69 |
+
total_bytes = sum(len(c) for c in chunks)
|
| 70 |
logger.info(
|
| 71 |
+
"Gemini TTS [%s]: received %d chunks (%d empty skipped), %d bytes total, aligned=%s, first32=%s",
|
| 72 |
+
model_name,
|
| 73 |
+
len(chunks),
|
| 74 |
+
len(all_chunks) - len(chunks),
|
| 75 |
+
total_bytes,
|
| 76 |
+
total_bytes % 2 == 0,
|
| 77 |
+
chunks[0][:32].hex() if chunks else "N/A",
|
| 78 |
)
|
| 79 |
return chunks
|
| 80 |
|