rein0421 commited on
Commit
0626c7f
·
verified ·
1 Parent(s): 1ca94c7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +764 -341
app.py CHANGED
@@ -1,495 +1,918 @@
1
  from flask import Flask, request, jsonify, render_template, send_from_directory
2
  import base64
3
- from pydub import AudioSegment
4
  import os
5
  import shutil
6
  import requests
7
  import tempfile
8
  import json
9
- from process import AudioProcessor
10
- from transcription import TranscriptionMaker
11
- from analyze import TextAnalyzer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  from flask_cors import CORS
 
 
 
 
13
  process = AudioProcessor()
14
  transcripter = TranscriptionMaker()
15
  app = Flask(__name__)
16
 
17
  # CORS設定: すべてのオリジンからのリクエストを許可
18
- # 必要であれば、特定のオリジンやメソッド、ヘッダーをより厳密に指定できます
19
- # 例: CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}}, supports_credentials=True)
20
- CORS(app, origins="*", methods=["GET", "POST", "DELETE", "OPTIONS"], headers=["Content-Type", "Authorization"])
21
 
22
- # GASのエンドポイントURL
23
  GAS_URL = "https://script.google.com/macros/s/AKfycbwR2cnMKVU1AxoT9NDaeZaNUaTiwRX64Ul0sH0AU4ccP49Byph-TpxtM_Lwm4G9zLnuYA/exec"
24
 
25
- users = [] # 選択されたユーザーの��スト
26
- all_users = [] # 利用可能なすべてのユーザーのリスト
27
- transcription_text = ""
 
 
28
  harassment_keywords = [
29
  "バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい",
30
  "きもい", "キモい", "ブス", "デブ", "ハゲ",
31
  "セクハラ", "パワハラ", "モラハラ"
32
  ]
33
- total_audio = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- @app.route('/index', methods=['GET', 'POST'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  def index():
38
  return render_template('index.html', users=users)
39
 
40
- # フィードバック画面(テンプレート: feedback.html)
41
- @app.route('/feedback', methods=['GET', 'POST'])
42
  def feedback():
43
  return render_template('feedback.html')
44
 
45
- # 会話詳細画面(テンプレート: talkDetail.html)
46
- @app.route('/talk_detail', methods=['GET', 'POST'])
47
  def talk_detail():
48
  return render_template('talkDetail.html')
49
 
50
- # 音声登録画面(テンプレート: userRegister.html)
51
- @app.route('/userregister', methods=['GET', 'POST'])
52
  def userregister():
53
  return render_template('userRegister.html')
54
 
55
- # 人数確認
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  @app.route('/confirm', methods=['GET'])
57
  def confirm():
 
58
  global all_users
59
- # 最新のユーザーリストを取得
60
  try:
61
- update_all_users()
62
  except Exception as e:
63
- print(f"ユーザーリストの更新エラー: {str(e)}")
64
- return jsonify({'members': users, 'all_members': all_users}), 200
 
65
 
66
- # リセット画面(テンプレート: reset.html)
67
- @app.route('/reset_html', methods=['GET', 'POST'])
68
- def reset_html():
69
- return render_template('reset.html')
70
 
71
- # メンバー削除&累積音声削除
72
- @app.route('/reset_member', methods=['GET', 'POST'])
73
  def reset_member():
74
  global users
 
75
  global total_audio
76
  global transcription_text
77
-
78
- # 一時ディレクトリのクリーンアップ
79
- if total_audio:
80
- process.delete_files_in_directory(total_audio)
81
- process.delete_files_in_directory('/tmp/data/transcription_audio')
82
-
83
- # 書き起こしテキストの削除
84
- if os.path.exists(transcription_text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  try:
86
  os.remove(transcription_text)
87
- print(f"{transcription_text} を削除しました。")
88
  except Exception as e:
89
- print(f"ファイル削除中にエラーが発生しました: {e}")
90
-
91
- transcription_text = ""
92
-
 
 
 
 
 
 
 
 
 
93
  try:
94
  data = request.get_json()
95
- if not data or "names" not in data:
96
- return jsonify({"status": "error", "message": "Invalid request body"}), 400
 
 
 
 
 
 
 
 
97
 
98
- names = data.get("names", [])
99
-
100
- # GASからファイルを削除
101
- for name in names:
102
  try:
103
- delete_from_cloud(f"{name}.wav")
104
- print(f"クラウドから {name}.wav を削除しました。")
 
 
 
105
  except Exception as e:
106
- print(f"クラウド削除中にエラーが発生しました: {e}")
107
- return jsonify({"status": "error", "message": f"Failed to delete {name} from cloud: {e}"}), 500
 
108
 
109
- # usersリストから削除するユーザーを除外
110
- users = [u for u in users if u not in names]
111
-
112
- # 全ユーザーリストの更新
113
- update_all_users()
114
-
115
- return jsonify({"status": "success", "message": "Members deleted successfully", "users": users}), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  except Exception as e:
118
- print(f"An unexpected error occurred: {e}")
119
  return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500
120
 
121
- # 書き起こし作成エンドポイント
122
- @app.route('/transcription', methods=['GET', 'POST'])
123
  def transcription():
124
  global transcription_text
125
  global total_audio
126
-
127
- if not os.path.exists(transcription_text) or not transcription_text:
128
- try:
129
- if not total_audio or not os.path.exists(total_audio):
130
- return jsonify({"error": "No audio segments provided"}), 400
 
 
 
 
 
 
 
 
 
131
  transcription_text = transcripter.create_transcription(total_audio)
132
- print("transcription")
133
- print(transcription_text)
 
 
134
  except Exception as e:
135
- return jsonify({"error": str(e)}), 500
136
-
 
 
 
137
  try:
138
  with open(transcription_text, 'r', encoding='utf-8') as file:
139
  file_content = file.read()
140
- print(file_content)
141
- return jsonify({'transcription': file_content}), 200
142
  except FileNotFoundError:
143
- return jsonify({"error": "Transcription file not found"}), 404
 
144
  except Exception as e:
145
- return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
 
146
 
147
- # AI分析エンドポイント
148
- @app.route('/analyze', methods=['GET', 'POST'])
149
  def analyze():
150
  global transcription_text
151
  global total_audio
152
-
153
- if not os.path.exists(transcription_text) or not transcription_text:
154
- try:
155
- if not total_audio:
156
- return jsonify({"error": "No audio segments provided"}), 400
 
 
 
157
  transcription_text = transcripter.create_transcription(total_audio)
 
 
 
158
  except Exception as e:
159
- return jsonify({"error": str(e)}), 500
160
-
161
- analyzer = TextAnalyzer(transcription_text, harassment_keywords)
 
 
162
  api_key = os.environ.get("DEEPSEEK")
163
- if api_key is None:
164
- raise ValueError("DEEPSEEK_API_KEY が設定されていません。")
165
-
166
- results = analyzer.analyze(api_key=api_key)
167
-
168
- print(json.dumps(results, ensure_ascii=False, indent=2))
169
-
170
- if "deepseek_analysis" in results and results["deepseek_analysis"]:
171
- deepseek_data = results["deepseek_analysis"]
172
- conversation_level = deepseek_data.get("conversationLevel")
173
- harassment_present = deepseek_data.get("harassmentPresent")
174
- harassment_type = deepseek_data.get("harassmentType")
175
- repetition = deepseek_data.get("repetition")
176
- pleasantConversation = deepseek_data.get("pleasantConversation")
177
- blameOrHarassment = deepseek_data.get("blameOrHarassment")
178
-
179
- print("\n--- DeepSeek 分析結果 ---")
180
- print(f"会話レベル: {conversation_level}")
181
- print(f"ハラスメントの有無: {harassment_present}")
182
- print(f"ハラスメントの種類: {harassment_type}")
183
- print(f"繰り返しの程度: {repetition}")
184
- print(f"会話の心地よさ: {pleasantConversation}")
185
- print(f"非難またはハラスメントの程度: {blameOrHarassment}")
186
-
187
- return jsonify({"results": results}), 200
188
-
189
-
190
- # クラウドから音声を取得してローカルに保存する関数
191
- def download_from_cloud(filename, local_path):
192
  try:
193
- payload = {
194
- "action": "download",
195
- "fileName": filename
196
- }
197
-
198
- print(f"クラウドから {filename} をダウンロード中...")
199
- response = requests.post(GAS_URL, json=payload)
200
- if response.status_code != 200:
201
- print(f"ダウンロードエラー: ステータスコード {response.status_code}")
202
- print(f"レスポンス: {response.text}")
203
- raise Exception(f"クラウドからのダウンロードに失敗しました: {response.text}")
204
-
205
- try:
206
- res_json = response.json()
207
- except:
208
- print("JSONデコードエラー、レスポンス内容:")
209
- print(response.text[:500]) # 最初の500文字だけ表示
210
- raise Exception("サーバーからの応答をJSONとして解析できませんでした")
211
-
212
- if res_json.get("status") != "success":
213
- print(f"ダウンロードステータスエラー: {res_json.get('message')}")
214
- raise Exception(f"クラウドからのダウンロードに失敗しました: {res_json.get('message')}")
215
-
216
- # Base64文字列をデコード
217
- base64_data = res_json.get("base64Data")
218
- if not base64_data:
219
- print("Base64データが存在しません")
220
- raise Exception("応答にBase64データが含まれていません")
221
-
222
- try:
223
- audio_binary = base64.b64decode(base64_data)
224
- except Exception as e:
225
- print(f"Base64デコードエラー: {str(e)}")
226
- raise Exception(f"音声データのデコードに失敗しました: {str(e)}")
227
-
228
- # 指定パスに保存
229
- os.makedirs(os.path.dirname(local_path), exist_ok=True)
230
- with open(local_path, 'wb') as f:
231
- f.write(audio_binary)
232
-
233
- print(f"{filename} をローカルに保存しました: {local_path}")
234
-
235
- # データの整合性チェック(ファイルサイズが0より大きいかなど)
236
- if os.path.getsize(local_path) <= 0:
237
- raise Exception(f"保存されたファイル {local_path} のサイズが0バイトです")
238
-
239
- return local_path
240
  except Exception as e:
241
- print(f"ダウンロード中にエラーが発生しました: {str(e)}")
242
- # エラーを上位に伝播させる
243
- raise
244
 
245
- # クラウドからファイルを削除する関数
246
- def delete_from_cloud(filename):
247
- payload = {
248
- "action": "delete",
249
- "fileName": filename
250
- }
251
- response = requests.post(GAS_URL, json=payload)
252
- if response.status_code != 200:
253
- raise Exception(f"クラウドからの削除に失敗しました: {response.text}")
254
-
255
- res_json = response.json()
256
- if res_json.get("status") != "success":
257
- raise Exception(f"クラウドからの削除に失敗しました: {res_json.get('message')}")
258
-
259
- return True
260
- # すべてのベース音声ユーザーリストを更新する関数
261
- def update_all_users():
262
- global all_users
263
-
264
- payload = {"action": "list"}
265
- response = requests.post(GAS_URL, json=payload)
266
- if response.status_code != 200:
267
- raise Exception(f"GAS一覧取得エラー: {response.text}")
268
-
269
- res_json = response.json()
270
- if res_json.get("status") != "success":
271
- raise Exception(f"GAS一覧取得失敗: {res_json.get('message')}")
272
-
273
- # ファイル名から拡張子を除去してユーザーリストを作成
274
- all_users = [os.path.splitext(filename)[0] for filename in res_json.get("fileNames", [])]
275
- return all_users
276
-
277
- # 音声アップロード&解析エンドポイント
278
  @app.route('/upload_audio', methods=['POST'])
279
  def upload_audio():
280
  global total_audio
281
  global users
282
-
 
 
 
283
  try:
284
  data = request.get_json()
285
  if not data or 'audio_data' not in data:
286
- return jsonify({"error": "音声データがありません"}), 400
287
-
288
- # リクエストからユーザーリストを取得(指定がなければ現在のusersを使用)
289
- if 'selected_users' in data and data['selected_users']:
290
- users = data['selected_users']
291
- print(f"選択されたユーザー: {users}")
 
 
 
 
 
 
 
292
 
293
- if not users:
294
- return jsonify({"error": "選択されたユーザーがいません"}), 400
295
 
296
- # Base64デコードして音声バイナリを取得
297
  audio_binary = base64.b64decode(data['audio_data'])
298
-
299
- upload_name = 'tmp'
300
- audio_dir = "/tmp/data"
 
 
301
  os.makedirs(audio_dir, exist_ok=True)
302
- audio_path = os.path.join(audio_dir, f"{upload_name}.wav")
303
- with open(audio_path, 'wb') as f:
 
 
304
  f.write(audio_binary)
305
-
306
- print(f"処理を行うユーザー: {users}")
307
-
308
- # ベース音声を一時ディレクトリにダウンロード
309
- temp_dir = "/tmp/data/base_audio"
310
- os.makedirs(temp_dir, exist_ok=True)
311
-
312
- # 各ユーザーの参照音声ファイルのパスをリストに格納
313
  reference_paths = []
314
- for user in users:
 
 
315
  try:
316
- ref_path = os.path.join(temp_dir, f"{user}.wav")
317
- if not os.path.exists(ref_path):
318
- # クラウドから取得
319
- download_from_cloud(f"{user}.wav", ref_path)
320
- print(f"クラウドから {user}.wav をダウンロードしました")
321
-
322
- if not os.path.exists(ref_path):
323
- return jsonify({"error": "参照音声ファイルが見つかりません", "details": ref_path}), 500
324
-
325
- reference_paths.append(ref_path)
 
 
 
326
  except Exception as e:
327
- return jsonify({"error": f"ユーザー {user} の音声取得に失敗しました", "details": str(e)}), 500
328
-
329
- # 複数人の場合は参照パスのリストを、1人の場合は単一のパスを渡す
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  if len(users) > 1:
331
- print("複数人の場合の処理")
332
- matched_times, merged_segments = process.process_multi_audio(reference_paths, audio_path, users, threshold=0.05)
333
- total_audio = transcripter.save_marged_segments(merged_segments)
334
- # 各メンバーのrateを計算
335
- total_time = sum(matched_times)
336
  rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times]
337
-
338
- # ユーザー名と話した割合をマッピング
339
  user_rates = {users[i]: rates[i] for i in range(len(users))}
340
- return jsonify({"rates": rates, "user_rates": user_rates}), 200
 
341
  else:
342
- matched_time, unmatched_time, merged_segments = process.process_audio(reference_paths[0], audio_path, users[0], threshold=0.05)
343
- total_audio = transcripter.save_marged_segments(merged_segments)
344
- print("単一ユーザーの処理")
 
345
  total_time = matched_time + unmatched_time
346
  rate = (matched_time / total_time) * 100 if total_time > 0 else 0
347
- return jsonify({"rate": rate, "user": users[0]}), 200
348
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  except Exception as e:
350
- print("Error in /upload_audio:", str(e))
351
- return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
- # ユーザー選択画面(テンプレート: userSelect.html)
354
- @app.route('/')
355
- @app.route('/userselect', methods=['GET'])
356
- def userselect():
357
- return render_template('userSelect.html')
358
 
359
  # 選択したユーザーを設定するエンドポイント
360
  @app.route('/select_users', methods=['POST'])
361
  def select_users():
362
  global users
363
-
 
364
  try:
365
  data = request.get_json()
366
- if not data or 'users' not in data:
367
- return jsonify({"error": "ユーザーリストがありません"}), 400
368
-
369
- users = data['users']
370
- print(f"選択されたユーザー: {users}")
371
-
372
- return jsonify({"status": "success", "selected_users": users}), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  except Exception as e:
374
- print("Error in /select_users:", str(e))
375
- return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
376
 
377
- @app.route('/reset', methods=['GET'])
 
378
  def reset():
379
  global users
380
- users = []
381
  global total_audio
382
  global transcription_text
383
-
384
- # 一時ディレクトリのクリーンアップ
385
- if total_audio:
386
- process.delete_files_in_directory(total_audio)
387
- process.delete_files_in_directory('/tmp/data/transcription_audio')
388
-
389
- # 書き起こしテキストの削除
390
- if os.path.exists(transcription_text):
391
  try:
392
- os.remove(transcription_text)
393
- print(f"{transcription_text} を削除しました。")
394
- except Exception as e:
395
- print(f"ファイル削除中にエラーが発生しました: {e}")
396
-
 
 
 
397
  transcription_text = ""
398
 
399
- return jsonify({"status": "success", "message": "Users reset"}), 200
 
 
 
 
 
 
 
400
 
 
 
401
  @app.route('/copy_selected_files', methods=['POST'])
402
  def copy_selected_files():
 
 
 
403
  try:
404
  data = request.get_json()
405
- if not data or "names" not in data:
406
- return jsonify({"error": "namesパラメータが存在しません"}), 400
 
 
 
 
407
 
408
- names = data["names"]
409
- dest_dir = "/tmp/data/selected_audio" # コピー先のフォルダ
410
  os.makedirs(dest_dir, exist_ok=True)
 
411
 
412
  copied_files = []
413
- for name in names:
 
 
414
  dest_path = os.path.join(dest_dir, f"{name}.wav")
415
  try:
416
- # クラウドから直接ダウンロード
417
  download_from_cloud(f"{name}.wav", dest_path)
418
  copied_files.append(name)
419
- print(f"{name}.wav {dest_path} にダウンロードしました。")
420
  except Exception as e:
421
- print(f"ダウンロード中にエラーが発生しました: {e}")
422
- continue
423
-
424
- return jsonify({"status": "success", "copied": copied_files}), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
  except Exception as e:
427
- print("Error in /copy_selected_files:", str(e))
428
- return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500
 
429
 
430
- @app.route('/clear_tmp', methods=['GET'])
 
431
  def clear_tmp():
 
 
 
 
 
 
 
 
432
  try:
433
- tmp_dir = "/tmp/data" # アプリケーションが使用しているtmpフォルダ
434
- # ファイルのみの削除
435
- process.delete_files_in_directory(tmp_dir)
436
- # フォルダがあれば再帰的に削除
437
- for item in os.listdir(tmp_dir):
438
- item_path = os.path.join(tmp_dir, item)
439
- if os.path.isdir(item_path):
440
- shutil.rmtree(item_path)
441
- print(f"ディレクトリを削除しました: {item_path}")
442
-
443
- return jsonify({"status": "success", "message": "tmp配下がすべて削除されました"}), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
  except Exception as e:
446
- print("Error in /clear_tmp:", str(e))
447
- return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500
 
448
 
 
449
  @app.route('/upload_base_audio', methods=['POST'])
450
  def upload_base_audio():
451
- global all_users
452
-
453
  try:
454
  data = request.get_json()
455
  if not data or 'audio_data' not in data or 'name' not in data:
456
- return jsonify({"error": "音声データまたは名前がありません"}), 400
457
- name = data['name']
458
- print(f"登録名: {name}")
459
-
460
- # GASのアップロードエンドポイントにリクエスト
 
 
 
 
 
 
461
  payload = {
462
  "action": "upload",
463
- "fileName": f"{name}.wav",
464
  "base64Data": data['audio_data']
465
  }
466
-
467
- response = requests.post(GAS_URL, json=payload)
468
- if response.status_code != 200:
469
- return jsonify({"error": "GASアップロードエラー", "details": response.text}), 500
470
-
471
- res_json = response.json()
472
- if res_json.get("status") != "success":
473
- return jsonify({"error": "GASアップロード失敗", "details": res_json.get("message")}), 500
474
-
475
- # 全ユーザーリストを更新
476
- update_all_users()
477
-
478
- return jsonify({"state": "Registration Success!", "driveFileId": res_json.get("fileId")}), 200
479
- except Exception as e:
480
- print("Error in /upload_base_audio:", str(e))
481
- return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
482
 
483
- @app.route('/list_base_audio', methods=['GET'])
484
- def list_base_audio():
485
- try:
486
- global all_users
487
- all_users = update_all_users()
488
- return jsonify({"status": "success", "fileNames": all_users}), 200
489
- except Exception as e:
490
- print("Error in /list_base_audio:", str(e))
491
- return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  if __name__ == '__main__':
494
  port = int(os.environ.get("PORT", 7860))
 
 
 
495
  app.run(debug=True, host="0.0.0.0", port=port)
 
1
  from flask import Flask, request, jsonify, render_template, send_from_directory
2
  import base64
3
+ # from pydub import AudioSegment # pydubは直接使われていないようなのでコメントアウトしても良いかも
4
  import os
5
  import shutil
6
  import requests
7
  import tempfile
8
  import json
9
+ # --- 必要に応じて実際のクラス/モジュールをインポート ---
10
+ try:
11
+ from process import AudioProcessor
12
+ from transcription import TranscriptionMaker
13
+ from analyze import TextAnalyzer
14
+ except ImportError:
15
+ print("警告: process, transcription, analyze モジュールが見つかりません。ダミークラスを使用します。")
16
+ # --- ダミークラス (実際のクラスがない場合のエラー回避用) ---
17
+ class AudioProcessor:
18
+ def delete_files_in_directory(self, path):
19
+ print(f"Dummy: Deleting files in {path}")
20
+ if os.path.isdir(path):
21
+ # ダミー実装: フォルダ内のファイルを削除するふり
22
+ for item in os.listdir(path):
23
+ item_path = os.path.join(path, item)
24
+ if os.path.isfile(item_path):
25
+ print(f"Dummy: Removing file {item_path}")
26
+ # os.remove(item_path) # 実際には削除しない
27
+ elif os.path.isdir(item_path):
28
+ print(f"Dummy: Removing directory {item_path}")
29
+ # shutil.rmtree(item_path) # 実際には削除しない
30
+
31
+ def process_multi_audio(self, *args, **kwargs):
32
+ print("Dummy: Processing multi audio")
33
+ return [10, 5], {} # ダミーの戻り値
34
+ def process_audio(self, *args, **kwargs):
35
+ print("Dummy: Processing single audio")
36
+ return 10, 5, {} # ダミーの戻り値
37
+ class TranscriptionMaker:
38
+ def create_transcription(self, path):
39
+ print(f"Dummy: Creating transcription for {path}")
40
+ dummy_path = os.path.join(tempfile.gettempdir(), "dummy_transcription.txt")
41
+ try:
42
+ with open(dummy_path, "w", encoding='utf-8') as f:
43
+ f.write("これはダミーの書き起こしです。")
44
+ except Exception as e:
45
+ print(f"Dummy transcription file write error: {e}")
46
+ # エラーが発生してもダミーパスを返す
47
+ return dummy_path
48
+ def save_marged_segments(self, segments):
49
+ print("Dummy: Saving merged segments")
50
+ dummy_path = os.path.join(tempfile.gettempdir(), "dummy_merged_audio.wav")
51
+ try:
52
+ # ダミーファイルを作成(空でも良い)
53
+ with open(dummy_path, 'w') as f: pass
54
+ except Exception as e:
55
+ print(f"Dummy merged audio file create error: {e}")
56
+ return dummy_path
57
+ class TextAnalyzer:
58
+ def __init__(self, text_path, keywords):
59
+ print(f"Dummy: Initializing analyzer for {text_path}")
60
+ self.text_path = text_path
61
+ self.keywords = keywords
62
+ def analyze(self, api_key):
63
+ print("Dummy: Analyzing text")
64
+ # 環境変数チェックのダミー
65
+ if not api_key:
66
+ print("Warning: Dummy DEEPSEEK API key not provided.")
67
+ return {"dummy_analysis": "ok", "deepseek_analysis": {"conversationLevel": 5, "harassmentPresent": False}} # ダミーの戻り値
68
+ # ------------------------------------------------------------
69
+
70
  from flask_cors import CORS
71
+
72
+ # --- グローバル変数と初期化 ---
73
+ # 注意: グローバル変数はリクエスト間で共有されるため、同時アクセス時に問題が発生する可能性があります。
74
+ # 本番環境では、データベースやセッション管理などのより堅牢な状態管理を検討してください。
75
  process = AudioProcessor()
76
  transcripter = TranscriptionMaker()
77
  app = Flask(__name__)
78
 
79
  # CORS設定: すべてのオリジンからのリクエストを許可
80
+ # DELETEメソッドや特定のヘッダー(Content-Typeなど)も許可する
81
+ CORS(app, origins="*", methods=["GET", "POST", "DELETE", "PUT", "OPTIONS"], headers=["Content-Type", "Authorization"])
 
82
 
83
+ # GASのエンドポイントURL (実際のURLに置き換えてください)
84
  GAS_URL = "https://script.google.com/macros/s/AKfycbwR2cnMKVU1AxoT9NDaeZaNUaTiwRX64Ul0sH0AU4ccP49Byph-TpxtM_Lwm4G9zLnuYA/exec"
85
 
86
+ users = [] # 現在選択されているユーザー名のリスト
87
+ all_users = [] # 利用可能なすべてのユーザー名のリスト (ファイル名から拡張子を除いたもの)
88
+ transcription_text = "" # 書き起こしファイルのパス
89
+ total_audio = "" # マージされた音声ファイルのパス
90
+
91
  harassment_keywords = [
92
  "バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい",
93
  "きもい", "キモい", "ブス", "デブ", "ハゲ",
94
  "セクハラ", "パワハラ", "モラハラ"
95
  ]
96
+ # --- ここまでグローバル変数と初期化 ---
97
+
98
+
99
+ # === ヘルパー関数 ===
100
+
101
+ def _make_gas_request(payload, timeout=30):
102
+ """GASエンドポイントへのPOSTリクエストを送信し、レスポンスJSONを返す"""
103
+ try:
104
+ print(f"GAS Request Payload: {payload}")
105
+ response = requests.post(GAS_URL, json=payload, timeout=timeout)
106
+ response.raise_for_status() # HTTPエラー (4xx, 5xx) があれば例外を発生させる
107
+ res_json = response.json()
108
+ print(f"GAS Response: {res_json}")
109
+ if res_json.get("status") != "success":
110
+ # GAS側でエラーが報告された場合
111
+ raise Exception(f"GAS Error: {res_json.get('message', 'Unknown error from GAS')}")
112
+ return res_json
113
+ except requests.exceptions.Timeout:
114
+ print(f"Error: GAS request timed out after {timeout} seconds.")
115
+ raise TimeoutError(f"GAS request timed out ({timeout}s)")
116
+ except requests.exceptions.RequestException as e:
117
+ print(f"Error: Failed to connect to GAS: {e}")
118
+ raise ConnectionError(f"Failed to connect to GAS: {e}")
119
+ except json.JSONDecodeError as e:
120
+ print(f"Error: Failed to decode JSON response from GAS: {e}")
121
+ print(f"GAS Raw Response Text: {response.text[:500]}...") # レスポンス内容の一部を表示
122
+ raise ValueError(f"Invalid JSON response from GAS: {e}")
123
+ except Exception as e:
124
+ # _make_gas_request 内で発生したその他の予期せぬエラー
125
+ print(f"Error during GAS request processing: {e}")
126
+ raise # 元のエラーを再発生させる
127
+
128
+
129
+ def download_from_cloud(filename_with_ext, local_path):
130
+ """クラウド(GAS)から指定されたファイルをダウンロードし、ローカルパスに保存する"""
131
+ payload = {"action": "download", "fileName": filename_with_ext}
132
+ try:
133
+ print(f"Downloading {filename_with_ext} from cloud...")
134
+ res_json = _make_gas_request(payload, timeout=60) # ダウンロードは時間がかかる可能性があるため長めのタイムアウト
135
+
136
+ base64_data = res_json.get("base64Data")
137
+ if not base64_data:
138
+ raise ValueError("No base64Data found in the download response.")
139
+
140
+ audio_binary = base64.b64decode(base64_data)
141
+ if not audio_binary:
142
+ raise ValueError("Failed to decode base64 data.")
143
+
144
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
145
+ with open(local_path, 'wb') as f:
146
+ f.write(audio_binary)
147
+
148
+ print(f"Successfully downloaded and saved to {local_path}")
149
+ if os.path.getsize(local_path) <= 0:
150
+ # ダウンロードしたがファイルサイズが0の場合
151
+ os.remove(local_path) # 不完全なファイルを削除
152
+ raise ValueError(f"Downloaded file {local_path} is empty.")
153
+
154
+ return local_path
155
+ except (TimeoutError, ConnectionError, ValueError, Exception) as e:
156
+ print(f"Error downloading {filename_with_ext}: {str(e)}")
157
+ # エラーが発生したら上位に伝播させる
158
+ raise
159
+
160
+
161
+ def delete_from_cloud(filename_with_ext):
162
+ """クラウド(GAS)から指定されたファイルを削除する"""
163
+ payload = {"action": "delete", "fileName": filename_with_ext}
164
+ try:
165
+ print(f"Deleting {filename_with_ext} from cloud...")
166
+ _make_gas_request(payload, timeout=30)
167
+ print(f"Successfully deleted {filename_with_ext} from cloud.")
168
+ return True
169
+ except (TimeoutError, ConnectionError, ValueError, Exception) as e:
170
+ print(f"Error deleting {filename_with_ext}: {str(e)}")
171
+ # エラーが発生したら上位に伝播させる
172
+ raise
173
+
174
+
175
+ def update_all_users():
176
+ """GASから最新のファイルリストを取得し、グローバル変数 all_users を更新する"""
177
+ global all_users
178
+ payload = {"action": "list"}
179
+ try:
180
+ print("Fetching user list from cloud...")
181
+ res_json = _make_gas_request(payload, timeout=30)
182
+ # ファイル名から拡張子を除去してリストを作成 (空のファイル名を除外)
183
+ current_users = [os.path.splitext(name)[0] for name in res_json.get("fileNames", []) if name and '.' in name]
184
+ all_users = sorted(list(set(current_users))) # 重複除去とソート
185
+ print(f"Updated all_users list: {all_users}")
186
+ return all_users # 更新後のリストを返す
187
+ except (TimeoutError, ConnectionError, ValueError, Exception) as e:
188
+ print(f"Error updating all_users list: {str(e)}")
189
+ # エラーが発生しても、既存の all_users は変更せず、エラーを上位に伝播
190
+ raise
191
+
192
+ # === Flask ルート定義 ===
193
 
194
+ # React用: ユーザーリスト取得エンドポイント (JSON構造は変更しない)
195
+ @app.route('/list_base_audio', methods=['GET'])
196
+ def list_base_audio():
197
+ """現在の all_users リストを返す (React側で処理が必要)"""
198
+ global all_users
199
+ try:
200
+ # 関数を呼び出して最新の状態を取得・更新
201
+ update_all_users()
202
+ # 元の形式で返す
203
+ return jsonify({"status": "success", "fileNames": all_users}), 200
204
+ except (TimeoutError, ConnectionError, ValueError, Exception) as e:
205
+ # GASとの通信エラーなどでリスト取得に失敗した場合
206
+ print(f"Error in /list_base_audio: {str(e)}")
207
+ # エラー発生時は空リストまたはエラーメッセージを返す
208
+ # return jsonify({"status": "error", "message": "Failed to retrieve user list", "details": str(e)}), 500
209
+ # または、現状保持しているリストがあればそれを返し、エラーをログに残すだけでも良いかもしれない
210
+ print("Returning potentially stale user list due to update error.")
211
+ return jsonify({"status": "warning", "message": "Could not refresh list, returning cached data.", "fileNames": all_users}), 200 # 警告付きで成功扱いにするか、5xxエラーにするかは要件次第
212
+
213
+ # React用: ユーザー削除エンドポイント
214
+ @app.route('/api/users/<user_name>', methods=['DELETE'])
215
+ def delete_user_api(user_name):
216
+ """指定されたユーザー名のファイルをクラウドから削除し、ローカルリストも更新"""
217
+ global users
218
+ global all_users
219
+
220
+ if not user_name:
221
+ return jsonify({"status": "error", "message": "User name cannot be empty"}), 400
222
 
223
+ filename_to_delete = f"{user_name}.wav" # GAS側で想定されるファイル名
224
+
225
+ try:
226
+ print(f"API delete request for user: {user_name}")
227
+ # 1. クラウドから削除
228
+ delete_from_cloud(filename_to_delete)
229
+
230
+ # 2. ローカルリストから削除 (成功した場合のみ)
231
+ deleted_from_local = False
232
+ if user_name in all_users:
233
+ all_users.remove(user_name)
234
+ print(f"Removed '{user_name}' from all_users list.")
235
+ deleted_from_local = True
236
+ if user_name in users:
237
+ users.remove(user_name)
238
+ print(f"Removed '{user_name}' from selected users (users) list.")
239
+ deleted_from_local = True
240
+
241
+ if not deleted_from_local:
242
+ print(f"Warning: User '{user_name}' was not found in local lists (all_users, users) but deletion from cloud was attempted/successful.")
243
+
244
+ # 成功レスポンス
245
+ return jsonify({"status": "success", "message": f"User '{user_name}' deleted successfully."}), 200
246
+
247
+ except FileNotFoundError:
248
+ # delete_from_cloud 内で発生した場合(GASがファイルなしと応答した場合など)
249
+ # または、ローカルリストにそもそも存在しなかった場合
250
+ print(f"User '{user_name}' not found for deletion (either on cloud or locally).")
251
+ # 念のためローカルリストからも削除試行(既になくてもエラーにはならない)
252
+ if user_name in all_users: all_users.remove(user_name)
253
+ if user_name in users: users.remove(user_name)
254
+ return jsonify({"status": "error", "message": f"User '{user_name}' not found."}), 404 # Not Found
255
+ except (TimeoutError, ConnectionError, ValueError, Exception) as e:
256
+ # GAS通信エラーやその他の予期せぬエラー
257
+ error_message = f"Failed to delete user '{user_name}'."
258
+ print(f"{error_message} Details: {str(e)}")
259
+ # クライアントには詳細なエラー原因を伏せる場合もある
260
+ return jsonify({"status": "error", "message": error_message, "details": str(e)}), 500 # Internal Server Error
261
+
262
+ # --- 既存のテンプレートレンダリング用ルート (変更なし) ---
263
+ @app.route('/index', methods=['GET']) # POST不要なら削除
264
  def index():
265
  return render_template('index.html', users=users)
266
 
267
+ @app.route('/feedback', methods=['GET'])
 
268
  def feedback():
269
  return render_template('feedback.html')
270
 
271
+ @app.route('/talk_detail', methods=['GET'])
 
272
  def talk_detail():
273
  return render_template('talkDetail.html')
274
 
275
+ @app.route('/userregister', methods=['GET'])
 
276
  def userregister():
277
  return render_template('userRegister.html')
278
 
279
+ @app.route('/reset_html', methods=['GET'])
280
+ def reset_html():
281
+ return render_template('reset.html')
282
+
283
+ @app.route('/', methods=['GET']) # ルートパス
284
+ @app.route('/userselect', methods=['GET'])
285
+ def userselect():
286
+ # ユーザー選択画面表示時に最新リストを取得・表示するならここで update_all_users() を呼ぶ
287
+ # try:
288
+ # update_all_users()
289
+ # except Exception as e:
290
+ # print(f"Failed to update users on loading userselect page: {e}")
291
+ # return render_template('userSelect.html', available_users=all_users) # テンプレートに渡す場合
292
+ return render_template('userSelect.html') # テンプレート側でAPIを叩く場合
293
+ # --- ここまでテンプレートレンダリング用ルート ---
294
+
295
+
296
+ # --- 既存のAPIエンドポイント (一部見直し) ---
297
+
298
+ # 人数確認 (最新リストを返すように修正)
299
  @app.route('/confirm', methods=['GET'])
300
  def confirm():
301
+ global users
302
  global all_users
 
303
  try:
304
+ update_all_users() # 最新の状態に更新試行
305
  except Exception as e:
306
+ print(f"ユーザーリストの更新エラー (/confirm): {str(e)}")
307
+ # エラーでも現在のリストを返す(あるいはエラーを示す)
308
+ return jsonify({'selected_members': users, 'all_available_members': all_users}), 200
309
 
 
 
 
 
310
 
311
+ # 複数メンバー削除 (POST推奨)
312
+ @app.route('/reset_member', methods=['POST']) # GETよりPOSTが適切
313
  def reset_member():
314
  global users
315
+ global all_users
316
  global total_audio
317
  global transcription_text
318
+
319
+ # --- 一時ファイル/変数クリア ---
320
+ print("Resetting temporary files and variables...")
321
+ # total_audio パスの処理 (存在チェックと種類判別)
322
+ if total_audio and os.path.exists(total_audio):
323
+ try:
324
+ if os.path.isfile(total_audio):
325
+ os.remove(total_audio)
326
+ print(f"Deleted file: {total_audio}")
327
+ elif os.path.isdir(total_audio):
328
+ # ディレクトリ内のファイルを削除 (AudioProcessorに任せる)
329
+ process.delete_files_in_directory(total_audio)
330
+ print(f"Cleared files in directory: {total_audio}")
331
+ # 必要ならディレクトリ自体も削除: shutil.rmtree(total_audio)
332
+ else:
333
+ print(f"Warning: Path exists but is not a file or directory: {total_audio}")
334
+ except Exception as e:
335
+ print(f"Error clearing total_audio path '{total_audio}': {e}")
336
+ total_audio = "" # パスをクリア
337
+
338
+ # 書き起こしテキストファイルの削除
339
+ if transcription_text and os.path.exists(transcription_text):
340
  try:
341
  os.remove(transcription_text)
342
+ print(f"Deleted transcription file: {transcription_text}")
343
  except Exception as e:
344
+ print(f"Error deleting transcription file '{transcription_text}': {e}")
345
+ transcription_text = "" # パスをクリア
346
+
347
+ # その他の関連一時ディレクトリのクリア (存在しなければ何もしない)
348
+ try:
349
+ transcription_audio_dir = '/tmp/data/transcription_audio'
350
+ if os.path.isdir(transcription_audio_dir):
351
+ process.delete_files_in_directory(transcription_audio_dir)
352
+ print(f"Cleared files in directory: {transcription_audio_dir}")
353
+ except Exception as e:
354
+ print(f"Error clearing transcription_audio directory: {e}")
355
+ # --- ここまで一時ファイル/変数クリア ---
356
+
357
  try:
358
  data = request.get_json()
359
+ if not data or "names" not in data or not isinstance(data["names"], list):
360
+ return jsonify({"status": "error", "message": "Invalid request body, 'names' array is required"}), 400
361
+
362
+ names_to_delete = data["names"]
363
+ if not names_to_delete:
364
+ return jsonify({"status": "warning", "message": "No names provided for deletion."}), 200 # 何もしないで成功
365
+
366
+ print(f"Bulk delete request for names: {names_to_delete}")
367
+ deleted_names = []
368
+ errors = []
369
 
370
+ for name in names_to_delete:
371
+ if not name: continue # 空の名前はスキップ
372
+ filename_to_delete = f"{name}.wav"
 
373
  try:
374
+ delete_from_cloud(filename_to_delete)
375
+ deleted_names.append(name)
376
+ # ローカルリストからも削除
377
+ if name in all_users: all_users.remove(name)
378
+ if name in users: users.remove(name)
379
  except Exception as e:
380
+ error_msg = f"Failed to delete '{name}': {str(e)}"
381
+ print(error_msg)
382
+ errors.append({"name": name, "error": str(e)})
383
 
384
+ # 削除処理後に最新のユーザーリストをGASから取得して同期するのが望ましい
385
+ final_users_list = all_users # エラーがあっても現在のリストを返す
386
+ try:
387
+ final_users_list = update_all_users()
388
+ except Exception as e:
389
+ print(f"Failed to refresh user list after bulk delete: {e}")
390
+ errors.append({"name": "N/A", "error": f"Failed to refresh user list: {e}"})
391
+
392
+
393
+ if errors:
394
+ # 一部成功、一部失敗、またはリスト更新失敗
395
+ return jsonify({
396
+ "status": "partial_success" if deleted_names else "error",
397
+ "message": f"Deleted {len(deleted_names)} out of {len(names_to_delete)} requested members. Some errors occurred.",
398
+ "deleted": deleted_names,
399
+ "errors": errors,
400
+ "current_users": final_users_list # 更新後のリスト
401
+ }), 207 # Multi-Status or 500 if no success
402
+ else:
403
+ # すべて成功
404
+ return jsonify({
405
+ "status": "success",
406
+ "message": f"Successfully deleted {len(deleted_names)} members.",
407
+ "deleted": deleted_names,
408
+ "current_users": final_users_list # 更新後のリスト
409
+ }), 200
410
 
411
  except Exception as e:
412
+ print(f"Unexpected error in /reset_member: {e}")
413
  return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500
414
 
415
+ # 書き起こし作成エンドポイント (エラーハンドリング改善)
416
+ @app.route('/transcription', methods=['POST']) # GET不要なら削除
417
  def transcription():
418
  global transcription_text
419
  global total_audio
420
+
421
+ # 書き起こしテキストが既に存在するかチェック
422
+ if transcription_text and os.path.exists(transcription_text):
423
+ print(f"Returning existing transcription file: {transcription_text}")
424
+ # 既存のファイルを返す
425
+ else:
426
+ # 書き起こしが存在しない、またはパスが無効な場合、新規作成を試みる
427
+ print("Transcription file not found or path invalid. Attempting to create...")
428
+ if not total_audio or not os.path.exists(total_audio):
429
+ print("Error: total_audio path is not set or file does not exist.")
430
+ return jsonify({"error": "Audio data for transcription is missing or invalid."}), 400
431
+
432
+ try:
433
+ # ここで実際に書き起こし処理を実行
434
  transcription_text = transcripter.create_transcription(total_audio)
435
+ if not transcription_text or not os.path.exists(transcription_text):
436
+ # create_transcription がパスを返さない、またはファイルが生成されなかった場合
437
+ raise FileNotFoundError("Transcription process did not generate a valid file path.")
438
+ print(f"Transcription created: {transcription_text}")
439
  except Exception as e:
440
+ print(f"Error during transcription creation: {str(e)}")
441
+ transcription_text = "" # エラー時はパスをクリア
442
+ return jsonify({"error": f"Failed to create transcription: {str(e)}"}), 500
443
+
444
+ # 有効な書き起こしファイルパスがある場合、内容を読み込んで返す
445
  try:
446
  with open(transcription_text, 'r', encoding='utf-8') as file:
447
  file_content = file.read()
448
+ # print(f"Transcription content: {file_content[:200]}...") # 内容の一部をログ表示
449
+ return jsonify({'transcription': file_content}), 200
450
  except FileNotFoundError:
451
+ print(f"Error: Transcription file not found at path: {transcription_text}")
452
+ return jsonify({"error": "Transcription file could not be read (not found)."}), 404
453
  except Exception as e:
454
+ print(f"Error reading transcription file: {str(e)}")
455
+ return jsonify({"error": f"Unexpected error reading transcription: {str(e)}"}), 500
456
 
457
+ # AI分析エンドポイント (APIキーチェック改善)
458
+ @app.route('/analyze', methods=['POST']) # GET不要なら削除
459
  def analyze():
460
  global transcription_text
461
  global total_audio
462
+
463
+ # まず書き起こしファイルが存在するか確認、なければ作成試行
464
+ if not transcription_text or not os.path.exists(transcription_text):
465
+ print("Transcription not found for analysis. Attempting to create...")
466
+ # /transcription エンドポイントと同様のロジックで書き起こし作成を試みる
467
+ if not total_audio or not os.path.exists(total_audio):
468
+ return jsonify({"error": "Audio data for analysis is missing or invalid."}), 400
469
+ try:
470
  transcription_text = transcripter.create_transcription(total_audio)
471
+ if not transcription_text or not os.path.exists(transcription_text):
472
+ raise FileNotFoundError("Transcription process did not generate a valid file path.")
473
+ print(f"Transcription created for analysis: {transcription_text}")
474
  except Exception as e:
475
+ print(f"Error creating transcription during analysis: {str(e)}")
476
+ transcription_text = ""
477
+ return jsonify({"error": f"Failed to create transcription for analysis: {str(e)}"}), 500
478
+
479
+ # APIキーのチェック
480
  api_key = os.environ.get("DEEPSEEK")
481
+ if not api_key:
482
+ print("Error: DEEPSEEK environment variable is not set.")
483
+ # サーバー内部の問題なので 500 Internal Server Error が適切
484
+ return jsonify({"error": "Analysis service API key is not configured on the server."}), 500
485
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  try:
487
+ analyzer = TextAnalyzer(transcription_text, harassment_keywords)
488
+ print(f"Analyzing text from: {transcription_text}")
489
+ results = analyzer.analyze(api_key=api_key) # analyzeメソッドにAPIキーを渡す
490
+
491
+ # 結果のログ出力(必要なら)
492
+ # print(json.dumps(results, ensure_ascii=False, indent=2))
493
+ if "deepseek_analysis" in results and results["deepseek_analysis"]:
494
+ print("--- DeepSeek Analysis Results ---")
495
+ for key, value in results["deepseek_analysis"].items():
496
+ print(f"{key}: {value}")
497
+ print("-------------------------------")
498
+
499
+ return jsonify({"results": results}), 200
500
+ except FileNotFoundError:
501
+ # TextAnalyzer初期化時や analyze メソッド内でファイルが見つからない場合
502
+ print(f"Error: Transcription file not found during analysis: {transcription_text}")
503
+ return jsonify({"error": "Transcription file could not be read for analysis."}), 404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  except Exception as e:
505
+ print(f"Error during text analysis: {str(e)}")
506
+ return jsonify({"error": f"Analysis failed: {str(e)}"}), 500
 
507
 
508
+
509
+ # 音声アップロード&解析エンドポイント (エラーハンドリング、一時ファイル管理改善)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  @app.route('/upload_audio', methods=['POST'])
511
  def upload_audio():
512
  global total_audio
513
  global users
514
+
515
+ temp_audio_file = None # 一時ファイルのパスを保持
516
+ temp_base_audio_dir = "/tmp/data/base_audio" # ベース音声の一時保存先
517
+
518
  try:
519
  data = request.get_json()
520
  if not data or 'audio_data' not in data:
521
+ return jsonify({"error": "音声データ(audio_data)がありません"}), 400
522
+ if 'selected_users' in data and isinstance(data['selected_users'], list):
523
+ # リクエストで指定されたユーザーリストを使用
524
+ current_selected_users = data['selected_users']
525
+ if not current_selected_users:
526
+ return jsonify({"error": "選択されたユーザー(selected_users)がいません"}), 400
527
+ users = current_selected_users # グローバル変数も更新 (必要なら)
528
+ print(f"Received selected users: {users}")
529
+ elif not users:
530
+ # リクエストにもグローバル変数にもユーザーがいない場合
531
+ return jsonify({"error": "処理対象のユーザーが選択されていません"}), 400
532
+ else:
533
+ print(f"Using globally selected users: {users}")
534
 
 
 
535
 
536
+ # --- アップロードされた音声の一時保存 ---
537
  audio_binary = base64.b64decode(data['audio_data'])
538
+ if not audio_binary:
539
+ return jsonify({"error": "音声データのデコードに失敗しました"}), 400
540
+
541
+ # 一時ファイルを作成 (アップロードされた音声)
542
+ audio_dir = "/tmp/data/uploads" # アップロード専用の一時フォルダ推奨
543
  os.makedirs(audio_dir, exist_ok=True)
544
+ # 一意なファイル名を使用 (例: UUIDやタイムスタンプ)
545
+ temp_audio_fd, temp_audio_file = tempfile.mkstemp(suffix=".wav", dir=audio_dir)
546
+ os.close(temp_audio_fd) # ファイルディスクリプタを閉じる
547
+ with open(temp_audio_file, 'wb') as f:
548
  f.write(audio_binary)
549
+ print(f"Uploaded audio saved temporarily to: {temp_audio_file}")
550
+ # --- ここまで一時保存 ---
551
+
552
+
553
+ # --- 参照音声の準備 (ダウンロード) ---
554
+ os.makedirs(temp_base_audio_dir, exist_ok=True)
 
 
555
  reference_paths = []
556
+ missing_users = []
557
+ for user_name in users:
558
+ ref_path = os.path.join(temp_base_audio_dir, f"{user_name}.wav")
559
  try:
560
+ if not os.path.exists(ref_path) or os.path.getsize(ref_path) == 0:
561
+ # ローカルにないか、空ファイルの場合はクラウドからダウンロード
562
+ print(f"Reference audio for '{user_name}' not found locally, downloading...")
563
+ download_from_cloud(f"{user_name}.wav", ref_path)
564
+ else:
565
+ print(f"Using local reference audio for '{user_name}': {ref_path}")
566
+
567
+ if os.path.exists(ref_path) and os.path.getsize(ref_path) > 0:
568
+ reference_paths.append(ref_path)
569
+ else:
570
+ # ダウンロード後もファイルが存在しないか空の場合
571
+ missing_users.append(user_name)
572
+
573
  except Exception as e:
574
+ print(f"Error preparing reference audio for user '{user_name}': {str(e)}")
575
+ missing_users.append(user_name)
576
+ # 一人のエラーで全体を失敗させるか、続行するかは要件次第
577
+ # ここではエラーリストに追加して最後にチェックする
578
+
579
+ if missing_users:
580
+ # 必要な参照音声の一部または全部が見つからなかった場合
581
+ return jsonify({"error": f"参照音声の準備に失敗しました。見つからない、またはダウンロードできないユーザー: {', '.join(missing_users)}"}), 404 # Not Found or 500
582
+
583
+ if not reference_paths:
584
+ return jsonify({"error": "有効な参照音声ファイルがありません"}), 500
585
+ # --- ここまで参照音声の準備 ---
586
+
587
+
588
+ # --- 音声処理の実行 ---
589
+ print(f"Processing audio with {len(users)} user(s)...")
590
+ merged_segments_result = None # 初期化
591
  if len(users) > 1:
592
+ # 複数ユーザーの場合
593
+ matched_times, merged_segments_result = process.process_multi_audio(
594
+ reference_paths, temp_audio_file, users, threshold=0.05
595
+ )
596
+ total_time = sum(matched_times) if matched_times else 0
597
  rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times]
 
 
598
  user_rates = {users[i]: rates[i] for i in range(len(users))}
599
+ result_data = {"rates": rates, "user_rates": user_rates}
600
+ print(f"Multi-user processing result: {result_data}")
601
  else:
602
+ # 単一ユーザーの場合
603
+ matched_time, unmatched_time, merged_segments_result = process.process_audio(
604
+ reference_paths[0], temp_audio_file, users[0], threshold=0.05
605
+ )
606
  total_time = matched_time + unmatched_time
607
  rate = (matched_time / total_time) * 100 if total_time > 0 else 0
608
+ result_data = {"rate": rate, "user": users[0]}
609
+ print(f"Single-user processing result: {result_data}")
610
+
611
+ # マージされたセグメントを保存し、グローバル変数 total_audio を更新
612
+ if merged_segments_result is not None:
613
+ # 以前の total_audio ファイルがあれば削除
614
+ if total_audio and os.path.exists(total_audio) and os.path.isfile(total_audio):
615
+ try: os.remove(total_audio)
616
+ except OSError as e: print(f"Could not remove previous total_audio file '{total_audio}': {e}")
617
+
618
+ total_audio = transcripter.save_marged_segments(merged_segments_result)
619
+ print(f"Merged audio saved to: {total_audio}")
620
+ if not total_audio or not os.path.exists(total_audio):
621
+ print("Warning: Saving merged segments did not produce a valid file path.")
622
+ # エラーにするか警告に留めるか
623
+ # return jsonify({"error": "Failed to save processed audio segments."}), 500
624
+ else:
625
+ print("Warning: No merged segments were generated by the process.")
626
+ # 必要に応じて total_audio をクリア
627
+ if total_audio and os.path.exists(total_audio) and os.path.isfile(total_audio):
628
+ try: os.remove(total_audio)
629
+ except OSError as e: print(f"Could not remove previous total_audio file '{total_audio}': {e}")
630
+ total_audio = ""
631
+
632
+
633
+ return jsonify(result_data), 200
634
+ # --- ここまで音声処理 ---
635
+
636
+ except base64.binascii.Error as e:
637
+ print(f"Error decoding base64 audio data: {e}")
638
+ return jsonify({"error": "無効な音声データ形式です (Base64デコード失敗)"}), 400
639
+ except FileNotFoundError as e:
640
+ print(f"File not found error during audio processing: {e}")
641
+ return jsonify({"error": "処理に必要なファイルが見つかりません", "details": str(e)}), 404
642
  except Exception as e:
643
+ # その他の予期せぬエラー
644
+ print(f"Unexpected error in /upload_audio: {str(e)}")
645
+ # traceback.print_exc() # 詳細なスタックトレースをログに出力する場合
646
+ return jsonify({"error": "音声処理中にサーバーエラーが発生しました", "details": str(e)}), 500
647
+ finally:
648
+ # --- 一時ファイルのクリーンアップ ---
649
+ if temp_audio_file and os.path.exists(temp_audio_file):
650
+ try:
651
+ os.remove(temp_audio_file)
652
+ print(f"Cleaned up temporary upload file: {temp_audio_file}")
653
+ except Exception as e:
654
+ print(f"Error cleaning up temporary file '{temp_audio_file}': {e}")
655
+ # ベース音声の一時ファイルは他の処理で使う可能性があるため、ここでは削除しない
656
+ # clear_tmp エンドポイントなどで定期的に削除することを推奨
657
+ # --- ここまでクリーンアップ ---
658
 
 
 
 
 
 
659
 
660
  # 選択したユーザーを設定するエンドポイント
661
  @app.route('/select_users', methods=['POST'])
662
  def select_users():
663
  global users
664
+ global all_users # 選択されたユーザーが実際に存在するか確認するために使用
665
+
666
  try:
667
  data = request.get_json()
668
+ if not data or 'users' not in data or not isinstance(data['users'], list):
669
+ return jsonify({"error": "リクエストボディに 'users' 配列がありません"}), 400
670
+
671
+ selected_user_names = data['users']
672
+ print(f"Received users to select: {selected_user_names}")
673
+
674
+ # (任意) 選択されたユーザーが実際に存在するか確認
675
+ try:
676
+ update_all_users() # 最新の全ユーザーリストを取得
677
+ except Exception as e:
678
+ print(f"Warning: Could not verify selected users against the latest list due to update error: {e}")
679
+ # エラーが発生しても処理は続行するが、存在しないユーザーが含まれる可能性がある
680
+
681
+ valid_selected_users = []
682
+ invalid_users = []
683
+ for name in selected_user_names:
684
+ if name in all_users:
685
+ valid_selected_users.append(name)
686
+ else:
687
+ invalid_users.append(name)
688
+
689
+ if invalid_users:
690
+ print(f"Warning: The following selected users do not exist in the available list: {invalid_users}")
691
+ # 無効なユーザーを除外するか、エラーにするかは要件次第
692
+ # ここでは有効なユーザーのみを設定する
693
+ users = sorted(list(set(valid_selected_users))) # 重複除去とソート
694
+ return jsonify({
695
+ "status": "warning",
696
+ "message": f"一部のユーザーが存在しませんでした: {', '.join(invalid_users)}",
697
+ "selected_users": users
698
+ }), 200 # 成功扱いにするか、クライアント側で処理しやすいステータスコード(例: 207 Multi-Status)
699
+ else:
700
+ # 全員有効な場合
701
+ users = sorted(list(set(valid_selected_users)))
702
+ print(f"Successfully selected users: {users}")
703
+ return jsonify({"status": "success", "selected_users": users}), 200
704
+
705
  except Exception as e:
706
+ print(f"Error in /select_users: {str(e)}")
707
+ return jsonify({"error": "サーバーエラーが発生しました", "details": str(e)}), 500
708
 
709
+ # リセットエンドポイント (選択中ユーザーと一時ファイルをクリア)
710
+ @app.route('/reset', methods=['GET']) # または POST
711
  def reset():
712
  global users
 
713
  global total_audio
714
  global transcription_text
715
+
716
+ print("Resetting selected users and temporary analysis files...")
717
+ # 選択中ユーザーをクリア
718
+ users = []
719
+ print("Selected users list cleared.")
720
+
721
+ # 一時ファイル/変数クリア (/reset_member と同様のロジック)
722
+ if total_audio and os.path.exists(total_audio):
723
  try:
724
+ if os.path.isfile(total_audio): os.remove(total_audio); print(f"Deleted: {total_audio}")
725
+ elif os.path.isdir(total_audio): process.delete_files_in_directory(total_audio); print(f"Cleared dir: {total_audio}")
726
+ except Exception as e: print(f"Error clearing total_audio path '{total_audio}': {e}")
727
+ total_audio = ""
728
+
729
+ if transcription_text and os.path.exists(transcription_text):
730
+ try: os.remove(transcription_text); print(f"Deleted: {transcription_text}")
731
+ except Exception as e: print(f"Error deleting transcription file '{transcription_text}': {e}")
732
  transcription_text = ""
733
 
734
+ try:
735
+ transcription_audio_dir = '/tmp/data/transcription_audio'
736
+ if os.path.isdir(transcription_audio_dir):
737
+ process.delete_files_in_directory(transcription_audio_dir)
738
+ print(f"Cleared dir: {transcription_audio_dir}")
739
+ except Exception as e: print(f"Error clearing transcription_audio directory: {e}")
740
+
741
+ return jsonify({"status": "success", "message": "Selected users and temporary analysis files have been reset."}), 200
742
 
743
+
744
+ # 選択されたファイルのコピー (ダウンロード)
745
  @app.route('/copy_selected_files', methods=['POST'])
746
  def copy_selected_files():
747
+ # このエンドポイントの目的が不明瞭(どこにコピーするのか?)
748
+ # もしクライアント側でダウンロードさせたいなら、ファイルリストを返す方が適切かもしれない
749
+ # ここでは指定された名前のファイルを /tmp/data/selected_audio にダウンロードする実装とする
750
  try:
751
  data = request.get_json()
752
+ if not data or "names" not in data or not isinstance(data["names"], list):
753
+ return jsonify({"error": "リクエストボディに 'names' 配列がありません"}), 400
754
+
755
+ names_to_copy = data["names"]
756
+ if not names_to_copy:
757
+ return jsonify({"status": "warning", "message": "No names provided for copying."}), 200
758
 
759
+ dest_dir = "/tmp/data/selected_audio" # コピー先 (サーバー上の一時フォルダ)
 
760
  os.makedirs(dest_dir, exist_ok=True)
761
+ print(f"Copying selected files to server directory: {dest_dir}")
762
 
763
  copied_files = []
764
+ errors = []
765
+ for name in names_to_copy:
766
+ if not name: continue
767
  dest_path = os.path.join(dest_dir, f"{name}.wav")
768
  try:
 
769
  download_from_cloud(f"{name}.wav", dest_path)
770
  copied_files.append(name)
771
+ print(f"Downloaded '{name}.wav' to {dest_path}")
772
  except Exception as e:
773
+ error_msg = f"Failed to download '{name}': {str(e)}"
774
+ print(error_msg)
775
+ errors.append({"name": name, "error": str(e)})
776
+ # エラーがあっても続行
777
+
778
+ if errors:
779
+ return jsonify({
780
+ "status": "partial_success" if copied_files else "error",
781
+ "message": f"Copied {len(copied_files)} files. Some errors occurred.",
782
+ "copied": copied_files,
783
+ "errors": errors,
784
+ "destination_directory": dest_dir # 参考情報
785
+ }), 207 # Multi-Status
786
+ else:
787
+ return jsonify({
788
+ "status": "success",
789
+ "message": f"Successfully copied {len(copied_files)} files.",
790
+ "copied": copied_files,
791
+ "destination_directory": dest_dir
792
+ }), 200
793
 
794
  except Exception as e:
795
+ print(f"Error in /copy_selected_files: {str(e)}")
796
+ return jsonify({"error": "サーバー内部エラーが発生しました", "details": str(e)}), 500
797
+
798
 
799
+ # 一時フォルダクリアエンドポイント (注意して使用)
800
+ @app.route('/clear_tmp', methods=['POST']) # GETよりPOST/DELETEが適切
801
  def clear_tmp():
802
+ # このエンドポイントはサーバー上の /tmp/data を再帰的に削除するため、非常に危険
803
+ # 実行すると、処理中のファイルも含めてす��て消える可能性がある
804
+ # 使用は慎重に行うべき
805
+ tmp_root_dir = "/tmp/data"
806
+ print(f"WARNING: Received request to clear directory: {tmp_root_dir}")
807
+ if not os.path.isdir(tmp_root_dir):
808
+ return jsonify({"status": "warning", "message": f"Directory not found, nothing to clear: {tmp_root_dir}"}), 200
809
+
810
  try:
811
+ # 安全のため、特定のサブディレクトリのみを対象にする方が良いかもしれない
812
+ # 例: subdirs_to_clear = ['uploads', 'base_audio', 'selected_audio', 'transcription_audio']
813
+ # for subdir_name in subdirs_to_clear:
814
+ # subdir_path = os.path.join(tmp_root_dir, subdir_name)
815
+ # if os.path.isdir(subdir_path):
816
+ # shutil.rmtree(subdir_path)
817
+ # print(f"Removed directory: {subdir_path}")
818
+
819
+ # 現在の実装: /tmp/data 直下のファイルとディレクトリをすべて削除
820
+ deleted_items = []
821
+ errors = []
822
+ for item_name in os.listdir(tmp_root_dir):
823
+ item_path = os.path.join(tmp_root_dir, item_name)
824
+ try:
825
+ if os.path.isfile(item_path) or os.path.islink(item_path):
826
+ os.unlink(item_path)
827
+ deleted_items.append(item_name)
828
+ print(f"Deleted file/link: {item_path}")
829
+ elif os.path.isdir(item_path):
830
+ shutil.rmtree(item_path)
831
+ deleted_items.append(item_name + "/") # ディレクトリを示す
832
+ print(f"Deleted directory: {item_path}")
833
+ except Exception as e:
834
+ error_msg = f"Failed to delete '{item_name}': {str(e)}"
835
+ print(error_msg)
836
+ errors.append({"item": item_name, "error": str(e)})
837
+
838
+ if errors:
839
+ return jsonify({
840
+ "status": "partial_success",
841
+ "message": f"Cleared some items in {tmp_root_dir}, but errors occurred.",
842
+ "deleted_items": deleted_items,
843
+ "errors": errors
844
+ }), 207
845
+ else:
846
+ return jsonify({
847
+ "status": "success",
848
+ "message": f"Successfully cleared contents of {tmp_root_dir}",
849
+ "deleted_items": deleted_items
850
+ }), 200
851
 
852
  except Exception as e:
853
+ print(f"Error in /clear_tmp: {str(e)}")
854
+ return jsonify({"error": "サーバー内部エラーが発生しました", "details": str(e)}), 500
855
+
856
 
857
+ # ベース音声アップロード (ユーザー登録)
858
  @app.route('/upload_base_audio', methods=['POST'])
859
  def upload_base_audio():
860
+ global all_users # 登録後にリストを更新するため
861
+
862
  try:
863
  data = request.get_json()
864
  if not data or 'audio_data' not in data or 'name' not in data:
865
+ return jsonify({"error": "音声データ(audio_data)または名前(name)がありません"}), 400
866
+
867
+ name = data['name'].strip() # 前後の空白を除去
868
+ if not name:
869
+ return jsonify({"error": "名前が空です"}), 400
870
+ # TODO: 名前に使用できない文字(ファイル名として不適切、セキュリティリスクなど)をチェックするバリデーションを追加推奨
871
+ # 例: if not re.match(r'^[a-zA-Z0-9_-]+$', name): return jsonify({"error": "Invalid characters in name"}), 400
872
+
873
+ filename_to_upload = f"{name}.wav"
874
+ print(f"Base audio upload request for name: {name} (filename: {filename_to_upload})")
875
+
876
  payload = {
877
  "action": "upload",
878
+ "fileName": filename_to_upload,
879
  "base64Data": data['audio_data']
880
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
881
 
882
+ # GASにアップロード実行
883
+ res_json = _make_gas_request(payload, timeout=60) # アップロードは時間がかかる可能性
 
 
 
 
 
 
 
884
 
885
+ print(f"Successfully uploaded base audio to cloud. File ID: {res_json.get('fileId')}")
886
+
887
+ # 全ユーザーリストを更新 (GASに再度問い合わせるのが確実)
888
+ try:
889
+ update_all_users()
890
+ except Exception as update_e:
891
+ print(f"Warning: Failed to refresh user list after upload: {update_e}")
892
+ # アップロード自体は成功しているので、警告付きで成功レスポンスを返す
893
+ return jsonify({
894
+ "state": "Registration Success (List update may be delayed)",
895
+ "driveFileId": res_json.get("fileId"),
896
+ "warning": f"User list update failed: {update_e}"
897
+ }), 200 # または 201 Created
898
+
899
+ return jsonify({"state": "Registration Success!", "driveFileId": res_json.get("fileId")}), 201 # 201 Created がより適切かも
900
+
901
+ except base64.binascii.Error as e:
902
+ print(f"Error decoding base64 audio data: {e}")
903
+ return jsonify({"error": "無効な音声データ形式です (Base64デコード失敗)"}), 400
904
+ except (TimeoutError, ConnectionError, ValueError, Exception) as e:
905
+ error_message = f"Failed to upload base audio for '{name}'."
906
+ details = str(e)
907
+ print(f"{error_message} Details: {details}")
908
+ # GASからのエラーメッセージに 'already exists' が含まれていたら 409 Conflict を返す
909
+ status_code = 409 if "already exists" in details.lower() else 500
910
+ return jsonify({"error": error_message, "details": details}), status_code
911
+
912
+ # === アプリケーションの実行 ===
913
  if __name__ == '__main__':
914
  port = int(os.environ.get("PORT", 7860))
915
+ # 本番環境では debug=False に設定すること
916
+ # host='0.0.0.0' はコンテナなど外部からアクセスする場合に必要
917
+ print(f"Starting Flask server on host 0.0.0.0 port {port} with debug mode {'ON' if app.debug else 'OFF'}")
918
  app.run(debug=True, host="0.0.0.0", port=port)