translateinpy 0.9.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- translateinpy-0.9.6/PKG-INFO +35 -0
- translateinpy-0.9.6/README.md +25 -0
- translateinpy-0.9.6/pyproject.toml +11 -0
- translateinpy-0.9.6/setup.cfg +4 -0
- translateinpy-0.9.6/src/translateinpy/__init__.py +1 -0
- translateinpy-0.9.6/src/translateinpy/fromAudioFile.py +742 -0
- translateinpy-0.9.6/src/translateinpy.egg-info/PKG-INFO +35 -0
- translateinpy-0.9.6/src/translateinpy.egg-info/SOURCES.txt +9 -0
- translateinpy-0.9.6/src/translateinpy.egg-info/dependency_links.txt +1 -0
- translateinpy-0.9.6/src/translateinpy.egg-info/requires.txt +3 -0
- translateinpy-0.9.6/src/translateinpy.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: translateinpy
|
|
3
|
+
Version: 0.9.6
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: google-genai>=1.35.0
|
|
8
|
+
Requires-Dist: pyaudiowpatch>=0.5.2
|
|
9
|
+
Requires-Dist: ffmpeg>=0.2.0
|
|
10
|
+
|
|
11
|
+
# TranslateINPy
|
|
12
|
+
A python translation module for translating live responses using Gemini API inside your application.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from translateinpy import Translator
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
api_key = os.getenv("GEMINI_API_KEY")
|
|
21
|
+
|
|
22
|
+
Translator("../sample/hi-1.wav", "en", api_key)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install TranslateINPY
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
or via UV:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv add TranslateINPy
|
|
35
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# TranslateINPy
|
|
2
|
+
A python translation module for translating live responses using Gemini API inside your application.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from translateinpy import Translator
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
api_key = os.getenv("GEMINI_API_KEY")
|
|
11
|
+
|
|
12
|
+
Translator("../sample/hi-1.wav", "en", api_key)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install TranslateINPY
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
or via UV:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv add TranslateINPy
|
|
25
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .fromAudioFile import Translator
|
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2026 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
To install the dependencies for this script, run:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install google-genai==2.8.0 pyaudio
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This script also requires `ffmpeg` to be installed on your system. For example, on macOS:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
brew install ffmpeg
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Before running this script, ensure the `GEMINI_API_KEY` environment
|
|
32
|
+
variable is set to the api-key you obtained from Google AI Studio.
|
|
33
|
+
|
|
34
|
+
## Run
|
|
35
|
+
|
|
36
|
+
To run the script:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
python Get_started_LiveTranslate.py --url "https://storage.googleapis.com/generativeai-downloads/gemini-cookbook/audio/gemini-live-translate-sample.wav" --target es
|
|
40
|
+
```
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import time
|
|
44
|
+
import argparse
|
|
45
|
+
import array
|
|
46
|
+
import asyncio
|
|
47
|
+
import os
|
|
48
|
+
import traceback
|
|
49
|
+
|
|
50
|
+
import pyaudio
|
|
51
|
+
from google import genai
|
|
52
|
+
from google.genai import types
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Audio configuration
|
|
56
|
+
FORMAT = pyaudio.paInt16
|
|
57
|
+
CHANNELS = 1
|
|
58
|
+
|
|
59
|
+
SEND_SAMPLE_RATE = 16_000
|
|
60
|
+
RECEIVE_SAMPLE_RATE = 24_000
|
|
61
|
+
|
|
62
|
+
# 100 ms at 16 kHz
|
|
63
|
+
CHUNK_SIZE = 1_600
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Queue markers
|
|
67
|
+
_INPUT_END = object()
|
|
68
|
+
_OUTPUT_END = object()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def adjust_volume(pcm_data: bytes, volume: float) -> bytes:
|
|
72
|
+
"""Reduce the volume of signed 16-bit PCM audio."""
|
|
73
|
+
|
|
74
|
+
if volume >= 1.0:
|
|
75
|
+
return pcm_data
|
|
76
|
+
|
|
77
|
+
if volume <= 0.0:
|
|
78
|
+
return b"\x00" * len(pcm_data)
|
|
79
|
+
|
|
80
|
+
samples = array.array("h", pcm_data)
|
|
81
|
+
|
|
82
|
+
for index in range(len(samples)):
|
|
83
|
+
samples[index] = int(samples[index] * volume)
|
|
84
|
+
|
|
85
|
+
return samples.tobytes()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def stream_audio_url(
|
|
89
|
+
url: str,
|
|
90
|
+
audio_queue: asyncio.Queue,
|
|
91
|
+
original_playback_queue: asyncio.Queue | None = None,
|
|
92
|
+
volume: float = 0.08,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Decode an audio file/URL with FFmpeg and place PCM chunks in the queue.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
print(f"\n[Info] Starting audio stream from: {url}", flush=True)
|
|
99
|
+
|
|
100
|
+
process = await asyncio.create_subprocess_exec(
|
|
101
|
+
"ffmpeg",
|
|
102
|
+
"-hide_banner",
|
|
103
|
+
"-loglevel",
|
|
104
|
+
"error",
|
|
105
|
+
"-i",
|
|
106
|
+
url,
|
|
107
|
+
"-f",
|
|
108
|
+
"s16le",
|
|
109
|
+
"-acodec",
|
|
110
|
+
"pcm_s16le",
|
|
111
|
+
"-ar",
|
|
112
|
+
str(SEND_SAMPLE_RATE),
|
|
113
|
+
"-ac",
|
|
114
|
+
str(CHANNELS),
|
|
115
|
+
"-",
|
|
116
|
+
stdout=asyncio.subprocess.PIPE,
|
|
117
|
+
stderr=asyncio.subprocess.PIPE,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
chunk_size_bytes = CHUNK_SIZE * 2
|
|
121
|
+
bytes_per_second = SEND_SAMPLE_RATE * 2
|
|
122
|
+
|
|
123
|
+
start_time = asyncio.get_running_loop().time()
|
|
124
|
+
bytes_sent = 0
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
while True:
|
|
128
|
+
data = await process.stdout.read(chunk_size_bytes)
|
|
129
|
+
|
|
130
|
+
if not data:
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
await audio_queue.put(data)
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
original_playback_queue is not None
|
|
137
|
+
and volume > 0.0
|
|
138
|
+
):
|
|
139
|
+
quiet_data = adjust_volume(data, volume)
|
|
140
|
+
await original_playback_queue.put(quiet_data)
|
|
141
|
+
|
|
142
|
+
bytes_sent += len(data)
|
|
143
|
+
|
|
144
|
+
expected_elapsed = bytes_sent / bytes_per_second
|
|
145
|
+
actual_elapsed = (
|
|
146
|
+
asyncio.get_running_loop().time() - start_time
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
sleep_time = expected_elapsed - actual_elapsed
|
|
150
|
+
|
|
151
|
+
if sleep_time > 0:
|
|
152
|
+
await asyncio.sleep(sleep_time)
|
|
153
|
+
|
|
154
|
+
# FFmpeg has reached EOF. Now wait for its exit status.
|
|
155
|
+
stderr_data = await process.stderr.read()
|
|
156
|
+
return_code = await process.wait()
|
|
157
|
+
|
|
158
|
+
if return_code != 0:
|
|
159
|
+
error_text = stderr_data.decode(
|
|
160
|
+
errors="replace"
|
|
161
|
+
).strip()
|
|
162
|
+
|
|
163
|
+
raise RuntimeError(
|
|
164
|
+
f"FFmpeg failed with exit code {return_code}"
|
|
165
|
+
+ (f": {error_text}" if error_text else "")
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
except asyncio.CancelledError:
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
finally:
|
|
172
|
+
if process.returncode is None:
|
|
173
|
+
try:
|
|
174
|
+
process.terminate()
|
|
175
|
+
await process.wait()
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
print("\n[Info] Audio stream finished.", flush=True)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def send_realtime(
|
|
183
|
+
session,
|
|
184
|
+
audio_queue: asyncio.Queue,
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Send PCM audio chunks to Gemini."""
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
while True:
|
|
190
|
+
chunk = await audio_queue.get()
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
if chunk is _INPUT_END:
|
|
194
|
+
await session.send_realtime_input(
|
|
195
|
+
audio_stream_end=True
|
|
196
|
+
)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
await session.send_realtime_input(
|
|
200
|
+
audio=types.Blob(
|
|
201
|
+
data=chunk,
|
|
202
|
+
mime_type=(
|
|
203
|
+
f"audio/pcm;rate={SEND_SAMPLE_RATE}"
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
finally:
|
|
209
|
+
audio_queue.task_done()
|
|
210
|
+
|
|
211
|
+
except asyncio.CancelledError:
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def receive_responses(
|
|
216
|
+
session,
|
|
217
|
+
audio_queue_output: asyncio.Queue,
|
|
218
|
+
response_complete_event: asyncio.Event,
|
|
219
|
+
last_activity: list,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Receive translated audio and transcription responses."""
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
async for response in session.receive():
|
|
225
|
+
server_content = getattr(
|
|
226
|
+
response,
|
|
227
|
+
"server_content",
|
|
228
|
+
None,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
last_activity[0] = time.monotonic()
|
|
232
|
+
|
|
233
|
+
if isinstance(audio_data, bytes):
|
|
234
|
+
last_activity[0] = time.monotonic()
|
|
235
|
+
await audio_queue_output.put(audio_data)
|
|
236
|
+
|
|
237
|
+
if server_content is None:
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
model_turn = getattr(
|
|
241
|
+
server_content,
|
|
242
|
+
"model_turn",
|
|
243
|
+
None,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
model_turn = getattr(
|
|
247
|
+
server_content,
|
|
248
|
+
"model_turn",
|
|
249
|
+
None,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if model_turn is not None:
|
|
253
|
+
for part in model_turn.parts:
|
|
254
|
+
inline_data = getattr(
|
|
255
|
+
part,
|
|
256
|
+
"inline_data",
|
|
257
|
+
None,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if inline_data is None:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
audio_data = getattr(
|
|
264
|
+
inline_data,
|
|
265
|
+
"data",
|
|
266
|
+
None,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if isinstance(audio_data, bytes):
|
|
270
|
+
last_activity[0] = time.monotonic()
|
|
271
|
+
|
|
272
|
+
await audio_queue_output.put(audio_data)
|
|
273
|
+
|
|
274
|
+
input_transcription = getattr(
|
|
275
|
+
server_content,
|
|
276
|
+
"input_transcription",
|
|
277
|
+
None,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if (
|
|
281
|
+
input_transcription is not None
|
|
282
|
+
and input_transcription.text
|
|
283
|
+
):
|
|
284
|
+
language_code = getattr(
|
|
285
|
+
input_transcription,
|
|
286
|
+
"language_code",
|
|
287
|
+
None,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
language = (
|
|
291
|
+
f" ({language_code})"
|
|
292
|
+
if language_code
|
|
293
|
+
else ""
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
print(
|
|
297
|
+
f"\n[Source{language}] "
|
|
298
|
+
f"{input_transcription.text}",
|
|
299
|
+
flush=True,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
output_transcription = getattr(
|
|
303
|
+
server_content,
|
|
304
|
+
"output_transcription",
|
|
305
|
+
None,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if (
|
|
309
|
+
output_transcription is not None
|
|
310
|
+
and output_transcription.text
|
|
311
|
+
):
|
|
312
|
+
language_code = getattr(
|
|
313
|
+
output_transcription,
|
|
314
|
+
"language_code",
|
|
315
|
+
None,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
language = (
|
|
319
|
+
f" ({language_code})"
|
|
320
|
+
if language_code
|
|
321
|
+
else ""
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
print(
|
|
325
|
+
f"\n[Translation{language}] "
|
|
326
|
+
f"{output_transcription.text}",
|
|
327
|
+
flush=True,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
turn_complete = getattr(
|
|
331
|
+
server_content,
|
|
332
|
+
"turn_complete",
|
|
333
|
+
False,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
generation_complete = getattr(
|
|
337
|
+
server_content,
|
|
338
|
+
"generation_complete",
|
|
339
|
+
False,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if turn_complete or generation_complete:
|
|
343
|
+
print(
|
|
344
|
+
"\n[Info] Gemini response complete.",
|
|
345
|
+
flush=True,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
response_complete_event.set()
|
|
349
|
+
|
|
350
|
+
# All audio parts from this response were processed
|
|
351
|
+
# before this marker was inserted.
|
|
352
|
+
await audio_queue_output.put(_OUTPUT_END)
|
|
353
|
+
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
except asyncio.CancelledError:
|
|
357
|
+
raise
|
|
358
|
+
|
|
359
|
+
except Exception:
|
|
360
|
+
print(
|
|
361
|
+
"\n[Error] Receiver crashed:",
|
|
362
|
+
flush=True,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
traceback.print_exc()
|
|
366
|
+
|
|
367
|
+
response_complete_event.set()
|
|
368
|
+
|
|
369
|
+
# Allow the player to shut down if receiving fails.
|
|
370
|
+
await audio_queue_output.put(_OUTPUT_END)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
async def play_audio(
|
|
374
|
+
audio_queue_output: asyncio.Queue,
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Play translated audio until _OUTPUT_END is received."""
|
|
377
|
+
|
|
378
|
+
pya = pyaudio.PyAudio()
|
|
379
|
+
stream = None
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
stream = await asyncio.to_thread(
|
|
383
|
+
pya.open,
|
|
384
|
+
format=FORMAT,
|
|
385
|
+
channels=CHANNELS,
|
|
386
|
+
rate=RECEIVE_SAMPLE_RATE,
|
|
387
|
+
output=True,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
while True:
|
|
391
|
+
audio_data = await audio_queue_output.get()
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
if audio_data is _OUTPUT_END:
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
await asyncio.to_thread(
|
|
398
|
+
stream.write,
|
|
399
|
+
audio_data,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
finally:
|
|
403
|
+
audio_queue_output.task_done()
|
|
404
|
+
|
|
405
|
+
except asyncio.CancelledError:
|
|
406
|
+
raise
|
|
407
|
+
|
|
408
|
+
finally:
|
|
409
|
+
if stream is not None:
|
|
410
|
+
try:
|
|
411
|
+
stream.stop_stream()
|
|
412
|
+
stream.close()
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
pya.terminate()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
async def play_original_audio(
|
|
420
|
+
original_playback_queue: asyncio.Queue,
|
|
421
|
+
) -> None:
|
|
422
|
+
"""Play the quiet original audio."""
|
|
423
|
+
|
|
424
|
+
pya = pyaudio.PyAudio()
|
|
425
|
+
stream = None
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
stream = await asyncio.to_thread(
|
|
429
|
+
pya.open,
|
|
430
|
+
format=FORMAT,
|
|
431
|
+
channels=CHANNELS,
|
|
432
|
+
rate=SEND_SAMPLE_RATE,
|
|
433
|
+
output=True,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
while True:
|
|
437
|
+
audio_data = await original_playback_queue.get()
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
await asyncio.to_thread(
|
|
441
|
+
stream.write,
|
|
442
|
+
audio_data,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
finally:
|
|
446
|
+
original_playback_queue.task_done()
|
|
447
|
+
|
|
448
|
+
except asyncio.CancelledError:
|
|
449
|
+
raise
|
|
450
|
+
|
|
451
|
+
finally:
|
|
452
|
+
if stream is not None:
|
|
453
|
+
try:
|
|
454
|
+
stream.stop_stream()
|
|
455
|
+
stream.close()
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
|
|
459
|
+
pya.terminate()
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
async def run(
|
|
463
|
+
url: str,
|
|
464
|
+
target_lang: str,
|
|
465
|
+
api_key: str,
|
|
466
|
+
original_volume: float = 0.08,
|
|
467
|
+
) -> None:
|
|
468
|
+
"""Run the live audio translation."""
|
|
469
|
+
|
|
470
|
+
if not api_key:
|
|
471
|
+
raise ValueError(
|
|
472
|
+
"A Gemini API key is required."
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
client = genai.Client(api_key=api_key)
|
|
476
|
+
|
|
477
|
+
config = types.LiveConnectConfig(
|
|
478
|
+
response_modalities=[types.Modality.AUDIO],
|
|
479
|
+
translation_config=types.TranslationConfig(
|
|
480
|
+
echo_target_language=True,
|
|
481
|
+
target_language_code=target_lang,
|
|
482
|
+
),
|
|
483
|
+
input_audio_transcription=(
|
|
484
|
+
types.AudioTranscriptionConfig()
|
|
485
|
+
),
|
|
486
|
+
output_audio_transcription=(
|
|
487
|
+
types.AudioTranscriptionConfig()
|
|
488
|
+
),
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
audio_queue_input = asyncio.Queue(maxsize=50)
|
|
492
|
+
audio_queue_output = asyncio.Queue()
|
|
493
|
+
original_playback_queue = asyncio.Queue()
|
|
494
|
+
|
|
495
|
+
response_complete_event = asyncio.Event()
|
|
496
|
+
last_activity = [time.monotonic()]
|
|
497
|
+
|
|
498
|
+
model = "gemini-3.5-live-translate-preview"
|
|
499
|
+
|
|
500
|
+
print(
|
|
501
|
+
f"[Info] Connecting to Gemini Live ({model})...",
|
|
502
|
+
flush=True,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
async with client.aio.live.connect(
|
|
506
|
+
model=model,
|
|
507
|
+
config=config,
|
|
508
|
+
) as session:
|
|
509
|
+
print(
|
|
510
|
+
"[Info] Connected successfully. Ready to stream!",
|
|
511
|
+
flush=True,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
async with asyncio.TaskGroup() as task_group:
|
|
515
|
+
stream_task = task_group.create_task(
|
|
516
|
+
stream_audio_url(
|
|
517
|
+
url=url,
|
|
518
|
+
audio_queue=audio_queue_input,
|
|
519
|
+
original_playback_queue=(
|
|
520
|
+
original_playback_queue
|
|
521
|
+
),
|
|
522
|
+
volume=original_volume,
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
send_task = task_group.create_task(
|
|
527
|
+
send_realtime(
|
|
528
|
+
session=session,
|
|
529
|
+
audio_queue=audio_queue_input,
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
receive_task = task_group.create_task(
|
|
534
|
+
receive_responses(
|
|
535
|
+
session=session,
|
|
536
|
+
audio_queue_output=audio_queue_output,
|
|
537
|
+
response_complete_event=response_complete_event,
|
|
538
|
+
last_activity=last_activity,
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
play_task = task_group.create_task(
|
|
543
|
+
play_audio(
|
|
544
|
+
audio_queue_output=audio_queue_output,
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
original_play_task = task_group.create_task(
|
|
549
|
+
play_original_audio(
|
|
550
|
+
original_playback_queue=(
|
|
551
|
+
original_playback_queue
|
|
552
|
+
),
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# Wait until FFmpeg has completely finished reading
|
|
557
|
+
# and queueing the input file.
|
|
558
|
+
await stream_task
|
|
559
|
+
|
|
560
|
+
print(
|
|
561
|
+
"[Info] Input finished. "
|
|
562
|
+
"Sending end-of-stream marker...",
|
|
563
|
+
flush=True,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# The sender is still alive here.
|
|
567
|
+
await audio_queue_input.put(_INPUT_END)
|
|
568
|
+
|
|
569
|
+
# Wait until every input chunk and the end marker
|
|
570
|
+
# have been sent to Gemini.
|
|
571
|
+
await audio_queue_input.join()
|
|
572
|
+
|
|
573
|
+
# send_realtime() returns after sending
|
|
574
|
+
# audio_stream_end=True.
|
|
575
|
+
await send_task
|
|
576
|
+
|
|
577
|
+
# Wait for the quiet original audio to finish.
|
|
578
|
+
await original_playback_queue.join()
|
|
579
|
+
|
|
580
|
+
original_play_task.cancel()
|
|
581
|
+
|
|
582
|
+
await asyncio.gather(
|
|
583
|
+
original_play_task,
|
|
584
|
+
return_exceptions=True,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Wait for Gemini's final response signal.
|
|
588
|
+
# Gemini may not send generation_complete or turn_complete
|
|
589
|
+
# for this translation stream. Wait until the response becomes
|
|
590
|
+
# quiet instead.
|
|
591
|
+
last_activity[0] = time.monotonic()
|
|
592
|
+
|
|
593
|
+
quiet_period = 5.0
|
|
594
|
+
maximum_wait = 45.0
|
|
595
|
+
deadline = time.monotonic() + maximum_wait
|
|
596
|
+
|
|
597
|
+
while time.monotonic() < deadline:
|
|
598
|
+
quiet_for = time.monotonic() - last_activity[0]
|
|
599
|
+
|
|
600
|
+
if quiet_for >= quiet_period:
|
|
601
|
+
break
|
|
602
|
+
|
|
603
|
+
await asyncio.sleep(0.25)
|
|
604
|
+
|
|
605
|
+
if time.monotonic() >= deadline:
|
|
606
|
+
print(
|
|
607
|
+
"[Warning] Gemini response timeout reached.",
|
|
608
|
+
flush=True,
|
|
609
|
+
)
|
|
610
|
+
else:
|
|
611
|
+
print(
|
|
612
|
+
"[Info] Gemini response is quiet. "
|
|
613
|
+
"Finishing playback.",
|
|
614
|
+
flush=True,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# This is now safe because Gemini has been quiet for 5 seconds.
|
|
618
|
+
await audio_queue_output.put(_OUTPUT_END)
|
|
619
|
+
|
|
620
|
+
# Let the player finish every queued audio chunk.
|
|
621
|
+
try:
|
|
622
|
+
await asyncio.wait_for(
|
|
623
|
+
play_task,
|
|
624
|
+
timeout=30.0,
|
|
625
|
+
)
|
|
626
|
+
except asyncio.TimeoutError:
|
|
627
|
+
print(
|
|
628
|
+
"[Warning] Final audio playback timed out.",
|
|
629
|
+
flush=True,
|
|
630
|
+
)
|
|
631
|
+
play_task.cancel()
|
|
632
|
+
|
|
633
|
+
# The receiver should already have ended after
|
|
634
|
+
# generation_complete or turn_complete.
|
|
635
|
+
if not receive_task.done():
|
|
636
|
+
receive_task.cancel()
|
|
637
|
+
|
|
638
|
+
await asyncio.gather(
|
|
639
|
+
receive_task,
|
|
640
|
+
play_task,
|
|
641
|
+
return_exceptions=True,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def Translator(
|
|
646
|
+
url: str,
|
|
647
|
+
target_lang: str,
|
|
648
|
+
api_key: str,
|
|
649
|
+
original_volume: float = 0.08,
|
|
650
|
+
):
|
|
651
|
+
"""Public synchronous API."""
|
|
652
|
+
|
|
653
|
+
if not url:
|
|
654
|
+
print("[Error] URL cannot be empty.")
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
if not target_lang:
|
|
658
|
+
print("[Error] Target language cannot be empty.")
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
if not api_key:
|
|
662
|
+
print("[Error] API key cannot be empty.")
|
|
663
|
+
return
|
|
664
|
+
|
|
665
|
+
if not 0.0 <= original_volume <= 1.0:
|
|
666
|
+
print(
|
|
667
|
+
"[Error] Original volume must be between 0 and 1."
|
|
668
|
+
)
|
|
669
|
+
return
|
|
670
|
+
|
|
671
|
+
return asyncio.run(
|
|
672
|
+
run(
|
|
673
|
+
url=url,
|
|
674
|
+
target_lang=target_lang,
|
|
675
|
+
api_key=api_key,
|
|
676
|
+
original_volume=original_volume,
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
if __name__ == "__main__":
|
|
682
|
+
parser = argparse.ArgumentParser(
|
|
683
|
+
description=(
|
|
684
|
+
"Stream audio from a file or URL and translate it "
|
|
685
|
+
"using Gemini Live."
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
parser.add_argument(
|
|
690
|
+
"--url",
|
|
691
|
+
default=(
|
|
692
|
+
"https://storage.googleapis.com/"
|
|
693
|
+
"generativeai-downloads/gemini-cookbook/audio/"
|
|
694
|
+
"gemini-live-translate-sample.wav"
|
|
695
|
+
),
|
|
696
|
+
help="Audio file path or URL.",
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
parser.add_argument(
|
|
700
|
+
"--target",
|
|
701
|
+
default="bn",
|
|
702
|
+
help="Target language code.",
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
parser.add_argument(
|
|
706
|
+
"--api_key",
|
|
707
|
+
default=os.getenv("GEMINI_API_KEY"),
|
|
708
|
+
help="Gemini API key.",
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
parser.add_argument(
|
|
712
|
+
"--original-volume",
|
|
713
|
+
type=float,
|
|
714
|
+
default=0.08,
|
|
715
|
+
help="Original audio volume from 0.0 to 1.0.",
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
args = parser.parse_args()
|
|
719
|
+
|
|
720
|
+
try:
|
|
721
|
+
asyncio.run(
|
|
722
|
+
run(
|
|
723
|
+
url=args.url,
|
|
724
|
+
target_lang=args.target,
|
|
725
|
+
api_key=args.api_key,
|
|
726
|
+
original_volume=args.original_volume,
|
|
727
|
+
)
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
except KeyboardInterrupt:
|
|
731
|
+
print(
|
|
732
|
+
"\n[Info] Interrupted by user.",
|
|
733
|
+
flush=True,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
except Exception as error:
|
|
737
|
+
print(
|
|
738
|
+
f"\n[Error] {error}",
|
|
739
|
+
flush=True,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
traceback.print_exc()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: translateinpy
|
|
3
|
+
Version: 0.9.6
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: google-genai>=1.35.0
|
|
8
|
+
Requires-Dist: pyaudiowpatch>=0.5.2
|
|
9
|
+
Requires-Dist: ffmpeg>=0.2.0
|
|
10
|
+
|
|
11
|
+
# TranslateINPy
|
|
12
|
+
A python translation module for translating live responses using Gemini API inside your application.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from translateinpy import Translator
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
api_key = os.getenv("GEMINI_API_KEY")
|
|
21
|
+
|
|
22
|
+
Translator("../sample/hi-1.wav", "en", api_key)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install TranslateINPY
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
or via UV:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv add TranslateINPy
|
|
35
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/translateinpy/__init__.py
|
|
4
|
+
src/translateinpy/fromAudioFile.py
|
|
5
|
+
src/translateinpy.egg-info/PKG-INFO
|
|
6
|
+
src/translateinpy.egg-info/SOURCES.txt
|
|
7
|
+
src/translateinpy.egg-info/dependency_links.txt
|
|
8
|
+
src/translateinpy.egg-info/requires.txt
|
|
9
|
+
src/translateinpy.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
translateinpy
|