C36-VCT commited on
Commit
7164652
·
verified ·
1 Parent(s): 5714b23

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -55
app.py CHANGED
@@ -19,7 +19,7 @@ from google import genai
19
  from google.genai import types
20
  from groq import Groq
21
 
22
- # ▼▼▼ [DNS解決パッチ] ▼▼▼
23
  _original_getaddrinfo = socket.getaddrinfo
24
 
25
  def patched_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
@@ -39,7 +39,6 @@ def patched_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
39
  return _original_getaddrinfo(host, port, family, type, proto, flags)
40
 
41
  socket.getaddrinfo = patched_getaddrinfo
42
- # ▲▲▲ パッチここまで ▲▲▲
43
 
44
  load_dotenv()
45
 
@@ -54,8 +53,8 @@ try:
54
  with open("cookies.txt", "w") as f:
55
  f.write(cookie_content)
56
  print("DEBUG: cookies.txt を生成しました")
57
- except Exception as e:
58
- print(f"DEBUG: Cookie生成エラー: {e}")
59
 
60
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
61
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
@@ -71,8 +70,6 @@ if GROQ_API_KEY:
71
  print("✅ Groq Client Initialized")
72
  except Exception as e:
73
  print(f"❌ Groq Init Error: {e}")
74
- else:
75
- print("⚠️ GROQ_API_KEYがありません。暫定文字起こし機能は動きません。")
76
 
77
  sys.stdout.reconfigure(line_buffering=True)
78
 
@@ -132,13 +129,12 @@ class StreamWorker:
132
  os.remove(self.fifo_path)
133
  os.mkfifo(self.fifo_path)
134
 
135
- # ▼▼▼ DEBUG: ログをフル出力設定に変更 ▼▼▼
136
  base_opts = {
137
  'format': 'bestaudio/best',
138
  'outtmpl': self.fifo_path,
139
- 'quiet': False, # ログを出す
140
- 'verbose': True, # 詳細ログを出す
141
- 'no_warnings': False, # 警告も出す
142
  'nocheckcertificate': True,
143
  'socket_timeout': 30,
144
  'retries': 10,
@@ -175,27 +171,43 @@ class StreamWorker:
175
  with yt_dlp.YoutubeDL(opts) as ydl:
176
  ydl.download([self.url])
177
 
178
- # ファイルが生成されたかチェック
179
  if os.path.exists(self.fifo_path):
180
  print(f"✅ ダウンロード成功 [{label}]")
181
  success = True
182
  break
183
  else:
184
- print(f"⚠️ ダウンロード完了したがファイルがない [{label}]")
185
  except Exception as e:
186
  print(f"❌ yt-dlpエラー [{label}]: {e}")
187
  time.sleep(1)
188
  continue
189
 
190
  if not success:
191
- print("❌ 全ての接続方法で失敗")
192
- self.socketio.emit("error_msg", {"msg": "動画にアクセスできませんでした。(ログを確認してください)"}, room=self.sid)
193
  self.is_running = False
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  def _transcribe_interim(self, audio_data):
196
  if not groq_client: return None
197
  try:
198
- audio_file = io.BytesIO(audio_data)
 
 
 
199
  audio_file.name = "audio.wav"
200
  transcription = groq_client.audio.transcriptions.create(
201
  file=audio_file,
@@ -206,26 +218,24 @@ class StreamWorker:
206
  )
207
  return transcription.strip()
208
  except Exception as e:
 
209
  print(f"DEBUG: Groq Error: {e}")
210
  return None
211
 
212
  def _process_audio_final(self, audio_data):
213
- print("DEBUG: Final処理開始 (Groq -> Gemini)")
214
  transcript_text = self._transcribe_interim(audio_data)
215
  if not transcript_text or len(transcript_text) <= 2:
216
- print("DEBUG: Groq結果が空、または短すぎます")
217
  return ""
218
-
219
- print(f"DEBUG: Transcript: {transcript_text[:30]}...")
220
 
221
  clean_text = transcript_text.lower().strip('.').strip()
222
  for phrase in HALLUCINATION_PHRASES:
223
- if clean_text == phrase.lower():
224
- print("DEBUG: 幻覚フィルター検���")
225
- return ""
226
- if transcript_text.startswith('[') and transcript_text.endswith(']'):
227
- return ""
228
 
 
 
 
229
  try:
230
  tgt_lang = self.settings.get('target_lang', 'ja')
231
  tgt_name = "Japanese" if tgt_lang == 'ja' else "English"
@@ -237,7 +247,15 @@ class StreamWorker:
237
  Output format:
238
  Speaker 1: [Translation]
239
  """
240
- contents = [prompt, types.Part.from_bytes(data=audio_data, mime_type="audio/wav")]
 
 
 
 
 
 
 
 
241
  config = types.GenerateContentConfig(temperature=0.0, system_instruction=self.system_instruction)
242
  response = self.client.models.generate_content(model=self.model_name, contents=contents, config=config)
243
 
@@ -257,23 +275,17 @@ class StreamWorker:
257
  self.download_thread.daemon = True
258
  self.download_thread.start()
259
 
260
- # パイプができるまで待機
261
  for i in range(200):
262
- if os.path.exists(self.fifo_path):
263
- print("DEBUG: FIFOパイプを確認しました")
264
- break
265
  time.sleep(0.1)
266
- if i % 50 == 0: print("DEBUG: パイプ生成待ち...")
267
 
268
  if not os.path.exists(self.fifo_path):
269
- print("ERROR: パイプが生成されませんでした (yt-dlp失敗)")
270
  self.stop(); return
271
 
272
  ffmpeg_cmd = ["ffmpeg", "-y", "-i", self.fifo_path, "-f", "s16le", "-acodec", "pcm_s16le", "-ac", "1", "-ar", "16000", "-bufsize", "0", "pipe:1"]
273
  try: self.ffmpeg_proc = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=None)
274
- except Exception as e:
275
- print(f"ERROR: FFmpeg起動失敗: {e}")
276
- self.stop(); return
277
 
278
  print(f"👂 音声待機中... ({self.sid})")
279
 
@@ -282,36 +294,37 @@ class StreamWorker:
282
  BYTES_PER_SAMPLE = 2
283
  CHUNK_BYTES = int(SAMPLE_RATE * CHUNK_DURATION * BYTES_PER_SAMPLE)
284
 
285
- # ▼▼▼ 設定値: 反応が悪い場合はここを調整 ▼▼▼
286
- SILENCE_THRESHOLD = 300 # 音量の閾値 (下げました: 500 -> 300)
287
- SILENCE_DURATION_TRIGGER = 0.8 # 区切りとみなす無音秒数
288
- MAX_RECORD_DURATION = 12.0 # 強制区切り秒数
289
- MIN_RECORD_DURATION = 1.0 # 最短秒数
290
- PREVIEW_INTERVAL = 1.0 # プレビュー更新間隔
291
 
292
  last_preview_time = time.time()
293
  audio_buffer = bytearray()
294
  silence_counter = 0.0
295
  recording_duration = 0.0
296
 
297
- debug_log_counter = 0
298
 
299
  while self.is_running:
300
  try:
301
  chunk = self.ffmpeg_proc.stdout.read(CHUNK_BYTES)
302
- if not chunk:
303
- print("DEBUG: FFmpegストリーム終了")
304
- break
305
 
306
  audio_np = np.frombuffer(chunk, dtype=np.int16)
307
  if len(audio_np) == 0: continue
308
 
309
- rms = np.sqrt(np.mean(audio_np**2))
 
 
 
 
 
310
 
311
- # ▼▼▼ DEBUG: 音量(RMS)を表示 (5回に1回表示してログ流れすぎを防ぐ) ▼▼▼
312
- debug_log_counter += 1
313
- if debug_log_counter % 5 == 0:
314
- print(f"DEBUG: RMS={rms:.1f} | Dur={recording_duration:.1f}s | Silence={silence_counter:.1f}s")
315
 
316
  audio_buffer.extend(chunk)
317
  recording_duration += CHUNK_DURATION
@@ -322,10 +335,10 @@ class StreamWorker:
322
  # 1. 暫定送信
323
  current_time = time.time()
324
  if recording_duration > 0.5 and (current_time - last_preview_time) > PREVIEW_INTERVAL:
325
- print("DEBUG: Interim送信試行...")
326
  interim_text = self._transcribe_interim(bytes(audio_buffer))
327
  if interim_text:
328
- print(f"DEBUG: Interim結果: {interim_text}")
329
  self.socketio.emit("new_subtitle", {
330
  "translated": interim_text,
331
  "is_final": False,
@@ -336,14 +349,13 @@ class StreamWorker:
336
  # 2. 確定条件
337
  should_process = False
338
  if recording_duration > MIN_RECORD_DURATION and silence_counter >= SILENCE_DURATION_TRIGGER:
339
- print("DEBUG: 無音検知による区切り")
340
  should_process = True
341
  elif recording_duration >= MAX_RECORD_DURATION:
342
- print("DEBUG: 最大時間到達による強制区切り")
343
  should_process = True
344
 
345
  # 3. 確定処理
346
  if should_process:
 
347
  audio_bytes = bytes(audio_buffer)
348
  result_text = self._process_audio_final(audio_bytes)
349
 
@@ -374,14 +386,13 @@ class StreamWorker:
374
 
375
  info = session_speakers[self.sid][raw_id]
376
 
377
- print(f"DEBUG: Final送信: {text_content}")
378
  self.socketio.emit("new_subtitle", {
379
  "translated": text_content,
380
  "is_final": True,
381
  "speaker": info["name"], "speaker_id": raw_id, "color": info["color"]
382
  }, room=self.sid)
383
  except Exception as e:
384
- print(f"ERROR: Main loop exception: {e}")
385
  continue
386
  self.stop()
387
 
 
19
  from google.genai import types
20
  from groq import Groq
21
 
22
+ # DNSパッチ
23
  _original_getaddrinfo = socket.getaddrinfo
24
 
25
  def patched_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
 
39
  return _original_getaddrinfo(host, port, family, type, proto, flags)
40
 
41
  socket.getaddrinfo = patched_getaddrinfo
 
42
 
43
  load_dotenv()
44
 
 
53
  with open("cookies.txt", "w") as f:
54
  f.write(cookie_content)
55
  print("DEBUG: cookies.txt を生成しました")
56
+ except Exception:
57
+ pass
58
 
59
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
60
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
 
70
  print("✅ Groq Client Initialized")
71
  except Exception as e:
72
  print(f"❌ Groq Init Error: {e}")
 
 
73
 
74
  sys.stdout.reconfigure(line_buffering=True)
75
 
 
129
  os.remove(self.fifo_path)
130
  os.mkfifo(self.fifo_path)
131
 
 
132
  base_opts = {
133
  'format': 'bestaudio/best',
134
  'outtmpl': self.fifo_path,
135
+ 'quiet': False,
136
+ 'verbose': True,
137
+ 'no_warnings': False,
138
  'nocheckcertificate': True,
139
  'socket_timeout': 30,
140
  'retries': 10,
 
171
  with yt_dlp.YoutubeDL(opts) as ydl:
172
  ydl.download([self.url])
173
 
 
174
  if os.path.exists(self.fifo_path):
175
  print(f"✅ ダウンロード成功 [{label}]")
176
  success = True
177
  break
178
  else:
179
+ print(f"⚠️ ファイル未生成 [{label}]")
180
  except Exception as e:
181
  print(f"❌ yt-dlpエラー [{label}]: {e}")
182
  time.sleep(1)
183
  continue
184
 
185
  if not success:
186
+ self.socketio.emit("error_msg", {"msg": "動画にアクセスできませんでした。"}, room=self.sid)
 
187
  self.is_running = False
188
 
189
+ # ▼▼▼ 重要: 生データをWAVファイル形式に変換する関数 ▼▼▼
190
+ def _create_wav_bytes(self, pcm_data):
191
+ wav_io = io.BytesIO()
192
+ try:
193
+ with wave.open(wav_io, 'wb') as wb:
194
+ wb.setnchannels(1) # モノラル
195
+ wb.setsampwidth(2) # 16bit (2byte)
196
+ wb.setframerate(16000) # 16kHz
197
+ wb.writeframes(pcm_data)
198
+ wav_io.seek(0) # 先頭に戻す
199
+ return wav_io
200
+ except Exception as e:
201
+ print(f"ERROR: WAV変換失敗: {e}")
202
+ return None
203
+
204
  def _transcribe_interim(self, audio_data):
205
  if not groq_client: return None
206
  try:
207
+ # ここで生データではなく、正規のWAVファイルを生成して渡す
208
+ audio_file = self._create_wav_bytes(audio_data)
209
+ if not audio_file: return None
210
+
211
  audio_file.name = "audio.wav"
212
  transcription = groq_client.audio.transcriptions.create(
213
  file=audio_file,
 
218
  )
219
  return transcription.strip()
220
  except Exception as e:
221
+ # 400エラーの原因がこれだったので、詳細を出力
222
  print(f"DEBUG: Groq Error: {e}")
223
  return None
224
 
225
  def _process_audio_final(self, audio_data):
226
+ # 1. Groqで確定文字起こし
227
  transcript_text = self._transcribe_interim(audio_data)
228
  if not transcript_text or len(transcript_text) <= 2:
 
229
  return ""
 
 
230
 
231
  clean_text = transcript_text.lower().strip('.').strip()
232
  for phrase in HALLUCINATION_PHRASES:
233
+ if clean_text == phrase.lower(): return ""
234
+ if transcript_text.startswith('[') and transcript_text.endswith(']'): return ""
 
 
 
235
 
236
+ print(f"DEBUG: Final Transcript: {transcript_text[:30]}...")
237
+
238
+ # 2. Geminiで翻訳
239
  try:
240
  tgt_lang = self.settings.get('target_lang', 'ja')
241
  tgt_name = "Japanese" if tgt_lang == 'ja' else "English"
 
247
  Output format:
248
  Speaker 1: [Translation]
249
  """
250
+
251
+ # Geminiにも生データではなくWAVコンテナに入れたものを渡す方が安全
252
+ wav_file = self._create_wav_bytes(audio_data)
253
+ if not wav_file: return ""
254
+
255
+ # BytesIOからバイト列を取り出して渡す
256
+ wav_bytes = wav_file.getvalue()
257
+
258
+ contents = [prompt, types.Part.from_bytes(data=wav_bytes, mime_type="audio/wav")]
259
  config = types.GenerateContentConfig(temperature=0.0, system_instruction=self.system_instruction)
260
  response = self.client.models.generate_content(model=self.model_name, contents=contents, config=config)
261
 
 
275
  self.download_thread.daemon = True
276
  self.download_thread.start()
277
 
 
278
  for i in range(200):
279
+ if os.path.exists(self.fifo_path): break
 
 
280
  time.sleep(0.1)
 
281
 
282
  if not os.path.exists(self.fifo_path):
283
+ print("ERROR: パイプ生成失敗")
284
  self.stop(); return
285
 
286
  ffmpeg_cmd = ["ffmpeg", "-y", "-i", self.fifo_path, "-f", "s16le", "-acodec", "pcm_s16le", "-ac", "1", "-ar", "16000", "-bufsize", "0", "pipe:1"]
287
  try: self.ffmpeg_proc = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=None)
288
+ except Exception: self.stop(); return
 
 
289
 
290
  print(f"👂 音声待機中... ({self.sid})")
291
 
 
294
  BYTES_PER_SAMPLE = 2
295
  CHUNK_BYTES = int(SAMPLE_RATE * CHUNK_DURATION * BYTES_PER_SAMPLE)
296
 
297
+ SILENCE_THRESHOLD = 300
298
+ SILENCE_DURATION_TRIGGER = 0.8
299
+ MAX_RECORD_DURATION = 12.0
300
+ MIN_RECORD_DURATION = 1.0
301
+ PREVIEW_INTERVAL = 1.0
 
302
 
303
  last_preview_time = time.time()
304
  audio_buffer = bytearray()
305
  silence_counter = 0.0
306
  recording_duration = 0.0
307
 
308
+ debug_cnt = 0
309
 
310
  while self.is_running:
311
  try:
312
  chunk = self.ffmpeg_proc.stdout.read(CHUNK_BYTES)
313
+ if not chunk: break
 
 
314
 
315
  audio_np = np.frombuffer(chunk, dtype=np.int16)
316
  if len(audio_np) == 0: continue
317
 
318
+ # ▼▼▼ nan対策: 数値が正常かチェック ▼▼▼
319
+ if np.isnan(audio_np).any():
320
+ print("DEBUG: NaN detected in audio chunk")
321
+ continue
322
+
323
+ rms = np.sqrt(np.mean(audio_np.astype(float)**2)) # float変換でオーバーフロー防止
324
 
325
+ debug_cnt += 1
326
+ if debug_cnt % 10 == 0:
327
+ print(f"DEBUG: RMS={rms:.1f}")
 
328
 
329
  audio_buffer.extend(chunk)
330
  recording_duration += CHUNK_DURATION
 
335
  # 1. 暫定送信
336
  current_time = time.time()
337
  if recording_duration > 0.5 and (current_time - last_preview_time) > PREVIEW_INTERVAL:
338
+ # print("DEBUG: Interim送信...")
339
  interim_text = self._transcribe_interim(bytes(audio_buffer))
340
  if interim_text:
341
+ # print(f"DEBUG: Interim={interim_text}")
342
  self.socketio.emit("new_subtitle", {
343
  "translated": interim_text,
344
  "is_final": False,
 
349
  # 2. 確定条件
350
  should_process = False
351
  if recording_duration > MIN_RECORD_DURATION and silence_counter >= SILENCE_DURATION_TRIGGER:
 
352
  should_process = True
353
  elif recording_duration >= MAX_RECORD_DURATION:
 
354
  should_process = True
355
 
356
  # 3. 確定処理
357
  if should_process:
358
+ print("DEBUG: Final処理実行")
359
  audio_bytes = bytes(audio_buffer)
360
  result_text = self._process_audio_final(audio_bytes)
361
 
 
386
 
387
  info = session_speakers[self.sid][raw_id]
388
 
 
389
  self.socketio.emit("new_subtitle", {
390
  "translated": text_content,
391
  "is_final": True,
392
  "speaker": info["name"], "speaker_id": raw_id, "color": info["color"]
393
  }, room=self.sid)
394
  except Exception as e:
395
+ print(f"ERROR Loop: {e}")
396
  continue
397
  self.stop()
398