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 process = AudioProcessor() transcripter = TranscriptionMaker() app = Flask(__name__) # 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) @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", "fileNames": 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)