Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,918 +1,495 @@
|
|
1 |
from flask import Flask, request, jsonify, render_template, send_from_directory
|
2 |
import base64
|
3 |
-
|
4 |
import os
|
5 |
import shutil
|
6 |
import requests
|
7 |
import tempfile
|
8 |
import json
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
from transcription import TranscriptionMaker
|
13 |
-
from analyze import TextAnalyzer
|
14 |
-
except ImportError:
|
15 |
-
print("警告: process, transcription, analyze モジュールが見つかりません。ダミークラスを使用します。")
|
16 |
-
# --- ダミークラス (実際のクラスがない場合のエラー回避用) ---
|
17 |
-
class AudioProcessor:
|
18 |
-
def delete_files_in_directory(self, path):
|
19 |
-
print(f"Dummy: Deleting files in {path}")
|
20 |
-
if os.path.isdir(path):
|
21 |
-
# ダミー実装: フォルダ内のファイルを削除するふり
|
22 |
-
for item in os.listdir(path):
|
23 |
-
item_path = os.path.join(path, item)
|
24 |
-
if os.path.isfile(item_path):
|
25 |
-
print(f"Dummy: Removing file {item_path}")
|
26 |
-
# os.remove(item_path) # 実際には削除しない
|
27 |
-
elif os.path.isdir(item_path):
|
28 |
-
print(f"Dummy: Removing directory {item_path}")
|
29 |
-
# shutil.rmtree(item_path) # 実際には削除しない
|
30 |
-
|
31 |
-
def process_multi_audio(self, *args, **kwargs):
|
32 |
-
print("Dummy: Processing multi audio")
|
33 |
-
return [10, 5], {} # ダミーの戻り値
|
34 |
-
def process_audio(self, *args, **kwargs):
|
35 |
-
print("Dummy: Processing single audio")
|
36 |
-
return 10, 5, {} # ダミーの戻り値
|
37 |
-
class TranscriptionMaker:
|
38 |
-
def create_transcription(self, path):
|
39 |
-
print(f"Dummy: Creating transcription for {path}")
|
40 |
-
dummy_path = os.path.join(tempfile.gettempdir(), "dummy_transcription.txt")
|
41 |
-
try:
|
42 |
-
with open(dummy_path, "w", encoding='utf-8') as f:
|
43 |
-
f.write("これはダミーの書き起こしです。")
|
44 |
-
except Exception as e:
|
45 |
-
print(f"Dummy transcription file write error: {e}")
|
46 |
-
# エラーが発生してもダミーパスを返す
|
47 |
-
return dummy_path
|
48 |
-
def save_marged_segments(self, segments):
|
49 |
-
print("Dummy: Saving merged segments")
|
50 |
-
dummy_path = os.path.join(tempfile.gettempdir(), "dummy_merged_audio.wav")
|
51 |
-
try:
|
52 |
-
# ダミーファイルを作成(空でも良い)
|
53 |
-
with open(dummy_path, 'w') as f: pass
|
54 |
-
except Exception as e:
|
55 |
-
print(f"Dummy merged audio file create error: {e}")
|
56 |
-
return dummy_path
|
57 |
-
class TextAnalyzer:
|
58 |
-
def __init__(self, text_path, keywords):
|
59 |
-
print(f"Dummy: Initializing analyzer for {text_path}")
|
60 |
-
self.text_path = text_path
|
61 |
-
self.keywords = keywords
|
62 |
-
def analyze(self, api_key):
|
63 |
-
print("Dummy: Analyzing text")
|
64 |
-
# 環境変数チェックのダミー
|
65 |
-
if not api_key:
|
66 |
-
print("Warning: Dummy DEEPSEEK API key not provided.")
|
67 |
-
return {"dummy_analysis": "ok", "deepseek_analysis": {"conversationLevel": 5, "harassmentPresent": False}} # ダミーの戻り値
|
68 |
-
# ------------------------------------------------------------
|
69 |
-
|
70 |
from flask_cors import CORS
|
71 |
-
|
72 |
-
# --- グローバル変数と初期化 ---
|
73 |
-
# 注意: グローバル変数はリクエスト間で共有されるため、同時アクセス時に問題が発生する可能性があります。
|
74 |
-
# 本番環境では、データベースやセッション管理などのより堅牢な状態管理を検討してください。
|
75 |
process = AudioProcessor()
|
76 |
transcripter = TranscriptionMaker()
|
77 |
app = Flask(__name__)
|
78 |
|
79 |
# CORS設定: すべてのオリジンからのリクエストを許可
|
80 |
-
#
|
81 |
-
CORS(app,
|
|
|
82 |
|
83 |
-
# GASのエンドポイントURL
|
84 |
GAS_URL = "https://script.google.com/macros/s/AKfycbwR2cnMKVU1AxoT9NDaeZaNUaTiwRX64Ul0sH0AU4ccP49Byph-TpxtM_Lwm4G9zLnuYA/exec"
|
85 |
|
86 |
-
users = [] #
|
87 |
-
all_users = [] #
|
88 |
-
transcription_text = ""
|
89 |
-
total_audio = "" # マージされた音声ファイルのパス
|
90 |
-
|
91 |
harassment_keywords = [
|
92 |
"バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい",
|
93 |
"きもい", "キモい", "ブス", "デブ", "ハゲ",
|
94 |
"セクハラ", "パワハラ", "モラハラ"
|
95 |
]
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
# === ヘルパー関数 ===
|
100 |
-
|
101 |
-
def _make_gas_request(payload, timeout=30):
|
102 |
-
"""GASエンドポイントへのPOSTリクエストを送信し、レスポンスJSONを返す"""
|
103 |
-
try:
|
104 |
-
print(f"GAS Request Payload: {payload}")
|
105 |
-
response = requests.post(GAS_URL, json=payload, timeout=timeout)
|
106 |
-
response.raise_for_status() # HTTPエラー (4xx, 5xx) があれば例外を発生させる
|
107 |
-
res_json = response.json()
|
108 |
-
print(f"GAS Response: {res_json}")
|
109 |
-
if res_json.get("status") != "success":
|
110 |
-
# GAS側でエラーが報告された場合
|
111 |
-
raise Exception(f"GAS Error: {res_json.get('message', 'Unknown error from GAS')}")
|
112 |
-
return res_json
|
113 |
-
except requests.exceptions.Timeout:
|
114 |
-
print(f"Error: GAS request timed out after {timeout} seconds.")
|
115 |
-
raise TimeoutError(f"GAS request timed out ({timeout}s)")
|
116 |
-
except requests.exceptions.RequestException as e:
|
117 |
-
print(f"Error: Failed to connect to GAS: {e}")
|
118 |
-
raise ConnectionError(f"Failed to connect to GAS: {e}")
|
119 |
-
except json.JSONDecodeError as e:
|
120 |
-
print(f"Error: Failed to decode JSON response from GAS: {e}")
|
121 |
-
print(f"GAS Raw Response Text: {response.text[:500]}...") # レスポンス内容の一部を表示
|
122 |
-
raise ValueError(f"Invalid JSON response from GAS: {e}")
|
123 |
-
except Exception as e:
|
124 |
-
# _make_gas_request 内で発生したその他の予期せぬエラー
|
125 |
-
print(f"Error during GAS request processing: {e}")
|
126 |
-
raise # 元のエラーを再発生させる
|
127 |
-
|
128 |
-
|
129 |
-
def download_from_cloud(filename_with_ext, local_path):
|
130 |
-
"""クラウド(GAS)から指定されたファイルをダウンロードし、ローカルパスに保存する"""
|
131 |
-
payload = {"action": "download", "fileName": filename_with_ext}
|
132 |
-
try:
|
133 |
-
print(f"Downloading {filename_with_ext} from cloud...")
|
134 |
-
res_json = _make_gas_request(payload, timeout=60) # ダウンロードは時間がかかる可能性があるため長めのタイムアウト
|
135 |
-
|
136 |
-
base64_data = res_json.get("base64Data")
|
137 |
-
if not base64_data:
|
138 |
-
raise ValueError("No base64Data found in the download response.")
|
139 |
-
|
140 |
-
audio_binary = base64.b64decode(base64_data)
|
141 |
-
if not audio_binary:
|
142 |
-
raise ValueError("Failed to decode base64 data.")
|
143 |
-
|
144 |
-
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
145 |
-
with open(local_path, 'wb') as f:
|
146 |
-
f.write(audio_binary)
|
147 |
-
|
148 |
-
print(f"Successfully downloaded and saved to {local_path}")
|
149 |
-
if os.path.getsize(local_path) <= 0:
|
150 |
-
# ダウンロードしたがファイルサイズが0の場合
|
151 |
-
os.remove(local_path) # 不完全なファイルを削除
|
152 |
-
raise ValueError(f"Downloaded file {local_path} is empty.")
|
153 |
-
|
154 |
-
return local_path
|
155 |
-
except (TimeoutError, ConnectionError, ValueError, Exception) as e:
|
156 |
-
print(f"Error downloading {filename_with_ext}: {str(e)}")
|
157 |
-
# エラーが発生したら上位に伝播させる
|
158 |
-
raise
|
159 |
-
|
160 |
-
|
161 |
-
def delete_from_cloud(filename_with_ext):
|
162 |
-
"""クラウド(GAS)から指定されたファイルを削除する"""
|
163 |
-
payload = {"action": "delete", "fileName": filename_with_ext}
|
164 |
-
try:
|
165 |
-
print(f"Deleting {filename_with_ext} from cloud...")
|
166 |
-
_make_gas_request(payload, timeout=30)
|
167 |
-
print(f"Successfully deleted {filename_with_ext} from cloud.")
|
168 |
-
return True
|
169 |
-
except (TimeoutError, ConnectionError, ValueError, Exception) as e:
|
170 |
-
print(f"Error deleting {filename_with_ext}: {str(e)}")
|
171 |
-
# エラーが発生したら上位に伝播させる
|
172 |
-
raise
|
173 |
-
|
174 |
-
|
175 |
-
def update_all_users():
|
176 |
-
"""GASから最新のファイルリストを取得し、グローバル変数 all_users を更新する"""
|
177 |
-
global all_users
|
178 |
-
payload = {"action": "list"}
|
179 |
-
try:
|
180 |
-
print("Fetching user list from cloud...")
|
181 |
-
res_json = _make_gas_request(payload, timeout=30)
|
182 |
-
# ファイル名から拡張子を除去してリストを作成 (空のファイル名を除外)
|
183 |
-
current_users = [os.path.splitext(name)[0] for name in res_json.get("fileNames", []) if name and '.' in name]
|
184 |
-
all_users = sorted(list(set(current_users))) # 重複除去とソート
|
185 |
-
print(f"Updated all_users list: {all_users}")
|
186 |
-
return all_users # 更新後のリストを返す
|
187 |
-
except (TimeoutError, ConnectionError, ValueError, Exception) as e:
|
188 |
-
print(f"Error updating all_users list: {str(e)}")
|
189 |
-
# エラーが発生しても、既存の all_users は変更せず、エラーを上位に伝播
|
190 |
-
raise
|
191 |
-
|
192 |
-
# === Flask ルート定義 ===
|
193 |
|
194 |
-
# React用: ユーザーリスト取得エンドポイント (JSON構造は変更しない)
|
195 |
-
@app.route('/list_base_audio', methods=['GET'])
|
196 |
-
def list_base_audio():
|
197 |
-
"""現在の all_users リストを返す (React側で処理が必要)"""
|
198 |
-
global all_users
|
199 |
-
try:
|
200 |
-
# 関数を呼び出して最新の状態を取得・更新
|
201 |
-
update_all_users()
|
202 |
-
# 元の形式で返す
|
203 |
-
return jsonify({"status": "success", "fileNames": all_users}), 200
|
204 |
-
except (TimeoutError, ConnectionError, ValueError, Exception) as e:
|
205 |
-
# GASとの通信エラーなどでリスト取得に失敗した場合
|
206 |
-
print(f"Error in /list_base_audio: {str(e)}")
|
207 |
-
# エラー発生時は空リストまたはエラーメッセージを返す
|
208 |
-
# return jsonify({"status": "error", "message": "Failed to retrieve user list", "details": str(e)}), 500
|
209 |
-
# または、現状保持しているリストがあればそれを返し、エラーをログに残すだけでも良いかもしれない
|
210 |
-
print("Returning potentially stale user list due to update error.")
|
211 |
-
return jsonify({"status": "warning", "message": "Could not refresh list, returning cached data.", "fileNames": all_users}), 200 # 警告付きで成功扱いにするか、5xxエラーにするかは要件次第
|
212 |
-
|
213 |
-
# React用: ユーザー削除エンドポイント
|
214 |
-
@app.route('/api/users/<user_name>', methods=['DELETE'])
|
215 |
-
def delete_user_api(user_name):
|
216 |
-
"""指定されたユーザー名のファイルをクラウドから削除し、ローカルリストも更新"""
|
217 |
-
global users
|
218 |
-
global all_users
|
219 |
-
|
220 |
-
if not user_name:
|
221 |
-
return jsonify({"status": "error", "message": "User name cannot be empty"}), 400
|
222 |
|
223 |
-
|
224 |
-
|
225 |
-
try:
|
226 |
-
print(f"API delete request for user: {user_name}")
|
227 |
-
# 1. クラウドから削除
|
228 |
-
delete_from_cloud(filename_to_delete)
|
229 |
-
|
230 |
-
# 2. ローカルリストから削除 (成功した場合のみ)
|
231 |
-
deleted_from_local = False
|
232 |
-
if user_name in all_users:
|
233 |
-
all_users.remove(user_name)
|
234 |
-
print(f"Removed '{user_name}' from all_users list.")
|
235 |
-
deleted_from_local = True
|
236 |
-
if user_name in users:
|
237 |
-
users.remove(user_name)
|
238 |
-
print(f"Removed '{user_name}' from selected users (users) list.")
|
239 |
-
deleted_from_local = True
|
240 |
-
|
241 |
-
if not deleted_from_local:
|
242 |
-
print(f"Warning: User '{user_name}' was not found in local lists (all_users, users) but deletion from cloud was attempted/successful.")
|
243 |
-
|
244 |
-
# 成功レスポンス
|
245 |
-
return jsonify({"status": "success", "message": f"User '{user_name}' deleted successfully."}), 200
|
246 |
-
|
247 |
-
except FileNotFoundError:
|
248 |
-
# delete_from_cloud 内で発生した場合(GASがファイルなしと応答した場合など)
|
249 |
-
# または、ローカルリストにそもそも存在しなかった場合
|
250 |
-
print(f"User '{user_name}' not found for deletion (either on cloud or locally).")
|
251 |
-
# 念のためローカルリストからも削除試行(既になくてもエラーにはならない)
|
252 |
-
if user_name in all_users: all_users.remove(user_name)
|
253 |
-
if user_name in users: users.remove(user_name)
|
254 |
-
return jsonify({"status": "error", "message": f"User '{user_name}' not found."}), 404 # Not Found
|
255 |
-
except (TimeoutError, ConnectionError, ValueError, Exception) as e:
|
256 |
-
# GAS通信エラーやその他の予期せぬエラー
|
257 |
-
error_message = f"Failed to delete user '{user_name}'."
|
258 |
-
print(f"{error_message} Details: {str(e)}")
|
259 |
-
# クライアントには詳細なエラー原因を伏せる場合もある
|
260 |
-
return jsonify({"status": "error", "message": error_message, "details": str(e)}), 500 # Internal Server Error
|
261 |
-
|
262 |
-
# --- 既存のテンプレートレンダリング用ルート (変更なし) ---
|
263 |
-
@app.route('/index', methods=['GET']) # POST不要なら削除
|
264 |
def index():
|
265 |
return render_template('index.html', users=users)
|
266 |
|
267 |
-
|
|
|
268 |
def feedback():
|
269 |
return render_template('feedback.html')
|
270 |
|
271 |
-
|
|
|
272 |
def talk_detail():
|
273 |
return render_template('talkDetail.html')
|
274 |
|
275 |
-
|
|
|
276 |
def userregister():
|
277 |
return render_template('userRegister.html')
|
278 |
|
279 |
-
|
280 |
-
def reset_html():
|
281 |
-
return render_template('reset.html')
|
282 |
-
|
283 |
-
@app.route('/', methods=['GET']) # ルートパス
|
284 |
-
@app.route('/userselect', methods=['GET'])
|
285 |
-
def userselect():
|
286 |
-
# ユーザー選択画面表示時に最新リストを取得・表示するならここで update_all_users() を呼ぶ
|
287 |
-
# try:
|
288 |
-
# update_all_users()
|
289 |
-
# except Exception as e:
|
290 |
-
# print(f"Failed to update users on loading userselect page: {e}")
|
291 |
-
# return render_template('userSelect.html', available_users=all_users) # テンプレートに渡す場合
|
292 |
-
return render_template('userSelect.html') # テンプレート側でAPIを叩く場合
|
293 |
-
# --- ここまでテンプレートレンダリング用ルート ---
|
294 |
-
|
295 |
-
|
296 |
-
# --- 既存のAPIエンドポイント (一部見直し) ---
|
297 |
-
|
298 |
-
# 人数確認 (最新リストを返すように修正)
|
299 |
@app.route('/confirm', methods=['GET'])
|
300 |
def confirm():
|
301 |
-
global users
|
302 |
global all_users
|
|
|
303 |
try:
|
304 |
-
update_all_users()
|
305 |
except Exception as e:
|
306 |
-
print(f"
|
307 |
-
|
308 |
-
return jsonify({'selected_members': users, 'all_available_members': all_users}), 200
|
309 |
|
|
|
|
|
|
|
|
|
310 |
|
311 |
-
#
|
312 |
-
@app.route('/reset_member', methods=['POST'])
|
313 |
def reset_member():
|
314 |
global users
|
315 |
-
global all_users
|
316 |
global total_audio
|
317 |
global transcription_text
|
318 |
-
|
319 |
-
#
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
print(f"Deleted file: {total_audio}")
|
327 |
-
elif os.path.isdir(total_audio):
|
328 |
-
# ディレクトリ内のファイルを削除 (AudioProcessorに任せる)
|
329 |
-
process.delete_files_in_directory(total_audio)
|
330 |
-
print(f"Cleared files in directory: {total_audio}")
|
331 |
-
# 必要ならディレクトリ自体も削除: shutil.rmtree(total_audio)
|
332 |
-
else:
|
333 |
-
print(f"Warning: Path exists but is not a file or directory: {total_audio}")
|
334 |
-
except Exception as e:
|
335 |
-
print(f"Error clearing total_audio path '{total_audio}': {e}")
|
336 |
-
total_audio = "" # パスをクリア
|
337 |
-
|
338 |
-
# 書き起こしテキストファイルの削除
|
339 |
-
if transcription_text and os.path.exists(transcription_text):
|
340 |
try:
|
341 |
os.remove(transcription_text)
|
342 |
-
print(f"
|
343 |
except Exception as e:
|
344 |
-
print(f"
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
try:
|
349 |
-
transcription_audio_dir = '/tmp/data/transcription_audio'
|
350 |
-
if os.path.isdir(transcription_audio_dir):
|
351 |
-
process.delete_files_in_directory(transcription_audio_dir)
|
352 |
-
print(f"Cleared files in directory: {transcription_audio_dir}")
|
353 |
-
except Exception as e:
|
354 |
-
print(f"Error clearing transcription_audio directory: {e}")
|
355 |
-
# --- ここまで一時ファイル/変数クリア ---
|
356 |
-
|
357 |
try:
|
358 |
data = request.get_json()
|
359 |
-
if not data or "names" not in data
|
360 |
-
return jsonify({"status": "error", "message": "Invalid request body
|
361 |
-
|
362 |
-
names_to_delete = data["names"]
|
363 |
-
if not names_to_delete:
|
364 |
-
return jsonify({"status": "warning", "message": "No names provided for deletion."}), 200 # 何もしないで成功
|
365 |
-
|
366 |
-
print(f"Bulk delete request for names: {names_to_delete}")
|
367 |
-
deleted_names = []
|
368 |
-
errors = []
|
369 |
|
370 |
-
|
371 |
-
|
372 |
-
|
|
|
373 |
try:
|
374 |
-
delete_from_cloud(
|
375 |
-
|
376 |
-
# ローカルリストからも削除
|
377 |
-
if name in all_users: all_users.remove(name)
|
378 |
-
if name in users: users.remove(name)
|
379 |
except Exception as e:
|
380 |
-
|
381 |
-
|
382 |
-
errors.append({"name": name, "error": str(e)})
|
383 |
|
384 |
-
#
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
if errors:
|
394 |
-
# 一部成功、一部失敗、またはリスト更新失敗
|
395 |
-
return jsonify({
|
396 |
-
"status": "partial_success" if deleted_names else "error",
|
397 |
-
"message": f"Deleted {len(deleted_names)} out of {len(names_to_delete)} requested members. Some errors occurred.",
|
398 |
-
"deleted": deleted_names,
|
399 |
-
"errors": errors,
|
400 |
-
"current_users": final_users_list # 更新後のリスト
|
401 |
-
}), 207 # Multi-Status or 500 if no success
|
402 |
-
else:
|
403 |
-
# すべて成功
|
404 |
-
return jsonify({
|
405 |
-
"status": "success",
|
406 |
-
"message": f"Successfully deleted {len(deleted_names)} members.",
|
407 |
-
"deleted": deleted_names,
|
408 |
-
"current_users": final_users_list # 更新後のリスト
|
409 |
-
}), 200
|
410 |
|
411 |
except Exception as e:
|
412 |
-
print(f"
|
413 |
return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500
|
414 |
|
415 |
-
# 書き起こし作成エンドポイント
|
416 |
-
@app.route('/transcription', methods=['POST'])
|
417 |
def transcription():
|
418 |
global transcription_text
|
419 |
global total_audio
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
else:
|
426 |
-
# 書き起こしが存在しない、またはパスが無効な場合、新規作成を試みる
|
427 |
-
print("Transcription file not found or path invalid. Attempting to create...")
|
428 |
-
if not total_audio or not os.path.exists(total_audio):
|
429 |
-
print("Error: total_audio path is not set or file does not exist.")
|
430 |
-
return jsonify({"error": "Audio data for transcription is missing or invalid."}), 400
|
431 |
-
|
432 |
-
try:
|
433 |
-
# ここで実際に書き起こし処理を実行
|
434 |
transcription_text = transcripter.create_transcription(total_audio)
|
435 |
-
|
436 |
-
|
437 |
-
raise FileNotFoundError("Transcription process did not generate a valid file path.")
|
438 |
-
print(f"Transcription created: {transcription_text}")
|
439 |
except Exception as e:
|
440 |
-
|
441 |
-
|
442 |
-
return jsonify({"error": f"Failed to create transcription: {str(e)}"}), 500
|
443 |
-
|
444 |
-
# 有効な書き起こしファイルパスがある場合、内容を読み込んで返す
|
445 |
try:
|
446 |
with open(transcription_text, 'r', encoding='utf-8') as file:
|
447 |
file_content = file.read()
|
448 |
-
|
449 |
-
|
450 |
except FileNotFoundError:
|
451 |
-
|
452 |
-
return jsonify({"error": "Transcription file could not be read (not found)."}), 404
|
453 |
except Exception as e:
|
454 |
-
|
455 |
-
return jsonify({"error": f"Unexpected error reading transcription: {str(e)}"}), 500
|
456 |
|
457 |
-
# AI分析エンドポイント
|
458 |
-
@app.route('/analyze', methods=['POST'])
|
459 |
def analyze():
|
460 |
global transcription_text
|
461 |
global total_audio
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
if not total_audio or not os.path.exists(total_audio):
|
468 |
-
return jsonify({"error": "Audio data for analysis is missing or invalid."}), 400
|
469 |
-
try:
|
470 |
transcription_text = transcripter.create_transcription(total_audio)
|
471 |
-
if not transcription_text or not os.path.exists(transcription_text):
|
472 |
-
raise FileNotFoundError("Transcription process did not generate a valid file path.")
|
473 |
-
print(f"Transcription created for analysis: {transcription_text}")
|
474 |
except Exception as e:
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
# APIキーのチェック
|
480 |
api_key = os.environ.get("DEEPSEEK")
|
481 |
-
if
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
486 |
try:
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
504 |
except Exception as e:
|
505 |
-
print(f"
|
506 |
-
|
507 |
-
|
508 |
|
509 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
510 |
@app.route('/upload_audio', methods=['POST'])
|
511 |
def upload_audio():
|
512 |
global total_audio
|
513 |
global users
|
514 |
-
|
515 |
-
temp_audio_file = None # 一時ファイルのパスを保持
|
516 |
-
temp_base_audio_dir = "/tmp/data/base_audio" # ベース音声の一時保存先
|
517 |
-
|
518 |
try:
|
519 |
data = request.get_json()
|
520 |
if not data or 'audio_data' not in data:
|
521 |
-
return jsonify({"error": "
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
users = current_selected_users # グローバル変数も更新 (必要なら)
|
528 |
-
print(f"Received selected users: {users}")
|
529 |
-
elif not users:
|
530 |
-
# リクエストにもグローバル変数にもユーザーがいない場合
|
531 |
-
return jsonify({"error": "処理対象のユーザーが選択されていません"}), 400
|
532 |
-
else:
|
533 |
-
print(f"Using globally selected users: {users}")
|
534 |
|
|
|
|
|
535 |
|
536 |
-
#
|
537 |
audio_binary = base64.b64decode(data['audio_data'])
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
# 一時ファイルを作成 (アップロードされた音声)
|
542 |
-
audio_dir = "/tmp/data/uploads" # アップロード専用の一時フォルダ推奨
|
543 |
os.makedirs(audio_dir, exist_ok=True)
|
544 |
-
|
545 |
-
|
546 |
-
os.close(temp_audio_fd) # ファイルディスクリプタを閉じる
|
547 |
-
with open(temp_audio_file, 'wb') as f:
|
548 |
f.write(audio_binary)
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
os.makedirs(
|
|
|
|
|
555 |
reference_paths = []
|
556 |
-
|
557 |
-
for user_name in users:
|
558 |
-
ref_path = os.path.join(temp_base_audio_dir, f"{user_name}.wav")
|
559 |
try:
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
download_from_cloud(f"{
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
# ダウンロード後もファイルが存在しないか空の場合
|
571 |
-
missing_users.append(user_name)
|
572 |
-
|
573 |
except Exception as e:
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
# ここではエラーリストに追加して最���にチェックする
|
578 |
-
|
579 |
-
if missing_users:
|
580 |
-
# 必要な参照音声の一部または全部が見つからなかった場合
|
581 |
-
return jsonify({"error": f"参照音声の準備に失敗しました。見つからない、またはダウンロードできないユーザー: {', '.join(missing_users)}"}), 404 # Not Found or 500
|
582 |
-
|
583 |
-
if not reference_paths:
|
584 |
-
return jsonify({"error": "有効な参照音声ファイルがありません"}), 500
|
585 |
-
# --- ここまで参照音声の準備 ---
|
586 |
-
|
587 |
-
|
588 |
-
# --- 音声処理の実行 ---
|
589 |
-
print(f"Processing audio with {len(users)} user(s)...")
|
590 |
-
merged_segments_result = None # 初期化
|
591 |
if len(users) > 1:
|
592 |
-
|
593 |
-
matched_times,
|
594 |
-
|
595 |
-
|
596 |
-
total_time = sum(matched_times)
|
597 |
rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times]
|
|
|
|
|
598 |
user_rates = {users[i]: rates[i] for i in range(len(users))}
|
599 |
-
|
600 |
-
print(f"Multi-user processing result: {result_data}")
|
601 |
else:
|
602 |
-
|
603 |
-
|
604 |
-
|
605 |
-
)
|
606 |
total_time = matched_time + unmatched_time
|
607 |
rate = (matched_time / total_time) * 100 if total_time > 0 else 0
|
608 |
-
|
609 |
-
|
610 |
-
|
611 |
-
# マージされたセグメントを保存し、グローバル変数 total_audio を更新
|
612 |
-
if merged_segments_result is not None:
|
613 |
-
# 以前の total_audio ファイルがあれば削除
|
614 |
-
if total_audio and os.path.exists(total_audio) and os.path.isfile(total_audio):
|
615 |
-
try: os.remove(total_audio)
|
616 |
-
except OSError as e: print(f"Could not remove previous total_audio file '{total_audio}': {e}")
|
617 |
-
|
618 |
-
total_audio = transcripter.save_marged_segments(merged_segments_result)
|
619 |
-
print(f"Merged audio saved to: {total_audio}")
|
620 |
-
if not total_audio or not os.path.exists(total_audio):
|
621 |
-
print("Warning: Saving merged segments did not produce a valid file path.")
|
622 |
-
# エラーにするか警告に留めるか
|
623 |
-
# return jsonify({"error": "Failed to save processed audio segments."}), 500
|
624 |
-
else:
|
625 |
-
print("Warning: No merged segments were generated by the process.")
|
626 |
-
# 必要に応じて total_audio をクリア
|
627 |
-
if total_audio and os.path.exists(total_audio) and os.path.isfile(total_audio):
|
628 |
-
try: os.remove(total_audio)
|
629 |
-
except OSError as e: print(f"Could not remove previous total_audio file '{total_audio}': {e}")
|
630 |
-
total_audio = ""
|
631 |
-
|
632 |
-
|
633 |
-
return jsonify(result_data), 200
|
634 |
-
# --- ここまで音声処理 ---
|
635 |
-
|
636 |
-
except base64.binascii.Error as e:
|
637 |
-
print(f"Error decoding base64 audio data: {e}")
|
638 |
-
return jsonify({"error": "無効な音声データ形式です (Base64デコード失敗)"}), 400
|
639 |
-
except FileNotFoundError as e:
|
640 |
-
print(f"File not found error during audio processing: {e}")
|
641 |
-
return jsonify({"error": "処理に必要なファイルが見つかりません", "details": str(e)}), 404
|
642 |
except Exception as e:
|
643 |
-
|
644 |
-
|
645 |
-
# traceback.print_exc() # 詳細なスタックトレースをログに出力する場合
|
646 |
-
return jsonify({"error": "音���処理中にサーバーエラーが発生しました", "details": str(e)}), 500
|
647 |
-
finally:
|
648 |
-
# --- 一時ファイルのクリーンアップ ---
|
649 |
-
if temp_audio_file and os.path.exists(temp_audio_file):
|
650 |
-
try:
|
651 |
-
os.remove(temp_audio_file)
|
652 |
-
print(f"Cleaned up temporary upload file: {temp_audio_file}")
|
653 |
-
except Exception as e:
|
654 |
-
print(f"Error cleaning up temporary file '{temp_audio_file}': {e}")
|
655 |
-
# ベース音声の一時ファイルは他の処理で使う可能性があるため、ここでは削除しない
|
656 |
-
# clear_tmp エンドポイントなどで定期的に削除することを推奨
|
657 |
-
# --- ここまでクリーンアップ ---
|
658 |
|
|
|
|
|
|
|
|
|
|
|
659 |
|
660 |
# 選択したユーザーを設定するエンドポイント
|
661 |
@app.route('/select_users', methods=['POST'])
|
662 |
def select_users():
|
663 |
global users
|
664 |
-
|
665 |
-
|
666 |
try:
|
667 |
data = request.get_json()
|
668 |
-
if not data or 'users' not in data
|
669 |
-
return jsonify({"error": "
|
670 |
-
|
671 |
-
|
672 |
-
print(f"
|
673 |
-
|
674 |
-
|
675 |
-
try:
|
676 |
-
update_all_users() # 最新の全ユーザーリストを取得
|
677 |
-
except Exception as e:
|
678 |
-
print(f"Warning: Could not verify selected users against the latest list due to update error: {e}")
|
679 |
-
# エラーが発生しても処理は続行するが、存在しないユーザーが含まれる可能性がある
|
680 |
-
|
681 |
-
valid_selected_users = []
|
682 |
-
invalid_users = []
|
683 |
-
for name in selected_user_names:
|
684 |
-
if name in all_users:
|
685 |
-
valid_selected_users.append(name)
|
686 |
-
else:
|
687 |
-
invalid_users.append(name)
|
688 |
-
|
689 |
-
if invalid_users:
|
690 |
-
print(f"Warning: The following selected users do not exist in the available list: {invalid_users}")
|
691 |
-
# 無効なユーザーを除外するか、エラーにするかは要件次第
|
692 |
-
# ここでは有効なユーザーのみを設定する
|
693 |
-
users = sorted(list(set(valid_selected_users))) # 重複除去とソート
|
694 |
-
return jsonify({
|
695 |
-
"status": "warning",
|
696 |
-
"message": f"一部のユーザーが存在しませんでした: {', '.join(invalid_users)}",
|
697 |
-
"selected_users": users
|
698 |
-
}), 200 # 成功扱いにするか、クライアント側で処理しやすいステータスコード(例: 207 Multi-Status)
|
699 |
-
else:
|
700 |
-
# 全員有効な場合
|
701 |
-
users = sorted(list(set(valid_selected_users)))
|
702 |
-
print(f"Successfully selected users: {users}")
|
703 |
-
return jsonify({"status": "success", "selected_users": users}), 200
|
704 |
-
|
705 |
except Exception as e:
|
706 |
-
print(
|
707 |
-
return jsonify({"error": "
|
708 |
|
709 |
-
|
710 |
-
@app.route('/reset', methods=['GET']) # または POST
|
711 |
def reset():
|
712 |
global users
|
|
|
713 |
global total_audio
|
714 |
global transcription_text
|
715 |
-
|
716 |
-
|
717 |
-
|
718 |
-
|
719 |
-
|
720 |
-
|
721 |
-
#
|
722 |
-
if
|
723 |
try:
|
724 |
-
|
725 |
-
|
726 |
-
except Exception as e:
|
727 |
-
|
728 |
-
|
729 |
-
if transcription_text and os.path.exists(transcription_text):
|
730 |
-
try: os.remove(transcription_text); print(f"Deleted: {transcription_text}")
|
731 |
-
except Exception as e: print(f"Error deleting transcription file '{transcription_text}': {e}")
|
732 |
transcription_text = ""
|
733 |
|
734 |
-
|
735 |
-
transcription_audio_dir = '/tmp/data/transcription_audio'
|
736 |
-
if os.path.isdir(transcription_audio_dir):
|
737 |
-
process.delete_files_in_directory(transcription_audio_dir)
|
738 |
-
print(f"Cleared dir: {transcription_audio_dir}")
|
739 |
-
except Exception as e: print(f"Error clearing transcription_audio directory: {e}")
|
740 |
-
|
741 |
-
return jsonify({"status": "success", "message": "Selected users and temporary analysis files have been reset."}), 200
|
742 |
|
743 |
-
|
744 |
-
# 選択されたファイルのコピー (ダウンロード)
|
745 |
@app.route('/copy_selected_files', methods=['POST'])
|
746 |
def copy_selected_files():
|
747 |
-
# このエンドポイントの目的が不明瞭(どこにコピーするのか?)
|
748 |
-
# もしクライアント側でダウンロードさせたいなら、ファイルリストを返す方が適切かもしれない
|
749 |
-
# ここでは指定された名前のファイルを /tmp/data/selected_audio にダウンロードする実装とする
|
750 |
try:
|
751 |
data = request.get_json()
|
752 |
-
if not data or "names" not in data
|
753 |
-
return jsonify({"error": "
|
754 |
-
|
755 |
-
names_to_copy = data["names"]
|
756 |
-
if not names_to_copy:
|
757 |
-
return jsonify({"status": "warning", "message": "No names provided for copying."}), 200
|
758 |
|
759 |
-
|
|
|
760 |
os.makedirs(dest_dir, exist_ok=True)
|
761 |
-
print(f"Copying selected files to server directory: {dest_dir}")
|
762 |
|
763 |
copied_files = []
|
764 |
-
|
765 |
-
for name in names_to_copy:
|
766 |
-
if not name: continue
|
767 |
dest_path = os.path.join(dest_dir, f"{name}.wav")
|
768 |
try:
|
|
|
769 |
download_from_cloud(f"{name}.wav", dest_path)
|
770 |
copied_files.append(name)
|
771 |
-
print(f"
|
772 |
except Exception as e:
|
773 |
-
|
774 |
-
|
775 |
-
errors.append({"name": name, "error": str(e)})
|
776 |
-
# エラーがあっても続行
|
777 |
-
|
778 |
-
if errors:
|
779 |
-
return jsonify({
|
780 |
-
"status": "partial_success" if copied_files else "error",
|
781 |
-
"message": f"Copied {len(copied_files)} files. Some errors occurred.",
|
782 |
-
"copied": copied_files,
|
783 |
-
"errors": errors,
|
784 |
-
"destination_directory": dest_dir # 参考情報
|
785 |
-
}), 207 # Multi-Status
|
786 |
-
else:
|
787 |
-
return jsonify({
|
788 |
-
"status": "success",
|
789 |
-
"message": f"Successfully copied {len(copied_files)} files.",
|
790 |
-
"copied": copied_files,
|
791 |
-
"destination_directory": dest_dir
|
792 |
-
}), 200
|
793 |
|
794 |
-
|
795 |
-
print(f"Error in /copy_selected_files: {str(e)}")
|
796 |
-
return jsonify({"error": "サーバー内部エラーが発生しました", "details": str(e)}), 500
|
797 |
|
|
|
|
|
|
|
798 |
|
799 |
-
|
800 |
-
@app.route('/clear_tmp', methods=['POST']) # GETよりPOST/DELETEが適切
|
801 |
def clear_tmp():
|
802 |
-
# このエンドポイントはサーバー上の /tmp/data を再帰的に削除するため、非常に危険
|
803 |
-
# 実行すると、処理中のファイルも含めてすべて消える可能性がある
|
804 |
-
# 使用は慎重に行うべき
|
805 |
-
tmp_root_dir = "/tmp/data"
|
806 |
-
print(f"WARNING: Received request to clear directory: {tmp_root_dir}")
|
807 |
-
if not os.path.isdir(tmp_root_dir):
|
808 |
-
return jsonify({"status": "warning", "message": f"Directory not found, nothing to clear: {tmp_root_dir}"}), 200
|
809 |
-
|
810 |
try:
|
811 |
-
#
|
812 |
-
#
|
813 |
-
|
814 |
-
#
|
815 |
-
|
816 |
-
|
817 |
-
|
818 |
-
|
819 |
-
|
820 |
-
|
821 |
-
|
822 |
-
for item_name in os.listdir(tmp_root_dir):
|
823 |
-
item_path = os.path.join(tmp_root_dir, item_name)
|
824 |
-
try:
|
825 |
-
if os.path.isfile(item_path) or os.path.islink(item_path):
|
826 |
-
os.unlink(item_path)
|
827 |
-
deleted_items.append(item_name)
|
828 |
-
print(f"Deleted file/link: {item_path}")
|
829 |
-
elif os.path.isdir(item_path):
|
830 |
-
shutil.rmtree(item_path)
|
831 |
-
deleted_items.append(item_name + "/") # ディレクトリを示す
|
832 |
-
print(f"Deleted directory: {item_path}")
|
833 |
-
except Exception as e:
|
834 |
-
error_msg = f"Failed to delete '{item_name}': {str(e)}"
|
835 |
-
print(error_msg)
|
836 |
-
errors.append({"item": item_name, "error": str(e)})
|
837 |
-
|
838 |
-
if errors:
|
839 |
-
return jsonify({
|
840 |
-
"status": "partial_success",
|
841 |
-
"message": f"Cleared some items in {tmp_root_dir}, but errors occurred.",
|
842 |
-
"deleted_items": deleted_items,
|
843 |
-
"errors": errors
|
844 |
-
}), 207
|
845 |
-
else:
|
846 |
-
return jsonify({
|
847 |
-
"status": "success",
|
848 |
-
"message": f"Successfully cleared contents of {tmp_root_dir}",
|
849 |
-
"deleted_items": deleted_items
|
850 |
-
}), 200
|
851 |
|
852 |
except Exception as e:
|
853 |
-
print(
|
854 |
-
return jsonify({"error": "
|
855 |
-
|
856 |
|
857 |
-
# ベース音声アップロード (ユーザー登録)
|
858 |
@app.route('/upload_base_audio', methods=['POST'])
|
859 |
def upload_base_audio():
|
860 |
-
global all_users
|
861 |
-
|
862 |
try:
|
863 |
data = request.get_json()
|
864 |
if not data or 'audio_data' not in data or 'name' not in data:
|
865 |
-
return jsonify({"error": "
|
866 |
-
|
867 |
-
|
868 |
-
|
869 |
-
|
870 |
-
# TODO: 名前に使用できない文字(ファイル名として不適切、セキュリティリスクなど)をチェックするバリデーションを追加推奨
|
871 |
-
# 例: if not re.match(r'^[a-zA-Z0-9_-]+$', name): return jsonify({"error": "Invalid characters in name"}), 400
|
872 |
-
|
873 |
-
filename_to_upload = f"{name}.wav"
|
874 |
-
print(f"Base audio upload request for name: {name} (filename: {filename_to_upload})")
|
875 |
-
|
876 |
payload = {
|
877 |
"action": "upload",
|
878 |
-
"fileName":
|
879 |
"base64Data": data['audio_data']
|
880 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
881 |
|
882 |
-
|
883 |
-
|
884 |
-
|
885 |
-
|
|
|
|
|
|
|
|
|
|
|
886 |
|
887 |
-
# 全ユーザーリストを更新 (GASに再度問い合わせるのが確実)
|
888 |
-
try:
|
889 |
-
update_all_users()
|
890 |
-
except Exception as update_e:
|
891 |
-
print(f"Warning: Failed to refresh user list after upload: {update_e}")
|
892 |
-
# アップロード自体は成功しているので、警告付きで成功レスポンスを返す
|
893 |
-
return jsonify({
|
894 |
-
"state": "Registration Success (List update may be delayed)",
|
895 |
-
"driveFileId": res_json.get("fileId"),
|
896 |
-
"warning": f"User list update failed: {update_e}"
|
897 |
-
}), 200 # または 201 Created
|
898 |
-
|
899 |
-
return jsonify({"state": "Registration Success!", "driveFileId": res_json.get("fileId")}), 201 # 201 Created がより適切かも
|
900 |
-
|
901 |
-
except base64.binascii.Error as e:
|
902 |
-
print(f"Error decoding base64 audio data: {e}")
|
903 |
-
return jsonify({"error": "無効な音声データ形式です (Base64デコード失敗)"}), 400
|
904 |
-
except (TimeoutError, ConnectionError, ValueError, Exception) as e:
|
905 |
-
error_message = f"Failed to upload base audio for '{name}'."
|
906 |
-
details = str(e)
|
907 |
-
print(f"{error_message} Details: {details}")
|
908 |
-
# GASからのエラーメッセージに 'already exists' が含まれていたら 409 Conflict を返す
|
909 |
-
status_code = 409 if "already exists" in details.lower() else 500
|
910 |
-
return jsonify({"error": error_message, "details": details}), status_code
|
911 |
-
|
912 |
-
# === アプリケーションの実行 ===
|
913 |
if __name__ == '__main__':
|
914 |
port = int(os.environ.get("PORT", 7860))
|
915 |
-
# 本番環境では debug=False に設定すること
|
916 |
-
# host='0.0.0.0' はコンテナなど外部からアクセスする場合に必要
|
917 |
-
print(f"Starting Flask server on host 0.0.0.0 port {port} with debug mode {'ON' if app.debug else 'OFF'}")
|
918 |
app.run(debug=True, host="0.0.0.0", port=port)
|
|
|
1 |
from flask import Flask, request, jsonify, render_template, send_from_directory
|
2 |
import base64
|
3 |
+
from pydub import AudioSegment
|
4 |
import os
|
5 |
import shutil
|
6 |
import requests
|
7 |
import tempfile
|
8 |
import json
|
9 |
+
from process import AudioProcessor
|
10 |
+
from transcription import TranscriptionMaker
|
11 |
+
from analyze import TextAnalyzer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
from flask_cors import CORS
|
|
|
|
|
|
|
|
|
13 |
process = AudioProcessor()
|
14 |
transcripter = TranscriptionMaker()
|
15 |
app = Flask(__name__)
|
16 |
|
17 |
# CORS設定: すべてのオリジンからのリクエストを許可
|
18 |
+
# 必要であれば、特定のオリジンやメソッド、ヘッダーをより厳密に指定できます
|
19 |
+
# 例: CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}}, supports_credentials=True)
|
20 |
+
CORS(app, origins="*", methods=["GET", "POST", "DELETE", "OPTIONS"], headers=["Content-Type", "Authorization"])
|
21 |
|
22 |
+
# GASのエンドポイントURL
|
23 |
GAS_URL = "https://script.google.com/macros/s/AKfycbwR2cnMKVU1AxoT9NDaeZaNUaTiwRX64Ul0sH0AU4ccP49Byph-TpxtM_Lwm4G9zLnuYA/exec"
|
24 |
|
25 |
+
users = [] # 選択されたユーザーのリスト
|
26 |
+
all_users = [] # 利用可能なすべてのユーザーのリスト
|
27 |
+
transcription_text = ""
|
|
|
|
|
28 |
harassment_keywords = [
|
29 |
"バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい",
|
30 |
"きもい", "キモい", "ブス", "デブ", "ハゲ",
|
31 |
"セクハラ", "パワハラ", "モラハラ"
|
32 |
]
|
33 |
+
total_audio = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
|
36 |
+
@app.route('/index', methods=['GET', 'POST'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
def index():
|
38 |
return render_template('index.html', users=users)
|
39 |
|
40 |
+
# フィードバック画面(テンプレート: feedback.html)
|
41 |
+
@app.route('/feedback', methods=['GET', 'POST'])
|
42 |
def feedback():
|
43 |
return render_template('feedback.html')
|
44 |
|
45 |
+
# 会話詳細画面(テンプレート: talkDetail.html)
|
46 |
+
@app.route('/talk_detail', methods=['GET', 'POST'])
|
47 |
def talk_detail():
|
48 |
return render_template('talkDetail.html')
|
49 |
|
50 |
+
# 音声登録画面(テンプレート: userRegister.html)
|
51 |
+
@app.route('/userregister', methods=['GET', 'POST'])
|
52 |
def userregister():
|
53 |
return render_template('userRegister.html')
|
54 |
|
55 |
+
# 人数確認
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
@app.route('/confirm', methods=['GET'])
|
57 |
def confirm():
|
|
|
58 |
global all_users
|
59 |
+
# 最新のユーザーリストを取得
|
60 |
try:
|
61 |
+
update_all_users()
|
62 |
except Exception as e:
|
63 |
+
print(f"ユーザーリストの更新エラー: {str(e)}")
|
64 |
+
return jsonify({'members': users, 'all_members': all_users}), 200
|
|
|
65 |
|
66 |
+
# リセット画面(テンプレート: reset.html)
|
67 |
+
@app.route('/reset_html', methods=['GET', 'POST'])
|
68 |
+
def reset_html():
|
69 |
+
return render_template('reset.html')
|
70 |
|
71 |
+
# メンバー削除&累積音声削除
|
72 |
+
@app.route('/reset_member', methods=['GET', 'POST'])
|
73 |
def reset_member():
|
74 |
global users
|
|
|
75 |
global total_audio
|
76 |
global transcription_text
|
77 |
+
|
78 |
+
# 一時ディレクトリのクリーンアップ
|
79 |
+
if total_audio:
|
80 |
+
process.delete_files_in_directory(total_audio)
|
81 |
+
process.delete_files_in_directory('/tmp/data/transcription_audio')
|
82 |
+
|
83 |
+
# 書き起こしテキストの削除
|
84 |
+
if os.path.exists(transcription_text):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
try:
|
86 |
os.remove(transcription_text)
|
87 |
+
print(f"{transcription_text} を削除しました。")
|
88 |
except Exception as e:
|
89 |
+
print(f"ファイル削除中にエラーが発生しました: {e}")
|
90 |
+
|
91 |
+
transcription_text = ""
|
92 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
try:
|
94 |
data = request.get_json()
|
95 |
+
if not data or "names" not in data:
|
96 |
+
return jsonify({"status": "error", "message": "Invalid request body"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
|
98 |
+
names = data.get("names", [])
|
99 |
+
|
100 |
+
# GASからファイルを削除
|
101 |
+
for name in names:
|
102 |
try:
|
103 |
+
delete_from_cloud(f"{name}.wav")
|
104 |
+
print(f"クラウドから {name}.wav を削除しました。")
|
|
|
|
|
|
|
105 |
except Exception as e:
|
106 |
+
print(f"クラウド削除中にエラーが発生しました: {e}")
|
107 |
+
return jsonify({"status": "error", "message": f"Failed to delete {name} from cloud: {e}"}), 500
|
|
|
108 |
|
109 |
+
# usersリストから削除するユーザーを除外
|
110 |
+
users = [u for u in users if u not in names]
|
111 |
+
|
112 |
+
# 全ユーザーリストの更新
|
113 |
+
update_all_users()
|
114 |
+
|
115 |
+
return jsonify({"status": "success", "message": "Members deleted successfully", "users": users}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
|
117 |
except Exception as e:
|
118 |
+
print(f"An unexpected error occurred: {e}")
|
119 |
return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500
|
120 |
|
121 |
+
# 書き起こし作成エンドポイント
|
122 |
+
@app.route('/transcription', methods=['GET', 'POST'])
|
123 |
def transcription():
|
124 |
global transcription_text
|
125 |
global total_audio
|
126 |
+
|
127 |
+
if not os.path.exists(transcription_text) or not transcription_text:
|
128 |
+
try:
|
129 |
+
if not total_audio or not os.path.exists(total_audio):
|
130 |
+
return jsonify({"error": "No audio segments provided"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
transcription_text = transcripter.create_transcription(total_audio)
|
132 |
+
print("transcription")
|
133 |
+
print(transcription_text)
|
|
|
|
|
134 |
except Exception as e:
|
135 |
+
return jsonify({"error": str(e)}), 500
|
136 |
+
|
|
|
|
|
|
|
137 |
try:
|
138 |
with open(transcription_text, 'r', encoding='utf-8') as file:
|
139 |
file_content = file.read()
|
140 |
+
print(file_content)
|
141 |
+
return jsonify({'transcription': file_content}), 200
|
142 |
except FileNotFoundError:
|
143 |
+
return jsonify({"error": "Transcription file not found"}), 404
|
|
|
144 |
except Exception as e:
|
145 |
+
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
|
|
146 |
|
147 |
+
# AI分析エンドポイント
|
148 |
+
@app.route('/analyze', methods=['GET', 'POST'])
|
149 |
def analyze():
|
150 |
global transcription_text
|
151 |
global total_audio
|
152 |
+
|
153 |
+
if not os.path.exists(transcription_text) or not transcription_text:
|
154 |
+
try:
|
155 |
+
if not total_audio:
|
156 |
+
return jsonify({"error": "No audio segments provided"}), 400
|
|
|
|
|
|
|
157 |
transcription_text = transcripter.create_transcription(total_audio)
|
|
|
|
|
|
|
158 |
except Exception as e:
|
159 |
+
return jsonify({"error": str(e)}), 500
|
160 |
+
|
161 |
+
analyzer = TextAnalyzer(transcription_text, harassment_keywords)
|
|
|
|
|
162 |
api_key = os.environ.get("DEEPSEEK")
|
163 |
+
if api_key is None:
|
164 |
+
raise ValueError("DEEPSEEK_API_KEY が設定されていません。")
|
165 |
+
|
166 |
+
results = analyzer.analyze(api_key=api_key)
|
167 |
+
|
168 |
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
169 |
+
|
170 |
+
if "deepseek_analysis" in results and results["deepseek_analysis"]:
|
171 |
+
deepseek_data = results["deepseek_analysis"]
|
172 |
+
conversation_level = deepseek_data.get("conversationLevel")
|
173 |
+
harassment_present = deepseek_data.get("harassmentPresent")
|
174 |
+
harassment_type = deepseek_data.get("harassmentType")
|
175 |
+
repetition = deepseek_data.get("repetition")
|
176 |
+
pleasantConversation = deepseek_data.get("pleasantConversation")
|
177 |
+
blameOrHarassment = deepseek_data.get("blameOrHarassment")
|
178 |
+
|
179 |
+
print("\n--- DeepSeek 分析結果 ---")
|
180 |
+
print(f"会話レベル: {conversation_level}")
|
181 |
+
print(f"ハラスメントの有無: {harassment_present}")
|
182 |
+
print(f"ハラスメントの種類: {harassment_type}")
|
183 |
+
print(f"繰り返しの程度: {repetition}")
|
184 |
+
print(f"会話の心地よさ: {pleasantConversation}")
|
185 |
+
print(f"非難またはハラスメントの程度: {blameOrHarassment}")
|
186 |
+
|
187 |
+
return jsonify({"results": results}), 200
|
188 |
+
|
189 |
+
|
190 |
+
# クラウドから音声を取得してローカルに保存する関数
|
191 |
+
def download_from_cloud(filename, local_path):
|
192 |
try:
|
193 |
+
payload = {
|
194 |
+
"action": "download",
|
195 |
+
"fileName": filename
|
196 |
+
}
|
197 |
+
|
198 |
+
print(f"クラウドから {filename} をダウンロード中...")
|
199 |
+
response = requests.post(GAS_URL, json=payload)
|
200 |
+
if response.status_code != 200:
|
201 |
+
print(f"ダウンロードエラー: ステータスコード {response.status_code}")
|
202 |
+
print(f"レスポンス: {response.text}")
|
203 |
+
raise Exception(f"クラウドからのダウンロードに失敗しました: {response.text}")
|
204 |
+
|
205 |
+
try:
|
206 |
+
res_json = response.json()
|
207 |
+
except:
|
208 |
+
print("JSONデコードエラー、レスポンス内容:")
|
209 |
+
print(response.text[:500]) # 最初の500文字だけ表示
|
210 |
+
raise Exception("サーバーからの応答をJSONとして解析できませんでした")
|
211 |
+
|
212 |
+
if res_json.get("status") != "success":
|
213 |
+
print(f"ダウンロードステータスエラー: {res_json.get('message')}")
|
214 |
+
raise Exception(f"クラウドからのダウンロードに失敗しました: {res_json.get('message')}")
|
215 |
+
|
216 |
+
# Base64文字列をデコード
|
217 |
+
base64_data = res_json.get("base64Data")
|
218 |
+
if not base64_data:
|
219 |
+
print("Base64データが存在しません")
|
220 |
+
raise Exception("応答にBase64データが含まれていません")
|
221 |
+
|
222 |
+
try:
|
223 |
+
audio_binary = base64.b64decode(base64_data)
|
224 |
+
except Exception as e:
|
225 |
+
print(f"Base64デコードエラー: {str(e)}")
|
226 |
+
raise Exception(f"音声データのデコードに失敗しました: {str(e)}")
|
227 |
+
|
228 |
+
# 指定パスに保存
|
229 |
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
230 |
+
with open(local_path, 'wb') as f:
|
231 |
+
f.write(audio_binary)
|
232 |
+
|
233 |
+
print(f"{filename} をローカルに保存しました: {local_path}")
|
234 |
+
|
235 |
+
# データの整合性チェック(ファイルサイズが0より大きいかなど)
|
236 |
+
if os.path.getsize(local_path) <= 0:
|
237 |
+
raise Exception(f"保存されたファイル {local_path} のサイズが0バイトです")
|
238 |
+
|
239 |
+
return local_path
|
240 |
except Exception as e:
|
241 |
+
print(f"ダウンロード中にエラーが発生しました: {str(e)}")
|
242 |
+
# エラーを上位に伝播させる
|
243 |
+
raise
|
244 |
|
245 |
+
# クラウドからファイルを削除する関数
|
246 |
+
def delete_from_cloud(filename):
|
247 |
+
payload = {
|
248 |
+
"action": "delete",
|
249 |
+
"fileName": filename
|
250 |
+
}
|
251 |
+
response = requests.post(GAS_URL, json=payload)
|
252 |
+
if response.status_code != 200:
|
253 |
+
raise Exception(f"クラウドからの削除に失敗しました: {response.text}")
|
254 |
+
|
255 |
+
res_json = response.json()
|
256 |
+
if res_json.get("status") != "success":
|
257 |
+
raise Exception(f"クラウドからの削除に失敗しました: {res_json.get('message')}")
|
258 |
+
|
259 |
+
return True
|
260 |
+
# すべてのベース音声ユーザーリストを更新する関数
|
261 |
+
def update_all_users():
|
262 |
+
global all_users
|
263 |
+
|
264 |
+
payload = {"action": "list"}
|
265 |
+
response = requests.post(GAS_URL, json=payload)
|
266 |
+
if response.status_code != 200:
|
267 |
+
raise Exception(f"GAS一覧取得エラー: {response.text}")
|
268 |
+
|
269 |
+
res_json = response.json()
|
270 |
+
if res_json.get("status") != "success":
|
271 |
+
raise Exception(f"GAS一覧取得失敗: {res_json.get('message')}")
|
272 |
+
|
273 |
+
# ファイル名から拡張子を除去してユーザーリストを作成
|
274 |
+
all_users = [os.path.splitext(filename)[0] for filename in res_json.get("fileNames", [])]
|
275 |
+
return all_users
|
276 |
+
|
277 |
+
# 音声アップロード&解析エンドポイント
|
278 |
@app.route('/upload_audio', methods=['POST'])
|
279 |
def upload_audio():
|
280 |
global total_audio
|
281 |
global users
|
282 |
+
|
|
|
|
|
|
|
283 |
try:
|
284 |
data = request.get_json()
|
285 |
if not data or 'audio_data' not in data:
|
286 |
+
return jsonify({"error": "音声データがありません"}), 400
|
287 |
+
|
288 |
+
# リクエストからユーザーリストを取得(指定がなければ現在のusersを使用)
|
289 |
+
if 'selected_users' in data and data['selected_users']:
|
290 |
+
users = data['selected_users']
|
291 |
+
print(f"選択されたユーザー: {users}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
292 |
|
293 |
+
if not users:
|
294 |
+
return jsonify({"error": "選択されたユーザーがいません"}), 400
|
295 |
|
296 |
+
# Base64デコードして音声バイナリを取得
|
297 |
audio_binary = base64.b64decode(data['audio_data'])
|
298 |
+
|
299 |
+
upload_name = 'tmp'
|
300 |
+
audio_dir = "/tmp/data"
|
|
|
|
|
301 |
os.makedirs(audio_dir, exist_ok=True)
|
302 |
+
audio_path = os.path.join(audio_dir, f"{upload_name}.wav")
|
303 |
+
with open(audio_path, 'wb') as f:
|
|
|
|
|
304 |
f.write(audio_binary)
|
305 |
+
|
306 |
+
print(f"処理を行うユーザー: {users}")
|
307 |
+
|
308 |
+
# ベース音声を一時ディレクトリにダウンロード
|
309 |
+
temp_dir = "/tmp/data/base_audio"
|
310 |
+
os.makedirs(temp_dir, exist_ok=True)
|
311 |
+
|
312 |
+
# 各ユーザーの参照音声ファイルのパスをリストに格納
|
313 |
reference_paths = []
|
314 |
+
for user in users:
|
|
|
|
|
315 |
try:
|
316 |
+
ref_path = os.path.join(temp_dir, f"{user}.wav")
|
317 |
+
if not os.path.exists(ref_path):
|
318 |
+
# クラウドから取得
|
319 |
+
download_from_cloud(f"{user}.wav", ref_path)
|
320 |
+
print(f"クラウドから {user}.wav をダウンロードしました")
|
321 |
+
|
322 |
+
if not os.path.exists(ref_path):
|
323 |
+
return jsonify({"error": "参照音声ファイルが見つかりません", "details": ref_path}), 500
|
324 |
+
|
325 |
+
reference_paths.append(ref_path)
|
|
|
|
|
|
|
326 |
except Exception as e:
|
327 |
+
return jsonify({"error": f"ユーザー {user} の音声取得に失敗しました", "details": str(e)}), 500
|
328 |
+
|
329 |
+
# 複数人の場合は参照パスのリストを、1人の場合は単一のパスを渡す
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
330 |
if len(users) > 1:
|
331 |
+
print("複数人の場合の処理")
|
332 |
+
matched_times, merged_segments = process.process_multi_audio(reference_paths, audio_path, users, threshold=0.05)
|
333 |
+
total_audio = transcripter.save_marged_segments(merged_segments)
|
334 |
+
# 各メンバーのrateを計算
|
335 |
+
total_time = sum(matched_times)
|
336 |
rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times]
|
337 |
+
|
338 |
+
# ユーザー名と話した割合をマッピング
|
339 |
user_rates = {users[i]: rates[i] for i in range(len(users))}
|
340 |
+
return jsonify({"rates": rates, "user_rates": user_rates}), 200
|
|
|
341 |
else:
|
342 |
+
matched_time, unmatched_time, merged_segments = process.process_audio(reference_paths[0], audio_path, users[0], threshold=0.05)
|
343 |
+
total_audio = transcripter.save_marged_segments(merged_segments)
|
344 |
+
print("単一ユーザーの処理")
|
|
|
345 |
total_time = matched_time + unmatched_time
|
346 |
rate = (matched_time / total_time) * 100 if total_time > 0 else 0
|
347 |
+
return jsonify({"rate": rate, "user": users[0]}), 200
|
348 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
349 |
except Exception as e:
|
350 |
+
print("Error in /upload_audio:", str(e))
|
351 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
352 |
|
353 |
+
# ユーザー選択画面(テンプレート: userSelect.html)
|
354 |
+
@app.route('/')
|
355 |
+
@app.route('/userselect', methods=['GET'])
|
356 |
+
def userselect():
|
357 |
+
return render_template('userSelect.html')
|
358 |
|
359 |
# 選択したユーザーを設定するエンドポイント
|
360 |
@app.route('/select_users', methods=['POST'])
|
361 |
def select_users():
|
362 |
global users
|
363 |
+
|
|
|
364 |
try:
|
365 |
data = request.get_json()
|
366 |
+
if not data or 'users' not in data:
|
367 |
+
return jsonify({"error": "ユーザーリストがありません"}), 400
|
368 |
+
|
369 |
+
users = data['users']
|
370 |
+
print(f"選択されたユーザー: {users}")
|
371 |
+
|
372 |
+
return jsonify({"status": "success", "selected_users": users}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
373 |
except Exception as e:
|
374 |
+
print("Error in /select_users:", str(e))
|
375 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
376 |
|
377 |
+
@app.route('/reset', methods=['GET'])
|
|
|
378 |
def reset():
|
379 |
global users
|
380 |
+
users = []
|
381 |
global total_audio
|
382 |
global transcription_text
|
383 |
+
|
384 |
+
# 一時ディレクトリのクリーンアップ
|
385 |
+
if total_audio:
|
386 |
+
process.delete_files_in_directory(total_audio)
|
387 |
+
process.delete_files_in_directory('/tmp/data/transcription_audio')
|
388 |
+
|
389 |
+
# 書き起こしテキストの削除
|
390 |
+
if os.path.exists(transcription_text):
|
391 |
try:
|
392 |
+
os.remove(transcription_text)
|
393 |
+
print(f"{transcription_text} を削除しました。")
|
394 |
+
except Exception as e:
|
395 |
+
print(f"ファイル削除中にエラーが発生しました: {e}")
|
396 |
+
|
|
|
|
|
|
|
397 |
transcription_text = ""
|
398 |
|
399 |
+
return jsonify({"status": "success", "message": "Users reset"}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
400 |
|
|
|
|
|
401 |
@app.route('/copy_selected_files', methods=['POST'])
|
402 |
def copy_selected_files():
|
|
|
|
|
|
|
403 |
try:
|
404 |
data = request.get_json()
|
405 |
+
if not data or "names" not in data:
|
406 |
+
return jsonify({"error": "namesパラメータが存在しません"}), 400
|
|
|
|
|
|
|
|
|
407 |
|
408 |
+
names = data["names"]
|
409 |
+
dest_dir = "/tmp/data/selected_audio" # コピー先のフォルダ
|
410 |
os.makedirs(dest_dir, exist_ok=True)
|
|
|
411 |
|
412 |
copied_files = []
|
413 |
+
for name in names:
|
|
|
|
|
414 |
dest_path = os.path.join(dest_dir, f"{name}.wav")
|
415 |
try:
|
416 |
+
# クラウドから直接ダウンロード
|
417 |
download_from_cloud(f"{name}.wav", dest_path)
|
418 |
copied_files.append(name)
|
419 |
+
print(f"{name}.wav を {dest_path} にダウンロードしました。")
|
420 |
except Exception as e:
|
421 |
+
print(f"ダウンロード中にエラーが発生しました: {e}")
|
422 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
423 |
|
424 |
+
return jsonify({"status": "success", "copied": copied_files}), 200
|
|
|
|
|
425 |
|
426 |
+
except Exception as e:
|
427 |
+
print("Error in /copy_selected_files:", str(e))
|
428 |
+
return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500
|
429 |
|
430 |
+
@app.route('/clear_tmp', methods=['GET'])
|
|
|
431 |
def clear_tmp():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
432 |
try:
|
433 |
+
tmp_dir = "/tmp/data" # アプリケーションが使用しているtmpフォルダ
|
434 |
+
# ファイルのみの削除
|
435 |
+
process.delete_files_in_directory(tmp_dir)
|
436 |
+
# フォルダがあれば再帰的に削除
|
437 |
+
for item in os.listdir(tmp_dir):
|
438 |
+
item_path = os.path.join(tmp_dir, item)
|
439 |
+
if os.path.isdir(item_path):
|
440 |
+
shutil.rmtree(item_path)
|
441 |
+
print(f"ディレクトリを削除しました: {item_path}")
|
442 |
+
|
443 |
+
return jsonify({"status": "success", "message": "tmp配下がすべて削除されました"}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
444 |
|
445 |
except Exception as e:
|
446 |
+
print("Error in /clear_tmp:", str(e))
|
447 |
+
return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500
|
|
|
448 |
|
|
|
449 |
@app.route('/upload_base_audio', methods=['POST'])
|
450 |
def upload_base_audio():
|
451 |
+
global all_users
|
452 |
+
|
453 |
try:
|
454 |
data = request.get_json()
|
455 |
if not data or 'audio_data' not in data or 'name' not in data:
|
456 |
+
return jsonify({"error": "音声データまたは名前がありません"}), 400
|
457 |
+
name = data['name']
|
458 |
+
print(f"登録名: {name}")
|
459 |
+
|
460 |
+
# GASのアップロードエンドポイントにリクエスト
|
|
|
|
|
|
|
|
|
|
|
|
|
461 |
payload = {
|
462 |
"action": "upload",
|
463 |
+
"fileName": f"{name}.wav",
|
464 |
"base64Data": data['audio_data']
|
465 |
}
|
466 |
+
|
467 |
+
response = requests.post(GAS_URL, json=payload)
|
468 |
+
if response.status_code != 200:
|
469 |
+
return jsonify({"error": "GASアップロードエラー", "details": response.text}), 500
|
470 |
+
|
471 |
+
res_json = response.json()
|
472 |
+
if res_json.get("status") != "success":
|
473 |
+
return jsonify({"error": "GASアップロード失敗", "details": res_json.get("message")}), 500
|
474 |
+
|
475 |
+
# 全ユーザーリストを更新
|
476 |
+
update_all_users()
|
477 |
+
|
478 |
+
return jsonify({"state": "Registration Success!", "driveFileId": res_json.get("fileId")}), 200
|
479 |
+
except Exception as e:
|
480 |
+
print("Error in /upload_base_audio:", str(e))
|
481 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
482 |
|
483 |
+
@app.route('/list_base_audio', methods=['GET'])
|
484 |
+
def list_base_audio():
|
485 |
+
try:
|
486 |
+
global all_users
|
487 |
+
all_users = update_all_users()
|
488 |
+
return jsonify({"status": "success", "fileNames": all_users}), 200
|
489 |
+
except Exception as e:
|
490 |
+
print("Error in /list_base_audio:", str(e))
|
491 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
492 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
493 |
if __name__ == '__main__':
|
494 |
port = int(os.environ.get("PORT", 7860))
|
|
|
|
|
|
|
495 |
app.run(debug=True, host="0.0.0.0", port=port)
|