diff --git a/.gitattributes b/.gitattributes index b041bcafc40688039894f941c32e65ad8d72e10f..a6344aac8c09253b3b630fb776ae94478aa0275b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,37 +1,35 @@ -*.7z filter=lfs diff=lfs merge=lfs -text -*.arrow filter=lfs diff=lfs merge=lfs -text -*.bin filter=lfs diff=lfs merge=lfs -text -*.bz2 filter=lfs diff=lfs merge=lfs -text -*.ckpt filter=lfs diff=lfs merge=lfs -text -*.ftz filter=lfs diff=lfs merge=lfs -text -*.gz filter=lfs diff=lfs merge=lfs -text -*.h5 filter=lfs diff=lfs merge=lfs -text -*.joblib filter=lfs diff=lfs merge=lfs -text -*.lfs.* filter=lfs diff=lfs merge=lfs -text -*.mlmodel filter=lfs diff=lfs merge=lfs -text -*.model filter=lfs diff=lfs merge=lfs -text -*.msgpack filter=lfs diff=lfs merge=lfs -text -*.npy filter=lfs diff=lfs merge=lfs -text -*.npz filter=lfs diff=lfs merge=lfs -text -*.onnx filter=lfs diff=lfs merge=lfs -text -*.ot filter=lfs diff=lfs merge=lfs -text -*.parquet filter=lfs diff=lfs merge=lfs -text -*.pb filter=lfs diff=lfs merge=lfs -text -*.pickle filter=lfs diff=lfs merge=lfs -text -*.pkl filter=lfs diff=lfs merge=lfs -text -*.pt filter=lfs diff=lfs merge=lfs -text -*.pth filter=lfs diff=lfs merge=lfs -text -*.rar filter=lfs diff=lfs merge=lfs -text -*.safetensors filter=lfs diff=lfs merge=lfs -text -saved_model/**/* filter=lfs diff=lfs merge=lfs -text -*.tar.* filter=lfs diff=lfs merge=lfs -text -*.tar filter=lfs diff=lfs merge=lfs -text -*.tflite filter=lfs diff=lfs merge=lfs -text -*.tgz filter=lfs diff=lfs merge=lfs -text -*.wasm filter=lfs diff=lfs merge=lfs -text -*.xz filter=lfs diff=lfs merge=lfs -text -*.zip filter=lfs diff=lfs merge=lfs -text -*.zst filter=lfs diff=lfs merge=lfs -text -*tfevents* filter=lfs diff=lfs merge=lfs -text -segment_0[[:space:]](2).wav filter=lfs diff=lfs merge=lfs -text -sample.wav filter=lfs diff=lfs merge=lfs -text +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore deleted file mode 100644 index bf28fad48d89996ff75bb93d5dfbcdb571e6267b..0000000000000000000000000000000000000000 Binary files a/.gitignore and /dev/null differ diff --git a/Dockerfile b/Dockerfile index ba0cbf0bbfdaa2dcd279b7375d18ea37231c6e4f..a7428f122df32bf9fc270501e752475f448afecb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,17 @@ -FROM nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04 - -# タイムゾーン設定 -RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime - -# Python3、pip、ffmpegをインストール -RUN apt-get update && \ - apt-get install -y python3 python3-pip ffmpeg && \ - rm -rf /var/lib/apt/lists/* - -# pipを最新版にアップグレード -RUN python3 -m pip install --upgrade pip - -WORKDIR /app - -# requirements.txt をコンテナ内にコピーして、必要なパッケージをインストール -COPY requirements.txt /app/ - -RUN python3 -m pip install --no-cache-dir -r requirements.txt - -COPY . . - -CMD ["python3", "app.py"] \ No newline at end of file +# Dockerfile +FROM python:3.9-slim + +WORKDIR /app + +# 依存関係のインストール +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# アプリの全ファイルをコピー +COPY . . +RUN touch weak_phrases.json && chmod 666 weak_phrases.json + +# Hugging Face Spaces ではポート 7860 を使用する +EXPOSE 7860 + +CMD ["python", "app.py"] diff --git a/README.md b/README.md index 5086688e050012af584d3809ae49f194c48bc3eb..a0df14e3bd6107dc433bcaec0d9544ddc193101b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ ---- -title: JusTalk -emoji: ⚡ -colorFrom: gray -colorTo: blue -sdk: docker -pinned: false ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +--- +title: JusTalk +emoji: ⚡ +colorFrom: gray +colorTo: blue +sdk: docker +pinned: false +--- + +Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/__pycache__/analyze.cpython-310.pyc b/__pycache__/analyze.cpython-310.pyc deleted file mode 100644 index 57e0787600dbdcd4accae13b047c9a871b2600f1..0000000000000000000000000000000000000000 Binary files a/__pycache__/analyze.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc deleted file mode 100644 index 9cae6e87f4a7b488357eed10954c8553edff2221..0000000000000000000000000000000000000000 Binary files a/__pycache__/app.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/database.cpython-310.pyc b/__pycache__/database.cpython-310.pyc deleted file mode 100644 index 0972fe47eee514a6ec9078f76f9f90b0338eb33b..0000000000000000000000000000000000000000 Binary files a/__pycache__/database.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/database.cpython-311.pyc b/__pycache__/database.cpython-311.pyc deleted file mode 100644 index 1dca8c2800e97e7f7d8869bf57f39110e749e278..0000000000000000000000000000000000000000 Binary files a/__pycache__/database.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/login.cpython-310.pyc b/__pycache__/login.cpython-310.pyc deleted file mode 100644 index 0ebf0713d6f605166aeba8011d96252b8190bc31..0000000000000000000000000000000000000000 Binary files a/__pycache__/login.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/models.cpython-310.pyc b/__pycache__/models.cpython-310.pyc deleted file mode 100644 index ea3111a26f61ccfc815b418387e230883d61f721..0000000000000000000000000000000000000000 Binary files a/__pycache__/models.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/new_record.cpython-310.pyc b/__pycache__/new_record.cpython-310.pyc deleted file mode 100644 index 1366e177b783a1b75e970e01002819bde498bb28..0000000000000000000000000000000000000000 Binary files a/__pycache__/new_record.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/process.cpython-310.pyc b/__pycache__/process.cpython-310.pyc deleted file mode 100644 index 45ab864bb90e5589b00ecd46182bd1228fd11f18..0000000000000000000000000000000000000000 Binary files a/__pycache__/process.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/transcription.cpython-310.pyc b/__pycache__/transcription.cpython-310.pyc deleted file mode 100644 index a395eb2d6259f2cb34a37d6feaca1320ced5fdf5..0000000000000000000000000000000000000000 Binary files a/__pycache__/transcription.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/users.cpython-310.pyc b/__pycache__/users.cpython-310.pyc deleted file mode 100644 index 790f44463cffdd5163dc530ae4dbdd766e63ecc5..0000000000000000000000000000000000000000 Binary files a/__pycache__/users.cpython-310.pyc and /dev/null differ diff --git a/__pycache__/users.cpython-311.pyc b/__pycache__/users.cpython-311.pyc deleted file mode 100644 index a29bfdf59bb9143d162b4b0f486ed99b85bfff2e..0000000000000000000000000000000000000000 Binary files a/__pycache__/users.cpython-311.pyc and /dev/null differ diff --git a/analyze.py b/analyze.py deleted file mode 100644 index 4bb8c8f0a6e834b52b5e665f6181e894ffc0921b..0000000000000000000000000000000000000000 --- a/analyze.py +++ /dev/null @@ -1,224 +0,0 @@ -import json -import requests -import os - -class TextAnalyzer: - """ - テキストのハラスメント検出と会話評価を行うクラス。 - """ - def __init__(self, file_path, keywords): - """ - TextAnalyzer クラスのコンストラクタ。 - - Args: - file_path (str): 分析するテキストファイルのパス。 - keywords (list): ハラスメント検出に使用するキーワードのリスト。 - """ - self.file_path = file_path - self.keywords = keywords - self.text_content = None # テキストファイルの内容を格納 - self.harassment_detected = False # ハラスメントが検出されたかどうか - self.harassment_keywords = [] # 検出されたハラスメントキーワードのリスト - self.deepseek_analysis = {} # DeepSeek API による分析結果を格納する辞書 - self.api_key = None - - def load_text(self): - """ - テキストファイルを読み込み、その内容を self.text_content に格納する。 - - Returns: - bool: ファイルの読み込みに成功した場合は True、失敗した場合は False。 - """ - try: - with open(self.file_path, 'r', encoding='utf-8') as file: - self.text_content = file.read() - return True - except Exception as e: - print(f"ファイル読み込みエラー: {e}") - return False - - def detect_harassment(self): - """ - テキスト内容からハラスメントを検出する。 - - Returns: - bool: ハラスメントが検出された場合は True、それ以外は False。 - """ - if not self.text_content: - return False - - self.harassment_keywords = [] - for keyword in self.keywords: - if keyword in self.text_content: - self.harassment_detected = True - self.harassment_keywords.append(keyword) - - return self.harassment_detected - - def analyze_with_deepseek(self, api_key=None, api_url="https://api.deepseek.com/v1/chat/completions"): - """ - DeepSeek API を使用して会話を分析する。会話レベルやハラスメントの詳細な検出を行う。 - - Args: - api_key (str, optional): DeepSeek API キー。指定されない場合は環境変数から取得。 - api_url (str, optional): DeepSeek API の URL。デフォルトは標準のチャット補完エンドポイント。 - - Returns: - bool: 分析に成功した場合は True、失敗した場合は False。 - """ - if not self.text_content: - return False - - # 提供された API キーを使用するか、環境変数から取得する - if api_key: - self.api_key = api_key - else: - self.api_key = os.environ.get("DEEPSEEK_API_KEY") - if not self.api_key: - print("DeepSeek API キーが提供されておらず、環境変数にも見つかりませんでした。") - return False - - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.api_key}" - } - - prompt = f""" - 以下の会話を分析し、結果を JSON 形式で返してください。1 から 10 のスケールで評価し、10 が最高です。 - 厳密に評価してください。ハラスメントが存在する場合は、その種類を具体的に記述してください。 - 評価基準: - 1. conversationLevel: 会話のレベル (初心者、中級者、上級者)。 - 2. harassmentPresent: 会話にハラスメント表現が含まれているかどうか (true/false)。 - 3. harassmentType: ハラスメントが存在する場合、その種類を具体的に記述。 - 4. topicAppropriateness: 会話のトピックが適切かどうか。 - 5. improvementSuggestions: 会話を改善するための具体的な提案。 - 6. repetition: 同じことがどの程度繰り返されているか。(1-10) - 7. pleasantConversation: 会話がどの程度心地よいか。(1-10) - 8. blameOrHarassment: 会話がどの程度相手を責めたり、ハラスメントをしているか。(1-10) - - 会話内容: - {self.text_content} - - JSON 形式のみを返してください。 - """ - - data = { - "model": "deepseek-chat", - "messages": [{"role": "user", "content": prompt}], - "response_format": {"type": "json_object"} - } - - try: - response = requests.post(api_url, headers=headers, json=data) - response.raise_for_status() - - result = response.json() - deepseek_response = json.loads(result["choices"][0]["message"]["content"]) - - # 指定されたキーを使用して、インスタンス変数に値を割り当てる - self.deepseek_analysis = { - "conversationLevel": deepseek_response.get("conversationLevel"), - "harassmentPresent": deepseek_response.get("harassmentPresent"), - "harassmentType": deepseek_response.get("harassmentType"), - "topicAppropriateness": deepseek_response.get("topicAppropriateness"), - "improvementSuggestions": deepseek_response.get("improvementSuggestions"), - "repetition": deepseek_response.get("repetition"), - "pleasantConversation": deepseek_response.get("pleasantConversation"), - "blameOrHarassment": deepseek_response.get("blameOrHarassment"), - } - - return True - except requests.exceptions.RequestException as e: - print(f"DeepSeek API リクエストエラー: {e}") - return False - except json.JSONDecodeError as e: - print(f"DeepSeek API レスポンスの JSON デコードエラー: {e}") - print(f"レスポンス内容: {response.text}") - return False - except KeyError as e: - print(f"DeepSeek API レスポンスのキーエラー: {e}") - print(f"レスポンス内容: {response.text}") - return False - except Exception as e: - print(f"DeepSeek API エラー: {e}") - return False - - def get_analysis_results(self): - """ - 分析結果を返す。 - - Returns: - dict: 分析結果を含む辞書。 - """ - results = { - "text_content": self.text_content, - "basic_harassment_detection": { - "detected": self.harassment_detected, - "matching_keywords": self.harassment_keywords - }, - "deepseek_analysis": self.deepseek_analysis - } - - return results - - def analyze(self, api_key=None): - """ - すべての分析を実行し、結果を返す。 - - Args: - api_key (str, optional): DeepSeek API キー。 - - Returns: - dict: 分析結果またはエラーメッセージを含む辞書。 - """ - if not self.load_text(): - return {"error": "テキストファイルの読み込みに失敗しました。"} - - self.detect_harassment() - - if not self.analyze_with_deepseek(api_key): - return {"error": "DeepSeek API 分析に失敗しました。"} - - return self.get_analysis_results() - -''' -# 使用例 -if __name__ == "__main__": - # ハラスメント検出用のキーワード例 - harassment_keywords = [ - "バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい", - "きもい", "キモい", "ブス", "デブ", "ハゲ", - "セクハラ", "パワハラ", "モラハラ" - ] - - # 分析インスタンスの作成 - analyzer = TextAnalyzer("./2.txt", harassment_keywords) - - # DeepSeek API キー (環境変数から取得するか、直接渡す) - # api_key = os.environ.get("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}") -''' \ No newline at end of file diff --git a/app.py b/app.py index 17d57b3085b4899cd83c599ce26c0765cb01bef9..4f7121047d1e0141dc7dd52b98775c488c5e2715 100644 --- a/app.py +++ b/app.py @@ -1,497 +1,48 @@ -from flask import Flask, request, jsonify, render_template, send_from_directory +from flask import Flask, request, jsonify, 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 = Flask(__name__) -@app.route('/index', methods=['GET', 'POST']) +@app.route('/') def index(): - return render_template('index.html', users=users) + return send_from_directory(".", "index.html") -# フィードバック画面(テンプレート: feedback.html) -@app.route('/feedback', methods=['GET', 'POST']) +@app.route('/feedback',methods=['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 + return send_from_directory(".","feedback.html") -# 音声アップロード&解析エンドポイント @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 + if not data: + return jsonify({"error": "JSONが送信されていません"}), 400 -# ユーザー選択画面(テンプレート: userSelect.html) + audio_data = data.get('audio_data') + if not audio_data: + return jsonify({"error": "音声データが送信されていません"}), 400 -# ユーザー選択画面(テンプレート: 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): + # Base64デコード 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 + audio_binary = base64.b64decode(audio_data) + except Exception as decode_err: + return jsonify({"error": "Base64デコードに失敗しました", "details": str(decode_err)}), 400 - return jsonify({"status": "success", "copied": copied_files}), 200 + # 書き込み用ディレクトリとして /tmp/data を使用(/tmp は書き込み可能) + persist_dir = "/tmp/data" + os.makedirs(persist_dir, exist_ok=True) - 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}") + filepath = os.path.join(persist_dir, "recorded_audio.wav") + with open(filepath, 'wb') as f: + f.write(audio_binary) - return jsonify({"status": "success", "message": "tmp配下がすべて削除されました"}), 200 + return jsonify({"message": "音声が正常に保存されました", "filepath": filepath}), 200 except Exception as e: - print("Error in /clear_tmp:", str(e)) + app.logger.error("エラー: %s", 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) \ No newline at end of file diff --git a/feedback.html b/feedback.html new file mode 100644 index 0000000000000000000000000000000000000000..5f571e5267fcad3e3ac42ed63f67dbe930f11481 --- /dev/null +++ b/feedback.html @@ -0,0 +1,136 @@ + + + + + + 会話フィードバック画面 + + + + +
+
話者Lv: 85
+
素晴らしい
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ + + +
+ + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..44ef7688be8af4f73ea849084a0ce2b3c0c98f01 --- /dev/null +++ b/index.html @@ -0,0 +1,171 @@ + + + + + + Voice Recorder Interface + + + + +
+ +
+ + + +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/init/createdatabase.sql b/init/createdatabase.sql deleted file mode 100644 index 8085be377736e2488c256ca62d7ce61fb8061ec2..0000000000000000000000000000000000000000 --- a/init/createdatabase.sql +++ /dev/null @@ -1,14 +0,0 @@ -USE app; - -CREATE TABLE users( - user_id INT PRIMARY KEY AUTO_INCREMENT, - username VARCHAR(255), - password VARCHAR(255), - -); - -INSERT INTO users(username,password) VALUES('sample','sample'); -INSERT INTO users(username,password) VALUES('test','test'); -INSERT INTO users(username,password) VALUES('app','app'); - -GRANT ALL ON app.* TO test; \ No newline at end of file diff --git a/instance/site.db b/instance/site.db deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/process.py b/process.py deleted file mode 100644 index 917b085b1dec2bf73a9012e5fc0b69ab01c73217..0000000000000000000000000000000000000000 --- a/process.py +++ /dev/null @@ -1,539 +0,0 @@ -import os -import shutil -import numpy as np -import string -import random -from datetime import datetime -from pyannote.audio import Model, Inference -from pydub import AudioSegment -import base64 -import binascii -import warnings - -class AudioProcessor(): - def __init__(self, cache_dir="/tmp/hf_cache", standard_duration=5.0): - hf_token = os.environ.get("HF") - if hf_token is None: - raise ValueError("HUGGINGFACE_HUB_TOKEN が設定されていません。") - os.makedirs(cache_dir, exist_ok=True) - # pyannote モデルの読み込み - model = Model.from_pretrained("pyannote/embedding", use_auth_token=hf_token, cache_dir=cache_dir) - self.inference = Inference(model) - # 標準の音声長さ(秒) - self.standard_duration = standard_duration - - def normalize_audio_duration(self, input_path, target_duration_seconds=None, output_path=None): - """ - 音声ファイルの長さを指定された時間(秒)にそろえる関数 - 短すぎる場合は無音を追加し、長すぎる場合は切り詰める - - Parameters: - input_path (str): 入力音声ファイルのパス - target_duration_seconds (float, optional): 目標となる音声の長さ(秒)。Noneの場合はself.standard_durationを使用 - output_path (str, optional): 出力先のパス。Noneの場合は一時ファイルを生成 - - Returns: - str: 処理された音声ファイルのパス - """ - try: - # デフォルト値の設定 - if target_duration_seconds is None: - target_duration_seconds = self.standard_duration - - # 音声ファイルを読み込む - audio = AudioSegment.from_file(input_path) - - # 現在の長さ(ミリ秒) - current_duration_ms = len(audio) - target_duration_ms = int(target_duration_seconds * 1000) - - # 出力パスが指定されていない場合は一時ファイルを生成 - if output_path is None: - random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) - timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - output_dir = os.path.dirname(input_path) if os.path.dirname(input_path) else '/tmp' - output_path = os.path.join(output_dir, f"normalized_{timestamp}_{random_str}.wav") - - # 長さの調整 - if current_duration_ms < target_duration_ms: - # 短い場合は無音を追加 - silence_duration = target_duration_ms - current_duration_ms - silence = AudioSegment.silent(duration=silence_duration) - normalized_audio = audio + silence - else: - # 長い場合は切り詰め - normalized_audio = audio[:target_duration_ms] - - # ファイルに保存 - normalized_audio.export(output_path, format="wav") - - return output_path - - except Exception as e: - print(f"音声の長さをそろえる処理でエラーが発生しました: {e}") - return None - - def batch_normalize_audio_duration(self, input_directory, target_duration_seconds=None, output_directory=None): - """ - ディレクトリ内の全音声ファイルの長さをそろえる関数 - - Parameters: - input_directory (str): 入力音声ファイルが格納されているディレクトリ - target_duration_seconds (float, optional): 目標となる音声の長さ(秒)。Noneの場合はself.standard_durationを使用 - output_directory (str, optional): 出力先のディレクトリ。Noneの場合は入力と同じディレクトリに処理結果を保存 - - Returns: - list: 処理された音声ファイルのパスのリスト - """ - try: - # デフォルト値の設定 - if target_duration_seconds is None: - target_duration_seconds = self.standard_duration - - # 出力ディレクトリが指定されていない場合は入力ディレクトリを使用 - if output_directory is None: - output_directory = input_directory - else: - os.makedirs(output_directory, exist_ok=True) - - output_files = [] - - # ディレクトリ内の全ファイルを処理 - for filename in os.listdir(input_directory): - if filename.lower().endswith(('.wav', '.mp3', '.webm', '.ogg', '.flac')): - input_path = os.path.join(input_directory, filename) - output_filename = f"normalized_{filename}" - output_path = os.path.join(output_directory, output_filename) - - # 音声の長さをそろえる - processed_file = self.normalize_audio_duration( - input_path, - target_duration_seconds, - output_path - ) - - if processed_file: - output_files.append(processed_file) - - return output_files - - except Exception as e: - print(f"バッチ処理でエラーが発生しました: {e}") - return [] - - def cosine_similarity(self, vec1, vec2): - """ - 2つのベクトル間のコサイン類似度を計算する - 次元数が異なる場合はエラーを発生させる - - Parameters: - vec1, vec2: 比較する2つのベクトル - - Returns: - float: コサイン類似度 (-1 から 1 の範囲) - """ - try: - # 次元数チェック - if vec1.shape != vec2.shape: - raise ValueError(f"ベクトルの次元数が一致しません: {vec1.shape} vs {vec2.shape}") - - # 正規化 - vec1 = vec1 / np.linalg.norm(vec1) - vec2 = vec2 / np.linalg.norm(vec2) - - return np.dot(vec1, vec2) - except Exception as e: - print(f"コサイン類似度計算でエラーが発生しました: {e}") - return None - - def segment_audio(self, path, target_path='/tmp/setup_voice', seg_duration=1.0): - """ - 音声ファイルを一定の長さのセグメントに分割する - - Parameters: - path (str): 入力音声ファイルのパス - target_path (str): 分割されたセグメントを保存するディレクトリ - seg_duration (float): 各セグメントの長さ(秒) - - Returns: - tuple: (セグメントが保存されたディレクトリのパス, 元の音声の総時間(ミリ秒)) - """ - # 出力先ディレクトリが存在していれば中身をクリアする - if os.path.exists(target_path): - for file in os.listdir(target_path): - file_path = os.path.join(target_path, file) - if os.path.isfile(file_path): - os.remove(file_path) - else: - os.makedirs(target_path, exist_ok=True) - - base_sound = AudioSegment.from_file(path) - duration_ms = len(base_sound) - seg_duration_ms = int(seg_duration * 1000) - - for i, start in enumerate(range(0, duration_ms, seg_duration_ms)): - end = min(start + seg_duration_ms, duration_ms) - segment = base_sound[start:end] - # セグメントが指定長さに満たない場合、無音でパディングする - if len(segment) < seg_duration_ms: - silence = AudioSegment.silent(duration=(seg_duration_ms - len(segment))) - segment = segment + silence - - segment.export(os.path.join(target_path, f'{i}.wav'), format="wav") - - return target_path, duration_ms - - def calculate_embedding(self, audio_path): - """ - 音声ファイルからエンベディングを計算する - 必要に応じて音声の長さを標準化する - - Parameters: - audio_path (str): 音声ファイルのパス - - Returns: - numpy.ndarray: 計算されたエンベディング - """ - try: - # 一時的に長さを標準化した音声ファイルを作成 - normalized_path = self.normalize_audio_duration(audio_path) - if normalized_path is None: - raise ValueError("音声の長さの標準化に失敗しました") - - # エンベディングを計算 - embedding = self.inference(normalized_path) - - # 一時ファイルを削除(必要に応じて) - if normalized_path != audio_path: - try: - os.remove(normalized_path) - except Exception as e: - warnings.warn(f"一時ファイルの削除に失敗しました: {e}") - - return embedding.data.flatten() - - except Exception as e: - print(f"エンベディング計算でエラーが発生しました: {e}") - return None - - def calculate_similarity(self, path1, path2): - """ - 2つの音声ファイル間の類似度を計算する - 音声の長さを標準化してからエンベディングを計算 - - Parameters: - path1, path2 (str): 比較する2つの音声ファイルのパス - - Returns: - float: コサイン類似度 (-1 から 1 の範囲)、エラー時はNone - """ - try: - # エンベディングを計算 - embedding1 = self.calculate_embedding(path1) - embedding2 = self.calculate_embedding(path2) - - if embedding1 is None or embedding2 is None: - raise ValueError("エンベディングの計算に失敗しました") - - # 次元数チェック(念のため) - if embedding1.shape != embedding2.shape: - raise ValueError(f"エンベディングの次元数が一致しません: {embedding1.shape} vs {embedding2.shape}") - - # 類似度を計算 - return float(self.cosine_similarity(embedding1, embedding2)) - except Exception as e: - print(f"類似度計算でエラーが発生しました: {e}") - return None - - def process_audio(self, reference_path, input_path, user, output_folder='/tmp/data/matched_segments', seg_duration=1.0, threshold=0.5): - """ - 入力音声からリファレンス音声に類似したセグメントを抽出する - - Parameters: - reference_path (str): リファレンス音声のパス - input_path (str): 入力音声のパス - user(str): ユーザー名 - output_folder (str): 類似セグメントを保存するディレクトリ - seg_duration (float): セグメントの長さ(秒) - threshold (float): 類似度の閾値 - - Returns: - tuple: (マッチした時間(ミリ秒), マッチしなかった時間(ミリ秒), 分類済みのセグメント) - """ - - isSpeaking = None - wasSpeaking = None - current_segment=[] - merged_segments=[] - - try: - # リファレンス音声のエンベディングを計算(長さを標準化) - reference_embedding = self.calculate_embedding(reference_path) - if reference_embedding is None: - raise ValueError("リファレンス音声のエンベディング計算に失敗しました") - - # 出力先ディレクトリの中身をクリアする - if os.path.exists(output_folder): - for file in os.listdir(output_folder): - file_path = os.path.join(output_folder, file) - if os.path.isfile(file_path): - os.remove(file_path) - else: - os.makedirs(output_folder, exist_ok=True) - - # 入力音声をセグメントに分割 - segmented_path, total_duration_ms = self.segment_audio(input_path, seg_duration=seg_duration) - - matched_time_ms = 0 - for file in sorted(os.listdir(segmented_path)): - segment_file = os.path.join(segmented_path, file) - - # セグメントのエンベディングを計算 - segment_embedding = self.calculate_embedding(segment_file) - if segment_embedding is None: - print(f"警告: セグメント {file} のエンベディング計算に失敗しました。スキップします。") - continue - - try: - # 類似度を計算 - similarity = float(self.cosine_similarity(segment_embedding, reference_embedding)) - - if similarity > threshold: - shutil.copy(segment_file, output_folder) - matched_time_ms += len(AudioSegment.from_file(segment_file)) - isSpeaking = True - else: - isSpeaking = False - - # 話者が変わった場合、保存 - if wasSpeaking != isSpeaking: - if current_segment: - if wasSpeaking: - merged_segments.append((user, current_segment)) - else: - merged_segments.append(("other",current_segment)) - wasSpeaking = isSpeaking - current_segment = [segment_file] - # 変わらなかった場合、結合 - else: - current_segment.append(segment_file) - - except Exception as e: - print(f"セグメント {file} の類似度計算でエラーが発生しました: {e}") - # 余りを保存 - if current_segment: - if wasSpeaking: - merged_segments.append((user, current_segment)) - else: - merged_segments.append(("other",current_segment)) - - unmatched_time_ms = total_duration_ms - matched_time_ms - return matched_time_ms, unmatched_time_ms, merged_segments - - except Exception as e: - print(f"音声処理でエラーが発生しました: {e}") - return 0, 0, merged_segments - - def process_multi_audio(self, reference_pathes, input_path, users, output_folder='/tmp/data/matched_multi_segments', seg_duration=1.0, threshold=0.5): - """ - 入力音声から複数のリファレンス音声に類似したセグメントを抽出する - - Parameters: - reference_pathes (list): リファレンス音声のパスのリスト - input_path (str): 入力音声のパス - users(list): ユーザーのリスト - output_folder (str): 類似セグメントを保存するディレクトリ - seg_duration (float): セグメントの長さ(秒) - threshold (float): 類似度の閾値 - - Returns: - tuple: (各リファレンスごとのマッチした時間のリスト, 分類済みのセグメント) - """ - try: - # 出力先ディレクトリの中身をクリアする - if os.path.exists(output_folder): - for file in os.listdir(output_folder): - file_path = os.path.join(output_folder, file) - if os.path.isfile(file_path): - os.remove(file_path) - else: - os.makedirs(output_folder, exist_ok=True) - - # リファレンス音声のエンベディングを事前計算 - reference_embeddings = [] - for ref_path in reference_pathes: - embedding = self.calculate_embedding(ref_path) - if embedding is None: - print(f"警告: リファレンス {ref_path} のエンベディング計算に失敗しました") - # ダミーエンベディングを挿入(後で処理をスキップ) - reference_embeddings.append(None) - else: - reference_embeddings.append(embedding) - - # 入力音声をセグメントに分割 - segmented_path, total_duration_ms = self.segment_audio(input_path, seg_duration=seg_duration) - segment_files = sorted(os.listdir(segmented_path)) - num_segments = len(segment_files) - - # 各セグメントのエンベディングを計算 - segment_embeddings = [] - for file in segment_files: - segment_file = os.path.join(segmented_path, file) - embedding = self.calculate_embedding(segment_file) - if embedding is None: - print(f"警告: セグメント {file} のエンベディング計算に失敗しました") - segment_embeddings.append(None) - else: - segment_embeddings.append(embedding) - - # 各リファレンスごとにセグメントとの類似度を計算 - similarity = [] - for ref_embedding in reference_embeddings: - if ref_embedding is None: - # リファレンスのエンベディングが計算できなかった場合 - similarity.append([0.0] * num_segments) - continue - - ref_similarity = [] - for seg_embedding in segment_embeddings: - if seg_embedding is None: - # セグメントのエンベディングが計算できなかった場合 - ref_similarity.append(0.0) - continue - - try: - # 次元数チェック - if ref_embedding.shape != seg_embedding.shape: - print(f"警告: エンベディングの次元数が一致しません: {ref_embedding.shape} vs {seg_embedding.shape}") - ref_similarity.append(0.0) - continue - - # 類似度を計算 - sim = float(self.cosine_similarity(seg_embedding, ref_embedding)) - ref_similarity.append(sim) - except Exception as e: - print(f"類似度計算でエラーが発生しました: {e}") - ref_similarity.append(0.0) - - similarity.append(ref_similarity) - - # 転置行列を作成 (rows: segment, columns: reference) - similarity_transposed = [] - for seg_idx in range(num_segments): - seg_sim = [] - for ref_idx in range(len(reference_pathes)): - seg_sim.append(similarity[ref_idx][seg_idx]) - similarity_transposed.append(seg_sim) - - # 各セグメントについて、最も高い類似度のリファレンスを選択 - best_matches = [] - speakers = [] - for seg_sim in similarity_transposed: - best_ref = np.argmax(seg_sim) # 最も類似度の高いリファレンスのインデックス - # 閾値チェック - if seg_sim[best_ref] < threshold: - best_matches.append(None) # 閾値未満の場合はマッチなしとする - speakers.append(-1) # Noneは都合が悪いので-1 - else: - best_matches.append(best_ref) - speakers.append(best_ref) - - current_speaker = None - current_segments = [] - merged_segments = [] - for index,file in enumerate(segment_files,start=0): - file_path = os.path.join(segmented_path, file) - speaker = users[speakers[index]] - if speaker == -1: - continue - if current_speaker != speaker: - if current_segments: - merged_segments.append((current_speaker,current_segments)) - current_speaker = speaker - current_segments = [file_path] - else: - current_segments.append(file_path) - if current_segments: - merged_segments.append((current_speaker,current_segments)) - - # 各リファレンスごとに一致時間を集計 - matched_time = [0] * len(reference_pathes) - for match in best_matches: - if match is not None: - matched_time[match] += seg_duration - - return matched_time, merged_segments - - except Exception as e: - print(f"マルチ音声処理でエラーが発生しました: {e}") - return [0] * len(reference_pathes), None - - def save_audio_from_base64(self, base64_audio, output_dir, output_filename, temp_format='webm'): - """ - Base64エンコードされた音声データをデコードして保存する - - Parameters: - base64_audio (str): Base64エンコードされた音声データ - output_dir (str): 出力先ディレクトリ - output_filename (str): 出力ファイル名 - temp_format (str): 一時ファイルのフォーマット - - Returns: - str: 保存された音声ファイルのパス、エラー時はNone - """ - try: - # Base64デコードして音声バイナリを取得 - try: - audio_binary = base64.b64decode(base64_audio) - except binascii.Error: - raise ValueError("Invalid Base64 input data") - - # 保存するディレクトリを作成 - os.makedirs(output_dir, exist_ok=True) - - # 一時ファイルに保存 - temp_audio_path = os.path.join(output_dir, "temp_audio") - try: - with open(temp_audio_path, 'wb') as f: - f.write(audio_binary) - - # pydub を使って一時ファイルを WAV に変換 - try: - audio = AudioSegment.from_file(temp_audio_path, format=temp_format) - except Exception as e: - # 形式が不明な場合は自動判別 - audio = AudioSegment.from_file(temp_audio_path) - - # 音声ファイルを保存 - wav_audio_path = os.path.join(output_dir, output_filename) - audio.export(wav_audio_path, format="wav") - finally: - # 一時ファイルを削除 - if os.path.exists(temp_audio_path): - os.remove(temp_audio_path) - return wav_audio_path - except ValueError as e: - print(f"Value Error: {e}") - except FileNotFoundError as e: - print(f"File Not Found Error: {e}") - except Exception as e: - print(f"Unexpected Error: {e}") - return None - - def delete_files_in_directory(self, directory_path): - """ - ディレクトリ内のすべてのファイルを削除する - - Parameters: - directory_path (str): 削除対象のディレクトリパス - """ - try: - # ディレクトリ内のすべてのファイルを取得 - for filename in os.listdir(directory_path): - file_path = os.path.join(directory_path, filename) - # ファイルのみ削除する - if os.path.isfile(file_path): - os.remove(file_path) - print(f"{file_path} を削除しました") - except Exception as e: - print(f"ファイル削除でエラーが発生しました: {e}") \ No newline at end of file diff --git a/record/save.py b/record/save.py new file mode 100644 index 0000000000000000000000000000000000000000..3463f81a08f89ac72fac7e8a9f0d10e2914b759d --- /dev/null +++ b/record/save.py @@ -0,0 +1,31 @@ +from flask import Flask, request, jsonify,render_template +import base64 + +app = Flask(__name__) + +@app.route('/') +def root(): + return render_template("record.html") + +@app.route('/upload_audio', methods=['POST']) +def upload_audio(): + try: + data = request.get_json() # クライアントから送られてきたJSONデータ + audio_data = data.get('audio_data') # Base64エンコードされた音声データ + + if not audio_data: + return jsonify({"error": "音声データが送信されていません"}), 400 + + # Base64デコード + audio_binary = base64.b64decode(audio_data) + + # WAVファイルとして保存 + with open('recorded_audio.wav', 'wb') as f: + f.write(audio_binary) + + return jsonify({"message": "音声が正常に保存されました"}), 200 + except Exception as e: + return jsonify({"error": str(e)}), 500 + +if __name__ == '__main__': + app.run(debug=True) diff --git a/record/templates/record.html b/record/templates/record.html new file mode 100644 index 0000000000000000000000000000000000000000..60770e3f1141cf2feea8ad0d5ef4b034ca68ce8d --- /dev/null +++ b/record/templates/record.html @@ -0,0 +1,76 @@ + + + + + + 音声録音 + + +

音声録音

+ + +

録音した音声を送信する準備ができました。

+ + + + + diff --git a/requirements.txt b/requirements.txt index 37b3286e5b67790ce79d3e3931a53922569b16f9..eea8b236d32deaa9a919bb7dfb0134dba7aa98b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1 @@ -Flask==2.2.5 -Flask-WTF -pyannote.audio==2.1.1 -numpy==1.23.5 -pydub==0.25.1 -matplotlib==3.6.3 -python-dotenv -uwsgi -Flask-SQLAlchemy==3.0.5 -PyMySQL -Flask-Login==0.6.3 -requests==2.32.3 -google-auth==2.38.0 -google-auth-oauthlib==1.2.1 -google-auth-httplib2==0.2.0 -faster-whisper -Flask-Migrate -requests -Flask-CORS +Flask diff --git a/room.js b/room.js deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/sample.wav b/sample.wav deleted file mode 100644 index 92911c5e03f43a06fab2d7d47fabde2dcf486868..0000000000000000000000000000000000000000 --- a/sample.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0f64da9fcf28836e98f50ac7a9ce3c213c186a9d444f295e5bd6a66b5b26d8c5 -size 882044 diff --git a/static/feedback.js b/static/feedback.js deleted file mode 100644 index e8e366a206304260a0278ec24d4e64bd88be4d27..0000000000000000000000000000000000000000 --- a/static/feedback.js +++ /dev/null @@ -1,66 +0,0 @@ -async function getTranscription() { - try { - const response = await fetch("/transcription"); - if (!response.ok) { - throw new Error("HTTP error! status: ${response.status}"); - } - const data = await response.json(); - const results = data.response; - } catch (error) { - console.error("Failed to fetch transcription", error); - } -} - -async function getAnalysis() { - const loader = document.getElementById("loader"); - loader.style.display = "block"; - try { - await getTranscription(); - - const response = await fetch("/analyze"); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - console.log("分析データ取得:", data); // ←構造確認用 - const results = data.results; - const analysis = results.deepseek_analysis; - - // 変数に格納 - const conversationLevel = analysis.conversationLevel; - const harassmentPresent = analysis.harassmentPresent; - const harassmentType = analysis.harassmentType; - const repetition = analysis.repetition; - const pleasantConversation = analysis.pleasantConversation; - const blameOrHarassment = analysis.blameOrHarassment; - - loader.style.display = "none"; - // DOMに表示 - document.getElementById( - "level" - ).innerText = `会話レベル: ${conversationLevel}`; - document.getElementById( - "Harassment_bool" - ).innerText = `ハラスメントの有無: ${harassmentPresent}`; - document.getElementById( - "Harassment_type" - ).innerText = `ハラスメントの種類: ${harassmentType}`; - document.getElementById( - "Harassment_loop" - ).innerText = `繰り返しの程度: ${repetition}`; - document.getElementById( - "Harassment_comfort" - ).innerText = `会話の心地よさ: ${pleasantConversation}`; - document.getElementById( - "Harassment_volume" - ).innerText = `非難またはハラスメントの程度: ${blameOrHarassment}`; - } catch (error) { - loader.style.display = "none"; - console.error("Failed to fetch analysis data:", error); - } -} - -window.onload = () => { - getAnalysis(); -}; diff --git a/static/loading.css b/static/loading.css deleted file mode 100644 index 7b41799bd554a5edb58618132319551deed3fcd4..0000000000000000000000000000000000000000 --- a/static/loading.css +++ /dev/null @@ -1,56 +0,0 @@ -.loader { - position: absolute; - top: calc(50% - 32px); - left: calc(50% - 32px); - width: 64px; - height: 64px; -} - -.loader div { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border-radius: 50%; - box-sizing: border-box; - opacity: 0.8; -} - -.one { - border-top: 1px solid #8834e8; - animation: rotate-left 1s linear infinite; -} - -.two { - border-right: 1px solid #a28ecb; - animation: rotate-right 1s linear infinite; -} - -.three { - border-bottom: 1px solid #ffd933; - animation: rotate-right 1s linear infinite; -} - -.four { - border-left: 1px solid #ff7f00; - animation: rotate-right 1s linear infinite; -} - -@keyframes rotate-left { - 0% { - transform: rotate(360deg); - } - 100% { - transform: rotate(0deg); - } -} - -@keyframes rotate-right { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} diff --git a/static/loading.js b/static/loading.js deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/static/main.css b/static/main.css deleted file mode 100644 index ea7842cf1909e1a61d8f7d140b7b90fccab16420..0000000000000000000000000000000000000000 --- a/static/main.css +++ /dev/null @@ -1,40 +0,0 @@ -/* Responsive Design */ -@media (max-width: 640px) { - .w-72 { - width: 95%; - } - .h-72 { - height: 350px; - } - } - /* Main Container */ - body { - background: linear-gradient(135deg, #2c3e50, #1f2937); - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - font-family: "Arial", sans-serif; - color: #fff; - } - - /* Main Content Wrapper */ - .main-content { - border: 5px solid rgba(255, 255, 255, 0.2); - padding: 2rem; - border-radius: 1rem; - width: 90%; - max-width: 500px; - background-color: rgba(0, 0, 0, 0.3); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4); - text-align: center; - } - - /* Title */ - .main-title { - font-size: 2.5rem; - font-weight: bold; - margin-bottom: 1.5rem; - color: #fff; - text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); - } diff --git a/static/menu.css b/static/menu.css deleted file mode 100644 index 4d4c824ec410f3a9963223c3e76577a12c0a257b..0000000000000000000000000000000000000000 --- a/static/menu.css +++ /dev/null @@ -1,56 +0,0 @@ -/* Hamburger Menu Styles */ -#menu { - position: absolute; - top: 0; - left: 0; - z-index: 10; - transform: translateX(-100%); - visibility: hidden; - opacity: 0; - background-color: rgb(31, 41, 55); - transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; - backdrop-filter: blur(10px); - border-right: 1px solid rgba(255, 255, 255, 0.2); - } - - #menu.open { - transform: translateX(0); - visibility: visible; - opacity: 1; - } - - #menu button { - transition: background-color 0.2s ease; - background-color: rgba(0, 0, 0, 0.1); - margin: 2px; - border-radius: 8px; /* 少し角を丸める */ - display: flex; - align-items: center; - justify-content: flex-start; - gap: 10px; - padding: 0.75rem 1rem; - width: 100%; - text-align: left; - border: none; - color: #fff; - font-size: 1rem; - cursor: pointer; - } - - #menu button:hover { - background-color: rgba(55, 65, 81, 0.7); - } - - /* Hamburger Menu Button */ - #menuButton { - background-color: rgba(255, 255, 255, 0.1); - border: none; - border-radius: 50%; - padding: 0.75rem; /* サイズを少し大きく */ - cursor: pointer; - transition: background-color 0.2s ease; - } - - #menuButton:hover { - background-color: rgba(255, 255, 255, 0.2); - } \ No newline at end of file diff --git a/static/menu.js b/static/menu.js deleted file mode 100644 index 3da358e8a0dbed9e9202eafc32220dbc2ca88b37..0000000000000000000000000000000000000000 --- a/static/menu.js +++ /dev/null @@ -1,53 +0,0 @@ - // Show user registration page - function showUserRegister() { - fetch("/reset"); - window.location.href = "userregister"; - } -// メンバー選択画面表示 - function showUserSelect() { - window.location.href = "/userselect"; - } - // Show recorder page - function showRecorder() { - window.location.href = "index"; - } - - // Show results page - function showResults() { - window.location.href = "feedback"; - } - - // Show talk detail page - function showTalkDetail() { - window.location.href = "talk_detail"; - } - - // Reset action page - function resetAction() { - window.location.href = "reset_html"; - } - - // Toggle hamburger menu visibility - function toggleMenu(event) { - event.stopPropagation(); // Prevents click event from propagating to the document - const menu = document.getElementById("menu"); - menu.classList.toggle("open"); - } - - // Close the menu if clicked outside - function closeMenu(event) { - const menu = document.getElementById("menu"); - if ( - menu.classList.contains("open") && - !menu.contains(event.target) && - !event.target.closest("#menuButton") - ) { - menu.classList.remove("open"); - } - } - - // Add event listener for closing the menu when clicking outside - document.addEventListener("click", closeMenu); - - // Show recorder page 名前に気を付けて! - document.getElementById("add-btn").addEventListener("click", showRecorder); \ No newline at end of file diff --git a/static/process.js b/static/process.js deleted file mode 100644 index 1254bd55f3ecf43f253c96b135b12115d6a51df6..0000000000000000000000000000000000000000 --- a/static/process.js +++ /dev/null @@ -1,222 +0,0 @@ - - let isRecording = false; - let mediaRecorder; - let audioChunks = []; - let recordingInterval; - let count_voice = 0; - let before_rate = []; - const RECORDING_INTERVAL_MS = 5000; // 5秒 - // メンバーとチャートの初期化 - let members = []; - let voiceData = []; - let baseMemberColors = ["#4caf50", "#007bff", "#ffc107", "#dc3545", "#28a745", "#9c27b0", "#ff9800"]; - // Chart.js の初期化 - const ctx = document.getElementById("speechChart").getContext("2d"); - const speechChart = new Chart(ctx, { - type: "doughnut", - data: { - labels: members, - datasets: [ - { - data: voiceData, - backgroundColor: getMemberColors(members.length), - }, - ], - }, - options: { - responsive: true, - plugins: { - legend: { - display: true, - position: "bottom", - labels: { color: "white" }, - }, - }, - }, - }); - // サーバーからメンバー情報を取得してチャートを更新する関数 - async function updateChartFrom() { - try { - const response = await fetch("/confirm"); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - if (!data || !data.members || !Array.isArray(data.members)) { - console.error("Invalid member data received:", data); - members = ["member1"]; - voiceData = [50, 50]; - updateChart(); - return; - } - members = data.members; - voiceData = []; - for (let i = 0; i < members.length; i++) { - voiceData.push(100 / members.length); - } - updateChart(); - } catch (error) { - console.error("Failed to fetch member data:", error); - members = ["member1"]; - voiceData = [50, 50]; - updateChart(); - } - } - function updateChart() { - // 一人モードの場合は、ユーザーとグレー(無音)の比率をチャートに表示 - if (members.length === 1) { - const userName = members[0]; - speechChart.data.labels = [userName, "無音"]; - speechChart.data.datasets[0].backgroundColor = ["#4caf50", "#757575"]; - } else { - // 複数メンバーの場合は通常通りの処理 - speechChart.data.labels = members; - speechChart.data.datasets[0].backgroundColor = getMemberColors(members.length); - } - speechChart.data.datasets[0].data = voiceData; - speechChart.update(); - } - - // ページ読み込み時にチャート情報を更新 - updateChartFrom(); - // 録音ボタンの録音開始/停止処理 - async function toggleRecording() { - const recordButton = document.getElementById("recordButton"); - if (!isRecording) { - // 録音開始 - isRecording = true; - recordButton.classList.add("recording"); - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - mediaRecorder = new MediaRecorder(stream); - audioChunks = []; - mediaRecorder.ondataavailable = (event) => { - if (event.data.size > 0) { - audioChunks.push(event.data); - } - }; - mediaRecorder.onstop = () => { - sendAudioChunks([...audioChunks]); - audioChunks = []; - }; - mediaRecorder.start(); - // 5秒ごとに録音を停止して送信するインターバルを設定 - recordingInterval = setInterval(() => { - if (mediaRecorder && mediaRecorder.state === "recording") { - mediaRecorder.stop(); - mediaRecorder.start(); - } - }, RECORDING_INTERVAL_MS); - } catch (error) { - console.error("マイクへのアクセスに失敗しました:", error); - isRecording = false; - recordButton.classList.remove("recording"); - } - } else { - // 録音停止 - isRecording = false; - recordButton.classList.remove("recording"); - if (mediaRecorder && mediaRecorder.state === "recording") { - mediaRecorder.stop(); - } - clearInterval(recordingInterval); - count_voice = 0; - //before_rate = []; - } - } - function sendAudioChunks(chunks) { - const audioBlob = new Blob(chunks, { type: "audio/wav" }); - const reader = new FileReader(); - reader.onloadend = () => { - const base64String = reader.result.split(",")[1]; - const form = document.getElementById("recordForm"); - const nameInput = form.querySelector('input[name="name"]'); - const name = nameInput ? nameInput.value : "unknown"; - fetch("/upload_audio", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ audio_data: base64String, name: name }), - }) - .then((response) => response.json()) - .then((data) => { - if (data.error) { - alert("エラー: " + data.error); - console.error(data.details); - } else if (data.rate !== undefined) { - updateChartData(data.rate); - } else if (data.rates !== undefined) { - updateChartData(data.rates); - } - }) - .catch((error) => { - console.error("エラー:", error); - }); - }; - reader.readAsDataURL(audioBlob); - } - function getMemberColors(memberCount) { - // 一人モードの場合は特別な処理をしない(updateChartで処理するため) - if (memberCount <= 1) { - return ["#4caf50", "#757575"]; - } else { - let colors = []; - for (let i = 0; i < memberCount; i++) { - colors.push(baseMemberColors[i % baseMemberColors.length]); - } - return colors; - } - } - function updateChartData(newRate) { - // 一人モードの時の処理 - if (members.length === 1) { - if (count_voice === 0) { - speechChart.data.datasets[0].data = [newRate, 100 - newRate]; - before_rate = [newRate]; - } else { - // 一人モードでは、過去のデータと現在のデータを加重平均する - let tmp_rate = (newRate + before_rate[0] * count_voice) / (count_voice + 1); - speechChart.data.datasets[0].data = [tmp_rate, 100 - tmp_rate]; - before_rate = [tmp_rate]; - } - count_voice++; - // 一人モードでは常に緑色とグレーの組み合わせを使用 - speechChart.data.labels = [members[0], "無音"]; - speechChart.data.datasets[0].backgroundColor = ["#4caf50", "#757575"]; - } else { - console.log(before_rate) - // 複数人モードの処理 - if (!Array.isArray(newRate)) { - console.error("newRate is not an array:", newRate); - return; - } - if (newRate.length !== members.length) { - console.error( - "newRate length does not match members length:", - newRate, - members - ); - return; - } - let averagedRates = new Array(newRate.length); - for (let i = 0; i < newRate.length; i++) { - let tmp_rate; - if (count_voice === 0) { - // 初回はそのまま - tmp_rate = newRate[i]; - } else { - // 2回目以降は、過去の平均値と現在の値を加重平均する - tmp_rate = (newRate[i] + before_rate[i] * count_voice) / (count_voice + 1); - } - averagedRates[i] = tmp_rate; - } - // before_rateを更新 - before_rate = averagedRates; - //グラフに反映 - speechChart.data.datasets[0].data = averagedRates; - count_voice++; - speechChart.data.datasets[0].backgroundColor = getMemberColors( - members.length - ); - } - speechChart.update(); - } \ No newline at end of file diff --git a/static/process1.js b/static/process1.js deleted file mode 100644 index 9bce589a125108d6d140487ae8ec1234e734ef3b..0000000000000000000000000000000000000000 --- a/static/process1.js +++ /dev/null @@ -1,294 +0,0 @@ -let allUsers = []; -let selectedUsers = []; -let userToDelete = null; - -// ページ読み込み時にユーザーリストを取得 -document.addEventListener('DOMContentLoaded', fetchUserList); - -// ユーザーリスト取得 - Flask APIの変更に合わせて修正 -function fetchUserList() { - fetch('/list_base_audio') - .then(response => response.json()) - .then(data => { - if (data.status === 'success' && Array.isArray(data.id)) { - // APIの変更: data.fileNames → data.id - allUsers = data.id; - renderUserList(allUsers); - } else { - showError('メンバーリストの取得に失敗しました'); - } - }) - .catch(error => { - console.error('Error fetching user list:', error); - showError('サーバーとの通信中にエラーが発生しました'); - }); -} - -// ユーザーリストの表示 -function renderUserList(users) { - const userListElement = document.getElementById('userList'); - - if (!users || users.length === 0) { - userListElement.innerHTML = ` -
-

登録されているメンバーがいません。

-

「新規登録」から音声を登録してください。

-
- `; - return; - } - - let html = ''; - users.forEach(user => { - const firstLetter = user.substr(0, 1).toUpperCase(); - html += ` -
- - -
${firstLetter}
- -
- `; - }); - - userListElement.innerHTML = html; - - // 既に選択済みのユーザーがあればチェックを入れる - checkStoredSelections(); -} - -// ユーザー選択の切り替え -function toggleUserSelection(username) { - const index = selectedUsers.indexOf(username); - if (index === -1) { - selectedUsers.push(username); - } else { - selectedUsers.splice(index, 1); - } - - updateSelectedCount(); - updateProceedButton(); - saveSelections(); -} - -// すべてのユーザーを選択 -function selectAllUsers() { - selectedUsers = [...allUsers]; - - // チェックボックスを更新 - allUsers.forEach(user => { - const checkbox = document.getElementById(`user-${user}`); - if (checkbox) checkbox.checked = true; - }); - - updateSelectedCount(); - updateProceedButton(); - saveSelections(); -} - -// すべての選択を解除 -function deselectAllUsers() { - selectedUsers = []; - - // チェックボックスを更新 - allUsers.forEach(user => { - const checkbox = document.getElementById(`user-${user}`); - if (checkbox) checkbox.checked = false; - }); - - updateSelectedCount(); - updateProceedButton(); - saveSelections(); -} - -// 選択数の表示を更新 -function updateSelectedCount() { - document.getElementById('selectedCount').textContent = `選択中: ${selectedUsers.length}人`; -} - -// 進むボタンの有効/無効を更新 -function updateProceedButton() { - document.getElementById('proceedButton').disabled = selectedUsers.length === 0; -} - -// 選択を保存 -function saveSelections() { - localStorage.setItem('selectedUsers', JSON.stringify(selectedUsers)); -} - -// 保存されている選択を読み込み -function checkStoredSelections() { - const storedSelections = localStorage.getItem('selectedUsers'); - if (storedSelections) { - try { - selectedUsers = JSON.parse(storedSelections); - selectedUsers = selectedUsers.filter(user => allUsers.includes(user)); // 存在するユーザーのみ選択 - - // チェックボックスに反映 - selectedUsers.forEach(user => { - const checkbox = document.getElementById(`user-${user}`); - if (checkbox) checkbox.checked = true; - }); - - updateSelectedCount(); - updateProceedButton(); - } catch (e) { - console.error('保存された選択の読み込みに失敗しました', e); - selectedUsers = []; - } - } -} - -// エラー表示 -function showError(message) { - const userListElement = document.getElementById('userList'); - userListElement.innerHTML = ` -
-

${message}

- -
- `; -} - -// 選択されたユーザーでサーバーに送信して次のページに進む -function proceedWithSelectedUsers() { - if (selectedUsers.length === 0) { - alert('少なくとも1人のメンバーを選択してください'); - return; - } - - // 選択したユーザーをサーバーに送信 - fetch('/select_users', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - users: selectedUsers - }) - }) - .then(response => response.json()) - .then(data => { - if (data.status === 'success') { - // 成功したらインデックスページに進む - window.location.href = '/index'; - } else { - alert('エラーが発生しました: ' + (data.error || 'Unknown error')); - } - }) - .catch(error => { - console.error('Error selecting users:', error); - alert('サーバーとの通信中にエラーが発生しました'); - }); -} - -// 削除確認モーダルを表示 -function showDeleteModal(username) { - userToDelete = username; - document.getElementById('deleteModalText').textContent = `メンバー「${username}」を削除しますか?削除すると元に戻せません。`; - document.getElementById('deleteModal').style.display = 'flex'; -} - -// 削除確認モーダルを非表示 -function hideDeleteModal() { - document.getElementById('deleteModal').style.display = 'none'; - userToDelete = null; -} - -// メンバーの削除を実行 -function confirmDelete() { - if (!userToDelete) return; - - // 削除中の表示 - document.getElementById('deleteModalText').innerHTML = ` -
-
-

メンバー「${userToDelete}」を削除中...

-
- `; - - fetch('/reset_member', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - names: [userToDelete] - }) - }) - .then(response => response.json()) - .then(data => { - if (data.status === 'success') { - // 選択リストからも削除 - const index = selectedUsers.indexOf(userToDelete); - if (index !== -1) { - selectedUsers.splice(index, 1); - saveSelections(); - } - - // リストから削除して再表示 - allUsers = allUsers.filter(user => user !== userToDelete); - renderUserList(allUsers); - - // モーダルを閉じる - hideDeleteModal(); - - // 成功メッセージ表示(オプション) - const successMessage = document.createElement('div'); - successMessage.className = 'success-message'; - successMessage.innerHTML = `
メンバーを削除しました
`; - document.querySelector('.container').prepend(successMessage); - - // 数秒後にメッセージを消す - setTimeout(() => { - successMessage.remove(); - }, 3000); - } else { - alert('削除に失敗しました: ' + (data.message || 'Unknown error')); - hideDeleteModal(); - } - }) - .catch(error => { - console.error('Error deleting user:', error); - alert('サーバーとの通信中にエラーが発生しました'); - hideDeleteModal(); - }); -} - -// ハンバーガーメニュー表示/非表示の切り替え -function toggleMenu(event) { - event.stopPropagation(); - const menu = document.getElementById('menu'); - menu.classList.toggle('open'); -} - -// メニュー外クリックでメニューを閉じる -function closeMenu(event) { - const menu = document.getElementById('menu'); - if (menu.classList.contains('open') && !menu.contains(event.target) && event.target.id !== 'menuButton') { - menu.classList.remove('open'); - } -} - -// 各画面へのナビゲーション関数 -function showUserRegister() { - window.location.href = '/userregister'; -} - -function showIndex() { - window.location.href = '/index'; -} - -function showResults() { - window.location.href = '/feedback'; -} - -function showTalkDetail() { - window.location.href = '/talk_detail'; -} - -function resetAction() { - window.location.href = '/reset_html'; -} \ No newline at end of file diff --git a/static/register_record.js b/static/register_record.js deleted file mode 100644 index 74e2c09ee56a617c64ac98f5b25686229ddcec89..0000000000000000000000000000000000000000 --- a/static/register_record.js +++ /dev/null @@ -1,150 +0,0 @@ -let mediaRecorder; -let audioChunks = []; -let userCount = 0; // 追加されたメンバー数を保持 -let isRecording = false; // 録音中かどうかを判定するフラグ -let currentRecordingButton = null; // 現在録音中のボタンを保持 -let userNames = []; - -function toggleRecording(button) { - button.classList.toggle("recording"); -} - -async function startRecording(button) { - if (isRecording && currentRecordingButton !== button) return; // 他の人が録音中なら何もしない - isRecording = true; // 録音中に設定 - currentRecordingButton = button; // 録音中のボタンを記録 - - try { - const stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - }); - mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm" }); - audioChunks = []; - mediaRecorder.ondataavailable = (e) => audioChunks.push(e.data); - mediaRecorder.onstop = () => { - sendAudioChunks(audioChunks, button); // ボタン情報を渡す - audioChunks = []; - isRecording = false; // 録音停止後はフラグを戻す - currentRecordingButton = null; // 録音ボタンを解除 - }; - mediaRecorder.start(); - toggleRecording(button); - } catch (err) { - console.error("マイクアクセスに失敗しました:", err); - isRecording = false; // エラー発生時もフラグを戻す - currentRecordingButton = null; - } -} - -function stopRecording(button) { - if (!isRecording) return; // 録音中でない場合は停止しない - mediaRecorder.stop(); - toggleRecording(button); -} - -function handleRecording(e) { - const button = e.target.closest(".record-button"); - if (button) { - if (isRecording && currentRecordingButton !== button) { - // 他の人が録音中なら反応しない - return; - } - if (mediaRecorder && mediaRecorder.state === "recording") { - stopRecording(button); - } else { - startRecording(button); - } - } -} - -function sendAudioChunks(chunks, button) { - // 引数に button を追加 - const audioBlob = new Blob(chunks, { type: "audio/wav" }); - const reader = new FileReader(); - reader.onloadend = () => { - const base64String = reader.result.split(",")[1]; // Base64エンコードされた音声データ - // フォームの取得方法を変更 - const form = button.closest(".user-item")?.querySelector("form") - const nameInput = form?.querySelector('input[name="name"]'); - const name = nameInput ? nameInput.value : "unknown"; // 名前がない - fetch("/upload_base_audio", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ audio_data: base64String, name: name }), - }) - .then((response) => response.json()) - .then((data) => { - // エラー処理のみ残す - if (data.error) { - alert("エラー: " + data.error); - console.error(data.details); - } - // 成功時の処理(ボタンの有効化など) - else { - console.log("音声データ送信成功:", data); - userNames.push(name); - // 必要に応じて、ここでUIの変更(ボタンの有効化など)を行う - // 例: button.disabled = true; // 送信ボタンを無効化 - // 例: button.classList.remove("recording"); //録音中のスタイルを解除 - } - }) - .catch((error) => { - console.error("エラー:", error); - }); - }; - reader.readAsDataURL(audioBlob); -} - -// メンバー選択画面表示 -function showUserSelect() { - window.location.href = "/userselect"; - } - -// Add user function -function addUser() { - const userName = prompt("ユーザー名を入力してください"); - if (userName) { - const userList = document.getElementById("people-list"); - const userDiv = document.createElement("div"); - userDiv.classList.add( - "user-item", // 追加 - "bg-gray-700", - "p-4", - "rounded-lg", - "text-white", - "flex", - "justify-between", - "items-center", - "flex-wrap", // 追加 - "gap-3" // 追加 - ); - userDiv.innerHTML = ` -
- - -
- `; - userDiv - .querySelector(".record-button") - .addEventListener("click", handleRecording); - userList.appendChild(userDiv); - userCount++; - } -} - -document.getElementById("add-btn").addEventListener("click", addUser); diff --git a/static/reset.js b/static/reset.js deleted file mode 100644 index 6256373930f18432d708d57d6dcb530b465988c3..0000000000000000000000000000000000000000 --- a/static/reset.js +++ /dev/null @@ -1,97 +0,0 @@ -async function fetchAndRenderMembers() { - try { - const response = await fetch("/confirm"); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - if (!data || !data.members || !Array.isArray(data.members)) { - console.error("Invalid member data received:", data); - return; - } - - const members = data.members; - console.log(members); - const container = document.getElementById("memberCheckboxes"); - container.innerHTML = ""; // 既存の中身を消去 - - members.forEach((name) => { - const newItem = document.createElement("div"); - newItem.className = "flex items-center gap-3 mb-2"; - console.log(name); - newItem.innerHTML = ` - - ${name} - `; - - container.appendChild(newItem); - }); - } catch (error) { - console.error("Error fetching members:", error); - } -} - -// メンバー削除ボタンのイベントリスナー(修正箇所) -document.getElementById("reset_btn").addEventListener("click", () => { - // メンバー送信処理 - - const checkboxes = document.querySelectorAll( - '#memberCheckboxes input[type="checkbox"]:checked' - ); - - const selectedNames = Array.from(checkboxes).map((cb) => cb.value); - - if (selectedNames.length === 0) { - alert("メンバーを1人以上選択してください。"); - return; - } - - fetch("/reset_member", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ names: selectedNames }), - }) - .then((response) => { - if (!response.ok) { - throw new Error("送信に失敗しました"); - } - // サーバーからのJSONレスポンスを期待する - return response.json(); - }) - .then((data) => { - alert("選択されたメンバーを削除しました。"); - fetchAndRenderMembers(); // 再描画 - }) - .catch((error) => { - console.error("送信エラー:", error); - // エラーメッセージを表示する - alert(`送信エラー: ${error.message}`); - }); -}); - -// ページが表示されたときにチェックボックスを生成 -window.addEventListener("DOMContentLoaded", fetchAndRenderMembers); - -// 「全選択」ボタン処理 -document.getElementById("select-all").addEventListener("click", () => { - const checkboxes = document.querySelectorAll( - '#memberCheckboxes input[type="checkbox"]' - ); - checkboxes.forEach((checkbox) => { - checkbox.checked = true; - }); -}); - -// 他のページに移動する関数 -function showRecorder() { - window.location.href = "/index"; -} diff --git a/static/style.css b/static/style.css deleted file mode 100644 index 164e3579767647accae1e3f3a843dff7210f1c64..0000000000000000000000000000000000000000 --- a/static/style.css +++ /dev/null @@ -1,128 +0,0 @@ -@charset "UTF-8"; -body { - font-family: Arial, sans-serif; - padding: 20px; - background-color: #f4f4f4; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; -} - -h2 { - margin-bottom: 20px; - text-align: center; -} - -a { - text-decoration: none; - color: #000000cc; -} -a:hover { - text-decoration: underline; -} -.container { - max-width: 800px; - - background-color: #fff; - padding: 20px 80px; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -#transcription { - white-space: pre-wrap; - padding: 10px; - background-color: #e9e9e9; - border-radius: 4px; - margin-bottom: 20px; - max-height: 400px; - overflow-y: auto; -} -button { - margin: 5px; - padding: 10px 10px; - border: none; - border-radius: 4px; - background-color: #007bff; - color: #fff; - cursor: pointer; -} -.history-button { - margin-top: 20px; - - padding: 10px 20px; - background-color: #007bff; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; -} -history-button:hover { - background-color: #0056b3; -} - -.flex { - display: flex; - justify-content: center; -} -.new-person { - text-align: center; -} - -.controls { - display: flex; - flex-direction: column; - align-items: center; -} -.record-button { - width: 80px; - height: 80px; - background-color: transparent; - border-radius: 50%; - - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4); - transition: all 0.2s ease; -} - -.record-icon { - width: 60px; - height: 60px; - background-color: #d32f2f; - border-radius: 50%; - transition: all 0.2s ease; -} - -.recording .record-icon { - width: 40px; - height: 40px; - border-radius: 10%; -} - -.record-p { - border: 2px dashed #0000008c; -} - -.disabled { - background-color: gray; - cursor: not-allowed; -} - -.record-icon.recording { - width: 40px; - height: 40px; - border-radius: 0; -} - -.new-person-right-container { - padding-left: 20px; -} - -.record-container { - display: flex; - justify-content: center; -} diff --git a/static/talk_detail.js b/static/talk_detail.js deleted file mode 100644 index 871d83ef2af0eec3f2c40b8653ed0e21e5d61533..0000000000000000000000000000000000000000 --- a/static/talk_detail.js +++ /dev/null @@ -1,35 +0,0 @@ -async function displayTranscription() { - const transcriptionElement = document.getElementById("transcription"); - const loader = document.getElementById("loader"); - loader.style.display = "block"; - - try { - const response = await fetch("/transcription"); - if (!response.ok) throw new Error("データ取得に失敗しました。"); - - const data = await response.json(); - const conversations = data.transcription; - - if (!data || !data.transcription) { - throw new Error("会話データが見つかりませんでした。"); - } - - transcriptionElement.innerHTML = conversations; - loader.style.display = "none"; - console.log(conversations); - } catch (error) { - loader.style.display = "none"; - transcriptionElement.textContent = `エラー: ${error.message}`; - console.error("データ取得エラー:", error); - } -} - -displayTranscription(); - -function showRecorder() { - window.location.href = "/index"; -} - -function showFeedback() { - window.location.href = "/feedback"; -} diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 2f67613912dd057b30a7ed42cd082c76df3a0311..0000000000000000000000000000000000000000 --- a/tailwind.config.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./templates/index.html", // HTMLファイルのパス(Flaskなどのテンプレートエンジンを考慮) - "./templates/feedback.html", - "./templates/reset.html", - "./templates/talkDetail.html", - "./templates/userRegister.html", - "./templates/userSelect.html", - ], - darkMode: 'class', // ダークモードをクラスベースで適用 - theme: { - extend: { - colors: { - primary: '#1f2937', // メインカラー - secondary: '#2c3e50', // サブカラー - }, - fontFamily: { - sans: ['Arial', 'sans-serif'], // デフォルトフォント - }, - borderRadius: { - 'xl': '1rem', // 角丸の拡張 - }, - }, - }, - plugins: [], - }; \ No newline at end of file diff --git a/talkDetail.html b/talkDetail.html new file mode 100644 index 0000000000000000000000000000000000000000..8eb5a4672f8acc397c99fcb4e49ff93d3d927648 --- /dev/null +++ b/talkDetail.html @@ -0,0 +1,97 @@ + + + + + + 会話表示画面 + + + +
+

会話の文字起こし表示

+
ここに会話内容が表示されます。
+ + +
+ + + + \ No newline at end of file diff --git a/templates/feedback.html b/templates/feedback.html deleted file mode 100644 index 08d1ffcf946508e1e3f8658d896fa99bc4332c88..0000000000000000000000000000000000000000 --- a/templates/feedback.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - 会話フィードバック画面 - - - - - - - -
-
-
-
-
-
-
- -
会話フィードバック
- - -
- - - - -
- - -
会話レベル:
-
ハラスメントの有無:
-
ハラスメントの種類:
-
繰り返しの程度:
-
会話の心地よさ:
-
- 非難またはハラスメントの程度: -
-
- - - - - diff --git a/templates/history.html b/templates/history.html deleted file mode 100644 index 7877a69e57bb0bb5a7870f63b4ea17850f6ab8b3..0000000000000000000000000000000000000000 --- a/templates/history.html +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - 会話履歴 - - - - - -
All Recordings
- -
-
-
-
Recording Title
-
00:00:00 | 1/1/2025
-
-
-
- - - diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 0799d6754d6d0c8ed3a9a94849b3bdabb804b64a..0000000000000000000000000000000000000000 --- a/templates/index.html +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - JustTalk - Voice Analysis - - - - - - - - - -
- -
JustTalk
- - -
- - - - -
- - -
- -
- - -
- -
- - -
- - -
-
- - - - - - diff --git a/templates/reset.html b/templates/reset.html deleted file mode 100644 index 00ff7dfa8623296e8e53a3d58147f0c9aba29f4d..0000000000000000000000000000000000000000 --- a/templates/reset.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - リセット画面 - - - - - - -
-
-

- メンバーを消去しますか? -

- - -
- - - - -
- - - -
- -
- -
- - - -
-
-
- - - - diff --git a/templates/talkDetail.html b/templates/talkDetail.html deleted file mode 100644 index 5b55de9b8bbf4c7dbfb473b7f268ffb615fecf50..0000000000000000000000000000000000000000 --- a/templates/talkDetail.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - 会話詳細画面 - - - - - - - -
-
-
-
-
-
-
-
-

- 会話の文字起こし表示 -

- - -
- - - - -
- - -
- ここに会話内容が表示されます。 -
-
- - -
- - diff --git a/templates/test b/templates/test deleted file mode 100644 index c767de88b9cea00bcb556a84ccd4f95a08d6deca..0000000000000000000000000000000000000000 --- a/templates/test +++ /dev/null @@ -1 +0,0 @@ -//test \ No newline at end of file diff --git a/templates/userRegister.html b/templates/userRegister.html deleted file mode 100644 index 4a92db84b926c5da5bb45f117fa79f1f114d0f53..0000000000000000000000000000000000000000 --- a/templates/userRegister.html +++ /dev/null @@ -1,426 +0,0 @@ - - - - - ユーザー音声登録 - - - - - - - - -
- -
JustTalk
- -
- - - - -
- - - - - - - \ No newline at end of file diff --git a/templates/userSelect.html b/templates/userSelect.html deleted file mode 100644 index 659bd175c9974a0999bcda3b2eace4c64f1bd023..0000000000000000000000000000000000000000 --- a/templates/userSelect.html +++ /dev/null @@ -1,354 +0,0 @@ - - - - - - メンバー選択 - JustTalk - - - - - - -
- - - - - - -

会話分析に使用するメンバーを選択

- -
- - -
- -
-
-
-

メンバーリストを読み込み中...

-
-
- -
選択中: 0人
- -
- - -
-
- - - - - - diff --git a/transcription.py b/transcription.py deleted file mode 100644 index 80fd8d8edab46cc436844e4cf88892e2ae9b03fc..0000000000000000000000000000000000000000 --- a/transcription.py +++ /dev/null @@ -1,147 +0,0 @@ -import os -from faster_whisper import WhisperModel -from pydub import AudioSegment -import string -import random -from datetime import datetime - -# Matplotlibのキャッシュディレクトリを変更 -os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib" - -# Hugging Faceのキャッシュディレクトリを変更 -os.environ["HF_HOME"] = "/tmp/huggingface" -os.environ["HUGGINGFACE_HUB_CACHE"] = "/tmp/huggingface" - -class TranscriptionMaker(): - # 書き起こしファイルを吐き出すディレクトリを指定 - def __init__(self, output_dir="/tmp/data/transcriptions"): - self.model = WhisperModel("base", device="cpu", download_root="/tmp/huggingface") - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - - - #音声ファイルのディレクトリを受け取り、書き起こしファイルを作成する - def create_transcription(self,audio_directory): - conversation = [] - - #ディレクトリ内のファイルを全て取得 - if not os.path.isdir(audio_directory): - raise ValueError(f"The specified path is not a valid directory: {audio_directory}") - audio_files = self.sort_audio_files_in_directory(audio_directory) - merged_segments = self.combine_audio(audio_files) - merged_audio_directory = self.save_marged_segments(merged_segments, output_directory='/tmp/data/transcription_audio') - merged_files = self.sort_audio_files_in_directory(merged_audio_directory) - - for audio_file in merged_files: - if os.path.splitext(audio_file)[-1].lower() != '.wav': - continue - audio_path = os.path.join(merged_audio_directory, audio_file) - try: - segments,info = list(self.model.transcribe(audio_path)) - except Exception as e: - print(f"Error transcripting file {audio_path}: {e}") - raise - sorted_segments = sorted(segments, key=lambda s: s.start) - results = [] - for segment in sorted_segments: - results.append({ - "start": segment.start, - "end": segment.end, - "text": segment.text - }) - combined_text = "".join([result["text"] for result in results]) - speaker = os.path.basename(audio_file).split("_")[0] - # 無音ならスキップ - if not combined_text: - continue - conversation.append(f"{speaker}: {combined_text}
") - - #ファイルの書き込み。ファイル名は"transcription.txt" - output_file=os.path.join(self.output_dir,"transcription.txt") - print(conversation) - try: - with open(output_file,"w",encoding="utf-8") as f: - for result in conversation: - f.write(result) - except OSError as e: - print(f"Error writing transcription file: {e}") - raise - return output_file - - # 受け取った音声ファイルを話者ごとに整理する - def combine_audio(self,audio_files): - if not audio_files: - raise - merged_segments = [] - current_speaker = None - current_segment = [] - for segment in audio_files: - speaker = os.path.basename(segment).split("_")[0] - if speaker != current_speaker: - # 話者が変わった場合はセグメントを保存 - if current_segment: - merged_segments.append((current_speaker, current_segment)) - current_speaker = speaker - current_segment = [segment] - else: - # 話者が同一の場合はセグメントを結合 - current_segment.append(segment) - # 最後のセグメントを保存 - if current_segment: - merged_segments.append((current_speaker, current_segment)) - - return merged_segments - - # ディレクトリ内の音声ファイルを並べ替える - def sort_audio_files_in_directory(self, directory): - files = os.listdir(directory) - audio_files = [f for f in files if f.endswith(".wav")] - - audio_files.sort(key=lambda x: datetime.strptime(x.split("_")[1].split(".")[0], "%Y%m%d%H%M%S")) - return [os.path.join(directory, f) for f in audio_files] - - def save_marged_segments(self,merged_segments,output_directory='/tmp/data/conversations'): - if not merged_segments: - print("merged_segmentsが見つかりませんでした。") - raise - - conversation = [] - for speaker, segments in merged_segments: - combined_audio = self.merge_segments(segments) - conversation.append((speaker,combined_audio)) - if not os.path.exists(output_directory): - os.makedirs(output_directory) - - for i, (speaker, combined_audio) in enumerate(conversation): - current_time = datetime.now().strftime("%Y%m%d%H%M%S") - filename = f"{speaker}_{current_time}.wav" - file_path = os.path.join(output_directory,filename) - combined_audio.export(file_path,format = "wav") - print(f"Saved: {file_path}") - - return output_directory - - def merge_segments(self,segments): - combined = AudioSegment.empty() # 空のAudioSegmentを初期化 - - for segment in segments: - if isinstance(segment, str): - # セグメントがファイルパスの場合、読み込む - audio = AudioSegment.from_file(segment) - elif isinstance(segment, AudioSegment): - # セグメントがすでにAudioSegmentの場合、そのまま使用 - audio = segment - else: - raise ValueError("Invalid segment type. Must be file path or AudioSegment.") - - combined += audio - return combined - - def generate_random_string(self,length): - letters = string.ascii_letters + string.digits - return ''.join(random.choice(letters) for i in range(length)) - - def generate_filename(self,random_length): - current_time = datetime.now().strftime("%Y%m%d%H%M%S") - filename = f"{current_time}.wav" - return filename \ No newline at end of file