ishaq101 commited on
Commit
e0ee8f7
·
1 Parent(s): 6eb38ac

Add creds, Update used TTS model

Browse files
.env.example CHANGED
@@ -11,10 +11,13 @@ WAKE_WORD_ENABLED=false
11
 
12
  # Gemini TTS (optional — hanya dibutuhkan jika provider=gemini)
13
  GOOGLE_API_KEY=
14
- GEMINI_TTS_MODEL=gemini-2.5-flash-preview-tts
 
 
15
  GEMINI_TTS_VOICE=Autonoe
16
  GEMINI_TTS_LANGUAGE=id-ID
17
 
 
18
 
19
  # Google Cloud Project
20
  GOOGLE_PROJECT_NAME=
 
11
 
12
  # Gemini TTS (optional — hanya dibutuhkan jika provider=gemini)
13
  GOOGLE_API_KEY=
14
+ # Comma-separated list — dirotasi round-robin per request untuk distribusi quota RPM
15
+ GEMINI_TTS_MODELS=gemini-2.5-flash-tts
16
+ # GEMINI_TTS_MODELS=gemini-2.5-flash-tts,gemini-3.1-flash-tts-preview,gemini-2.5-pro-tts
17
  GEMINI_TTS_VOICE=Autonoe
18
  GEMINI_TTS_LANGUAGE=id-ID
19
 
20
+ GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
21
 
22
  # Google Cloud Project
23
  GOOGLE_PROJECT_NAME=
.gitignore CHANGED
@@ -25,4 +25,6 @@ convert_audio.py
25
  API_CONTRACT_CHATBOT.md
26
  API_CONTRACT_VOICE.md
27
  HIGHLIGHT_VOICE.md
28
- HIGHLIGHT_STT_TTS.md
 
 
 
25
  API_CONTRACT_CHATBOT.md
26
  API_CONTRACT_VOICE.md
27
  HIGHLIGHT_VOICE.md
28
+ HIGHLIGHT_STT_TTS.md
29
+
30
+ credentials/
Dockerfile CHANGED
@@ -12,7 +12,10 @@ RUN uv sync --no-dev --no-install-project
12
  # Copy source
13
  COPY . .
14
 
 
 
15
  ENV PATH="/app/.venv/bin:$PATH"
 
16
 
17
  EXPOSE 7860
18
 
 
12
  # Copy source
13
  COPY . .
14
 
15
+ COPY credentials/creds.json /app/credentials/creds.json
16
+
17
  ENV PATH="/app/.venv/bin:$PATH"
18
+ ENV GOOGLE_APPLICATION_CREDENTIALS=/app/credentials/creds.json
19
 
20
  EXPOSE 7860
21
 
src/config.py CHANGED
@@ -19,6 +19,13 @@ GOOGLE_PROJECT_NUMBER: str = os.getenv("GOOGLE_PROJECT_NUMBER", "")
19
  GOOGLE_CLOUD_LOCATION: str = os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1")
20
 
21
  GEMINI_TTS_MODEL: str = os.getenv("GEMINI_TTS_MODEL", "gemini-2.5-flash-tts")
 
 
 
 
 
 
 
22
  GEMINI_TTS_VOICE: str = os.getenv("GEMINI_TTS_VOICE", "Autonoe")
23
  GEMINI_TTS_LANGUAGE: str = os.getenv("GEMINI_TTS_LANGUAGE", "id-ID")
24
 
 
19
  GOOGLE_CLOUD_LOCATION: str = os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1")
20
 
21
  GEMINI_TTS_MODEL: str = os.getenv("GEMINI_TTS_MODEL", "gemini-2.5-flash-tts")
22
+ GEMINI_TTS_MODELS: list[str] = [
23
+ m.strip() for m in os.getenv(
24
+ "GEMINI_TTS_MODELS",
25
+ "gemini-2.5-flash-tts",
26
+ # "gemini-2.5-flash-tts,gemini-3.1-flash-tts-preview,gemini-2.5-pro-tts",
27
+ ).split(",") if m.strip()
28
+ ]
29
  GEMINI_TTS_VOICE: str = os.getenv("GEMINI_TTS_VOICE", "Autonoe")
30
  GEMINI_TTS_LANGUAGE: str = os.getenv("GEMINI_TTS_LANGUAGE", "id-ID")
31
 
src/google_auth.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from typing import Optional
4
+
5
+ import google.auth
6
+ import google.auth.credentials
7
+ from google.oauth2 import service_account
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ _SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
12
+
13
+
14
+ def get_google_credentials() -> google.auth.credentials.Credentials:
15
+ """
16
+ Resolution order:
17
+ 1. GOOGLE_APPLICATION_CREDENTIALS env var set & file exists → service account JSON
18
+ 2. Fallback → ADC (gcloud auth application-default login)
19
+ """
20
+ creds_path: Optional[str] = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
21
+
22
+ if creds_path:
23
+ if not os.path.isfile(creds_path):
24
+ raise FileNotFoundError(
25
+ f"GOOGLE_APPLICATION_CREDENTIALS='{creds_path}' tapi file tidak ditemukan."
26
+ )
27
+ logger.info("Google auth: service account dari %s", creds_path)
28
+ return service_account.Credentials.from_service_account_file(
29
+ creds_path, scopes=_SCOPES
30
+ )
31
+
32
+ logger.info("Google auth: menggunakan ADC (gcloud login)")
33
+ credentials, _ = google.auth.default(scopes=_SCOPES)
34
+ return credentials
src/stt/chirp3_client.py CHANGED
@@ -9,9 +9,12 @@ from google.cloud.speech_v2.types import cloud_speech as cloud_speech_types
9
  from google.api_core.client_options import ClientOptions
10
 
11
  from src.config import GOOGLE_PROJECT_ID, CHIRP3_REGION, CHIRP3_LANGUAGE
 
12
 
13
  logger = logging.getLogger(__name__)
14
 
 
 
15
  OnTranscriptCallback = Callable[[str], Awaitable[None]]
16
 
17
  _CHUNK_SIZE = 24 * 1024 # stay under the 25 KB gRPC streaming limit
@@ -20,7 +23,9 @@ _CHUNK_SIZE = 24 * 1024 # stay under the 25 KB gRPC streaming limit
20
  async def transcribe_audio(data: bytes, mimetype: str = "audio/wav") -> dict:
21
  """Transcribes a full audio file using Chirp 3 (Google Cloud Speech-to-Text V2)."""
22
  def _run() -> str:
 
23
  client = SpeechClient(
 
24
  client_options=ClientOptions(
25
  api_endpoint=f"{CHIRP3_REGION}-speech.googleapis.com"
26
  )
@@ -48,10 +53,13 @@ async def transcribe_audio(data: bytes, mimetype: str = "audio/wav") -> dict:
48
  yield from audio_requests
49
 
50
  transcripts: list[str] = []
 
51
  for response in client.streaming_recognize(requests=requests()):
 
52
  for result in response.results:
53
  if result.alternatives:
54
  transcripts.append(result.alternatives[0].transcript)
 
55
 
56
  transcript = " ".join(transcripts).strip()
57
  if transcript:
@@ -111,6 +119,7 @@ class Chirp3STTStreamer:
111
 
112
  def _recognize(self, wav_data: bytes) -> str:
113
  client = SpeechClient(
 
114
  client_options=ClientOptions(
115
  api_endpoint=f"{CHIRP3_REGION}-speech.googleapis.com"
116
  )
 
9
  from google.api_core.client_options import ClientOptions
10
 
11
  from src.config import GOOGLE_PROJECT_ID, CHIRP3_REGION, CHIRP3_LANGUAGE
12
+ from src.google_auth import get_google_credentials
13
 
14
  logger = logging.getLogger(__name__)
15
 
16
+ _credentials = get_google_credentials()
17
+
18
  OnTranscriptCallback = Callable[[str], Awaitable[None]]
19
 
20
  _CHUNK_SIZE = 24 * 1024 # stay under the 25 KB gRPC streaming limit
 
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, region=%s, project=%s", len(data), CHIRP3_REGION, GOOGLE_PROJECT_ID)
27
  client = SpeechClient(
28
+ credentials=_credentials,
29
  client_options=ClientOptions(
30
  api_endpoint=f"{CHIRP3_REGION}-speech.googleapis.com"
31
  )
 
53
  yield from audio_requests
54
 
55
  transcripts: list[str] = []
56
+ response_count = 0
57
  for response in client.streaming_recognize(requests=requests()):
58
+ response_count += 1
59
  for result in response.results:
60
  if result.alternatives:
61
  transcripts.append(result.alternatives[0].transcript)
62
+ logger.info("chirp3 transcribe_audio: %d responses, %d transcripts", response_count, len(transcripts))
63
 
64
  transcript = " ".join(transcripts).strip()
65
  if transcript:
 
119
 
120
  def _recognize(self, wav_data: bytes) -> str:
121
  client = SpeechClient(
122
+ credentials=_credentials,
123
  client_options=ClientOptions(
124
  api_endpoint=f"{CHIRP3_REGION}-speech.googleapis.com"
125
  )
src/tts/gemini_client.py CHANGED
@@ -1,39 +1,86 @@
1
  import asyncio
2
  import logging
 
3
  from typing import AsyncIterator
4
 
 
5
  from google.cloud import texttospeech
6
 
7
- from src.config import GEMINI_TTS_MODEL, GEMINI_TTS_VOICE, GEMINI_TTS_LANGUAGE
 
8
 
9
  logger = logging.getLogger(__name__)
10
 
 
 
11
  GEMINI_SAMPLE_RATE = 24000 # Cloud TTS Gemini model outputs PCM Linear16 at 24kHz
12
 
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  async def synthesize_stream(text: str) -> AsyncIterator[bytes]:
15
- """Calls Cloud TTS Gemini model and yields raw PCM Linear16 chunks at 24kHz."""
16
- logger.info("Gemini TTS: synthesizing '%s'", text[:60])
17
-
18
- def _collect() -> list[bytes]:
19
- client = texttospeech.TextToSpeechClient()
20
- config_req = texttospeech.StreamingSynthesizeRequest(
21
- streaming_config=texttospeech.StreamingSynthesizeConfig(
22
- voice=texttospeech.VoiceSelectionParams(
23
- name=GEMINI_TTS_VOICE,
24
- language_code=GEMINI_TTS_LANGUAGE,
25
- model_name=GEMINI_TTS_MODEL,
 
 
 
 
 
 
 
 
 
26
  )
27
  )
28
- )
29
 
30
- def _gen():
31
- yield config_req
32
- yield texttospeech.StreamingSynthesizeRequest(
33
- input=texttospeech.StreamingSynthesisInput(text=text)
 
 
 
 
 
 
34
  )
 
35
 
36
- return [r.audio_content for r in client.streaming_synthesize(_gen())]
 
 
 
 
 
 
 
 
 
 
37
 
38
- for chunk in await asyncio.to_thread(_collect):
39
- yield chunk
 
1
  import asyncio
2
  import logging
3
+ import threading
4
  from typing import AsyncIterator
5
 
6
+ from google.api_core import exceptions as gcp_exceptions
7
  from google.cloud import texttospeech
8
 
9
+ from src.config import GEMINI_TTS_MODELS, GEMINI_TTS_VOICE, GEMINI_TTS_LANGUAGE
10
+ from src.google_auth import get_google_credentials
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
+ _credentials = get_google_credentials()
15
+
16
  GEMINI_SAMPLE_RATE = 24000 # Cloud TTS Gemini model outputs PCM Linear16 at 24kHz
17
 
18
 
19
+ class _ModelRotator:
20
+ def __init__(self, models: list[str]) -> None:
21
+ self._models = models
22
+ self._index = 0
23
+ self._lock = threading.Lock()
24
+
25
+ def get_rotation(self) -> list[str]:
26
+ """Returns models starting from current index, then advances for next call."""
27
+ with self._lock:
28
+ n = len(self._models)
29
+ start = self._index % n
30
+ self._index += 1
31
+ return self._models[start:] + self._models[:start]
32
+
33
+
34
+ _rotator = _ModelRotator(GEMINI_TTS_MODELS)
35
+
36
+
37
  async def synthesize_stream(text: str) -> AsyncIterator[bytes]:
38
+ """Calls Cloud TTS Gemini model and yields raw PCM Linear16 chunks at 24kHz.
39
+
40
+ Rotates across GEMINI_TTS_MODELS round-robin and falls back to the next model
41
+ on ResourceExhausted (quota exceeded).
42
+ """
43
+ models_to_try = _rotator.get_rotation()
44
+ last_exc: Exception | None = None
45
+
46
+ for model in models_to_try:
47
+ logger.info("Gemini TTS [%s]: synthesizing '%s'", model, text[:60])
48
+
49
+ def _collect(model_name: str = model) -> list[bytes]:
50
+ client = texttospeech.TextToSpeechClient(credentials=_credentials)
51
+ config_req = texttospeech.StreamingSynthesizeRequest(
52
+ streaming_config=texttospeech.StreamingSynthesizeConfig(
53
+ voice=texttospeech.VoiceSelectionParams(
54
+ name=GEMINI_TTS_VOICE,
55
+ language_code=GEMINI_TTS_LANGUAGE,
56
+ model_name=model_name,
57
+ )
58
  )
59
  )
 
60
 
61
+ def _gen():
62
+ yield config_req
63
+ yield texttospeech.StreamingSynthesizeRequest(
64
+ input=texttospeech.StreamingSynthesisInput(text=text)
65
+ )
66
+
67
+ chunks = [r.audio_content for r in client.streaming_synthesize(_gen())]
68
+ logger.info(
69
+ "Gemini TTS [%s]: received %d chunks, %d bytes total",
70
+ model_name, len(chunks), sum(len(c) for c in chunks),
71
  )
72
+ return chunks
73
 
74
+ try:
75
+ chunks = await asyncio.to_thread(_collect)
76
+ for chunk in chunks:
77
+ yield chunk
78
+ return
79
+ except gcp_exceptions.ResourceExhausted:
80
+ logger.warning("Gemini TTS quota exhausted for model '%s', trying next model", model)
81
+ last_exc = gcp_exceptions.ResourceExhausted(f"Quota exhausted on {model}")
82
+ continue
83
+ except Exception:
84
+ raise
85
 
86
+ raise last_exc # type: ignore[misc]