filmy commited on
Commit
d7c2ba6
·
verified ·
1 Parent(s): 3f3e4c9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +169 -339
app.py CHANGED
@@ -11,6 +11,7 @@ import json
11
  import re
12
  import numpy as np
13
  import yt_dlp
 
14
  from flask import Flask, render_template, request
15
  from flask_socketio import SocketIO, emit
16
  from dotenv import load_dotenv
@@ -19,17 +20,21 @@ 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):
26
- if host and isinstance(host, str) and ("youtube" in host or "youtu.be" in host or "googlevideo" in host):
 
 
27
  try:
28
  return _original_getaddrinfo(host, port, family, type, proto, flags)
29
  except Exception:
30
  try:
31
  url = f"https://8.8.8.8/resolve?name={host}&type=A"
32
- r = requests.get(url, timeout=2)
33
  data = r.json()
34
  if "Answer" in data:
35
  ip = data["Answer"][0]["data"]
@@ -40,385 +45,210 @@ def patched_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
40
 
41
  socket.getaddrinfo = patched_getaddrinfo
42
 
 
 
 
43
  load_dotenv()
44
 
45
- # Cookie読み込み
46
- try:
47
- cookie_content = os.getenv("YOUTUBE_COOKIES")
48
- if cookie_content:
49
- if "\\n" in cookie_content:
50
- cookie_content = cookie_content.replace("\\n", "\n")
51
- if not cookie_content.endswith("\n"):
52
- cookie_content += "\n"
53
- with open("cookies.txt", "w") as f:
54
- f.write(cookie_content)
55
- except Exception:
56
- pass
57
-
58
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
59
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
60
- model_name = "gemini-2.0-flash"
61
-
62
- if not GEMINI_API_KEY:
63
- print("⚠️ GEMINI_API_KEYが設定されていません")
64
 
65
- groq_client = None
66
- if GROQ_API_KEY:
67
- try:
68
- groq_client = Groq(api_key=GROQ_API_KEY)
69
- print("✅ Groq Client Initialized")
70
- except Exception:
71
- pass
72
-
73
- sys.stdout.reconfigure(line_buffering=True)
74
 
75
  app = Flask(__name__)
76
- app.config['SECRET_KEY'] = os.getenv("SECRET_KEY", "secret!")
77
- socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
78
 
79
- MAX_CONCURRENT_STREAMS = 10
 
 
 
80
  SPEAKER_COLORS = ["#FFFFFF", "#03DAC6", "#FFB400", "#CF6679", "#BB86FC"]
81
 
82
- # ▼▼▼ 修正: 幻覚フィルターの大幅強化 ▼▼▼
83
- # Whisperが無音時によく出力してしまう無意味なフレーズ集
84
  HALLUCINATION_PHRASES = [
85
- "You", "Thank you", "Thank you.", "Thanks for watching", "Bye",
86
- "Okay", "Okay.", "All right", "music", "[Music]", "Subscribe",
87
- "Copyright", "MBC", "SBS", "news", "Amara", "Subtitles by",
88
- "Caption", "Captions", "Provided by", "http", "www", ".com",
89
- "Silence", "[Silence]", "Audio", "Sound", "noise"
90
  ]
91
 
92
- session_speakers = {}
93
  active_sessions = {}
 
94
 
95
- # ログ抑制用
96
- class QuietLogger:
97
- def debug(self, msg): pass
98
- def warning(self, msg): pass
99
- def error(self, msg): pass
100
-
101
  class StreamWorker:
102
- def __init__(self, sid, url, settings, socket_instance):
103
  self.sid = sid
104
  self.url = url
105
  self.settings = settings
106
- self.socketio = socket_instance
107
  self.is_running = False
108
- self.ffmpeg_proc = None
109
  self.fifo_path = f"/tmp/stream_{sid}.fifo"
110
-
111
- # ▼▼▼ 修正: システムプロンプトの強化 ▼▼▼
112
- # フィラー(言い淀み)の削除と、要約的な翻訳を指示
113
  self.system_instruction = """
114
- You are a professional simultaneous interpreter.
115
-
116
- INPUT DATA:
117
- 1. Transcript: English text transcribed from audio.
118
- 2. Audio: Source sound clip.
119
-
120
- YOUR TASK:
121
- 1. Identify the speaker (Speaker 1, Speaker 2...).
122
- 2. Translate the Transcript into natural, concise Japanese.
123
-
124
- CRITICAL RULES for Translation:
125
- - **REMOVE FILLERS**: Ignore words like "uh", "um", "like", "you know", "I mean", "actually".
126
- - **IGNORE REPETITIONS**: If the user stutters (e.g., "I- I- I went"), translate only the final meaning ("I went").
127
- - **NO HALLUCINATIONS**: If the input text is just "Thank you" or "[Music]" or random noise, output NOTHING (empty string).
128
- - **CONCISE**: Do not translate word-for-word. Capture the *intent*.
129
- - Output format must be strictly: "Speaker X: [Japanese Translation]"
130
- """
131
- self.client = genai.Client(api_key=GEMINI_API_KEY)
132
- self.model_name = model_name
133
 
134
  def start(self):
135
- if self.is_running: return
136
  self.is_running = True
137
- self.socketio.start_background_task(target=self._main_loop)
138
 
139
  def stop(self):
140
  self.is_running = False
141
- if self.ffmpeg_proc and self.ffmpeg_proc.poll() is None:
142
  self.ffmpeg_proc.kill()
143
- if os.path.exists(self.fifo_path):
144
- try: os.remove(self.fifo_path)
145
- except: pass
146
- print(f"🛑 Worker停止: {self.sid}")
147
-
148
- def _run_ytdlp(self):
149
  if os.path.exists(self.fifo_path):
150
  os.remove(self.fifo_path)
151
- os.mkfifo(self.fifo_path)
152
 
153
- base_opts = {
154
- 'format': 'bestaudio/best',
155
- 'outtmpl': self.fifo_path,
156
- 'quiet': True,
157
- 'no_warnings': True,
158
- 'logger': QuietLogger(),
159
- 'nocheckcertificate': True,
160
- 'socket_timeout': 30,
161
- 'retries': 10,
162
- 'fragment_retries': 10,
163
- 'force_ipv4': False,
164
- 'live_from_start': False,
165
- 'wait_for_video': (1, 15),
166
- 'ignoreerrors': True,
167
- 'cachedir': False,
168
- }
169
-
170
- strategies = []
171
- if os.path.exists("cookies.txt"):
172
- opts_cookie = base_opts.copy()
173
- opts_cookie['cookiefile'] = 'cookies.txt'
174
- opts_cookie['extractor_args'] = {'youtube': {'impersonate': ['chrome']}}
175
- strategies.append(("Cookie(Web)", opts_cookie))
176
-
177
- opts_ios = base_opts.copy()
178
- opts_ios['cookiefile'] = None
179
- opts_ios['extractor_args'] = {'youtube': {'player_client': ['ios']}}
180
- strategies.append(("NoCookie(iOS)", opts_ios))
181
-
182
- opts_tv = base_opts.copy()
183
- opts_tv['cookiefile'] = None
184
- opts_tv['extractor_args'] = {'youtube': {'player_client': ['tv_embedded']}}
185
- strategies.append(("NoCookie(TV)", opts_tv))
186
-
187
- success = False
188
- for label, opts in strategies:
189
- if not self.is_running: break
190
- try:
191
- with yt_dlp.YoutubeDL(opts) as ydl:
192
- ydl.download([self.url])
193
- if os.path.exists(self.fifo_path):
194
- success = True
195
- break
196
- except Exception:
197
- time.sleep(1)
198
- continue
199
-
200
- if not success:
201
- if self.is_running: # 停止時はエラー出さない
202
- self.socketio.emit("error_msg", {"msg": "動画にアクセスできませんでした。"}, room=self.sid)
203
- self.is_running = False
204
 
205
- def _create_wav_bytes(self, pcm_data):
206
- wav_io = io.BytesIO()
207
- try:
208
- with wave.open(wav_io, 'wb') as wb:
209
- wb.setnchannels(1)
210
- wb.setsampwidth(2)
211
- wb.setframerate(16000)
212
- wb.writeframes(pcm_data)
213
- wav_io.seek(0)
214
- return wav_io
215
- except Exception:
216
- return None
217
 
218
- def _transcribe_groq(self, audio_data):
219
- if not groq_client: return None
220
- try:
221
- audio_file = self._create_wav_bytes(audio_data)
222
- if not audio_file: return None
223
-
224
- audio_file.name = "audio.wav"
225
- transcription = groq_client.audio.transcriptions.create(
226
- file=audio_file,
227
- model="whisper-large-v3",
228
- response_format="text",
229
- language="en",
230
- temperature=0.0
 
 
 
231
  )
232
- return transcription.strip()
233
- except Exception:
234
- return None
235
-
236
- def _process_audio_hybrid(self, audio_data):
237
- transcript_text = self._transcribe_groq(audio_data)
238
-
239
- # 1. 空文字チェック
240
- if not transcript_text or len(transcript_text) <= 2: return ""
241
-
242
- # 2. 幻覚(NGワード)チェック
243
- clean_text = transcript_text.lower().strip('.').strip()
244
- for phrase in HALLUCINATION_PHRASES:
245
- # フレーズが含まれている、または完全に一致する場合
246
- if phrase.lower() in clean_text or clean_text == phrase.lower():
247
- return ""
248
-
249
- # 3. 記号だけの出力を弾く
250
- if re.match(r'^[\W_]+$', transcript_text): return ""
251
-
252
- # 4. 繰り返し検知("Yeah yeah yeah" -> "Yeah")
253
- # 単純なロジック: 同じ単語が3回以上連続したら怪しいとみなす
254
- words = clean_text.split()
255
- if len(words) > 4 and len(set(words)) == 1: # 全部同じ単語
256
- return ""
257
 
258
- try:
259
- if tgt_lang == 'ja':
260
- filtered = [l for l in raw_text.split('\n') if any(ord(c) > 128 for c in l.split(':',1)[-1])]
261
- return "\n".join(filtered)
262
- return raw_text
263
-
264
- prompt = f"""
265
- Target Transcript: "{transcript_text}"
266
- Instruction:
267
- 1. Identify speakers from the Audio.
268
- 2. Translate the "Target Transcript" above into japanese.
269
- Output format:
270
- Speaker 1: [Translation]
271
- """
272
-
273
- wav_file = self._create_wav_bytes(audio_data)
274
- if not wav_file: return ""
275
- wav_bytes = wav_file.getvalue()
276
-
277
- contents = [prompt, types.Part.from_bytes(data=wav_bytes, mime_type="audio/wav")]
278
- config = types.GenerateContentConfig(temperature=0.0, system_instruction=self.system_instruction)
279
- response = self.client.models.generate_content(model=self.model_name, contents=contents, config=config)
280
-
281
- raw_text = response.text.strip()
282
- if not raw_text: return ""
283
- if tgt_lang == 'ja':
284
- filtered = [l for l in raw_text.split('\n') if any(ord(c) > 128 for c in l.split(':',1)[-1])]
285
- return "\n".join(filtered)
286
- return raw_text
287
- except Exception: return ""
288
 
 
289
  def _main_loop(self):
290
- print(f"🔁 処理開始 [User: {self.sid}] URL: {self.url}")
291
- self.download_thread = threading.Thread(target=self._run_ytdlp)
292
- self.download_thread.daemon = True
293
- self.download_thread.start()
294
-
295
- for _ in range(200):
296
- if os.path.exists(self.fifo_path): break
297
- time.sleep(0.1)
298
-
299
- if not os.path.exists(self.fifo_path): self.stop(); return
300
-
301
- ffmpeg_cmd = ["ffmpeg", "-y", "-i", self.fifo_path, "-f", "s16le", "-acodec", "pcm_s16le", "-ac", "1", "-ar", "16000", "-bufsize", "0", "pipe:1"]
302
- try: self.ffmpeg_proc = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=None)
303
- except Exception: self.stop(); return
304
-
305
- print(f"👂 音声待機中... ({self.sid})")
306
-
307
- CHUNK_DURATION = 0.1
308
- SAMPLE_RATE = 16000
309
- BYTES_PER_SAMPLE = 2
310
- CHUNK_BYTES = int(SAMPLE_RATE * CHUNK_DURATION * BYTES_PER_SAMPLE)
311
-
312
- SILENCE_THRESHOLD = 300
313
- SILENCE_DURATION_TRIGGER = 0.6
314
- MAX_RECORD_DURATION = 6.0
315
- MIN_RECORD_DURATION = 1.0
316
-
317
- audio_buffer = bytearray()
318
- silence_counter = 0.0
319
- recording_duration = 0.0
320
 
321
  while self.is_running:
322
- try:
323
- chunk = self.ffmpeg_proc.stdout.read(CHUNK_BYTES)
324
- if not chunk: break
325
-
326
- audio_np = np.frombuffer(chunk, dtype=np.int16)
327
- if len(audio_np) == 0: continue
328
-
329
- if np.isnan(audio_np).any(): continue
330
- rms = np.sqrt(np.mean(audio_np.astype(float)**2))
331
-
332
- audio_buffer.extend(chunk)
333
- recording_duration += CHUNK_DURATION
334
-
335
- if rms < SILENCE_THRESHOLD:
336
- silence_counter += CHUNK_DURATION
337
- else:
338
- silence_counter = 0.0
339
-
340
- should_process = False
341
-
342
- if recording_duration > MIN_RECORD_DURATION and silence_counter >= SILENCE_DURATION_TRIGGER:
343
- should_process = True
344
-
345
- elif recording_duration >= MAX_RECORD_DURATION:
346
- should_process = True
347
-
348
- if should_process:
349
- audio_bytes = bytes(audio_buffer)
350
- result_text = self._process_audio_hybrid(audio_bytes)
351
-
352
- audio_buffer = bytearray()
353
- recording_duration = 0.0
354
- silence_counter = 0.0
355
-
356
- if not result_text: continue
357
-
358
- for line in result_text.split('\n'):
359
- if ':' in line:
360
- parts = line.split(':', 1)
361
- if len(parts) < 2: continue
362
- raw_id, text_content = parts[0].strip(), parts[1].strip()
363
- if not text_content: continue
364
-
365
- max_spk = self.settings.get('max_speakers', 1)
366
- if max_spk == 1: raw_id = "Speaker 1"
367
- spk_match = re.search(r'\d+', raw_id)
368
- if spk_match and int(spk_match.group()) > max_spk: raw_id = "Speaker 1"
369
-
370
- if self.sid not in session_speakers: session_speakers[self.sid] = {}
371
- if raw_id not in session_speakers[self.sid]:
372
- new_idx = len(session_speakers[self.sid])
373
- session_speakers[self.sid][raw_id] = {"name": raw_id, "color": SPEAKER_COLORS[new_idx % len(SPEAKER_COLORS)]}
374
- self.socketio.emit("initial_speaker_profiles", session_speakers[self.sid], room=self.sid)
375
-
376
- info = session_speakers[self.sid][raw_id]
377
-
378
- self.socketio.emit("new_subtitle", {
379
- "original": "...",
380
- "translated": text_content,
381
- "speaker": info["name"],
382
- "speaker_id": raw_id,
383
- "color": info["color"]
384
- }, room=self.sid)
385
- except Exception: continue
386
  self.stop()
387
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  @app.route("/")
389
- def index(): return render_template("index.html")
390
- @socketio.on('connect')
391
- def handle_connect(): pass
392
- @socketio.on('disconnect')
393
- def handle_disconnect():
394
- if request.sid in active_sessions: active_sessions[request.sid].stop(); del active_sessions[request.sid]
395
- if request.sid in session_speakers: del session_speakers[request.sid]
396
- @socketio.on('start_stream')
397
- def handle_start_stream(data):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  sid = request.sid
399
- if sid in active_sessions: active_sessions[sid].stop()
400
- if len(active_sessions) >= MAX_CONCURRENT_STREAMS: emit("error_msg", {"msg": "混雑中"}, room=sid); return
401
- worker = StreamWorker(sid, data.get('url'), {'max_speakers': int(data.get('max_speakers', 1))}, socketio)
402
- active_sessions[sid] = worker; worker.start(); emit("status_msg", {"msg": "開始"}, room=sid)
403
- @socketio.on('stop_stream')
404
- def handle_stop_stream():
405
- if request.sid in active_sessions: active_sessions[request.sid].stop(); emit("status_msg", {"msg": "停止"}, room=request.sid)
406
- @socketio.on('update_settings')
407
- def handle_settings_update(data):
408
- if request.sid in active_sessions: active_sessions[request.sid].settings.update(data)
409
- @socketio.on('reset_speakers')
410
- def handle_reset_speakers():
411
- if request.sid in session_speakers: session_speakers[request.sid] = {}
412
- @socketio.on('update_speaker_name')
413
- def handle_speaker_name_update(data):
414
- if request.sid in session_speakers and data.get("id") in session_speakers[request.sid]:
415
- session_speakers[request.sid][data.get("id")]["name"] = data.get("new_name")
416
- emit("speaker_name_changed", data, room=request.sid)
417
- @socketio.on('update_speaker_color')
418
- def handle_speaker_color_update(data):
419
- if request.sid in session_speakers and data.get("id") in session_speakers[request.sid]:
420
- session_speakers[request.sid][data.get("id")]["color"] = data.get("color")
421
- emit("speaker_color_changed", data, room=request.sid)
422
 
423
  if __name__ == "__main__":
424
- socketio.run(app, host='0.0.0.0', port=7860, allow_unsafe_werkzeug=True)
 
11
  import re
12
  import numpy as np
13
  import yt_dlp
14
+
15
  from flask import Flask, render_template, request
16
  from flask_socketio import SocketIO, emit
17
  from dotenv import load_dotenv
 
20
  from google.genai import types
21
  from groq import Groq
22
 
23
+ # =====================
24
+ # DNS パッチ
25
+ # =====================
26
  _original_getaddrinfo = socket.getaddrinfo
27
 
28
  def patched_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
29
+ if host and isinstance(host, str) and (
30
+ "youtube" in host or "youtu.be" in host or "googlevideo" in host
31
+ ):
32
  try:
33
  return _original_getaddrinfo(host, port, family, type, proto, flags)
34
  except Exception:
35
  try:
36
  url = f"https://8.8.8.8/resolve?name={host}&type=A"
37
+ r = requests.get(url, timeout=2)
38
  data = r.json()
39
  if "Answer" in data:
40
  ip = data["Answer"][0]["data"]
 
45
 
46
  socket.getaddrinfo = patched_getaddrinfo
47
 
48
+ # =====================
49
+ # 初期化
50
+ # =====================
51
  load_dotenv()
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
54
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
 
 
 
 
55
 
56
+ groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
57
+ genai_client = genai.Client(api_key=GEMINI_API_KEY)
 
 
 
 
 
 
 
58
 
59
  app = Flask(__name__)
60
+ app.config["SECRET_KEY"] = "secret!"
61
+ socketio = SocketIO(app, cors_allowed_origins="*", async_mode="threading")
62
 
63
+ # =====================
64
+ # 定数
65
+ # =====================
66
+ MAX_CONCURRENT_STREAMS = 10
67
  SPEAKER_COLORS = ["#FFFFFF", "#03DAC6", "#FFB400", "#CF6679", "#BB86FC"]
68
 
 
 
69
  HALLUCINATION_PHRASES = [
70
+ "thank you", "thanks", "music", "[music]", "subscribe",
71
+ "copyright", "silence", "[silence]"
 
 
 
72
  ]
73
 
 
74
  active_sessions = {}
75
+ session_speakers = {}
76
 
77
+ # =====================
78
+ # Worker
79
+ # =====================
 
 
 
80
  class StreamWorker:
81
+ def __init__(self, sid, url, settings, socketio_instance):
82
  self.sid = sid
83
  self.url = url
84
  self.settings = settings
85
+ self.socketio = socketio_instance
86
  self.is_running = False
 
87
  self.fifo_path = f"/tmp/stream_{sid}.fifo"
88
+ self.ffmpeg_proc = None
89
+
 
90
  self.system_instruction = """
91
+ You are a professional interpreter.
92
+ Translate spoken English into concise natural Japanese.
93
+ Remove fillers and repetitions.
94
+ If meaningless audio, output nothing.
95
+ Format:
96
+ Speaker 1: 翻訳文
97
+ """
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  def start(self):
 
100
  self.is_running = True
101
+ self.socketio.start_background_task(self._main_loop)
102
 
103
  def stop(self):
104
  self.is_running = False
105
+ if self.ffmpeg_proc:
106
  self.ffmpeg_proc.kill()
 
 
 
 
 
 
107
  if os.path.exists(self.fifo_path):
108
  os.remove(self.fifo_path)
 
109
 
110
+ # ---------- Audio ----------
111
+ def _create_wav(self, pcm):
112
+ buf = io.BytesIO()
113
+ with wave.open(buf, "wb") as w:
114
+ w.setnchannels(1)
115
+ w.setsampwidth(2)
116
+ w.setframerate(16000)
117
+ w.writeframes(pcm)
118
+ buf.seek(0)
119
+ return buf
120
+
121
+ def _transcribe(self, pcm):
122
+ if not groq_client:
123
+ return ""
124
+ wav = self._create_wav(pcm)
125
+ wav.name = "audio.wav"
126
+ result = groq_client.audio.transcriptions.create(
127
+ file=wav,
128
+ model="whisper-large-v3",
129
+ response_format="text",
130
+ language="en",
131
+ temperature=0
132
+ )
133
+ return result.strip()
134
+
135
+ # ---------- Translate ----------
136
+ def _process_audio(self, pcm):
137
+ transcript = self._transcribe(pcm)
138
+ print("🎧 TRANSCRIPT:", transcript)
139
+
140
+ if not transcript or len(transcript) < 3:
141
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ clean = transcript.lower()
144
+ for p in HALLUCINATION_PHRASES:
145
+ if p in clean:
146
+ return ""
 
 
 
 
 
 
 
 
147
 
148
+ prompt = f"""
149
+ Target Transcript: "{transcript}"
150
+ Translate into Japanese.
151
+ """
152
+
153
+ wav = self._create_wav(pcm)
154
+
155
+ response = genai_client.models.generate_content(
156
+ model="gemini-2.0-flash",
157
+ contents=[
158
+ prompt,
159
+ types.Part.from_bytes(wav.getvalue(), "audio/wav")
160
+ ],
161
+ config=types.GenerateContentConfig(
162
+ temperature=0,
163
+ system_instruction=self.system_instruction
164
  )
165
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
+ text = response.text.strip()
168
+ print("📝 TRANSLATED:", text)
169
+ return text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
+ # ---------- Main loop ----------
172
  def _main_loop(self):
173
+ self.is_running = True
174
+
175
+ if os.path.exists(self.fifo_path):
176
+ os.remove(self.fifo_path)
177
+ os.mkfifo(self.fifo_path)
178
+
179
+ threading.Thread(target=self._run_ytdlp, daemon=True).start()
180
+
181
+ ffmpeg = [
182
+ "ffmpeg", "-i", self.fifo_path,
183
+ "-f", "s16le", "-ac", "1", "-ar", "16000", "pipe:1"
184
+ ]
185
+ self.ffmpeg_proc = subprocess.Popen(ffmpeg, stdout=subprocess.PIPE)
186
+
187
+ buffer = bytearray()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  while self.is_running:
190
+ chunk = self.ffmpeg_proc.stdout.read(3200)
191
+ if not chunk:
192
+ break
193
+
194
+ buffer.extend(chunk)
195
+ if len(buffer) > 16000 * 2:
196
+ result = self._process_audio(bytes(buffer))
197
+ buffer.clear()
198
+
199
+ if result:
200
+ self.socketio.emit(
201
+ "new_subtitle",
202
+ {
203
+ "translated": result,
204
+ "speaker": "Speaker 1",
205
+ "color": SPEAKER_COLORS[0]
206
+ },
207
+ room=self.sid
208
+ )
209
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  self.stop()
211
 
212
+ def _run_ytdlp(self):
213
+ opts = {
214
+ "format": "bestaudio",
215
+ "outtmpl": self.fifo_path,
216
+ "quiet": True
217
+ }
218
+ with yt_dlp.YoutubeDL(opts):
219
+ yt_dlp.YoutubeDL(opts).download([self.url])
220
+
221
+ # =====================
222
+ # Routes & Socket
223
+ # =====================
224
  @app.route("/")
225
+ def index():
226
+ return render_template("index.html")
227
+
228
+ @socketio.on("start_stream")
229
+ def start_stream(data):
230
+ sid = request.sid
231
+ if sid in active_sessions:
232
+ active_sessions[sid].stop()
233
+
234
+ worker = StreamWorker(
235
+ sid,
236
+ data["url"],
237
+ {
238
+ "max_speakers": data.get("max_speakers", 1),
239
+ "target_lang": data.get("target_lang", "ja")
240
+ },
241
+ socketio
242
+ )
243
+ active_sessions[sid] = worker
244
+ worker.start()
245
+
246
+ @socketio.on("stop_stream")
247
+ def stop_stream():
248
  sid = request.sid
249
+ if sid in active_sessions:
250
+ active_sessions[sid].stop()
251
+ del active_sessions[sid]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
  if __name__ == "__main__":
254
+ socketio.run(app, host="0.0.0.0", port=7860, allow_unsafe_werkzeug=True)