Spaces:
Runtime error
Runtime error
| 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構造は変更しない) | |
| 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用: ユーザー削除エンドポイント | |
| 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 | |
| # --- 既存のテンプレートレンダリング用ルート (変更なし) --- | |
| # POST不要なら削除 | |
| def index(): | |
| return render_template('index.html', users=users) | |
| def feedback(): | |
| return render_template('feedback.html') | |
| def talk_detail(): | |
| return render_template('talkDetail.html') | |
| def userregister(): | |
| return render_template('userRegister.html') | |
| def reset_html(): | |
| return render_template('reset.html') | |
| # ルートパス | |
| 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エンドポイント (一部見直し) --- | |
| # 人数確認 (最新リストを返すように修正) | |
| 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推奨) | |
| # 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 | |
| # 書き起こし作成エンドポイント (エラーハンドリング改善) | |
| # 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キーチェック改善) | |
| # 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 | |
| # 音声アップロード&解析エンドポイント (エラーハンドリング、一時ファイル管理改善) | |
| 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 エンドポイントなどで定期的に削除することを推奨 | |
| # --- ここまでクリーンアップ --- | |
| # 選択したユーザーを設定するエンドポイント | |
| 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 | |
| # リセットエンドポイント (選択中ユーザーと一時ファイルをクリア) | |
| # または 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 | |
| # 選択されたファイルのコピー (ダウンロード) | |
| 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 | |
| # 一時フォルダクリアエンドポイント (注意して使用) | |
| # 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 | |
| # ベース音声アップロード (ユーザー登録) | |
| 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) |