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