from flask import Flask, request, jsonify, render_template, send_from_directory import base64 from pydub import AudioSegment import os import shutil import requests import tempfile import json from process import AudioProcessor from transcription import TranscriptionMaker from analyze import TextAnalyzer from flask_cors import CORS process = AudioProcessor() transcripter = TranscriptionMaker() app = Flask(__name__) # CORS設定: すべてのオリジンからのリクエストを許可 # 必要であれば、特定のオリジンやメソッド、ヘッダーをより厳密に指定できます # 例: CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}}, supports_credentials=True) CORS(app, origins="*", methods=["GET", "POST", "DELETE", "OPTIONS"], headers=["Content-Type", "Authorization"]) # GASのエンドポイントURL GAS_URL = "https://script.google.com/macros/s/AKfycbwR2cnMKVU1AxoT9NDaeZaNUaTiwRX64Ul0sH0AU4ccP49Byph-TpxtM_Lwm4G9zLnuYA/exec" users = [] # 選択されたユーザーのリスト all_users = [] # 利用可能なすべてのユーザーのリスト transcription_text = "" harassment_keywords = [ "バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい", "きもい", "キモい", "ブス", "デブ", "ハゲ", "セクハラ", "パワハラ", "モラハラ" ] total_audio = "" @app.route('/index', methods=['GET', 'POST']) def index(): return render_template('index.html', users=users) # フィードバック画面(テンプレート: feedback.html) @app.route('/feedback', methods=['GET', 'POST']) def feedback(): return render_template('feedback.html') # 会話詳細画面(テンプレート: talkDetail.html) @app.route('/talk_detail', methods=['GET', 'POST']) def talk_detail(): return render_template('talkDetail.html') # 音声登録画面(テンプレート: userRegister.html) @app.route('/userregister', methods=['GET', 'POST']) def userregister(): return render_template('userRegister.html') # 人数確認 @app.route('/confirm', methods=['GET']) def confirm(): global all_users # 最新のユーザーリストを取得 try: update_all_users() except Exception as e: print(f"ユーザーリストの更新エラー: {str(e)}") return jsonify({'members': users, 'all_members': all_users}), 200 # リセット画面(テンプレート: reset.html) @app.route('/reset_html', methods=['GET', 'POST']) def reset_html(): return render_template('reset.html') # メンバー削除&累積音声削除 @app.route('/reset_member', methods=['GET', 'POST']) def reset_member(): global users global total_audio global transcription_text # 一時ディレクトリのクリーンアップ if total_audio: process.delete_files_in_directory(total_audio) process.delete_files_in_directory('/tmp/data/transcription_audio') # 書き起こしテキストの削除 if os.path.exists(transcription_text): try: os.remove(transcription_text) print(f"{transcription_text} を削除しました。") except Exception as e: print(f"ファイル削除中にエラーが発生しました: {e}") transcription_text = "" try: data = request.get_json() if not data or "names" not in data: return jsonify({"status": "error", "message": "Invalid request body"}), 400 names = data.get("names", []) # GASからファイルを削除 for name in names: try: delete_from_cloud(f"{name}.wav") print(f"クラウドから {name}.wav を削除しました。") except Exception as e: print(f"クラウド削除中にエラーが発生しました: {e}") return jsonify({"status": "error", "message": f"Failed to delete {name} from cloud: {e}"}), 500 # usersリストから削除するユーザーを除外 users = [u for u in users if u not in names] # 全ユーザーリストの更新 update_all_users() return jsonify({"status": "success", "message": "Members deleted successfully", "users": users}), 200 except Exception as e: print(f"An unexpected error occurred: {e}") return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500 # 書き起こし作成エンドポイント @app.route('/transcription', methods=['GET', 'POST']) def transcription(): global transcription_text global total_audio if not os.path.exists(transcription_text) or not transcription_text: try: if not total_audio or not os.path.exists(total_audio): return jsonify({"error": "No audio segments provided"}), 400 transcription_text = transcripter.create_transcription(total_audio) print("transcription") print(transcription_text) except Exception as e: return jsonify({"error": str(e)}), 500 try: with open(transcription_text, 'r', encoding='utf-8') as file: file_content = file.read() print(file_content) return jsonify({'transcription': file_content}), 200 except FileNotFoundError: return jsonify({"error": "Transcription file not found"}), 404 except Exception as e: return jsonify({"error": f"Unexpected error: {str(e)}"}), 500 # AI分析エンドポイント @app.route('/analyze', methods=['GET', 'POST']) def analyze(): global transcription_text global total_audio if not os.path.exists(transcription_text) or not transcription_text: try: if not total_audio: return jsonify({"error": "No audio segments provided"}), 400 transcription_text = transcripter.create_transcription(total_audio) except Exception as e: return jsonify({"error": str(e)}), 500 analyzer = TextAnalyzer(transcription_text, harassment_keywords) api_key = os.environ.get("DEEPSEEK") if api_key is None: raise ValueError("DEEPSEEK_API_KEY が設定されていません。") results = analyzer.analyze(api_key=api_key) print(json.dumps(results, ensure_ascii=False, indent=2)) if "deepseek_analysis" in results and results["deepseek_analysis"]: deepseek_data = results["deepseek_analysis"] conversation_level = deepseek_data.get("conversationLevel") harassment_present = deepseek_data.get("harassmentPresent") harassment_type = deepseek_data.get("harassmentType") repetition = deepseek_data.get("repetition") pleasantConversation = deepseek_data.get("pleasantConversation") blameOrHarassment = deepseek_data.get("blameOrHarassment") print("\n--- DeepSeek 分析結果 ---") print(f"会話レベル: {conversation_level}") print(f"ハラスメントの有無: {harassment_present}") print(f"ハラスメントの種類: {harassment_type}") print(f"繰り返しの程度: {repetition}") print(f"会話の心地よさ: {pleasantConversation}") print(f"非難またはハラスメントの程度: {blameOrHarassment}") return jsonify({"results": results}), 200 # クラウドから音声を取得してローカルに保存する関数 def download_from_cloud(filename, local_path): try: payload = { "action": "download", "fileName": filename } print(f"クラウドから {filename} をダウンロード中...") response = requests.post(GAS_URL, json=payload) if response.status_code != 200: print(f"ダウンロードエラー: ステータスコード {response.status_code}") print(f"レスポンス: {response.text}") raise Exception(f"クラウドからのダウンロードに失敗しました: {response.text}") try: res_json = response.json() except: print("JSONデコードエラー、レスポンス内容:") print(response.text[:500]) # 最初の500文字だけ表示 raise Exception("サーバーからの応答をJSONとして解析できませんでした") if res_json.get("status") != "success": print(f"ダウンロードステータスエラー: {res_json.get('message')}") raise Exception(f"クラウドからのダウンロードに失敗しました: {res_json.get('message')}") # Base64文字列をデコード base64_data = res_json.get("base64Data") if not base64_data: print("Base64データが存在しません") raise Exception("応答にBase64データが含まれていません") try: audio_binary = base64.b64decode(base64_data) except Exception as e: print(f"Base64デコードエラー: {str(e)}") raise Exception(f"音声データのデコードに失敗しました: {str(e)}") # 指定パスに保存 os.makedirs(os.path.dirname(local_path), exist_ok=True) with open(local_path, 'wb') as f: f.write(audio_binary) print(f"{filename} をローカルに保存しました: {local_path}") # データの整合性チェック(ファイルサイズが0より大きいかなど) if os.path.getsize(local_path) <= 0: raise Exception(f"保存されたファイル {local_path} のサイズが0バイトです") return local_path except Exception as e: print(f"ダウンロード中にエラーが発生しました: {str(e)}") # エラーを上位に伝播させる raise # クラウドからファイルを削除する関数 def delete_from_cloud(filename): payload = { "action": "delete", "fileName": filename } response = requests.post(GAS_URL, json=payload) if response.status_code != 200: raise Exception(f"クラウドからの削除に失敗しました: {response.text}") res_json = response.json() if res_json.get("status") != "success": raise Exception(f"クラウドからの削除に失敗しました: {res_json.get('message')}") return True # すべてのベース音声ユーザーリストを更新する関数 def update_all_users(): global all_users payload = {"action": "list"} response = requests.post(GAS_URL, json=payload) if response.status_code != 200: raise Exception(f"GAS一覧取得エラー: {response.text}") res_json = response.json() if res_json.get("status") != "success": raise Exception(f"GAS一覧取得失敗: {res_json.get('message')}") # ファイル名から拡張子を除去してユーザーリストを作成 all_users = [os.path.splitext(filename)[0] for filename in res_json.get("fileNames", [])] return all_users # 音声アップロード&解析エンドポイント @app.route('/upload_audio', methods=['POST']) def upload_audio(): global total_audio global users try: data = request.get_json() if not data or 'audio_data' not in data: return jsonify({"error": "音声データがありません"}), 400 # リクエストからユーザーリストを取得(指定がなければ現在のusersを使用) if 'selected_users' in data and data['selected_users']: users = data['selected_users'] print(f"選択されたユーザー: {users}") if not users: return jsonify({"error": "選択されたユーザーがいません"}), 400 # Base64デコードして音声バイナリを取得 audio_binary = base64.b64decode(data['audio_data']) upload_name = 'tmp' audio_dir = "/tmp/data" os.makedirs(audio_dir, exist_ok=True) audio_path = os.path.join(audio_dir, f"{upload_name}.wav") with open(audio_path, 'wb') as f: f.write(audio_binary) print(f"処理を行うユーザー: {users}") # ベース音声を一時ディレクトリにダウンロード temp_dir = "/tmp/data/base_audio" os.makedirs(temp_dir, exist_ok=True) # 各ユーザーの参照音声ファイルのパスをリストに格納 reference_paths = [] for user in users: try: ref_path = os.path.join(temp_dir, f"{user}.wav") if not os.path.exists(ref_path): # クラウドから取得 download_from_cloud(f"{user}.wav", ref_path) print(f"クラウドから {user}.wav をダウンロードしました") if not os.path.exists(ref_path): return jsonify({"error": "参照音声ファイルが見つかりません", "details": ref_path}), 500 reference_paths.append(ref_path) except Exception as e: return jsonify({"error": f"ユーザー {user} の音声取得に失敗しました", "details": str(e)}), 500 # 複数人の場合は参照パスのリストを、1人の場合は単一のパスを渡す if len(users) > 1: print("複数人の場合の処理") matched_times, merged_segments = process.process_multi_audio(reference_paths, audio_path, users, threshold=0.05) total_audio = transcripter.save_marged_segments(merged_segments) # 各メンバーのrateを計算 total_time = sum(matched_times) 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))} return jsonify({"rates": rates, "user_rates": user_rates}), 200 else: matched_time, unmatched_time, merged_segments = process.process_audio(reference_paths[0], audio_path, users[0], threshold=0.05) total_audio = transcripter.save_marged_segments(merged_segments) print("単一ユーザーの処理") total_time = matched_time + unmatched_time rate = (matched_time / total_time) * 100 if total_time > 0 else 0 return jsonify({"rate": rate, "user": users[0]}), 200 except Exception as e: print("Error in /upload_audio:", str(e)) return jsonify({"error": "サーバーエラー", "details": str(e)}), 500 # ユーザー選択画面(テンプレート: userSelect.html) # ユーザー選択画面(テンプレート: userSelect.html) @app.route('/') @app.route('/userselect', methods=['GET']) def userselect(): return render_template('userSelect.html') # 選択したユーザーを設定するエンドポイント @app.route('/select_users', methods=['POST']) def select_users(): global users try: data = request.get_json() if not data or 'users' not in data: return jsonify({"error": "ユーザーリストがありません"}), 400 users = data['users'] print(f"選択されたユーザー: {users}") return jsonify({"status": "success", "selected_users": users}), 200 except Exception as e: print("Error in /select_users:", str(e)) return jsonify({"error": "サーバーエラー", "details": str(e)}), 500 @app.route('/reset', methods=['GET']) def reset(): global users users = [] global total_audio global transcription_text # 一時ディレクトリのクリーンアップ if total_audio: process.delete_files_in_directory(total_audio) process.delete_files_in_directory('/tmp/data/transcription_audio') # 書き起こしテキストの削除 if os.path.exists(transcription_text): try: os.remove(transcription_text) print(f"{transcription_text} を削除しました。") except Exception as e: print(f"ファイル削除中にエラーが発生しました: {e}") transcription_text = "" return jsonify({"status": "success", "message": "Users reset"}), 200 @app.route('/copy_selected_files', methods=['POST']) def copy_selected_files(): try: data = request.get_json() if not data or "names" not in data: return jsonify({"error": "namesパラメータが存在しません"}), 400 names = data["names"] dest_dir = "/tmp/data/selected_audio" # コピー先のフォルダ os.makedirs(dest_dir, exist_ok=True) copied_files = [] for name in names: 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"{name}.wav を {dest_path} にダウンロードしました。") except Exception as e: print(f"ダウンロード中にエラーが発生しました: {e}") continue return jsonify({"status": "success", "copied": copied_files}), 200 except Exception as e: print("Error in /copy_selected_files:", str(e)) return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500 @app.route('/clear_tmp', methods=['GET']) def clear_tmp(): try: tmp_dir = "/tmp/data" # アプリケーションが使用しているtmpフォルダ # ファイルのみの削除 process.delete_files_in_directory(tmp_dir) # フォルダがあれば再帰的に削除 for item in os.listdir(tmp_dir): item_path = os.path.join(tmp_dir, item) if os.path.isdir(item_path): shutil.rmtree(item_path) print(f"ディレクトリを削除しました: {item_path}") return jsonify({"status": "success", "message": "tmp配下がすべて削除されました"}), 200 except Exception as e: print("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": "音声データまたは名前がありません"}), 400 name = data['name'] print(f"登録名: {name}") # GASのアップロードエンドポイントにリクエスト payload = { "action": "upload", "fileName": f"{name}.wav", "base64Data": data['audio_data'] } response = requests.post(GAS_URL, json=payload) if response.status_code != 200: return jsonify({"error": "GASアップロードエラー", "details": response.text}), 500 res_json = response.json() if res_json.get("status") != "success": return jsonify({"error": "GASアップロード失敗", "details": res_json.get("message")}), 500 # 全ユーザーリストを更新 update_all_users() return jsonify({"state": "Registration Success!", "driveFileId": res_json.get("fileId")}), 200 except Exception as e: print("Error in /upload_base_audio:", str(e)) return jsonify({"error": "サーバーエラー", "details": str(e)}), 500 @app.route('/list_base_audio', methods=['GET']) def list_base_audio(): try: global all_users all_users = update_all_users() return jsonify({"status": "success", "id": all_users}), 200 except Exception as e: print("Error in /list_base_audio:", str(e)) return jsonify({"error": "サーバーエラー", "details": str(e)}), 500 if __name__ == '__main__': port = int(os.environ.get("PORT", 7860)) app.run(debug=True, host="0.0.0.0", port=port)