JusTalk / app.py
A-yum1's picture
Update talk_detail.js
917a40d
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)