from flask import Flask, request, jsonify, render_template, send_from_directory import base64 # from pydub import AudioSegment # pydubは直接使われていないようなのでコメントアウトしても良いかも import os import shutil import requests import tempfile import json # --- 必要に応じて実際のクラス/モジュールをインポート --- try: from process import AudioProcessor from transcription import TranscriptionMaker from analyze import TextAnalyzer except ImportError: print("警告: process, transcription, analyze モジュールが見つかりません。ダミークラスを使用します。") # --- ダミークラス (実際のクラスがない場合のエラー回避用) --- class AudioProcessor: def delete_files_in_directory(self, path): print(f"Dummy: Deleting files in {path}") if os.path.isdir(path): # ダミー実装: フォルダ内のファイルを削除するふり for item in os.listdir(path): item_path = os.path.join(path, item) if os.path.isfile(item_path): print(f"Dummy: Removing file {item_path}") # os.remove(item_path) # 実際には削除しない elif os.path.isdir(item_path): print(f"Dummy: Removing directory {item_path}") # shutil.rmtree(item_path) # 実際には削除しない def process_multi_audio(self, *args, **kwargs): print("Dummy: Processing multi audio") return [10, 5], {} # ダミーの戻り値 def process_audio(self, *args, **kwargs): print("Dummy: Processing single audio") return 10, 5, {} # ダミーの戻り値 class TranscriptionMaker: def create_transcription(self, path): print(f"Dummy: Creating transcription for {path}") dummy_path = os.path.join(tempfile.gettempdir(), "dummy_transcription.txt") try: with open(dummy_path, "w", encoding='utf-8') as f: f.write("これはダミーの書き起こしです。") except Exception as e: print(f"Dummy transcription file write error: {e}") # エラーが発生してもダミーパスを返す return dummy_path def save_marged_segments(self, segments): print("Dummy: Saving merged segments") dummy_path = os.path.join(tempfile.gettempdir(), "dummy_merged_audio.wav") try: # ダミーファイルを作成(空でも良い) with open(dummy_path, 'w') as f: pass except Exception as e: print(f"Dummy merged audio file create error: {e}") return dummy_path class TextAnalyzer: def __init__(self, text_path, keywords): print(f"Dummy: Initializing analyzer for {text_path}") self.text_path = text_path self.keywords = keywords def analyze(self, api_key): print("Dummy: Analyzing text") # 環境変数チェックのダミー if not api_key: print("Warning: Dummy DEEPSEEK API key not provided.") return {"dummy_analysis": "ok", "deepseek_analysis": {"conversationLevel": 5, "harassmentPresent": False}} # ダミーの戻り値 # ------------------------------------------------------------ from flask_cors import CORS # --- グローバル変数と初期化 --- # 注意: グローバル変数はリクエスト間で共有されるため、同時アクセス時に問題が発生する可能性があります。 # 本番環境では、データベースやセッション管理などのより堅牢な状態管理を検討してください。 process = AudioProcessor() transcripter = TranscriptionMaker() app = Flask(__name__) # CORS設定: すべてのオリジンからのリクエストを許可 # DELETEメソッドや特定のヘッダー(Content-Typeなど)も許可する CORS(app, origins="*", methods=["GET", "POST", "DELETE", "PUT", "OPTIONS"], headers=["Content-Type", "Authorization"]) # GASのエンドポイントURL (実際のURLに置き換えてください) GAS_URL = "https://script.google.com/macros/s/AKfycbwR2cnMKVU1AxoT9NDaeZaNUaTiwRX64Ul0sH0AU4ccP49Byph-TpxtM_Lwm4G9zLnuYA/exec" users = [] # 現在選択されているユーザー名のリスト all_users = [] # 利用可能なすべてのユーザー名のリスト (ファイル名から拡張子を除いたもの) transcription_text = "" # 書き起こしファイルのパス total_audio = "" # マージされた音声ファイルのパス harassment_keywords = [ "バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい", "きもい", "キモい", "ブス", "デブ", "ハゲ", "セクハラ", "パワハラ", "モラハラ" ] # --- ここまでグローバル変数と初期化 --- # === ヘルパー関数 === def _make_gas_request(payload, timeout=30): """GASエンドポイントへのPOSTリクエストを送信し、レスポンスJSONを返す""" try: print(f"GAS Request Payload: {payload}") response = requests.post(GAS_URL, json=payload, timeout=timeout) response.raise_for_status() # HTTPエラー (4xx, 5xx) があれば例外を発生させる res_json = response.json() print(f"GAS Response: {res_json}") if res_json.get("status") != "success": # GAS側でエラーが報告された場合 raise Exception(f"GAS Error: {res_json.get('message', 'Unknown error from GAS')}") return res_json except requests.exceptions.Timeout: print(f"Error: GAS request timed out after {timeout} seconds.") raise TimeoutError(f"GAS request timed out ({timeout}s)") except requests.exceptions.RequestException as e: print(f"Error: Failed to connect to GAS: {e}") raise ConnectionError(f"Failed to connect to GAS: {e}") except json.JSONDecodeError as e: print(f"Error: Failed to decode JSON response from GAS: {e}") print(f"GAS Raw Response Text: {response.text[:500]}...") # レスポンス内容の一部を表示 raise ValueError(f"Invalid JSON response from GAS: {e}") except Exception as e: # _make_gas_request 内で発生したその他の予期せぬエラー print(f"Error during GAS request processing: {e}") raise # 元のエラーを再発生させる def download_from_cloud(filename_with_ext, local_path): """クラウド(GAS)から指定されたファイルをダウンロードし、ローカルパスに保存する""" payload = {"action": "download", "fileName": filename_with_ext} try: print(f"Downloading {filename_with_ext} from cloud...") res_json = _make_gas_request(payload, timeout=60) # ダウンロードは時間がかかる可能性があるため長めのタイムアウト base64_data = res_json.get("base64Data") if not base64_data: raise ValueError("No base64Data found in the download response.") audio_binary = base64.b64decode(base64_data) if not audio_binary: raise ValueError("Failed to decode base64 data.") os.makedirs(os.path.dirname(local_path), exist_ok=True) with open(local_path, 'wb') as f: f.write(audio_binary) print(f"Successfully downloaded and saved to {local_path}") if os.path.getsize(local_path) <= 0: # ダウンロードしたがファイルサイズが0の場合 os.remove(local_path) # 不完全なファイルを削除 raise ValueError(f"Downloaded file {local_path} is empty.") return local_path except (TimeoutError, ConnectionError, ValueError, Exception) as e: print(f"Error downloading {filename_with_ext}: {str(e)}") # エラーが発生したら上位に伝播させる raise def delete_from_cloud(filename_with_ext): """クラウド(GAS)から指定されたファイルを削除する""" payload = {"action": "delete", "fileName": filename_with_ext} try: print(f"Deleting {filename_with_ext} from cloud...") _make_gas_request(payload, timeout=30) print(f"Successfully deleted {filename_with_ext} from cloud.") return True except (TimeoutError, ConnectionError, ValueError, Exception) as e: print(f"Error deleting {filename_with_ext}: {str(e)}") # エラーが発生したら上位に伝播させる raise def update_all_users(): """GASから最新のファイルリストを取得し、グローバル変数 all_users を更新する""" global all_users payload = {"action": "list"} try: print("Fetching user list from cloud...") res_json = _make_gas_request(payload, timeout=30) # ファイル名から拡張子を除去してリストを作成 (空のファイル名を除外) current_users = [os.path.splitext(name)[0] for name in res_json.get("fileNames", []) if name and '.' in name] all_users = sorted(list(set(current_users))) # 重複除去とソート print(f"Updated all_users list: {all_users}") return all_users # 更新後のリストを返す except (TimeoutError, ConnectionError, ValueError, Exception) as e: print(f"Error updating all_users list: {str(e)}") # エラーが発生しても、既存の all_users は変更せず、エラーを上位に伝播 raise # === Flask ルート定義 === # React用: ユーザーリスト取得エンドポイント (JSON構造は変更しない) @app.route('/list_base_audio', methods=['GET']) def list_base_audio(): """現在の all_users リストを返す (React側で処理が必要)""" global all_users try: # 関数を呼び出して最新の状態を取得・更新 update_all_users() # 元の形式で返す return jsonify({"status": "success", "fileNames": all_users}), 200 except (TimeoutError, ConnectionError, ValueError, Exception) as e: # GASとの通信エラーなどでリスト取得に失敗した場合 print(f"Error in /list_base_audio: {str(e)}") # エラー発生時は空リストまたはエラーメッセージを返す # return jsonify({"status": "error", "message": "Failed to retrieve user list", "details": str(e)}), 500 # または、現状保持しているリストがあればそれを返し、エラーをログに残すだけでも良いかもしれない print("Returning potentially stale user list due to update error.") return jsonify({"status": "warning", "message": "Could not refresh list, returning cached data.", "fileNames": all_users}), 200 # 警告付きで成功扱いにするか、5xxエラーにするかは要件次第 # React用: ユーザー削除エンドポイント @app.route('/api/users/', methods=['DELETE']) def delete_user_api(user_name): """指定されたユーザー名のファイルをクラウドから削除し、ローカルリストも更新""" global users global all_users if not user_name: return jsonify({"status": "error", "message": "User name cannot be empty"}), 400 filename_to_delete = f"{user_name}.wav" # GAS側で想定されるファイル名 try: print(f"API delete request for user: {user_name}") # 1. クラウドから削除 delete_from_cloud(filename_to_delete) # 2. ローカルリストから削除 (成功した場合のみ) deleted_from_local = False if user_name in all_users: all_users.remove(user_name) print(f"Removed '{user_name}' from all_users list.") deleted_from_local = True if user_name in users: users.remove(user_name) print(f"Removed '{user_name}' from selected users (users) list.") deleted_from_local = True if not deleted_from_local: print(f"Warning: User '{user_name}' was not found in local lists (all_users, users) but deletion from cloud was attempted/successful.") # 成功レスポンス return jsonify({"status": "success", "message": f"User '{user_name}' deleted successfully."}), 200 except FileNotFoundError: # delete_from_cloud 内で発生した場合(GASがファイルなしと応答した場合など) # または、ローカルリストにそもそも存在しなかった場合 print(f"User '{user_name}' not found for deletion (either on cloud or locally).") # 念のためローカルリストからも削除試行(既になくてもエラーにはならない) if user_name in all_users: all_users.remove(user_name) if user_name in users: users.remove(user_name) return jsonify({"status": "error", "message": f"User '{user_name}' not found."}), 404 # Not Found except (TimeoutError, ConnectionError, ValueError, Exception) as e: # GAS通信エラーやその他の予期せぬエラー error_message = f"Failed to delete user '{user_name}'." print(f"{error_message} Details: {str(e)}") # クライアントには詳細なエラー原因を伏せる場合もある return jsonify({"status": "error", "message": error_message, "details": str(e)}), 500 # Internal Server Error # --- 既存のテンプレートレンダリング用ルート (変更なし) --- @app.route('/index', methods=['GET']) # POST不要なら削除 def index(): return render_template('index.html', users=users) @app.route('/feedback', methods=['GET']) def feedback(): return render_template('feedback.html') @app.route('/talk_detail', methods=['GET']) def talk_detail(): return render_template('talkDetail.html') @app.route('/userregister', methods=['GET']) def userregister(): return render_template('userRegister.html') @app.route('/reset_html', methods=['GET']) def reset_html(): return render_template('reset.html') @app.route('/', methods=['GET']) # ルートパス @app.route('/userselect', methods=['GET']) def userselect(): # ユーザー選択画面表示時に最新リストを取得・表示するならここで update_all_users() を呼ぶ # try: # update_all_users() # except Exception as e: # print(f"Failed to update users on loading userselect page: {e}") # return render_template('userSelect.html', available_users=all_users) # テンプレートに渡す場合 return render_template('userSelect.html') # テンプレート側でAPIを叩く場合 # --- ここまでテンプレートレンダリング用ルート --- # --- 既存のAPIエンドポイント (一部見直し) --- # 人数確認 (最新リストを返すように修正) @app.route('/confirm', methods=['GET']) def confirm(): global users global all_users try: update_all_users() # 最新の状態に更新試行 except Exception as e: print(f"ユーザーリストの更新エラー (/confirm): {str(e)}") # エラーでも現在のリストを返す(あるいはエラーを示す) return jsonify({'selected_members': users, 'all_available_members': all_users}), 200 # 複数メンバー削除 (POST推奨) @app.route('/reset_member', methods=['POST']) # GETよりPOSTが適切 def reset_member(): global users global all_users global total_audio global transcription_text # --- 一時ファイル/変数クリア --- print("Resetting temporary files and variables...") # total_audio パスの処理 (存在チェックと種類判別) if total_audio and os.path.exists(total_audio): try: if os.path.isfile(total_audio): os.remove(total_audio) print(f"Deleted file: {total_audio}") elif os.path.isdir(total_audio): # ディレクトリ内のファイルを削除 (AudioProcessorに任せる) process.delete_files_in_directory(total_audio) print(f"Cleared files in directory: {total_audio}") # 必要ならディレクトリ自体も削除: shutil.rmtree(total_audio) else: print(f"Warning: Path exists but is not a file or directory: {total_audio}") except Exception as e: print(f"Error clearing total_audio path '{total_audio}': {e}") total_audio = "" # パスをクリア # 書き起こしテキストファイルの削除 if transcription_text and os.path.exists(transcription_text): try: os.remove(transcription_text) print(f"Deleted transcription file: {transcription_text}") except Exception as e: print(f"Error deleting transcription file '{transcription_text}': {e}") transcription_text = "" # パスをクリア # その他の関連一時ディレクトリのクリア (存在しなければ何もしない) try: transcription_audio_dir = '/tmp/data/transcription_audio' if os.path.isdir(transcription_audio_dir): process.delete_files_in_directory(transcription_audio_dir) print(f"Cleared files in directory: {transcription_audio_dir}") except Exception as e: print(f"Error clearing transcription_audio directory: {e}") # --- ここまで一時ファイル/変数クリア --- try: data = request.get_json() if not data or "names" not in data or not isinstance(data["names"], list): return jsonify({"status": "error", "message": "Invalid request body, 'names' array is required"}), 400 names_to_delete = data["names"] if not names_to_delete: return jsonify({"status": "warning", "message": "No names provided for deletion."}), 200 # 何もしないで成功 print(f"Bulk delete request for names: {names_to_delete}") deleted_names = [] errors = [] for name in names_to_delete: if not name: continue # 空の名前はスキップ filename_to_delete = f"{name}.wav" try: delete_from_cloud(filename_to_delete) deleted_names.append(name) # ローカルリストからも削除 if name in all_users: all_users.remove(name) if name in users: users.remove(name) except Exception as e: error_msg = f"Failed to delete '{name}': {str(e)}" print(error_msg) errors.append({"name": name, "error": str(e)}) # 削除処理後に最新のユーザーリストをGASから取得して同期するのが望ましい final_users_list = all_users # エラーがあっても現在のリストを返す try: final_users_list = update_all_users() except Exception as e: print(f"Failed to refresh user list after bulk delete: {e}") errors.append({"name": "N/A", "error": f"Failed to refresh user list: {e}"}) if errors: # 一部成功、一部失敗、またはリスト更新失敗 return jsonify({ "status": "partial_success" if deleted_names else "error", "message": f"Deleted {len(deleted_names)} out of {len(names_to_delete)} requested members. Some errors occurred.", "deleted": deleted_names, "errors": errors, "current_users": final_users_list # 更新後のリスト }), 207 # Multi-Status or 500 if no success else: # すべて成功 return jsonify({ "status": "success", "message": f"Successfully deleted {len(deleted_names)} members.", "deleted": deleted_names, "current_users": final_users_list # 更新後のリスト }), 200 except Exception as e: print(f"Unexpected error in /reset_member: {e}") return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500 # 書き起こし作成エンドポイント (エラーハンドリング改善) @app.route('/transcription', methods=['POST']) # GET不要なら削除 def transcription(): global transcription_text global total_audio # 書き起こしテキストが既に存在するかチェック if transcription_text and os.path.exists(transcription_text): print(f"Returning existing transcription file: {transcription_text}") # 既存のファイルを返す else: # 書き起こしが存在しない、またはパスが無効な場合、新規作成を試みる print("Transcription file not found or path invalid. Attempting to create...") if not total_audio or not os.path.exists(total_audio): print("Error: total_audio path is not set or file does not exist.") return jsonify({"error": "Audio data for transcription is missing or invalid."}), 400 try: # ここで実際に書き起こし処理を実行 transcription_text = transcripter.create_transcription(total_audio) if not transcription_text or not os.path.exists(transcription_text): # create_transcription がパスを返さない、またはファイルが生成されなかった場合 raise FileNotFoundError("Transcription process did not generate a valid file path.") print(f"Transcription created: {transcription_text}") except Exception as e: print(f"Error during transcription creation: {str(e)}") transcription_text = "" # エラー時はパスをクリア return jsonify({"error": f"Failed to create transcription: {str(e)}"}), 500 # 有効な書き起こしファイルパスがある場合、内容を読み込んで返す try: with open(transcription_text, 'r', encoding='utf-8') as file: file_content = file.read() # print(f"Transcription content: {file_content[:200]}...") # 内容の一部をログ表示 return jsonify({'transcription': file_content}), 200 except FileNotFoundError: print(f"Error: Transcription file not found at path: {transcription_text}") return jsonify({"error": "Transcription file could not be read (not found)."}), 404 except Exception as e: print(f"Error reading transcription file: {str(e)}") return jsonify({"error": f"Unexpected error reading transcription: {str(e)}"}), 500 # AI分析エンドポイント (APIキーチェック改善) @app.route('/analyze', methods=['POST']) # GET不要なら削除 def analyze(): global transcription_text global total_audio # まず書き起こしファイルが存在するか確認、なければ作成試行 if not transcription_text or not os.path.exists(transcription_text): print("Transcription not found for analysis. Attempting to create...") # /transcription エンドポイントと同様のロジックで書き起こし作成を試みる if not total_audio or not os.path.exists(total_audio): return jsonify({"error": "Audio data for analysis is missing or invalid."}), 400 try: transcription_text = transcripter.create_transcription(total_audio) if not transcription_text or not os.path.exists(transcription_text): raise FileNotFoundError("Transcription process did not generate a valid file path.") print(f"Transcription created for analysis: {transcription_text}") except Exception as e: print(f"Error creating transcription during analysis: {str(e)}") transcription_text = "" return jsonify({"error": f"Failed to create transcription for analysis: {str(e)}"}), 500 # APIキーのチェック api_key = os.environ.get("DEEPSEEK") if not api_key: print("Error: DEEPSEEK environment variable is not set.") # サーバー内部の問題なので 500 Internal Server Error が適切 return jsonify({"error": "Analysis service API key is not configured on the server."}), 500 try: analyzer = TextAnalyzer(transcription_text, harassment_keywords) print(f"Analyzing text from: {transcription_text}") results = analyzer.analyze(api_key=api_key) # analyzeメソッドにAPIキーを渡す # 結果のログ出力(必要なら) # print(json.dumps(results, ensure_ascii=False, indent=2)) if "deepseek_analysis" in results and results["deepseek_analysis"]: print("--- DeepSeek Analysis Results ---") for key, value in results["deepseek_analysis"].items(): print(f"{key}: {value}") print("-------------------------------") return jsonify({"results": results}), 200 except FileNotFoundError: # TextAnalyzer初期化時や analyze メソッド内でファイルが見つからない場合 print(f"Error: Transcription file not found during analysis: {transcription_text}") return jsonify({"error": "Transcription file could not be read for analysis."}), 404 except Exception as e: print(f"Error during text analysis: {str(e)}") return jsonify({"error": f"Analysis failed: {str(e)}"}), 500 # 音声アップロード&解析エンドポイント (エラーハンドリング、一時ファイル管理改善) @app.route('/upload_audio', methods=['POST']) def upload_audio(): global total_audio global users temp_audio_file = None # 一時ファイルのパスを保持 temp_base_audio_dir = "/tmp/data/base_audio" # ベース音声の一時保存先 try: data = request.get_json() if not data or 'audio_data' not in data: return jsonify({"error": "音声データ(audio_data)がありません"}), 400 if 'selected_users' in data and isinstance(data['selected_users'], list): # リクエストで指定されたユーザーリストを使用 current_selected_users = data['selected_users'] if not current_selected_users: return jsonify({"error": "選択されたユーザー(selected_users)がいません"}), 400 users = current_selected_users # グローバル変数も更新 (必要なら) print(f"Received selected users: {users}") elif not users: # リクエストにもグローバル変数にもユーザーがいない場合 return jsonify({"error": "処理対象のユーザーが選択されていません"}), 400 else: print(f"Using globally selected users: {users}") # --- アップロードされた音声の一時保存 --- audio_binary = base64.b64decode(data['audio_data']) if not audio_binary: return jsonify({"error": "音声データのデコードに失敗しました"}), 400 # 一時ファイルを作成 (アップロードされた音声) audio_dir = "/tmp/data/uploads" # アップロード専用の一時フォルダ推奨 os.makedirs(audio_dir, exist_ok=True) # 一意なファイル名を使用 (例: UUIDやタイムスタンプ) temp_audio_fd, temp_audio_file = tempfile.mkstemp(suffix=".wav", dir=audio_dir) os.close(temp_audio_fd) # ファイルディスクリプタを閉じる with open(temp_audio_file, 'wb') as f: f.write(audio_binary) print(f"Uploaded audio saved temporarily to: {temp_audio_file}") # --- ここまで一時保存 --- # --- 参照音声の準備 (ダウンロード) --- os.makedirs(temp_base_audio_dir, exist_ok=True) reference_paths = [] missing_users = [] for user_name in users: ref_path = os.path.join(temp_base_audio_dir, f"{user_name}.wav") try: if not os.path.exists(ref_path) or os.path.getsize(ref_path) == 0: # ローカルにないか、空ファイルの場合はクラウドからダウンロード print(f"Reference audio for '{user_name}' not found locally, downloading...") download_from_cloud(f"{user_name}.wav", ref_path) else: print(f"Using local reference audio for '{user_name}': {ref_path}") if os.path.exists(ref_path) and os.path.getsize(ref_path) > 0: reference_paths.append(ref_path) else: # ダウンロード後もファイルが存在しないか空の場合 missing_users.append(user_name) except Exception as e: print(f"Error preparing reference audio for user '{user_name}': {str(e)}") missing_users.append(user_name) # 一人のエラーで全体を失敗させるか、続行するかは要件次第 # ここではエラーリストに追加して最後にチェックする if missing_users: # 必要な参照音声の一部または全部が見つからなかった場合 return jsonify({"error": f"参照音声の準備に失敗しました。見つからない、またはダウンロードできないユーザー: {', '.join(missing_users)}"}), 404 # Not Found or 500 if not reference_paths: return jsonify({"error": "有効な参照音声ファイルがありません"}), 500 # --- ここまで参照音声の準備 --- # --- 音声処理の実行 --- print(f"Processing audio with {len(users)} user(s)...") merged_segments_result = None # 初期化 if len(users) > 1: # 複数ユーザーの場合 matched_times, merged_segments_result = process.process_multi_audio( reference_paths, temp_audio_file, users, threshold=0.05 ) total_time = sum(matched_times) if matched_times else 0 rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times] user_rates = {users[i]: rates[i] for i in range(len(users))} result_data = {"rates": rates, "user_rates": user_rates} print(f"Multi-user processing result: {result_data}") else: # 単一ユーザーの場合 matched_time, unmatched_time, merged_segments_result = process.process_audio( reference_paths[0], temp_audio_file, users[0], threshold=0.05 ) total_time = matched_time + unmatched_time rate = (matched_time / total_time) * 100 if total_time > 0 else 0 result_data = {"rate": rate, "user": users[0]} print(f"Single-user processing result: {result_data}") # マージされたセグメントを保存し、グローバル変数 total_audio を更新 if merged_segments_result is not None: # 以前の total_audio ファイルがあれば削除 if total_audio and os.path.exists(total_audio) and os.path.isfile(total_audio): try: os.remove(total_audio) except OSError as e: print(f"Could not remove previous total_audio file '{total_audio}': {e}") total_audio = transcripter.save_marged_segments(merged_segments_result) print(f"Merged audio saved to: {total_audio}") if not total_audio or not os.path.exists(total_audio): print("Warning: Saving merged segments did not produce a valid file path.") # エラーにするか警告に留めるか # return jsonify({"error": "Failed to save processed audio segments."}), 500 else: print("Warning: No merged segments were generated by the process.") # 必要に応じて total_audio をクリア if total_audio and os.path.exists(total_audio) and os.path.isfile(total_audio): try: os.remove(total_audio) except OSError as e: print(f"Could not remove previous total_audio file '{total_audio}': {e}") total_audio = "" return jsonify(result_data), 200 # --- ここまで音声処理 --- except base64.binascii.Error as e: print(f"Error decoding base64 audio data: {e}") return jsonify({"error": "無効な音声データ形式です (Base64デコード失敗)"}), 400 except FileNotFoundError as e: print(f"File not found error during audio processing: {e}") return jsonify({"error": "処理に必要なファイルが見つかりません", "details": str(e)}), 404 except Exception as e: # その他の予期せぬエラー print(f"Unexpected error in /upload_audio: {str(e)}") # traceback.print_exc() # 詳細なスタックトレースをログに出力する場合 return jsonify({"error": "音声処理中にサーバーエラーが発生しました", "details": str(e)}), 500 finally: # --- 一時ファイルのクリーンアップ --- if temp_audio_file and os.path.exists(temp_audio_file): try: os.remove(temp_audio_file) print(f"Cleaned up temporary upload file: {temp_audio_file}") except Exception as e: print(f"Error cleaning up temporary file '{temp_audio_file}': {e}") # ベース音声の一時ファイルは他の処理で使う可能性があるため、ここでは削除しない # clear_tmp エンドポイントなどで定期的に削除することを推奨 # --- ここまでクリーンアップ --- # 選択したユーザーを設定するエンドポイント @app.route('/select_users', methods=['POST']) def select_users(): global users global all_users # 選択されたユーザーが実際に存在するか確認するために使用 try: data = request.get_json() if not data or 'users' not in data or not isinstance(data['users'], list): return jsonify({"error": "リクエストボディに 'users' 配列がありません"}), 400 selected_user_names = data['users'] print(f"Received users to select: {selected_user_names}") # (任意) 選択されたユーザーが実際に存在するか確認 try: update_all_users() # 最新の全ユーザーリストを取得 except Exception as e: print(f"Warning: Could not verify selected users against the latest list due to update error: {e}") # エラーが発生しても処理は続行するが、存在しないユーザーが含まれる可能性がある valid_selected_users = [] invalid_users = [] for name in selected_user_names: if name in all_users: valid_selected_users.append(name) else: invalid_users.append(name) if invalid_users: print(f"Warning: The following selected users do not exist in the available list: {invalid_users}") # 無効なユーザーを除外するか、エラーにするかは要件次第 # ここでは有効なユーザーのみを設定する users = sorted(list(set(valid_selected_users))) # 重複除去とソート return jsonify({ "status": "warning", "message": f"一部のユーザーが存在しませんでした: {', '.join(invalid_users)}", "selected_users": users }), 200 # 成功扱いにするか、クライアント側で処理しやすいステータスコード(例: 207 Multi-Status) else: # 全員有効な場合 users = sorted(list(set(valid_selected_users))) print(f"Successfully selected users: {users}") return jsonify({"status": "success", "selected_users": users}), 200 except Exception as e: print(f"Error in /select_users: {str(e)}") return jsonify({"error": "サーバーエラーが発生しました", "details": str(e)}), 500 # リセットエンドポイント (選択中ユーザーと一時ファイルをクリア) @app.route('/reset', methods=['GET']) # または POST def reset(): global users global total_audio global transcription_text print("Resetting selected users and temporary analysis files...") # 選択中ユーザーをクリア users = [] print("Selected users list cleared.") # 一時ファイル/変数クリア (/reset_member と同様のロジック) if total_audio and os.path.exists(total_audio): try: if os.path.isfile(total_audio): os.remove(total_audio); print(f"Deleted: {total_audio}") elif os.path.isdir(total_audio): process.delete_files_in_directory(total_audio); print(f"Cleared dir: {total_audio}") except Exception as e: print(f"Error clearing total_audio path '{total_audio}': {e}") total_audio = "" if transcription_text and os.path.exists(transcription_text): try: os.remove(transcription_text); print(f"Deleted: {transcription_text}") except Exception as e: print(f"Error deleting transcription file '{transcription_text}': {e}") transcription_text = "" try: transcription_audio_dir = '/tmp/data/transcription_audio' if os.path.isdir(transcription_audio_dir): process.delete_files_in_directory(transcription_audio_dir) print(f"Cleared dir: {transcription_audio_dir}") except Exception as e: print(f"Error clearing transcription_audio directory: {e}") return jsonify({"status": "success", "message": "Selected users and temporary analysis files have been reset."}), 200 # 選択されたファイルのコピー (ダウンロード) @app.route('/copy_selected_files', methods=['POST']) def copy_selected_files(): # このエンドポイントの目的が不明瞭(どこにコピーするのか?) # もしクライアント側でダウンロードさせたいなら、ファイルリストを返す方が適切かもしれない # ここでは指定された名前のファイルを /tmp/data/selected_audio にダウンロードする実装とする try: data = request.get_json() if not data or "names" not in data or not isinstance(data["names"], list): return jsonify({"error": "リクエストボディに 'names' 配列がありません"}), 400 names_to_copy = data["names"] if not names_to_copy: return jsonify({"status": "warning", "message": "No names provided for copying."}), 200 dest_dir = "/tmp/data/selected_audio" # コピー先 (サーバー上の一時フォルダ) os.makedirs(dest_dir, exist_ok=True) print(f"Copying selected files to server directory: {dest_dir}") copied_files = [] errors = [] for name in names_to_copy: if not name: continue dest_path = os.path.join(dest_dir, f"{name}.wav") try: download_from_cloud(f"{name}.wav", dest_path) copied_files.append(name) print(f"Downloaded '{name}.wav' to {dest_path}") except Exception as e: error_msg = f"Failed to download '{name}': {str(e)}" print(error_msg) errors.append({"name": name, "error": str(e)}) # エラーがあっても続行 if errors: return jsonify({ "status": "partial_success" if copied_files else "error", "message": f"Copied {len(copied_files)} files. Some errors occurred.", "copied": copied_files, "errors": errors, "destination_directory": dest_dir # 参考情報 }), 207 # Multi-Status else: return jsonify({ "status": "success", "message": f"Successfully copied {len(copied_files)} files.", "copied": copied_files, "destination_directory": dest_dir }), 200 except Exception as e: print(f"Error in /copy_selected_files: {str(e)}") return jsonify({"error": "サーバー内部エラーが発生しました", "details": str(e)}), 500 # 一時フォルダクリアエンドポイント (注意して使用) @app.route('/clear_tmp', methods=['POST']) # GETよりPOST/DELETEが適切 def clear_tmp(): # このエンドポイントはサーバー上の /tmp/data を再帰的に削除するため、非常に危険 # 実行すると、処理中のファイルも含めてすべて消える可能性がある # 使用は慎重に行うべき tmp_root_dir = "/tmp/data" print(f"WARNING: Received request to clear directory: {tmp_root_dir}") if not os.path.isdir(tmp_root_dir): return jsonify({"status": "warning", "message": f"Directory not found, nothing to clear: {tmp_root_dir}"}), 200 try: # 安全のため、特定のサブディレクトリのみを対象にする方が良いかもしれない # 例: subdirs_to_clear = ['uploads', 'base_audio', 'selected_audio', 'transcription_audio'] # for subdir_name in subdirs_to_clear: # subdir_path = os.path.join(tmp_root_dir, subdir_name) # if os.path.isdir(subdir_path): # shutil.rmtree(subdir_path) # print(f"Removed directory: {subdir_path}") # 現在の実装: /tmp/data 直下のファイルとディレクトリをすべて削除 deleted_items = [] errors = [] for item_name in os.listdir(tmp_root_dir): item_path = os.path.join(tmp_root_dir, item_name) try: if os.path.isfile(item_path) or os.path.islink(item_path): os.unlink(item_path) deleted_items.append(item_name) print(f"Deleted file/link: {item_path}") elif os.path.isdir(item_path): shutil.rmtree(item_path) deleted_items.append(item_name + "/") # ディレクトリを示す print(f"Deleted directory: {item_path}") except Exception as e: error_msg = f"Failed to delete '{item_name}': {str(e)}" print(error_msg) errors.append({"item": item_name, "error": str(e)}) if errors: return jsonify({ "status": "partial_success", "message": f"Cleared some items in {tmp_root_dir}, but errors occurred.", "deleted_items": deleted_items, "errors": errors }), 207 else: return jsonify({ "status": "success", "message": f"Successfully cleared contents of {tmp_root_dir}", "deleted_items": deleted_items }), 200 except Exception as e: print(f"Error in /clear_tmp: {str(e)}") return jsonify({"error": "サーバー内部エラーが発生しました", "details": str(e)}), 500 # ベース音声アップロード (ユーザー登録) @app.route('/upload_base_audio', methods=['POST']) def upload_base_audio(): global all_users # 登録後にリストを更新するため try: data = request.get_json() if not data or 'audio_data' not in data or 'name' not in data: return jsonify({"error": "音声データ(audio_data)または名前(name)がありません"}), 400 name = data['name'].strip() # 前後の空白を除去 if not name: return jsonify({"error": "名前が空です"}), 400 # TODO: 名前に使用できない文字(ファイル名として不適切、セキュリティリスクなど)をチェックするバリデーションを追加推奨 # 例: if not re.match(r'^[a-zA-Z0-9_-]+$', name): return jsonify({"error": "Invalid characters in name"}), 400 filename_to_upload = f"{name}.wav" print(f"Base audio upload request for name: {name} (filename: {filename_to_upload})") payload = { "action": "upload", "fileName": filename_to_upload, "base64Data": data['audio_data'] } # GASにアップロード実行 res_json = _make_gas_request(payload, timeout=60) # アップロードは時間がかかる可能性 print(f"Successfully uploaded base audio to cloud. File ID: {res_json.get('fileId')}") # 全ユーザーリストを更新 (GASに再度問い合わせるのが確実) try: update_all_users() except Exception as update_e: print(f"Warning: Failed to refresh user list after upload: {update_e}") # アップロード自体は成功しているので、警告付きで成功レスポンスを返す return jsonify({ "state": "Registration Success (List update may be delayed)", "driveFileId": res_json.get("fileId"), "warning": f"User list update failed: {update_e}" }), 200 # または 201 Created return jsonify({"state": "Registration Success!", "driveFileId": res_json.get("fileId")}), 201 # 201 Created がより適切かも except base64.binascii.Error as e: print(f"Error decoding base64 audio data: {e}") return jsonify({"error": "無効な音声データ形式です (Base64デコード失敗)"}), 400 except (TimeoutError, ConnectionError, ValueError, Exception) as e: error_message = f"Failed to upload base audio for '{name}'." details = str(e) print(f"{error_message} Details: {details}") # GASからのエラーメッセージに 'already exists' が含まれていたら 409 Conflict を返す status_code = 409 if "already exists" in details.lower() else 500 return jsonify({"error": error_message, "details": details}), status_code # === アプリケーションの実行 === if __name__ == '__main__': port = int(os.environ.get("PORT", 7860)) # 本番環境では debug=False に設定すること # host='0.0.0.0' はコンテナなど外部からアクセスする場合に必要 print(f"Starting Flask server on host 0.0.0.0 port {port} with debug mode {'ON' if app.debug else 'OFF'}") app.run(debug=True, host="0.0.0.0", port=port)