Spaces:
Running
Running
jinjaくんを使おう
#1
by
A-yum1
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .gitattributes +35 -37
- .gitignore +0 -0
- Dockerfile +17 -23
- README.md +10 -10
- __pycache__/analyze.cpython-310.pyc +0 -0
- __pycache__/app.cpython-311.pyc +0 -0
- __pycache__/database.cpython-310.pyc +0 -0
- __pycache__/database.cpython-311.pyc +0 -0
- __pycache__/login.cpython-310.pyc +0 -0
- __pycache__/models.cpython-310.pyc +0 -0
- __pycache__/new_record.cpython-310.pyc +0 -0
- __pycache__/process.cpython-310.pyc +0 -0
- __pycache__/transcription.cpython-310.pyc +0 -0
- __pycache__/users.cpython-310.pyc +0 -0
- __pycache__/users.cpython-311.pyc +0 -0
- analyze.py +0 -224
- app.py +23 -472
- feedback.html +136 -0
- index.html +171 -0
- init/createdatabase.sql +0 -14
- instance/site.db +0 -0
- process.py +0 -539
- record/save.py +31 -0
- record/templates/record.html +76 -0
- requirements.txt +1 -19
- room.js +0 -0
- sample.wav +0 -3
- static/feedback.js +0 -66
- static/loading.css +0 -56
- static/loading.js +0 -0
- static/main.css +0 -40
- static/menu.css +0 -56
- static/menu.js +0 -53
- static/process.js +0 -222
- static/process1.js +0 -294
- static/register_record.js +0 -150
- static/reset.js +0 -97
- static/style.css +0 -128
- static/talk_detail.js +0 -35
- tailwind.config.js +0 -27
- talkDetail.html +97 -0
- templates/feedback.html +0 -81
- templates/history.html +0 -125
- templates/index.html +0 -219
- templates/reset.html +0 -85
- templates/talkDetail.html +0 -83
- templates/test +0 -1
- templates/userRegister.html +0 -426
- templates/userSelect.html +0 -354
- transcription.py +0 -147
.gitattributes
CHANGED
@@ -1,37 +1,35 @@
|
|
1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
-
segment_0[[:space:]](2).wav filter=lfs diff=lfs merge=lfs -text
|
37 |
-
sample.wav filter=lfs diff=lfs merge=lfs -text
|
|
|
1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
.gitignore
DELETED
Binary file (73 Bytes)
|
|
Dockerfile
CHANGED
@@ -1,23 +1,17 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
#
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
RUN
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
RUN python3 -m pip install --no-cache-dir -r requirements.txt
|
20 |
-
|
21 |
-
COPY . .
|
22 |
-
|
23 |
-
CMD ["python3", "app.py"]
|
|
|
1 |
+
# Dockerfile
|
2 |
+
FROM python:3.9-slim
|
3 |
+
|
4 |
+
WORKDIR /app
|
5 |
+
|
6 |
+
# 依存関係のインストール
|
7 |
+
COPY requirements.txt .
|
8 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
9 |
+
|
10 |
+
# アプリの全ファイルをコピー
|
11 |
+
COPY . .
|
12 |
+
RUN touch weak_phrases.json && chmod 666 weak_phrases.json
|
13 |
+
|
14 |
+
# Hugging Face Spaces ではポート 7860 を使用する
|
15 |
+
EXPOSE 7860
|
16 |
+
|
17 |
+
CMD ["python", "app.py"]
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1 |
-
---
|
2 |
-
title: JusTalk
|
3 |
-
emoji: ⚡
|
4 |
-
colorFrom: gray
|
5 |
-
colorTo: blue
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
---
|
9 |
-
|
10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
+
---
|
2 |
+
title: JusTalk
|
3 |
+
emoji: ⚡
|
4 |
+
colorFrom: gray
|
5 |
+
colorTo: blue
|
6 |
+
sdk: docker
|
7 |
+
pinned: false
|
8 |
+
---
|
9 |
+
|
10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
__pycache__/analyze.cpython-310.pyc
DELETED
Binary file (6.45 kB)
|
|
__pycache__/app.cpython-311.pyc
DELETED
Binary file (13.8 kB)
|
|
__pycache__/database.cpython-310.pyc
DELETED
Binary file (172 Bytes)
|
|
__pycache__/database.cpython-311.pyc
DELETED
Binary file (275 Bytes)
|
|
__pycache__/login.cpython-310.pyc
DELETED
Binary file (2.4 kB)
|
|
__pycache__/models.cpython-310.pyc
DELETED
Binary file (943 Bytes)
|
|
__pycache__/new_record.cpython-310.pyc
DELETED
Binary file (1.4 kB)
|
|
__pycache__/process.cpython-310.pyc
DELETED
Binary file (14.9 kB)
|
|
__pycache__/transcription.cpython-310.pyc
DELETED
Binary file (5.4 kB)
|
|
__pycache__/users.cpython-310.pyc
DELETED
Binary file (727 Bytes)
|
|
__pycache__/users.cpython-311.pyc
DELETED
Binary file (1.17 kB)
|
|
analyze.py
DELETED
@@ -1,224 +0,0 @@
|
|
1 |
-
import json
|
2 |
-
import requests
|
3 |
-
import os
|
4 |
-
|
5 |
-
class TextAnalyzer:
|
6 |
-
"""
|
7 |
-
テキストのハラスメント検出と会話評価を行うクラス。
|
8 |
-
"""
|
9 |
-
def __init__(self, file_path, keywords):
|
10 |
-
"""
|
11 |
-
TextAnalyzer クラスのコンストラクタ。
|
12 |
-
|
13 |
-
Args:
|
14 |
-
file_path (str): 分析するテキストファイルのパス。
|
15 |
-
keywords (list): ハラスメント検出に使用するキーワードのリスト。
|
16 |
-
"""
|
17 |
-
self.file_path = file_path
|
18 |
-
self.keywords = keywords
|
19 |
-
self.text_content = None # テキストファイルの内容を格納
|
20 |
-
self.harassment_detected = False # ハラスメントが検出されたかどうか
|
21 |
-
self.harassment_keywords = [] # 検出されたハラスメントキーワードのリスト
|
22 |
-
self.deepseek_analysis = {} # DeepSeek API による分析結果を格納する辞書
|
23 |
-
self.api_key = None
|
24 |
-
|
25 |
-
def load_text(self):
|
26 |
-
"""
|
27 |
-
テキストファイルを読み込み、その内容を self.text_content に格納する。
|
28 |
-
|
29 |
-
Returns:
|
30 |
-
bool: ファイルの読み込みに成功した場合は True、失敗した場合は False。
|
31 |
-
"""
|
32 |
-
try:
|
33 |
-
with open(self.file_path, 'r', encoding='utf-8') as file:
|
34 |
-
self.text_content = file.read()
|
35 |
-
return True
|
36 |
-
except Exception as e:
|
37 |
-
print(f"ファイル読み込みエラー: {e}")
|
38 |
-
return False
|
39 |
-
|
40 |
-
def detect_harassment(self):
|
41 |
-
"""
|
42 |
-
テキスト内容からハラスメントを検出する。
|
43 |
-
|
44 |
-
Returns:
|
45 |
-
bool: ハラスメントが検出された場合は True、それ以外は False。
|
46 |
-
"""
|
47 |
-
if not self.text_content:
|
48 |
-
return False
|
49 |
-
|
50 |
-
self.harassment_keywords = []
|
51 |
-
for keyword in self.keywords:
|
52 |
-
if keyword in self.text_content:
|
53 |
-
self.harassment_detected = True
|
54 |
-
self.harassment_keywords.append(keyword)
|
55 |
-
|
56 |
-
return self.harassment_detected
|
57 |
-
|
58 |
-
def analyze_with_deepseek(self, api_key=None, api_url="https://api.deepseek.com/v1/chat/completions"):
|
59 |
-
"""
|
60 |
-
DeepSeek API を使用して会話を分析する。会話レベルやハラスメントの詳細な検出を行う。
|
61 |
-
|
62 |
-
Args:
|
63 |
-
api_key (str, optional): DeepSeek API キー。指定されない場合は環境変数から取得。
|
64 |
-
api_url (str, optional): DeepSeek API の URL。デフォルトは標準のチャット補完エンドポイント。
|
65 |
-
|
66 |
-
Returns:
|
67 |
-
bool: 分析に成功した場合は True、失敗した場合は False。
|
68 |
-
"""
|
69 |
-
if not self.text_content:
|
70 |
-
return False
|
71 |
-
|
72 |
-
# 提供された API キーを使用するか、環境変数から取得する
|
73 |
-
if api_key:
|
74 |
-
self.api_key = api_key
|
75 |
-
else:
|
76 |
-
self.api_key = os.environ.get("DEEPSEEK_API_KEY")
|
77 |
-
if not self.api_key:
|
78 |
-
print("DeepSeek API キーが提供されておらず、環境変数にも見つかりませんでした。")
|
79 |
-
return False
|
80 |
-
|
81 |
-
headers = {
|
82 |
-
"Content-Type": "application/json",
|
83 |
-
"Authorization": f"Bearer {self.api_key}"
|
84 |
-
}
|
85 |
-
|
86 |
-
prompt = f"""
|
87 |
-
以下の会話を分析し、結果を JSON 形式で返してください。1 から 10 のスケールで評価し、10 が最高です。
|
88 |
-
厳密に評価してください。ハラスメントが存在する場合は、その種類を具体的に記述してください。
|
89 |
-
評価基準:
|
90 |
-
1. conversationLevel: 会話のレベル (初心者、中級者、上級者)。
|
91 |
-
2. harassmentPresent: 会話にハラスメント表現が含まれているかどうか (true/false)。
|
92 |
-
3. harassmentType: ハラスメントが存在する場合、その種類を具体的に記述。
|
93 |
-
4. topicAppropriateness: 会話のトピックが適切かどうか。
|
94 |
-
5. improvementSuggestions: 会話を改善するための具体的な提案。
|
95 |
-
6. repetition: 同じことがどの程度繰り返されているか。(1-10)
|
96 |
-
7. pleasantConversation: 会話がどの程度心地よいか。(1-10)
|
97 |
-
8. blameOrHarassment: 会話がどの程度相手を責めたり、ハラスメントをしているか。(1-10)
|
98 |
-
|
99 |
-
会話内容:
|
100 |
-
{self.text_content}
|
101 |
-
|
102 |
-
JSON 形式のみを返してください。
|
103 |
-
"""
|
104 |
-
|
105 |
-
data = {
|
106 |
-
"model": "deepseek-chat",
|
107 |
-
"messages": [{"role": "user", "content": prompt}],
|
108 |
-
"response_format": {"type": "json_object"}
|
109 |
-
}
|
110 |
-
|
111 |
-
try:
|
112 |
-
response = requests.post(api_url, headers=headers, json=data)
|
113 |
-
response.raise_for_status()
|
114 |
-
|
115 |
-
result = response.json()
|
116 |
-
deepseek_response = json.loads(result["choices"][0]["message"]["content"])
|
117 |
-
|
118 |
-
# 指定されたキーを使用して、インスタンス変数に値を割り当てる
|
119 |
-
self.deepseek_analysis = {
|
120 |
-
"conversationLevel": deepseek_response.get("conversationLevel"),
|
121 |
-
"harassmentPresent": deepseek_response.get("harassmentPresent"),
|
122 |
-
"harassmentType": deepseek_response.get("harassmentType"),
|
123 |
-
"topicAppropriateness": deepseek_response.get("topicAppropriateness"),
|
124 |
-
"improvementSuggestions": deepseek_response.get("improvementSuggestions"),
|
125 |
-
"repetition": deepseek_response.get("repetition"),
|
126 |
-
"pleasantConversation": deepseek_response.get("pleasantConversation"),
|
127 |
-
"blameOrHarassment": deepseek_response.get("blameOrHarassment"),
|
128 |
-
}
|
129 |
-
|
130 |
-
return True
|
131 |
-
except requests.exceptions.RequestException as e:
|
132 |
-
print(f"DeepSeek API リクエストエラー: {e}")
|
133 |
-
return False
|
134 |
-
except json.JSONDecodeError as e:
|
135 |
-
print(f"DeepSeek API レスポンスの JSON デコードエラー: {e}")
|
136 |
-
print(f"レスポンス内容: {response.text}")
|
137 |
-
return False
|
138 |
-
except KeyError as e:
|
139 |
-
print(f"DeepSeek API レスポンスのキーエラー: {e}")
|
140 |
-
print(f"レスポンス内容: {response.text}")
|
141 |
-
return False
|
142 |
-
except Exception as e:
|
143 |
-
print(f"DeepSeek API エラー: {e}")
|
144 |
-
return False
|
145 |
-
|
146 |
-
def get_analysis_results(self):
|
147 |
-
"""
|
148 |
-
分析結果を返す。
|
149 |
-
|
150 |
-
Returns:
|
151 |
-
dict: 分析結果を含む辞書。
|
152 |
-
"""
|
153 |
-
results = {
|
154 |
-
"text_content": self.text_content,
|
155 |
-
"basic_harassment_detection": {
|
156 |
-
"detected": self.harassment_detected,
|
157 |
-
"matching_keywords": self.harassment_keywords
|
158 |
-
},
|
159 |
-
"deepseek_analysis": self.deepseek_analysis
|
160 |
-
}
|
161 |
-
|
162 |
-
return results
|
163 |
-
|
164 |
-
def analyze(self, api_key=None):
|
165 |
-
"""
|
166 |
-
すべての分析を実行し、結果を返す。
|
167 |
-
|
168 |
-
Args:
|
169 |
-
api_key (str, optional): DeepSeek API キー。
|
170 |
-
|
171 |
-
Returns:
|
172 |
-
dict: 分析結果またはエラーメッセージを含む辞書。
|
173 |
-
"""
|
174 |
-
if not self.load_text():
|
175 |
-
return {"error": "テキストファイルの読み込みに失敗しました。"}
|
176 |
-
|
177 |
-
self.detect_harassment()
|
178 |
-
|
179 |
-
if not self.analyze_with_deepseek(api_key):
|
180 |
-
return {"error": "DeepSeek API 分析に失敗しました。"}
|
181 |
-
|
182 |
-
return self.get_analysis_results()
|
183 |
-
|
184 |
-
'''
|
185 |
-
# 使用例
|
186 |
-
if __name__ == "__main__":
|
187 |
-
# ハラスメント検出用のキーワード例
|
188 |
-
harassment_keywords = [
|
189 |
-
"バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい",
|
190 |
-
"きもい", "キモい", "ブス", "デブ", "ハゲ",
|
191 |
-
"セクハラ", "パワハラ", "モラハラ"
|
192 |
-
]
|
193 |
-
|
194 |
-
# 分析インスタンスの作成
|
195 |
-
analyzer = TextAnalyzer("./2.txt", harassment_keywords)
|
196 |
-
|
197 |
-
# DeepSeek API キー (環境変数から取得するか、直接渡す)
|
198 |
-
# api_key = os.environ.get("DEEPSEEK_API_KEY")
|
199 |
-
|
200 |
-
|
201 |
-
# 分析の実行
|
202 |
-
results = analyzer.analyze(api_key=api_key)
|
203 |
-
|
204 |
-
# 結果の出力
|
205 |
-
print(json.dumps(results, ensure_ascii=False, indent=2))
|
206 |
-
|
207 |
-
# 特定の値へのアクセス例
|
208 |
-
if "deepseek_analysis" in results and results["deepseek_analysis"]:
|
209 |
-
deepseek_data = results["deepseek_analysis"]
|
210 |
-
conversation_level = deepseek_data.get("conversationLevel")
|
211 |
-
harassment_present = deepseek_data.get("harassmentPresent")
|
212 |
-
harassment_type = deepseek_data.get("harassmentType")
|
213 |
-
repetition = deepseek_data.get("repetition")
|
214 |
-
pleasantConversation = deepseek_data.get("pleasantConversation")
|
215 |
-
blameOrHarassment = deepseek_data.get("blameOrHarassment")
|
216 |
-
|
217 |
-
print("\n--- DeepSeek 分析結果 ---")
|
218 |
-
print(f"会話レベル: {conversation_level}")
|
219 |
-
print(f"ハラスメントの有無: {harassment_present}")
|
220 |
-
print(f"ハラスメントの種類: {harassment_type}")
|
221 |
-
print(f"繰り返しの程度: {repetition}")
|
222 |
-
print(f"会話の心地よさ: {pleasantConversation}")
|
223 |
-
print(f"非難またはハラスメントの程度: {blameOrHarassment}")
|
224 |
-
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
@@ -1,497 +1,48 @@
|
|
1 |
-
from flask import Flask, request, jsonify,
|
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('/
|
37 |
def index():
|
38 |
-
return
|
39 |
|
40 |
-
|
41 |
-
@app.route('/feedback', methods=['GET', 'POST'])
|
42 |
def feedback():
|
43 |
-
return
|
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
|
286 |
-
return jsonify({"error": "
|
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 |
-
|
|
|
|
|
354 |
|
355 |
-
#
|
356 |
-
@app.route('/')
|
357 |
-
@app.route('/userselect', methods=['GET'])
|
358 |
-
def userselect():
|
359 |
-
return render_template('userSelect.html')
|
360 |
-
|
361 |
-
# 選択したユーザーを設定するエンドポイント
|
362 |
-
@app.route('/select_users', methods=['POST'])
|
363 |
-
def select_users():
|
364 |
-
global users
|
365 |
-
|
366 |
-
try:
|
367 |
-
data = request.get_json()
|
368 |
-
if not data or 'users' not in data:
|
369 |
-
return jsonify({"error": "ユーザーリストがありません"}), 400
|
370 |
-
|
371 |
-
users = data['users']
|
372 |
-
print(f"選択されたユーザー: {users}")
|
373 |
-
|
374 |
-
return jsonify({"status": "success", "selected_users": users}), 200
|
375 |
-
except Exception as e:
|
376 |
-
print("Error in /select_users:", str(e))
|
377 |
-
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
378 |
-
|
379 |
-
@app.route('/reset', methods=['GET'])
|
380 |
-
def reset():
|
381 |
-
global users
|
382 |
-
users = []
|
383 |
-
global total_audio
|
384 |
-
global transcription_text
|
385 |
-
|
386 |
-
# 一時ディレクトリのクリーンアップ
|
387 |
-
if total_audio:
|
388 |
-
process.delete_files_in_directory(total_audio)
|
389 |
-
process.delete_files_in_directory('/tmp/data/transcription_audio')
|
390 |
-
|
391 |
-
# 書き起こしテキストの削除
|
392 |
-
if os.path.exists(transcription_text):
|
393 |
try:
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
print(f"ファイル削除中にエラーが発生しました: {e}")
|
398 |
-
|
399 |
-
transcription_text = ""
|
400 |
-
|
401 |
-
return jsonify({"status": "success", "message": "Users reset"}), 200
|
402 |
-
|
403 |
-
@app.route('/copy_selected_files', methods=['POST'])
|
404 |
-
def copy_selected_files():
|
405 |
-
try:
|
406 |
-
data = request.get_json()
|
407 |
-
if not data or "names" not in data:
|
408 |
-
return jsonify({"error": "namesパラメータが存在しません"}), 400
|
409 |
-
|
410 |
-
names = data["names"]
|
411 |
-
dest_dir = "/tmp/data/selected_audio" # コピー先のフォルダ
|
412 |
-
os.makedirs(dest_dir, exist_ok=True)
|
413 |
-
|
414 |
-
copied_files = []
|
415 |
-
for name in names:
|
416 |
-
dest_path = os.path.join(dest_dir, f"{name}.wav")
|
417 |
-
try:
|
418 |
-
# クラウドから直接ダウンロード
|
419 |
-
download_from_cloud(f"{name}.wav", dest_path)
|
420 |
-
copied_files.append(name)
|
421 |
-
print(f"{name}.wav を {dest_path} にダウンロードしました。")
|
422 |
-
except Exception as e:
|
423 |
-
print(f"ダウンロード中にエラーが発生しました: {e}")
|
424 |
-
continue
|
425 |
|
426 |
-
|
|
|
|
|
427 |
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
@app.route('/clear_tmp', methods=['GET'])
|
433 |
-
def clear_tmp():
|
434 |
-
try:
|
435 |
-
tmp_dir = "/tmp/data" # アプリケーションが使用しているtmpフォルダ
|
436 |
-
# ファイルのみの削除
|
437 |
-
process.delete_files_in_directory(tmp_dir)
|
438 |
-
# フォルダがあれば再帰的に削除
|
439 |
-
for item in os.listdir(tmp_dir):
|
440 |
-
item_path = os.path.join(tmp_dir, item)
|
441 |
-
if os.path.isdir(item_path):
|
442 |
-
shutil.rmtree(item_path)
|
443 |
-
print(f"ディレクトリを削除しました: {item_path}")
|
444 |
|
445 |
-
return jsonify({"
|
446 |
|
447 |
except Exception as e:
|
448 |
-
|
449 |
return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500
|
450 |
|
451 |
-
@app.route('/upload_base_audio', methods=['POST'])
|
452 |
-
def upload_base_audio():
|
453 |
-
global all_users
|
454 |
-
|
455 |
-
try:
|
456 |
-
data = request.get_json()
|
457 |
-
if not data or 'audio_data' not in data or 'name' not in data:
|
458 |
-
return jsonify({"error": "音声データまたは名前がありません"}), 400
|
459 |
-
name = data['name']
|
460 |
-
print(f"登録名: {name}")
|
461 |
-
|
462 |
-
# GASのアップロードエンドポイントにリクエスト
|
463 |
-
payload = {
|
464 |
-
"action": "upload",
|
465 |
-
"fileName": f"{name}.wav",
|
466 |
-
"base64Data": data['audio_data']
|
467 |
-
}
|
468 |
-
|
469 |
-
response = requests.post(GAS_URL, json=payload)
|
470 |
-
if response.status_code != 200:
|
471 |
-
return jsonify({"error": "GASアップロードエラー", "details": response.text}), 500
|
472 |
-
|
473 |
-
res_json = response.json()
|
474 |
-
if res_json.get("status") != "success":
|
475 |
-
return jsonify({"error": "GASアップロード失敗", "details": res_json.get("message")}), 500
|
476 |
-
|
477 |
-
# 全ユーザーリストを更新
|
478 |
-
update_all_users()
|
479 |
-
|
480 |
-
return jsonify({"state": "Registration Success!", "driveFileId": res_json.get("fileId")}), 200
|
481 |
-
except Exception as e:
|
482 |
-
print("Error in /upload_base_audio:", str(e))
|
483 |
-
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
484 |
-
|
485 |
-
@app.route('/list_base_audio', methods=['GET'])
|
486 |
-
def list_base_audio():
|
487 |
-
try:
|
488 |
-
global all_users
|
489 |
-
all_users = update_all_users()
|
490 |
-
return jsonify({"status": "success", "id": all_users}), 200
|
491 |
-
except Exception as e:
|
492 |
-
print("Error in /list_base_audio:", str(e))
|
493 |
-
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
494 |
-
|
495 |
if __name__ == '__main__':
|
496 |
port = int(os.environ.get("PORT", 7860))
|
497 |
app.run(debug=True, host="0.0.0.0", port=port)
|
|
|
1 |
+
from flask import Flask, request, jsonify, send_from_directory
|
2 |
import base64
|
|
|
3 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
+
app = Flask(__name__)
|
6 |
|
7 |
+
@app.route('/')
|
8 |
def index():
|
9 |
+
return send_from_directory(".", "index.html")
|
10 |
|
11 |
+
@app.route('/feedback',methods=['POST'])
|
|
|
12 |
def feedback():
|
13 |
+
return send_from_directory(".","feedback.html")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
|
|
15 |
@app.route('/upload_audio', methods=['POST'])
|
16 |
def upload_audio():
|
|
|
|
|
|
|
17 |
try:
|
18 |
data = request.get_json()
|
19 |
+
if not data:
|
20 |
+
return jsonify({"error": "JSONが送信されていません"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
+
audio_data = data.get('audio_data')
|
23 |
+
if not audio_data:
|
24 |
+
return jsonify({"error": "音声データが送信されていません"}), 400
|
25 |
|
26 |
+
# Base64デコード
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
try:
|
28 |
+
audio_binary = base64.b64decode(audio_data)
|
29 |
+
except Exception as decode_err:
|
30 |
+
return jsonify({"error": "Base64デコードに失敗しました", "details": str(decode_err)}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
+
# 書き込み用ディレクトリとして /tmp/data を使用(/tmp は書き込み可能)
|
33 |
+
persist_dir = "/tmp/data"
|
34 |
+
os.makedirs(persist_dir, exist_ok=True)
|
35 |
|
36 |
+
filepath = os.path.join(persist_dir, "recorded_audio.wav")
|
37 |
+
with open(filepath, 'wb') as f:
|
38 |
+
f.write(audio_binary)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
|
40 |
+
return jsonify({"message": "音声が正常に保存されました", "filepath": filepath}), 200
|
41 |
|
42 |
except Exception as e:
|
43 |
+
app.logger.error("エラー: %s", str(e))
|
44 |
return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500
|
45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
if __name__ == '__main__':
|
47 |
port = int(os.environ.get("PORT", 7860))
|
48 |
app.run(debug=True, host="0.0.0.0", port=port)
|
feedback.html
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="ja">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>会話フィードバック画面</title>
|
7 |
+
<style>
|
8 |
+
body {
|
9 |
+
font-family: Arial, sans-serif;
|
10 |
+
display: flex;
|
11 |
+
justify-content: center;
|
12 |
+
align-items: center;
|
13 |
+
height: 100vh;
|
14 |
+
margin: 0;
|
15 |
+
background-color: #f5f5f5;
|
16 |
+
}
|
17 |
+
.card {
|
18 |
+
border: 2px solid #000;
|
19 |
+
border-radius: 20px;
|
20 |
+
padding: 30px;
|
21 |
+
width: 500px;
|
22 |
+
text-align: center;
|
23 |
+
background-color: white;
|
24 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
25 |
+
}
|
26 |
+
.level {
|
27 |
+
font-size: 28px;
|
28 |
+
font-weight: bold;
|
29 |
+
margin-bottom: 20px;
|
30 |
+
}
|
31 |
+
.message {
|
32 |
+
margin: 15px 0;
|
33 |
+
font-size: 20px;
|
34 |
+
font-weight: bold;
|
35 |
+
color: #333;
|
36 |
+
}
|
37 |
+
.bar-container {
|
38 |
+
display: flex;
|
39 |
+
align-items: center;
|
40 |
+
margin: 8px 0;
|
41 |
+
}
|
42 |
+
.bar-label {
|
43 |
+
width: 60px;
|
44 |
+
margin-right: 10px;
|
45 |
+
font-weight: bold;
|
46 |
+
}
|
47 |
+
.bar {
|
48 |
+
flex: 1;
|
49 |
+
height: 25px;
|
50 |
+
background-color: lightgray;
|
51 |
+
border-radius: 5px;
|
52 |
+
overflow: hidden;
|
53 |
+
}
|
54 |
+
.bar-fill {
|
55 |
+
height: 100%;
|
56 |
+
border-radius: 5px;
|
57 |
+
}
|
58 |
+
.back-button, .history-button {
|
59 |
+
margin-top: 20px;
|
60 |
+
padding: 10px 20px;
|
61 |
+
background-color: #007bff;
|
62 |
+
color: white;
|
63 |
+
border: none;
|
64 |
+
border-radius: 5px;
|
65 |
+
cursor: pointer;
|
66 |
+
}
|
67 |
+
.back-button:hover, .history-button:hover {
|
68 |
+
background-color: #0056b3;
|
69 |
+
}
|
70 |
+
</style>
|
71 |
+
<script>
|
72 |
+
function getMessage(level) {
|
73 |
+
if (level < 20) return "やばい";
|
74 |
+
if (level < 40) return "気をつけよう";
|
75 |
+
if (level < 60) return "まずまずですね";
|
76 |
+
if (level < 80) return "がんばれあとちょっと";
|
77 |
+
return "素晴らしい";
|
78 |
+
}
|
79 |
+
|
80 |
+
function showRecorder() {
|
81 |
+
window.location.href = "index.html";
|
82 |
+
}
|
83 |
+
|
84 |
+
function showHistory() {
|
85 |
+
window.location.href = "talkDetail.html";
|
86 |
+
}
|
87 |
+
|
88 |
+
window.onload = function() {
|
89 |
+
const level = 73; // レベル値
|
90 |
+
const percentages = [80, 50, 60, 100, 30]; // 各バーのパーセンテージ
|
91 |
+
const labels = ["項目1", "項目2", "項目3", "項目4", "項目5"]; // 各項目名
|
92 |
+
|
93 |
+
const message = getMessage(level);
|
94 |
+
document.getElementById("level").innerText = `話者Lv: ${level}`;
|
95 |
+
document.getElementById("message").innerText = message;
|
96 |
+
|
97 |
+
const barElements = document.getElementsByClassName("bar-fill");
|
98 |
+
const labelElements = document.getElementsByClassName("bar-label");
|
99 |
+
for (let i = 0; i < barElements.length; i++) {
|
100 |
+
barElements[i].style.width = `${percentages[i]}%`;
|
101 |
+
labelElements[i].innerText = labels[i];
|
102 |
+
}
|
103 |
+
};
|
104 |
+
</script>
|
105 |
+
</head>
|
106 |
+
<body>
|
107 |
+
<div class="card">
|
108 |
+
<div class="level" id="level">話者Lv: 85</div>
|
109 |
+
<div class="message" id="message">素晴らしい</div>
|
110 |
+
|
111 |
+
<div class="bar-container">
|
112 |
+
<span class="bar-label"></span>
|
113 |
+
<div class="bar"><div class="bar-fill" style="background-color: lightblue;"></div></div>
|
114 |
+
</div>
|
115 |
+
<div class="bar-container">
|
116 |
+
<span class="bar-label"></span>
|
117 |
+
<div class="bar"><div class="bar-fill" style="background-color: peachpuff;"></div></div>
|
118 |
+
</div>
|
119 |
+
<div class="bar-container">
|
120 |
+
<span class="bar-label"></span>
|
121 |
+
<div class="bar"><div class="bar-fill" style="background-color: lightblue;"></div></div>
|
122 |
+
</div>
|
123 |
+
<div class="bar-container">
|
124 |
+
<span class="bar-label"></span>
|
125 |
+
<div class="bar"><div class="bar-fill" style="background-color: peachpuff;"></div></div>
|
126 |
+
</div>
|
127 |
+
<div class="bar-container">
|
128 |
+
<span class="bar-label"></span>
|
129 |
+
<div class="bar"><div class="bar-fill" style="background-color: lightcoral;"></div></div>
|
130 |
+
</div>
|
131 |
+
|
132 |
+
<button class="back-button" onclick="showRecorder()">録音画面を表示</button>
|
133 |
+
<button class="history-button" onclick="showHistory()">会話履歴を表示</button>
|
134 |
+
</div>
|
135 |
+
</body>
|
136 |
+
</html>
|
index.html
ADDED
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="ja">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Voice Recorder Interface</title>
|
7 |
+
<style>
|
8 |
+
body {
|
9 |
+
display: flex;
|
10 |
+
flex-direction: column;
|
11 |
+
justify-content: center;
|
12 |
+
align-items: center;
|
13 |
+
height: 100vh;
|
14 |
+
margin: 0;
|
15 |
+
background-color: #121212;
|
16 |
+
color: white;
|
17 |
+
}
|
18 |
+
.chart {
|
19 |
+
width: 300px;
|
20 |
+
height: 300px;
|
21 |
+
margin-bottom: 20px;
|
22 |
+
}
|
23 |
+
.record-button {
|
24 |
+
position: fixed;
|
25 |
+
bottom: 30px;
|
26 |
+
width: 80px;
|
27 |
+
height: 80px;
|
28 |
+
background-color: transparent;
|
29 |
+
border-radius: 50%;
|
30 |
+
border: 4px solid white;
|
31 |
+
display: flex;
|
32 |
+
justify-content: center;
|
33 |
+
align-items: center;
|
34 |
+
cursor: pointer;
|
35 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
|
36 |
+
transition: all 0.2s ease;
|
37 |
+
}
|
38 |
+
.record-icon {
|
39 |
+
width: 60px;
|
40 |
+
height: 60px;
|
41 |
+
background-color: #d32f2f;
|
42 |
+
border-radius: 50%;
|
43 |
+
transition: all 0.2s ease;
|
44 |
+
}
|
45 |
+
.recording .record-icon {
|
46 |
+
width: 40px;
|
47 |
+
height: 40px;
|
48 |
+
border-radius: 10%;
|
49 |
+
}
|
50 |
+
.result-button {
|
51 |
+
margin-top: 20px;
|
52 |
+
padding: 10px 20px;
|
53 |
+
background-color: #4caf50;
|
54 |
+
border: none;
|
55 |
+
border-radius: 5px;
|
56 |
+
color: white;
|
57 |
+
cursor: pointer;
|
58 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
|
59 |
+
}
|
60 |
+
.result-button:hover {
|
61 |
+
background-color: #388e3c;
|
62 |
+
}
|
63 |
+
</style>
|
64 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
65 |
+
</head>
|
66 |
+
<body>
|
67 |
+
<div class="chart">
|
68 |
+
<canvas id="speechChart"></canvas>
|
69 |
+
</div>
|
70 |
+
<button class="record-button" id="recordButton" onclick="toggleRecording()">
|
71 |
+
<div class="record-icon" id="recordIcon"></div>
|
72 |
+
</button>
|
73 |
+
<!--
|
74 |
+
<button class="result-button" id="resultButton" onclick="showResults()">結果を表示</button>
|
75 |
+
-->
|
76 |
+
|
77 |
+
<form method="POST" action="/feedback">
|
78 |
+
<div class="feedback-space">
|
79 |
+
<input class="result-button" id="resultButton" type="submit" name="submit" value="フィードバック画面を表示">
|
80 |
+
</div>
|
81 |
+
</form>
|
82 |
+
|
83 |
+
|
84 |
+
<script>
|
85 |
+
let isRecording = false;
|
86 |
+
let mediaRecorder;
|
87 |
+
let audioChunks = [];
|
88 |
+
async function toggleRecording() {
|
89 |
+
const recordButton = document.getElementById('recordButton');
|
90 |
+
const recordIcon = document.getElementById('recordIcon');
|
91 |
+
if (!isRecording) {
|
92 |
+
// 録音開始
|
93 |
+
isRecording = true;
|
94 |
+
recordButton.classList.add('recording');
|
95 |
+
try {
|
96 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
97 |
+
mediaRecorder = new MediaRecorder(stream);
|
98 |
+
audioChunks = [];
|
99 |
+
mediaRecorder.ondataavailable = event => {
|
100 |
+
if (event.data.size > 0) {
|
101 |
+
audioChunks.push(event.data);
|
102 |
+
}
|
103 |
+
};
|
104 |
+
mediaRecorder.onstop = () => {
|
105 |
+
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
106 |
+
const reader = new FileReader();
|
107 |
+
reader.onloadend = () => {
|
108 |
+
const base64String = reader.result.split(',')[1]; // Base64 エンコードされた音声データ
|
109 |
+
// サーバーへ音声データを送信
|
110 |
+
fetch('/upload_audio', {
|
111 |
+
method: 'POST',
|
112 |
+
headers: {
|
113 |
+
'Content-Type': 'application/json',
|
114 |
+
},
|
115 |
+
body: JSON.stringify({ audio_data: base64String }),
|
116 |
+
})
|
117 |
+
.then(response => response.json())
|
118 |
+
.then(data => {
|
119 |
+
alert('音声がバックエンドに送信されました。');
|
120 |
+
})
|
121 |
+
.catch(error => {
|
122 |
+
console.error('エラー:', error);
|
123 |
+
});
|
124 |
+
};
|
125 |
+
reader.readAsDataURL(audioBlob);
|
126 |
+
};
|
127 |
+
mediaRecorder.start();
|
128 |
+
} catch (error) {
|
129 |
+
console.error('マイクへのアクセスに失敗しました:', error);
|
130 |
+
isRecording = false;
|
131 |
+
recordButton.classList.remove('recording');
|
132 |
+
}
|
133 |
+
} else {
|
134 |
+
// 録音停止
|
135 |
+
isRecording = false;
|
136 |
+
recordButton.classList.remove('recording');
|
137 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
138 |
+
mediaRecorder.stop();
|
139 |
+
}
|
140 |
+
}
|
141 |
+
}
|
142 |
+
function showResults() {
|
143 |
+
window.location.href = 'feedback.html';
|
144 |
+
}
|
145 |
+
// Chart.js の初期化
|
146 |
+
const ctx = document.getElementById('speechChart').getContext('2d');
|
147 |
+
const speechChart = new Chart(ctx, {
|
148 |
+
type: 'doughnut',
|
149 |
+
data: {
|
150 |
+
labels: ['自分', '他の人'],
|
151 |
+
datasets: [{
|
152 |
+
data: [30, 70],
|
153 |
+
backgroundColor: ['#4caf50', '#757575'],
|
154 |
+
}],
|
155 |
+
},
|
156 |
+
options: {
|
157 |
+
responsive: true,
|
158 |
+
plugins: {
|
159 |
+
legend: {
|
160 |
+
display: true,
|
161 |
+
position: 'bottom',
|
162 |
+
labels: {
|
163 |
+
color: 'white'
|
164 |
+
}
|
165 |
+
}
|
166 |
+
}
|
167 |
+
}
|
168 |
+
});
|
169 |
+
</script>
|
170 |
+
</body>
|
171 |
+
</html>
|
init/createdatabase.sql
DELETED
@@ -1,14 +0,0 @@
|
|
1 |
-
USE app;
|
2 |
-
|
3 |
-
CREATE TABLE users(
|
4 |
-
user_id INT PRIMARY KEY AUTO_INCREMENT,
|
5 |
-
username VARCHAR(255),
|
6 |
-
password VARCHAR(255),
|
7 |
-
|
8 |
-
);
|
9 |
-
|
10 |
-
INSERT INTO users(username,password) VALUES('sample','sample');
|
11 |
-
INSERT INTO users(username,password) VALUES('test','test');
|
12 |
-
INSERT INTO users(username,password) VALUES('app','app');
|
13 |
-
|
14 |
-
GRANT ALL ON app.* TO test;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
instance/site.db
DELETED
File without changes
|
process.py
DELETED
@@ -1,539 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
import shutil
|
3 |
-
import numpy as np
|
4 |
-
import string
|
5 |
-
import random
|
6 |
-
from datetime import datetime
|
7 |
-
from pyannote.audio import Model, Inference
|
8 |
-
from pydub import AudioSegment
|
9 |
-
import base64
|
10 |
-
import binascii
|
11 |
-
import warnings
|
12 |
-
|
13 |
-
class AudioProcessor():
|
14 |
-
def __init__(self, cache_dir="/tmp/hf_cache", standard_duration=5.0):
|
15 |
-
hf_token = os.environ.get("HF")
|
16 |
-
if hf_token is None:
|
17 |
-
raise ValueError("HUGGINGFACE_HUB_TOKEN が設定されていません。")
|
18 |
-
os.makedirs(cache_dir, exist_ok=True)
|
19 |
-
# pyannote モデルの読み込み
|
20 |
-
model = Model.from_pretrained("pyannote/embedding", use_auth_token=hf_token, cache_dir=cache_dir)
|
21 |
-
self.inference = Inference(model)
|
22 |
-
# 標準の音声長さ(秒)
|
23 |
-
self.standard_duration = standard_duration
|
24 |
-
|
25 |
-
def normalize_audio_duration(self, input_path, target_duration_seconds=None, output_path=None):
|
26 |
-
"""
|
27 |
-
音声ファイルの長さを指定された時間(秒)にそろえる関数
|
28 |
-
短すぎる場合は無音を追加し、長すぎる場合は切り詰める
|
29 |
-
|
30 |
-
Parameters:
|
31 |
-
input_path (str): 入力音声ファイルのパス
|
32 |
-
target_duration_seconds (float, optional): 目標となる音声の長さ(秒)。Noneの場合はself.standard_durationを使用
|
33 |
-
output_path (str, optional): 出力先のパス。Noneの場合は一時ファイルを生成
|
34 |
-
|
35 |
-
Returns:
|
36 |
-
str: 処理された音声ファイルのパス
|
37 |
-
"""
|
38 |
-
try:
|
39 |
-
# デフォルト値の設定
|
40 |
-
if target_duration_seconds is None:
|
41 |
-
target_duration_seconds = self.standard_duration
|
42 |
-
|
43 |
-
# 音声ファイルを読み込む
|
44 |
-
audio = AudioSegment.from_file(input_path)
|
45 |
-
|
46 |
-
# 現在の長さ(ミリ秒)
|
47 |
-
current_duration_ms = len(audio)
|
48 |
-
target_duration_ms = int(target_duration_seconds * 1000)
|
49 |
-
|
50 |
-
# 出力パスが指定されていない場合は一時ファイルを生成
|
51 |
-
if output_path is None:
|
52 |
-
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
53 |
-
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
54 |
-
output_dir = os.path.dirname(input_path) if os.path.dirname(input_path) else '/tmp'
|
55 |
-
output_path = os.path.join(output_dir, f"normalized_{timestamp}_{random_str}.wav")
|
56 |
-
|
57 |
-
# 長さの調整
|
58 |
-
if current_duration_ms < target_duration_ms:
|
59 |
-
# 短い場合は無音を追加
|
60 |
-
silence_duration = target_duration_ms - current_duration_ms
|
61 |
-
silence = AudioSegment.silent(duration=silence_duration)
|
62 |
-
normalized_audio = audio + silence
|
63 |
-
else:
|
64 |
-
# 長い場合は切り詰め
|
65 |
-
normalized_audio = audio[:target_duration_ms]
|
66 |
-
|
67 |
-
# ファイルに保存
|
68 |
-
normalized_audio.export(output_path, format="wav")
|
69 |
-
|
70 |
-
return output_path
|
71 |
-
|
72 |
-
except Exception as e:
|
73 |
-
print(f"音声の長さをそろえる処理でエラーが発生しました: {e}")
|
74 |
-
return None
|
75 |
-
|
76 |
-
def batch_normalize_audio_duration(self, input_directory, target_duration_seconds=None, output_directory=None):
|
77 |
-
"""
|
78 |
-
ディレクトリ内の全音声ファイルの長さをそろえる関数
|
79 |
-
|
80 |
-
Parameters:
|
81 |
-
input_directory (str): 入力音声ファイルが格納されているディレクトリ
|
82 |
-
target_duration_seconds (float, optional): 目標となる音声の長さ(秒)。Noneの場合はself.standard_durationを使用
|
83 |
-
output_directory (str, optional): 出力先のディレクトリ。Noneの場合は入力と同じディレクトリに処理結果を保存
|
84 |
-
|
85 |
-
Returns:
|
86 |
-
list: 処理された音声ファイルのパスのリスト
|
87 |
-
"""
|
88 |
-
try:
|
89 |
-
# デフォルト値の設定
|
90 |
-
if target_duration_seconds is None:
|
91 |
-
target_duration_seconds = self.standard_duration
|
92 |
-
|
93 |
-
# 出力ディレクトリが指定されていない場合は入力ディレクトリを使用
|
94 |
-
if output_directory is None:
|
95 |
-
output_directory = input_directory
|
96 |
-
else:
|
97 |
-
os.makedirs(output_directory, exist_ok=True)
|
98 |
-
|
99 |
-
output_files = []
|
100 |
-
|
101 |
-
# ディレクトリ内の全ファイルを処理
|
102 |
-
for filename in os.listdir(input_directory):
|
103 |
-
if filename.lower().endswith(('.wav', '.mp3', '.webm', '.ogg', '.flac')):
|
104 |
-
input_path = os.path.join(input_directory, filename)
|
105 |
-
output_filename = f"normalized_{filename}"
|
106 |
-
output_path = os.path.join(output_directory, output_filename)
|
107 |
-
|
108 |
-
# 音声の長さをそろえる
|
109 |
-
processed_file = self.normalize_audio_duration(
|
110 |
-
input_path,
|
111 |
-
target_duration_seconds,
|
112 |
-
output_path
|
113 |
-
)
|
114 |
-
|
115 |
-
if processed_file:
|
116 |
-
output_files.append(processed_file)
|
117 |
-
|
118 |
-
return output_files
|
119 |
-
|
120 |
-
except Exception as e:
|
121 |
-
print(f"バッチ処理でエラーが発生しました: {e}")
|
122 |
-
return []
|
123 |
-
|
124 |
-
def cosine_similarity(self, vec1, vec2):
|
125 |
-
"""
|
126 |
-
2つのベクトル間のコサイン類似度を計算する
|
127 |
-
次元数が異なる場合はエラーを発生させる
|
128 |
-
|
129 |
-
Parameters:
|
130 |
-
vec1, vec2: 比較する2つのベクトル
|
131 |
-
|
132 |
-
Returns:
|
133 |
-
float: コサイン類似度 (-1 から 1 の範囲)
|
134 |
-
"""
|
135 |
-
try:
|
136 |
-
# 次元数チェック
|
137 |
-
if vec1.shape != vec2.shape:
|
138 |
-
raise ValueError(f"ベクトルの次元数が一致しません: {vec1.shape} vs {vec2.shape}")
|
139 |
-
|
140 |
-
# 正規化
|
141 |
-
vec1 = vec1 / np.linalg.norm(vec1)
|
142 |
-
vec2 = vec2 / np.linalg.norm(vec2)
|
143 |
-
|
144 |
-
return np.dot(vec1, vec2)
|
145 |
-
except Exception as e:
|
146 |
-
print(f"コサイン類似度計算でエラーが発生しました: {e}")
|
147 |
-
return None
|
148 |
-
|
149 |
-
def segment_audio(self, path, target_path='/tmp/setup_voice', seg_duration=1.0):
|
150 |
-
"""
|
151 |
-
音声ファイルを一定の長さのセグメントに分割する
|
152 |
-
|
153 |
-
Parameters:
|
154 |
-
path (str): 入力音声ファイルのパス
|
155 |
-
target_path (str): 分割されたセグメントを保存するディレクトリ
|
156 |
-
seg_duration (float): 各セグメントの長さ(秒)
|
157 |
-
|
158 |
-
Returns:
|
159 |
-
tuple: (セグメントが保存されたディレクトリのパス, 元の音声の総時間(ミリ秒))
|
160 |
-
"""
|
161 |
-
# 出力先ディレクトリが存在していれば中身をクリアする
|
162 |
-
if os.path.exists(target_path):
|
163 |
-
for file in os.listdir(target_path):
|
164 |
-
file_path = os.path.join(target_path, file)
|
165 |
-
if os.path.isfile(file_path):
|
166 |
-
os.remove(file_path)
|
167 |
-
else:
|
168 |
-
os.makedirs(target_path, exist_ok=True)
|
169 |
-
|
170 |
-
base_sound = AudioSegment.from_file(path)
|
171 |
-
duration_ms = len(base_sound)
|
172 |
-
seg_duration_ms = int(seg_duration * 1000)
|
173 |
-
|
174 |
-
for i, start in enumerate(range(0, duration_ms, seg_duration_ms)):
|
175 |
-
end = min(start + seg_duration_ms, duration_ms)
|
176 |
-
segment = base_sound[start:end]
|
177 |
-
# セグメントが指定長さに満たない場合、無音でパディングする
|
178 |
-
if len(segment) < seg_duration_ms:
|
179 |
-
silence = AudioSegment.silent(duration=(seg_duration_ms - len(segment)))
|
180 |
-
segment = segment + silence
|
181 |
-
|
182 |
-
segment.export(os.path.join(target_path, f'{i}.wav'), format="wav")
|
183 |
-
|
184 |
-
return target_path, duration_ms
|
185 |
-
|
186 |
-
def calculate_embedding(self, audio_path):
|
187 |
-
"""
|
188 |
-
音声ファイルからエンベディングを計算する
|
189 |
-
必要に応じて音声の長さを標準化する
|
190 |
-
|
191 |
-
Parameters:
|
192 |
-
audio_path (str): 音声ファイルのパス
|
193 |
-
|
194 |
-
Returns:
|
195 |
-
numpy.ndarray: 計算されたエンベディング
|
196 |
-
"""
|
197 |
-
try:
|
198 |
-
# 一時的に長さを標準化した音声ファイルを作成
|
199 |
-
normalized_path = self.normalize_audio_duration(audio_path)
|
200 |
-
if normalized_path is None:
|
201 |
-
raise ValueError("音声の長さの標準化に失敗しました")
|
202 |
-
|
203 |
-
# エンベディングを計算
|
204 |
-
embedding = self.inference(normalized_path)
|
205 |
-
|
206 |
-
# 一時ファイルを削除(必要に応じて)
|
207 |
-
if normalized_path != audio_path:
|
208 |
-
try:
|
209 |
-
os.remove(normalized_path)
|
210 |
-
except Exception as e:
|
211 |
-
warnings.warn(f"一時ファイルの削除に失敗しました: {e}")
|
212 |
-
|
213 |
-
return embedding.data.flatten()
|
214 |
-
|
215 |
-
except Exception as e:
|
216 |
-
print(f"エンベディング計算でエラーが発生しました: {e}")
|
217 |
-
return None
|
218 |
-
|
219 |
-
def calculate_similarity(self, path1, path2):
|
220 |
-
"""
|
221 |
-
2つの音声ファイル間の類似度を計算する
|
222 |
-
音声の長さを標準化してからエンベディングを計算
|
223 |
-
|
224 |
-
Parameters:
|
225 |
-
path1, path2 (str): 比較する2つの音声ファイルのパス
|
226 |
-
|
227 |
-
Returns:
|
228 |
-
float: コサイン類似度 (-1 から 1 の範囲)、エラー時はNone
|
229 |
-
"""
|
230 |
-
try:
|
231 |
-
# エンベディングを計算
|
232 |
-
embedding1 = self.calculate_embedding(path1)
|
233 |
-
embedding2 = self.calculate_embedding(path2)
|
234 |
-
|
235 |
-
if embedding1 is None or embedding2 is None:
|
236 |
-
raise ValueError("エンベディングの計算に失敗しました")
|
237 |
-
|
238 |
-
# 次元数チェック(念のため)
|
239 |
-
if embedding1.shape != embedding2.shape:
|
240 |
-
raise ValueError(f"エンベディングの次元数が一致しません: {embedding1.shape} vs {embedding2.shape}")
|
241 |
-
|
242 |
-
# 類似度を計算
|
243 |
-
return float(self.cosine_similarity(embedding1, embedding2))
|
244 |
-
except Exception as e:
|
245 |
-
print(f"類似度計算でエラーが発生しました: {e}")
|
246 |
-
return None
|
247 |
-
|
248 |
-
def process_audio(self, reference_path, input_path, user, output_folder='/tmp/data/matched_segments', seg_duration=1.0, threshold=0.5):
|
249 |
-
"""
|
250 |
-
入力音声からリファレンス音声に類似したセグメントを抽出する
|
251 |
-
|
252 |
-
Parameters:
|
253 |
-
reference_path (str): リファレンス音声のパス
|
254 |
-
input_path (str): 入力音声のパス
|
255 |
-
user(str): ユーザー名
|
256 |
-
output_folder (str): 類似セグメントを保存するディレクトリ
|
257 |
-
seg_duration (float): セグメントの長さ(秒)
|
258 |
-
threshold (float): 類似度の閾値
|
259 |
-
|
260 |
-
Returns:
|
261 |
-
tuple: (マッチした時間(ミリ秒), マッチしなかった時間(ミリ秒), 分類済みのセグメント)
|
262 |
-
"""
|
263 |
-
|
264 |
-
isSpeaking = None
|
265 |
-
wasSpeaking = None
|
266 |
-
current_segment=[]
|
267 |
-
merged_segments=[]
|
268 |
-
|
269 |
-
try:
|
270 |
-
# リファレンス音声のエンベディングを計算(長さを標準化)
|
271 |
-
reference_embedding = self.calculate_embedding(reference_path)
|
272 |
-
if reference_embedding is None:
|
273 |
-
raise ValueError("リファレンス音声のエンベディング計算に失敗しました")
|
274 |
-
|
275 |
-
# 出力先ディレクトリの中身をクリアする
|
276 |
-
if os.path.exists(output_folder):
|
277 |
-
for file in os.listdir(output_folder):
|
278 |
-
file_path = os.path.join(output_folder, file)
|
279 |
-
if os.path.isfile(file_path):
|
280 |
-
os.remove(file_path)
|
281 |
-
else:
|
282 |
-
os.makedirs(output_folder, exist_ok=True)
|
283 |
-
|
284 |
-
# 入力音声をセグメントに分割
|
285 |
-
segmented_path, total_duration_ms = self.segment_audio(input_path, seg_duration=seg_duration)
|
286 |
-
|
287 |
-
matched_time_ms = 0
|
288 |
-
for file in sorted(os.listdir(segmented_path)):
|
289 |
-
segment_file = os.path.join(segmented_path, file)
|
290 |
-
|
291 |
-
# セグメントのエンベディングを計算
|
292 |
-
segment_embedding = self.calculate_embedding(segment_file)
|
293 |
-
if segment_embedding is None:
|
294 |
-
print(f"警告: セグメント {file} のエンベディング計算に失敗しました。スキップします。")
|
295 |
-
continue
|
296 |
-
|
297 |
-
try:
|
298 |
-
# 類似度を計算
|
299 |
-
similarity = float(self.cosine_similarity(segment_embedding, reference_embedding))
|
300 |
-
|
301 |
-
if similarity > threshold:
|
302 |
-
shutil.copy(segment_file, output_folder)
|
303 |
-
matched_time_ms += len(AudioSegment.from_file(segment_file))
|
304 |
-
isSpeaking = True
|
305 |
-
else:
|
306 |
-
isSpeaking = False
|
307 |
-
|
308 |
-
# 話者が変わった場合、保存
|
309 |
-
if wasSpeaking != isSpeaking:
|
310 |
-
if current_segment:
|
311 |
-
if wasSpeaking:
|
312 |
-
merged_segments.append((user, current_segment))
|
313 |
-
else:
|
314 |
-
merged_segments.append(("other",current_segment))
|
315 |
-
wasSpeaking = isSpeaking
|
316 |
-
current_segment = [segment_file]
|
317 |
-
# 変わらなかった場合、結合
|
318 |
-
else:
|
319 |
-
current_segment.append(segment_file)
|
320 |
-
|
321 |
-
except Exception as e:
|
322 |
-
print(f"セグメント {file} の類似度計算でエラーが発生しました: {e}")
|
323 |
-
# 余りを保存
|
324 |
-
if current_segment:
|
325 |
-
if wasSpeaking:
|
326 |
-
merged_segments.append((user, current_segment))
|
327 |
-
else:
|
328 |
-
merged_segments.append(("other",current_segment))
|
329 |
-
|
330 |
-
unmatched_time_ms = total_duration_ms - matched_time_ms
|
331 |
-
return matched_time_ms, unmatched_time_ms, merged_segments
|
332 |
-
|
333 |
-
except Exception as e:
|
334 |
-
print(f"音声処理でエラーが発生しました: {e}")
|
335 |
-
return 0, 0, merged_segments
|
336 |
-
|
337 |
-
def process_multi_audio(self, reference_pathes, input_path, users, output_folder='/tmp/data/matched_multi_segments', seg_duration=1.0, threshold=0.5):
|
338 |
-
"""
|
339 |
-
入力音声から複数のリファレンス音声に類似したセグメントを抽出する
|
340 |
-
|
341 |
-
Parameters:
|
342 |
-
reference_pathes (list): リファレンス音声のパスのリスト
|
343 |
-
input_path (str): 入力音声のパス
|
344 |
-
users(list): ユーザーのリスト
|
345 |
-
output_folder (str): 類似セグメントを保存するディレクトリ
|
346 |
-
seg_duration (float): セグメントの長さ(秒)
|
347 |
-
threshold (float): 類似度の閾値
|
348 |
-
|
349 |
-
Returns:
|
350 |
-
tuple: (各リファレンスごとのマッチした時間のリスト, 分類済みのセグメント)
|
351 |
-
"""
|
352 |
-
try:
|
353 |
-
# 出力先ディレクトリの中身をクリアする
|
354 |
-
if os.path.exists(output_folder):
|
355 |
-
for file in os.listdir(output_folder):
|
356 |
-
file_path = os.path.join(output_folder, file)
|
357 |
-
if os.path.isfile(file_path):
|
358 |
-
os.remove(file_path)
|
359 |
-
else:
|
360 |
-
os.makedirs(output_folder, exist_ok=True)
|
361 |
-
|
362 |
-
# リファレンス音声のエンベディングを事前計算
|
363 |
-
reference_embeddings = []
|
364 |
-
for ref_path in reference_pathes:
|
365 |
-
embedding = self.calculate_embedding(ref_path)
|
366 |
-
if embedding is None:
|
367 |
-
print(f"警告: リファレンス {ref_path} のエンベディング計算に失敗しました")
|
368 |
-
# ダミーエンベディングを挿入(後で処理をスキップ)
|
369 |
-
reference_embeddings.append(None)
|
370 |
-
else:
|
371 |
-
reference_embeddings.append(embedding)
|
372 |
-
|
373 |
-
# 入力音声をセグメントに分割
|
374 |
-
segmented_path, total_duration_ms = self.segment_audio(input_path, seg_duration=seg_duration)
|
375 |
-
segment_files = sorted(os.listdir(segmented_path))
|
376 |
-
num_segments = len(segment_files)
|
377 |
-
|
378 |
-
# 各セグメントのエンベディングを計算
|
379 |
-
segment_embeddings = []
|
380 |
-
for file in segment_files:
|
381 |
-
segment_file = os.path.join(segmented_path, file)
|
382 |
-
embedding = self.calculate_embedding(segment_file)
|
383 |
-
if embedding is None:
|
384 |
-
print(f"警告: セグメント {file} のエンベディング計算に失敗しました")
|
385 |
-
segment_embeddings.append(None)
|
386 |
-
else:
|
387 |
-
segment_embeddings.append(embedding)
|
388 |
-
|
389 |
-
# 各リファレンスごとにセグメントとの類似度を計算
|
390 |
-
similarity = []
|
391 |
-
for ref_embedding in reference_embeddings:
|
392 |
-
if ref_embedding is None:
|
393 |
-
# リファレンスのエンベディングが計算できなかった場合
|
394 |
-
similarity.append([0.0] * num_segments)
|
395 |
-
continue
|
396 |
-
|
397 |
-
ref_similarity = []
|
398 |
-
for seg_embedding in segment_embeddings:
|
399 |
-
if seg_embedding is None:
|
400 |
-
# セグメントのエンベディングが計算できなかった場合
|
401 |
-
ref_similarity.append(0.0)
|
402 |
-
continue
|
403 |
-
|
404 |
-
try:
|
405 |
-
# 次元数チェック
|
406 |
-
if ref_embedding.shape != seg_embedding.shape:
|
407 |
-
print(f"警告: エンベディングの次元数が一致しません: {ref_embedding.shape} vs {seg_embedding.shape}")
|
408 |
-
ref_similarity.append(0.0)
|
409 |
-
continue
|
410 |
-
|
411 |
-
# 類似度を計算
|
412 |
-
sim = float(self.cosine_similarity(seg_embedding, ref_embedding))
|
413 |
-
ref_similarity.append(sim)
|
414 |
-
except Exception as e:
|
415 |
-
print(f"類似度計算でエラーが発生しました: {e}")
|
416 |
-
ref_similarity.append(0.0)
|
417 |
-
|
418 |
-
similarity.append(ref_similarity)
|
419 |
-
|
420 |
-
# 転置行列を作成 (rows: segment, columns: reference)
|
421 |
-
similarity_transposed = []
|
422 |
-
for seg_idx in range(num_segments):
|
423 |
-
seg_sim = []
|
424 |
-
for ref_idx in range(len(reference_pathes)):
|
425 |
-
seg_sim.append(similarity[ref_idx][seg_idx])
|
426 |
-
similarity_transposed.append(seg_sim)
|
427 |
-
|
428 |
-
# 各セグメントについて、最も高い類似度のリファレンスを選択
|
429 |
-
best_matches = []
|
430 |
-
speakers = []
|
431 |
-
for seg_sim in similarity_transposed:
|
432 |
-
best_ref = np.argmax(seg_sim) # 最も類似度の高いリファレンスのインデックス
|
433 |
-
# 閾値チェック
|
434 |
-
if seg_sim[best_ref] < threshold:
|
435 |
-
best_matches.append(None) # 閾値未満の場合はマッチなしとする
|
436 |
-
speakers.append(-1) # Noneは都合が悪いので-1
|
437 |
-
else:
|
438 |
-
best_matches.append(best_ref)
|
439 |
-
speakers.append(best_ref)
|
440 |
-
|
441 |
-
current_speaker = None
|
442 |
-
current_segments = []
|
443 |
-
merged_segments = []
|
444 |
-
for index,file in enumerate(segment_files,start=0):
|
445 |
-
file_path = os.path.join(segmented_path, file)
|
446 |
-
speaker = users[speakers[index]]
|
447 |
-
if speaker == -1:
|
448 |
-
continue
|
449 |
-
if current_speaker != speaker:
|
450 |
-
if current_segments:
|
451 |
-
merged_segments.append((current_speaker,current_segments))
|
452 |
-
current_speaker = speaker
|
453 |
-
current_segments = [file_path]
|
454 |
-
else:
|
455 |
-
current_segments.append(file_path)
|
456 |
-
if current_segments:
|
457 |
-
merged_segments.append((current_speaker,current_segments))
|
458 |
-
|
459 |
-
# 各リファレンスごとに一致時間を集計
|
460 |
-
matched_time = [0] * len(reference_pathes)
|
461 |
-
for match in best_matches:
|
462 |
-
if match is not None:
|
463 |
-
matched_time[match] += seg_duration
|
464 |
-
|
465 |
-
return matched_time, merged_segments
|
466 |
-
|
467 |
-
except Exception as e:
|
468 |
-
print(f"マルチ音声処理でエラーが発生しました: {e}")
|
469 |
-
return [0] * len(reference_pathes), None
|
470 |
-
|
471 |
-
def save_audio_from_base64(self, base64_audio, output_dir, output_filename, temp_format='webm'):
|
472 |
-
"""
|
473 |
-
Base64エンコードされた音声データをデコードして保存する
|
474 |
-
|
475 |
-
Parameters:
|
476 |
-
base64_audio (str): Base64エンコードされた音声データ
|
477 |
-
output_dir (str): 出力先ディレクトリ
|
478 |
-
output_filename (str): 出力ファイル名
|
479 |
-
temp_format (str): 一時ファイルのフォーマット
|
480 |
-
|
481 |
-
Returns:
|
482 |
-
str: 保存された音声ファイルのパス、エラー時はNone
|
483 |
-
"""
|
484 |
-
try:
|
485 |
-
# Base64デコードして音声バイナリを取得
|
486 |
-
try:
|
487 |
-
audio_binary = base64.b64decode(base64_audio)
|
488 |
-
except binascii.Error:
|
489 |
-
raise ValueError("Invalid Base64 input data")
|
490 |
-
|
491 |
-
# 保存するディレクトリを作成
|
492 |
-
os.makedirs(output_dir, exist_ok=True)
|
493 |
-
|
494 |
-
# 一時ファイルに保存
|
495 |
-
temp_audio_path = os.path.join(output_dir, "temp_audio")
|
496 |
-
try:
|
497 |
-
with open(temp_audio_path, 'wb') as f:
|
498 |
-
f.write(audio_binary)
|
499 |
-
|
500 |
-
# pydub を使って一時ファイルを WAV に変換
|
501 |
-
try:
|
502 |
-
audio = AudioSegment.from_file(temp_audio_path, format=temp_format)
|
503 |
-
except Exception as e:
|
504 |
-
# 形式が不明な場合は自動判別
|
505 |
-
audio = AudioSegment.from_file(temp_audio_path)
|
506 |
-
|
507 |
-
# 音声ファイルを保存
|
508 |
-
wav_audio_path = os.path.join(output_dir, output_filename)
|
509 |
-
audio.export(wav_audio_path, format="wav")
|
510 |
-
finally:
|
511 |
-
# 一時ファイルを削除
|
512 |
-
if os.path.exists(temp_audio_path):
|
513 |
-
os.remove(temp_audio_path)
|
514 |
-
return wav_audio_path
|
515 |
-
except ValueError as e:
|
516 |
-
print(f"Value Error: {e}")
|
517 |
-
except FileNotFoundError as e:
|
518 |
-
print(f"File Not Found Error: {e}")
|
519 |
-
except Exception as e:
|
520 |
-
print(f"Unexpected Error: {e}")
|
521 |
-
return None
|
522 |
-
|
523 |
-
def delete_files_in_directory(self, directory_path):
|
524 |
-
"""
|
525 |
-
ディレクトリ内のすべてのファイルを削除する
|
526 |
-
|
527 |
-
Parameters:
|
528 |
-
directory_path (str): 削除対象のディレクトリパス
|
529 |
-
"""
|
530 |
-
try:
|
531 |
-
# ディレクトリ内のすべてのファイルを取得
|
532 |
-
for filename in os.listdir(directory_path):
|
533 |
-
file_path = os.path.join(directory_path, filename)
|
534 |
-
# ファイルのみ削除する
|
535 |
-
if os.path.isfile(file_path):
|
536 |
-
os.remove(file_path)
|
537 |
-
print(f"{file_path} を削除しました")
|
538 |
-
except Exception as e:
|
539 |
-
print(f"ファイル削除でエラーが発生しました: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
record/save.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, request, jsonify,render_template
|
2 |
+
import base64
|
3 |
+
|
4 |
+
app = Flask(__name__)
|
5 |
+
|
6 |
+
@app.route('/')
|
7 |
+
def root():
|
8 |
+
return render_template("record.html")
|
9 |
+
|
10 |
+
@app.route('/upload_audio', methods=['POST'])
|
11 |
+
def upload_audio():
|
12 |
+
try:
|
13 |
+
data = request.get_json() # クライアントから送られてきたJSONデータ
|
14 |
+
audio_data = data.get('audio_data') # Base64エンコードされた音声データ
|
15 |
+
|
16 |
+
if not audio_data:
|
17 |
+
return jsonify({"error": "音声データが送信されていません"}), 400
|
18 |
+
|
19 |
+
# Base64デコード
|
20 |
+
audio_binary = base64.b64decode(audio_data)
|
21 |
+
|
22 |
+
# WAVファイルとして保存
|
23 |
+
with open('recorded_audio.wav', 'wb') as f:
|
24 |
+
f.write(audio_binary)
|
25 |
+
|
26 |
+
return jsonify({"message": "音声が正常に保存されました"}), 200
|
27 |
+
except Exception as e:
|
28 |
+
return jsonify({"error": str(e)}), 500
|
29 |
+
|
30 |
+
if __name__ == '__main__':
|
31 |
+
app.run(debug=True)
|
record/templates/record.html
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="ja">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>音声録音</title>
|
7 |
+
</head>
|
8 |
+
<body>
|
9 |
+
<h1>音声録音</h1>
|
10 |
+
<button id="start-recording">録音開始</button>
|
11 |
+
<button id="stop-recording" disabled>録音停止</button>
|
12 |
+
<p>録音した音声を送信する準備ができました。</p>
|
13 |
+
<button id="send-to-server" disabled>音声を送信</button>
|
14 |
+
|
15 |
+
<script>
|
16 |
+
let mediaRecorder;
|
17 |
+
let audioChunks = [];
|
18 |
+
const startRecordingBtn = document.getElementById('start-recording');
|
19 |
+
const stopRecordingBtn = document.getElementById('stop-recording');
|
20 |
+
const sendToServerBtn = document.getElementById('send-to-server');
|
21 |
+
|
22 |
+
// 音声録音の開始
|
23 |
+
startRecordingBtn.addEventListener('click', async () => {
|
24 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
25 |
+
mediaRecorder = new MediaRecorder(stream);
|
26 |
+
|
27 |
+
mediaRecorder.ondataavailable = event => {
|
28 |
+
audioChunks.push(event.data);
|
29 |
+
};
|
30 |
+
|
31 |
+
mediaRecorder.onstop = () => {
|
32 |
+
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
33 |
+
const reader = new FileReader();
|
34 |
+
|
35 |
+
reader.onloadend = () => {
|
36 |
+
const base64String = reader.result.split(',')[1]; // Base64エンコードされた音声データを取得
|
37 |
+
sendToServerBtn.disabled = false;
|
38 |
+
|
39 |
+
sendToServerBtn.addEventListener('click', () => {
|
40 |
+
// 音声をバックエンドに送信
|
41 |
+
fetch('/upload_audio', {
|
42 |
+
method: 'POST',
|
43 |
+
headers: {
|
44 |
+
'Content-Type': 'application/json',
|
45 |
+
},
|
46 |
+
body: JSON.stringify({
|
47 |
+
audio_data: base64String,
|
48 |
+
}),
|
49 |
+
})
|
50 |
+
.then(response => response.json())
|
51 |
+
.then(data => {
|
52 |
+
alert('音声がバックエンドに送信されました。');
|
53 |
+
})
|
54 |
+
.catch(error => {
|
55 |
+
console.error('エラー:', error);
|
56 |
+
});
|
57 |
+
});
|
58 |
+
};
|
59 |
+
|
60 |
+
reader.readAsDataURL(audioBlob);
|
61 |
+
};
|
62 |
+
|
63 |
+
mediaRecorder.start();
|
64 |
+
startRecordingBtn.disabled = true;
|
65 |
+
stopRecordingBtn.disabled = false;
|
66 |
+
});
|
67 |
+
|
68 |
+
// 音声録音の停止
|
69 |
+
stopRecordingBtn.addEventListener('click', () => {
|
70 |
+
mediaRecorder.stop();
|
71 |
+
startRecordingBtn.disabled = false;
|
72 |
+
stopRecordingBtn.disabled = true;
|
73 |
+
});
|
74 |
+
</script>
|
75 |
+
</body>
|
76 |
+
</html>
|
requirements.txt
CHANGED
@@ -1,19 +1 @@
|
|
1 |
-
Flask
|
2 |
-
Flask-WTF
|
3 |
-
pyannote.audio==2.1.1
|
4 |
-
numpy==1.23.5
|
5 |
-
pydub==0.25.1
|
6 |
-
matplotlib==3.6.3
|
7 |
-
python-dotenv
|
8 |
-
uwsgi
|
9 |
-
Flask-SQLAlchemy==3.0.5
|
10 |
-
PyMySQL
|
11 |
-
Flask-Login==0.6.3
|
12 |
-
requests==2.32.3
|
13 |
-
google-auth==2.38.0
|
14 |
-
google-auth-oauthlib==1.2.1
|
15 |
-
google-auth-httplib2==0.2.0
|
16 |
-
faster-whisper
|
17 |
-
Flask-Migrate
|
18 |
-
requests
|
19 |
-
Flask-CORS
|
|
|
1 |
+
Flask
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
room.js
DELETED
File without changes
|
sample.wav
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:0f64da9fcf28836e98f50ac7a9ce3c213c186a9d444f295e5bd6a66b5b26d8c5
|
3 |
-
size 882044
|
|
|
|
|
|
|
|
static/feedback.js
DELETED
@@ -1,66 +0,0 @@
|
|
1 |
-
async function getTranscription() {
|
2 |
-
try {
|
3 |
-
const response = await fetch("/transcription");
|
4 |
-
if (!response.ok) {
|
5 |
-
throw new Error("HTTP error! status: ${response.status}");
|
6 |
-
}
|
7 |
-
const data = await response.json();
|
8 |
-
const results = data.response;
|
9 |
-
} catch (error) {
|
10 |
-
console.error("Failed to fetch transcription", error);
|
11 |
-
}
|
12 |
-
}
|
13 |
-
|
14 |
-
async function getAnalysis() {
|
15 |
-
const loader = document.getElementById("loader");
|
16 |
-
loader.style.display = "block";
|
17 |
-
try {
|
18 |
-
await getTranscription();
|
19 |
-
|
20 |
-
const response = await fetch("/analyze");
|
21 |
-
if (!response.ok) {
|
22 |
-
throw new Error(`HTTP error! status: ${response.status}`);
|
23 |
-
}
|
24 |
-
|
25 |
-
const data = await response.json();
|
26 |
-
console.log("分析データ取得:", data); // ←構造確認用
|
27 |
-
const results = data.results;
|
28 |
-
const analysis = results.deepseek_analysis;
|
29 |
-
|
30 |
-
// 変数に格納
|
31 |
-
const conversationLevel = analysis.conversationLevel;
|
32 |
-
const harassmentPresent = analysis.harassmentPresent;
|
33 |
-
const harassmentType = analysis.harassmentType;
|
34 |
-
const repetition = analysis.repetition;
|
35 |
-
const pleasantConversation = analysis.pleasantConversation;
|
36 |
-
const blameOrHarassment = analysis.blameOrHarassment;
|
37 |
-
|
38 |
-
loader.style.display = "none";
|
39 |
-
// DOMに表示
|
40 |
-
document.getElementById(
|
41 |
-
"level"
|
42 |
-
).innerText = `会話レベル: ${conversationLevel}`;
|
43 |
-
document.getElementById(
|
44 |
-
"Harassment_bool"
|
45 |
-
).innerText = `ハラスメントの有無: ${harassmentPresent}`;
|
46 |
-
document.getElementById(
|
47 |
-
"Harassment_type"
|
48 |
-
).innerText = `ハラスメントの種類: ${harassmentType}`;
|
49 |
-
document.getElementById(
|
50 |
-
"Harassment_loop"
|
51 |
-
).innerText = `繰り返しの程度: ${repetition}`;
|
52 |
-
document.getElementById(
|
53 |
-
"Harassment_comfort"
|
54 |
-
).innerText = `会話の心地よさ: ${pleasantConversation}`;
|
55 |
-
document.getElementById(
|
56 |
-
"Harassment_volume"
|
57 |
-
).innerText = `非難またはハラスメントの程度: ${blameOrHarassment}`;
|
58 |
-
} catch (error) {
|
59 |
-
loader.style.display = "none";
|
60 |
-
console.error("Failed to fetch analysis data:", error);
|
61 |
-
}
|
62 |
-
}
|
63 |
-
|
64 |
-
window.onload = () => {
|
65 |
-
getAnalysis();
|
66 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/loading.css
DELETED
@@ -1,56 +0,0 @@
|
|
1 |
-
.loader {
|
2 |
-
position: absolute;
|
3 |
-
top: calc(50% - 32px);
|
4 |
-
left: calc(50% - 32px);
|
5 |
-
width: 64px;
|
6 |
-
height: 64px;
|
7 |
-
}
|
8 |
-
|
9 |
-
.loader div {
|
10 |
-
position: absolute;
|
11 |
-
top: 0;
|
12 |
-
left: 0;
|
13 |
-
width: 100%;
|
14 |
-
height: 100%;
|
15 |
-
border-radius: 50%;
|
16 |
-
box-sizing: border-box;
|
17 |
-
opacity: 0.8;
|
18 |
-
}
|
19 |
-
|
20 |
-
.one {
|
21 |
-
border-top: 1px solid #8834e8;
|
22 |
-
animation: rotate-left 1s linear infinite;
|
23 |
-
}
|
24 |
-
|
25 |
-
.two {
|
26 |
-
border-right: 1px solid #a28ecb;
|
27 |
-
animation: rotate-right 1s linear infinite;
|
28 |
-
}
|
29 |
-
|
30 |
-
.three {
|
31 |
-
border-bottom: 1px solid #ffd933;
|
32 |
-
animation: rotate-right 1s linear infinite;
|
33 |
-
}
|
34 |
-
|
35 |
-
.four {
|
36 |
-
border-left: 1px solid #ff7f00;
|
37 |
-
animation: rotate-right 1s linear infinite;
|
38 |
-
}
|
39 |
-
|
40 |
-
@keyframes rotate-left {
|
41 |
-
0% {
|
42 |
-
transform: rotate(360deg);
|
43 |
-
}
|
44 |
-
100% {
|
45 |
-
transform: rotate(0deg);
|
46 |
-
}
|
47 |
-
}
|
48 |
-
|
49 |
-
@keyframes rotate-right {
|
50 |
-
0% {
|
51 |
-
transform: rotate(0deg);
|
52 |
-
}
|
53 |
-
100% {
|
54 |
-
transform: rotate(360deg);
|
55 |
-
}
|
56 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/loading.js
DELETED
File without changes
|
static/main.css
DELETED
@@ -1,40 +0,0 @@
|
|
1 |
-
/* Responsive Design */
|
2 |
-
@media (max-width: 640px) {
|
3 |
-
.w-72 {
|
4 |
-
width: 95%;
|
5 |
-
}
|
6 |
-
.h-72 {
|
7 |
-
height: 350px;
|
8 |
-
}
|
9 |
-
}
|
10 |
-
/* Main Container */
|
11 |
-
body {
|
12 |
-
background: linear-gradient(135deg, #2c3e50, #1f2937);
|
13 |
-
display: flex;
|
14 |
-
align-items: center;
|
15 |
-
justify-content: center;
|
16 |
-
min-height: 100vh;
|
17 |
-
font-family: "Arial", sans-serif;
|
18 |
-
color: #fff;
|
19 |
-
}
|
20 |
-
|
21 |
-
/* Main Content Wrapper */
|
22 |
-
.main-content {
|
23 |
-
border: 5px solid rgba(255, 255, 255, 0.2);
|
24 |
-
padding: 2rem;
|
25 |
-
border-radius: 1rem;
|
26 |
-
width: 90%;
|
27 |
-
max-width: 500px;
|
28 |
-
background-color: rgba(0, 0, 0, 0.3);
|
29 |
-
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
|
30 |
-
text-align: center;
|
31 |
-
}
|
32 |
-
|
33 |
-
/* Title */
|
34 |
-
.main-title {
|
35 |
-
font-size: 2.5rem;
|
36 |
-
font-weight: bold;
|
37 |
-
margin-bottom: 1.5rem;
|
38 |
-
color: #fff;
|
39 |
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
40 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/menu.css
DELETED
@@ -1,56 +0,0 @@
|
|
1 |
-
/* Hamburger Menu Styles */
|
2 |
-
#menu {
|
3 |
-
position: absolute;
|
4 |
-
top: 0;
|
5 |
-
left: 0;
|
6 |
-
z-index: 10;
|
7 |
-
transform: translateX(-100%);
|
8 |
-
visibility: hidden;
|
9 |
-
opacity: 0;
|
10 |
-
background-color: rgb(31, 41, 55);
|
11 |
-
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
12 |
-
backdrop-filter: blur(10px);
|
13 |
-
border-right: 1px solid rgba(255, 255, 255, 0.2);
|
14 |
-
}
|
15 |
-
|
16 |
-
#menu.open {
|
17 |
-
transform: translateX(0);
|
18 |
-
visibility: visible;
|
19 |
-
opacity: 1;
|
20 |
-
}
|
21 |
-
|
22 |
-
#menu button {
|
23 |
-
transition: background-color 0.2s ease;
|
24 |
-
background-color: rgba(0, 0, 0, 0.1);
|
25 |
-
margin: 2px;
|
26 |
-
border-radius: 8px; /* 少し角を丸める */
|
27 |
-
display: flex;
|
28 |
-
align-items: center;
|
29 |
-
justify-content: flex-start;
|
30 |
-
gap: 10px;
|
31 |
-
padding: 0.75rem 1rem;
|
32 |
-
width: 100%;
|
33 |
-
text-align: left;
|
34 |
-
border: none;
|
35 |
-
color: #fff;
|
36 |
-
font-size: 1rem;
|
37 |
-
cursor: pointer;
|
38 |
-
}
|
39 |
-
|
40 |
-
#menu button:hover {
|
41 |
-
background-color: rgba(55, 65, 81, 0.7);
|
42 |
-
}
|
43 |
-
|
44 |
-
/* Hamburger Menu Button */
|
45 |
-
#menuButton {
|
46 |
-
background-color: rgba(255, 255, 255, 0.1);
|
47 |
-
border: none;
|
48 |
-
border-radius: 50%;
|
49 |
-
padding: 0.75rem; /* サイズを少し大きく */
|
50 |
-
cursor: pointer;
|
51 |
-
transition: background-color 0.2s ease;
|
52 |
-
}
|
53 |
-
|
54 |
-
#menuButton:hover {
|
55 |
-
background-color: rgba(255, 255, 255, 0.2);
|
56 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/menu.js
DELETED
@@ -1,53 +0,0 @@
|
|
1 |
-
// Show user registration page
|
2 |
-
function showUserRegister() {
|
3 |
-
fetch("/reset");
|
4 |
-
window.location.href = "userregister";
|
5 |
-
}
|
6 |
-
// メンバー選択画面表示
|
7 |
-
function showUserSelect() {
|
8 |
-
window.location.href = "/userselect";
|
9 |
-
}
|
10 |
-
// Show recorder page
|
11 |
-
function showRecorder() {
|
12 |
-
window.location.href = "index";
|
13 |
-
}
|
14 |
-
|
15 |
-
// Show results page
|
16 |
-
function showResults() {
|
17 |
-
window.location.href = "feedback";
|
18 |
-
}
|
19 |
-
|
20 |
-
// Show talk detail page
|
21 |
-
function showTalkDetail() {
|
22 |
-
window.location.href = "talk_detail";
|
23 |
-
}
|
24 |
-
|
25 |
-
// Reset action page
|
26 |
-
function resetAction() {
|
27 |
-
window.location.href = "reset_html";
|
28 |
-
}
|
29 |
-
|
30 |
-
// Toggle hamburger menu visibility
|
31 |
-
function toggleMenu(event) {
|
32 |
-
event.stopPropagation(); // Prevents click event from propagating to the document
|
33 |
-
const menu = document.getElementById("menu");
|
34 |
-
menu.classList.toggle("open");
|
35 |
-
}
|
36 |
-
|
37 |
-
// Close the menu if clicked outside
|
38 |
-
function closeMenu(event) {
|
39 |
-
const menu = document.getElementById("menu");
|
40 |
-
if (
|
41 |
-
menu.classList.contains("open") &&
|
42 |
-
!menu.contains(event.target) &&
|
43 |
-
!event.target.closest("#menuButton")
|
44 |
-
) {
|
45 |
-
menu.classList.remove("open");
|
46 |
-
}
|
47 |
-
}
|
48 |
-
|
49 |
-
// Add event listener for closing the menu when clicking outside
|
50 |
-
document.addEventListener("click", closeMenu);
|
51 |
-
|
52 |
-
// Show recorder page 名前に気を付けて!
|
53 |
-
document.getElementById("add-btn").addEventListener("click", showRecorder);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/process.js
DELETED
@@ -1,222 +0,0 @@
|
|
1 |
-
|
2 |
-
let isRecording = false;
|
3 |
-
let mediaRecorder;
|
4 |
-
let audioChunks = [];
|
5 |
-
let recordingInterval;
|
6 |
-
let count_voice = 0;
|
7 |
-
let before_rate = [];
|
8 |
-
const RECORDING_INTERVAL_MS = 5000; // 5秒
|
9 |
-
// メンバーとチャートの初期化
|
10 |
-
let members = [];
|
11 |
-
let voiceData = [];
|
12 |
-
let baseMemberColors = ["#4caf50", "#007bff", "#ffc107", "#dc3545", "#28a745", "#9c27b0", "#ff9800"];
|
13 |
-
// Chart.js の初期化
|
14 |
-
const ctx = document.getElementById("speechChart").getContext("2d");
|
15 |
-
const speechChart = new Chart(ctx, {
|
16 |
-
type: "doughnut",
|
17 |
-
data: {
|
18 |
-
labels: members,
|
19 |
-
datasets: [
|
20 |
-
{
|
21 |
-
data: voiceData,
|
22 |
-
backgroundColor: getMemberColors(members.length),
|
23 |
-
},
|
24 |
-
],
|
25 |
-
},
|
26 |
-
options: {
|
27 |
-
responsive: true,
|
28 |
-
plugins: {
|
29 |
-
legend: {
|
30 |
-
display: true,
|
31 |
-
position: "bottom",
|
32 |
-
labels: { color: "white" },
|
33 |
-
},
|
34 |
-
},
|
35 |
-
},
|
36 |
-
});
|
37 |
-
// サーバーからメンバー情報を取得してチャートを更新する関数
|
38 |
-
async function updateChartFrom() {
|
39 |
-
try {
|
40 |
-
const response = await fetch("/confirm");
|
41 |
-
if (!response.ok) {
|
42 |
-
throw new Error(`HTTP error! status: ${response.status}`);
|
43 |
-
}
|
44 |
-
const data = await response.json();
|
45 |
-
if (!data || !data.members || !Array.isArray(data.members)) {
|
46 |
-
console.error("Invalid member data received:", data);
|
47 |
-
members = ["member1"];
|
48 |
-
voiceData = [50, 50];
|
49 |
-
updateChart();
|
50 |
-
return;
|
51 |
-
}
|
52 |
-
members = data.members;
|
53 |
-
voiceData = [];
|
54 |
-
for (let i = 0; i < members.length; i++) {
|
55 |
-
voiceData.push(100 / members.length);
|
56 |
-
}
|
57 |
-
updateChart();
|
58 |
-
} catch (error) {
|
59 |
-
console.error("Failed to fetch member data:", error);
|
60 |
-
members = ["member1"];
|
61 |
-
voiceData = [50, 50];
|
62 |
-
updateChart();
|
63 |
-
}
|
64 |
-
}
|
65 |
-
function updateChart() {
|
66 |
-
// 一人モードの場合は、ユーザーとグレー(無音)の比率をチャートに表示
|
67 |
-
if (members.length === 1) {
|
68 |
-
const userName = members[0];
|
69 |
-
speechChart.data.labels = [userName, "無音"];
|
70 |
-
speechChart.data.datasets[0].backgroundColor = ["#4caf50", "#757575"];
|
71 |
-
} else {
|
72 |
-
// 複数メンバーの場合は通常通りの処理
|
73 |
-
speechChart.data.labels = members;
|
74 |
-
speechChart.data.datasets[0].backgroundColor = getMemberColors(members.length);
|
75 |
-
}
|
76 |
-
speechChart.data.datasets[0].data = voiceData;
|
77 |
-
speechChart.update();
|
78 |
-
}
|
79 |
-
|
80 |
-
// ページ読み込み時にチャート情報を更新
|
81 |
-
updateChartFrom();
|
82 |
-
// 録音ボタンの録音開始/停止処理
|
83 |
-
async function toggleRecording() {
|
84 |
-
const recordButton = document.getElementById("recordButton");
|
85 |
-
if (!isRecording) {
|
86 |
-
// 録音開始
|
87 |
-
isRecording = true;
|
88 |
-
recordButton.classList.add("recording");
|
89 |
-
try {
|
90 |
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
91 |
-
mediaRecorder = new MediaRecorder(stream);
|
92 |
-
audioChunks = [];
|
93 |
-
mediaRecorder.ondataavailable = (event) => {
|
94 |
-
if (event.data.size > 0) {
|
95 |
-
audioChunks.push(event.data);
|
96 |
-
}
|
97 |
-
};
|
98 |
-
mediaRecorder.onstop = () => {
|
99 |
-
sendAudioChunks([...audioChunks]);
|
100 |
-
audioChunks = [];
|
101 |
-
};
|
102 |
-
mediaRecorder.start();
|
103 |
-
// 5秒ごとに録音を停止して送信するインターバルを設定
|
104 |
-
recordingInterval = setInterval(() => {
|
105 |
-
if (mediaRecorder && mediaRecorder.state === "recording") {
|
106 |
-
mediaRecorder.stop();
|
107 |
-
mediaRecorder.start();
|
108 |
-
}
|
109 |
-
}, RECORDING_INTERVAL_MS);
|
110 |
-
} catch (error) {
|
111 |
-
console.error("マイクへのアクセスに失敗しました:", error);
|
112 |
-
isRecording = false;
|
113 |
-
recordButton.classList.remove("recording");
|
114 |
-
}
|
115 |
-
} else {
|
116 |
-
// 録音停止
|
117 |
-
isRecording = false;
|
118 |
-
recordButton.classList.remove("recording");
|
119 |
-
if (mediaRecorder && mediaRecorder.state === "recording") {
|
120 |
-
mediaRecorder.stop();
|
121 |
-
}
|
122 |
-
clearInterval(recordingInterval);
|
123 |
-
count_voice = 0;
|
124 |
-
//before_rate = [];
|
125 |
-
}
|
126 |
-
}
|
127 |
-
function sendAudioChunks(chunks) {
|
128 |
-
const audioBlob = new Blob(chunks, { type: "audio/wav" });
|
129 |
-
const reader = new FileReader();
|
130 |
-
reader.onloadend = () => {
|
131 |
-
const base64String = reader.result.split(",")[1];
|
132 |
-
const form = document.getElementById("recordForm");
|
133 |
-
const nameInput = form.querySelector('input[name="name"]');
|
134 |
-
const name = nameInput ? nameInput.value : "unknown";
|
135 |
-
fetch("/upload_audio", {
|
136 |
-
method: "POST",
|
137 |
-
headers: { "Content-Type": "application/json" },
|
138 |
-
body: JSON.stringify({ audio_data: base64String, name: name }),
|
139 |
-
})
|
140 |
-
.then((response) => response.json())
|
141 |
-
.then((data) => {
|
142 |
-
if (data.error) {
|
143 |
-
alert("エラー: " + data.error);
|
144 |
-
console.error(data.details);
|
145 |
-
} else if (data.rate !== undefined) {
|
146 |
-
updateChartData(data.rate);
|
147 |
-
} else if (data.rates !== undefined) {
|
148 |
-
updateChartData(data.rates);
|
149 |
-
}
|
150 |
-
})
|
151 |
-
.catch((error) => {
|
152 |
-
console.error("エラー:", error);
|
153 |
-
});
|
154 |
-
};
|
155 |
-
reader.readAsDataURL(audioBlob);
|
156 |
-
}
|
157 |
-
function getMemberColors(memberCount) {
|
158 |
-
// 一人モードの場合は特別な処理をしない(updateChartで処理するため)
|
159 |
-
if (memberCount <= 1) {
|
160 |
-
return ["#4caf50", "#757575"];
|
161 |
-
} else {
|
162 |
-
let colors = [];
|
163 |
-
for (let i = 0; i < memberCount; i++) {
|
164 |
-
colors.push(baseMemberColors[i % baseMemberColors.length]);
|
165 |
-
}
|
166 |
-
return colors;
|
167 |
-
}
|
168 |
-
}
|
169 |
-
function updateChartData(newRate) {
|
170 |
-
// 一人モードの時の処理
|
171 |
-
if (members.length === 1) {
|
172 |
-
if (count_voice === 0) {
|
173 |
-
speechChart.data.datasets[0].data = [newRate, 100 - newRate];
|
174 |
-
before_rate = [newRate];
|
175 |
-
} else {
|
176 |
-
// 一人モードでは、過去のデータと現在のデータを加重平均する
|
177 |
-
let tmp_rate = (newRate + before_rate[0] * count_voice) / (count_voice + 1);
|
178 |
-
speechChart.data.datasets[0].data = [tmp_rate, 100 - tmp_rate];
|
179 |
-
before_rate = [tmp_rate];
|
180 |
-
}
|
181 |
-
count_voice++;
|
182 |
-
// 一人モードでは常に緑色とグレーの組み合わせを使用
|
183 |
-
speechChart.data.labels = [members[0], "無音"];
|
184 |
-
speechChart.data.datasets[0].backgroundColor = ["#4caf50", "#757575"];
|
185 |
-
} else {
|
186 |
-
console.log(before_rate)
|
187 |
-
// 複数人モードの処理
|
188 |
-
if (!Array.isArray(newRate)) {
|
189 |
-
console.error("newRate is not an array:", newRate);
|
190 |
-
return;
|
191 |
-
}
|
192 |
-
if (newRate.length !== members.length) {
|
193 |
-
console.error(
|
194 |
-
"newRate length does not match members length:",
|
195 |
-
newRate,
|
196 |
-
members
|
197 |
-
);
|
198 |
-
return;
|
199 |
-
}
|
200 |
-
let averagedRates = new Array(newRate.length);
|
201 |
-
for (let i = 0; i < newRate.length; i++) {
|
202 |
-
let tmp_rate;
|
203 |
-
if (count_voice === 0) {
|
204 |
-
// 初回はそのまま
|
205 |
-
tmp_rate = newRate[i];
|
206 |
-
} else {
|
207 |
-
// 2回目以降は、過去の平均値と現在の値を加重平均する
|
208 |
-
tmp_rate = (newRate[i] + before_rate[i] * count_voice) / (count_voice + 1);
|
209 |
-
}
|
210 |
-
averagedRates[i] = tmp_rate;
|
211 |
-
}
|
212 |
-
// before_rateを更新
|
213 |
-
before_rate = averagedRates;
|
214 |
-
//グラフに反映
|
215 |
-
speechChart.data.datasets[0].data = averagedRates;
|
216 |
-
count_voice++;
|
217 |
-
speechChart.data.datasets[0].backgroundColor = getMemberColors(
|
218 |
-
members.length
|
219 |
-
);
|
220 |
-
}
|
221 |
-
speechChart.update();
|
222 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/process1.js
DELETED
@@ -1,294 +0,0 @@
|
|
1 |
-
let allUsers = [];
|
2 |
-
let selectedUsers = [];
|
3 |
-
let userToDelete = null;
|
4 |
-
|
5 |
-
// ページ読み込み時にユーザーリストを取得
|
6 |
-
document.addEventListener('DOMContentLoaded', fetchUserList);
|
7 |
-
|
8 |
-
// ユーザーリスト取得 - Flask APIの変更に合わせて修正
|
9 |
-
function fetchUserList() {
|
10 |
-
fetch('/list_base_audio')
|
11 |
-
.then(response => response.json())
|
12 |
-
.then(data => {
|
13 |
-
if (data.status === 'success' && Array.isArray(data.id)) {
|
14 |
-
// APIの変更: data.fileNames → data.id
|
15 |
-
allUsers = data.id;
|
16 |
-
renderUserList(allUsers);
|
17 |
-
} else {
|
18 |
-
showError('メンバーリストの取得に失敗しました');
|
19 |
-
}
|
20 |
-
})
|
21 |
-
.catch(error => {
|
22 |
-
console.error('Error fetching user list:', error);
|
23 |
-
showError('サーバーとの通信中にエラーが発生しました');
|
24 |
-
});
|
25 |
-
}
|
26 |
-
|
27 |
-
// ユーザーリストの表示
|
28 |
-
function renderUserList(users) {
|
29 |
-
const userListElement = document.getElementById('userList');
|
30 |
-
|
31 |
-
if (!users || users.length === 0) {
|
32 |
-
userListElement.innerHTML = `
|
33 |
-
<div class="no-users">
|
34 |
-
<p>登録されているメンバーがいません。</p>
|
35 |
-
<p>「新規登録」から音声を登録してください。</p>
|
36 |
-
</div>
|
37 |
-
`;
|
38 |
-
return;
|
39 |
-
}
|
40 |
-
|
41 |
-
let html = '';
|
42 |
-
users.forEach(user => {
|
43 |
-
const firstLetter = user.substr(0, 1).toUpperCase();
|
44 |
-
html += `
|
45 |
-
<div class="user-item">
|
46 |
-
<input type="checkbox" id="user-${user}" value="${user}" onchange="toggleUserSelection('${user}')">
|
47 |
-
<label for="user-${user}">${user}</label>
|
48 |
-
<div class="user-avatar">${firstLetter}</div>
|
49 |
-
<button class="delete-button" onclick="showDeleteModal('${user}')">
|
50 |
-
<i class="fas fa-trash"></i>
|
51 |
-
</button>
|
52 |
-
</div>
|
53 |
-
`;
|
54 |
-
});
|
55 |
-
|
56 |
-
userListElement.innerHTML = html;
|
57 |
-
|
58 |
-
// 既に選択済みのユーザーがあればチェックを入れる
|
59 |
-
checkStoredSelections();
|
60 |
-
}
|
61 |
-
|
62 |
-
// ユーザー選択の切り替え
|
63 |
-
function toggleUserSelection(username) {
|
64 |
-
const index = selectedUsers.indexOf(username);
|
65 |
-
if (index === -1) {
|
66 |
-
selectedUsers.push(username);
|
67 |
-
} else {
|
68 |
-
selectedUsers.splice(index, 1);
|
69 |
-
}
|
70 |
-
|
71 |
-
updateSelectedCount();
|
72 |
-
updateProceedButton();
|
73 |
-
saveSelections();
|
74 |
-
}
|
75 |
-
|
76 |
-
// すべてのユーザーを選択
|
77 |
-
function selectAllUsers() {
|
78 |
-
selectedUsers = [...allUsers];
|
79 |
-
|
80 |
-
// チェックボックスを更新
|
81 |
-
allUsers.forEach(user => {
|
82 |
-
const checkbox = document.getElementById(`user-${user}`);
|
83 |
-
if (checkbox) checkbox.checked = true;
|
84 |
-
});
|
85 |
-
|
86 |
-
updateSelectedCount();
|
87 |
-
updateProceedButton();
|
88 |
-
saveSelections();
|
89 |
-
}
|
90 |
-
|
91 |
-
// すべての選択を解除
|
92 |
-
function deselectAllUsers() {
|
93 |
-
selectedUsers = [];
|
94 |
-
|
95 |
-
// チェックボックスを更新
|
96 |
-
allUsers.forEach(user => {
|
97 |
-
const checkbox = document.getElementById(`user-${user}`);
|
98 |
-
if (checkbox) checkbox.checked = false;
|
99 |
-
});
|
100 |
-
|
101 |
-
updateSelectedCount();
|
102 |
-
updateProceedButton();
|
103 |
-
saveSelections();
|
104 |
-
}
|
105 |
-
|
106 |
-
// 選択数の表示を更新
|
107 |
-
function updateSelectedCount() {
|
108 |
-
document.getElementById('selectedCount').textContent = `選択中: ${selectedUsers.length}人`;
|
109 |
-
}
|
110 |
-
|
111 |
-
// 進むボタンの有効/無効を更新
|
112 |
-
function updateProceedButton() {
|
113 |
-
document.getElementById('proceedButton').disabled = selectedUsers.length === 0;
|
114 |
-
}
|
115 |
-
|
116 |
-
// 選択を保存
|
117 |
-
function saveSelections() {
|
118 |
-
localStorage.setItem('selectedUsers', JSON.stringify(selectedUsers));
|
119 |
-
}
|
120 |
-
|
121 |
-
// 保存されている選択を読み込み
|
122 |
-
function checkStoredSelections() {
|
123 |
-
const storedSelections = localStorage.getItem('selectedUsers');
|
124 |
-
if (storedSelections) {
|
125 |
-
try {
|
126 |
-
selectedUsers = JSON.parse(storedSelections);
|
127 |
-
selectedUsers = selectedUsers.filter(user => allUsers.includes(user)); // 存在するユーザーのみ選択
|
128 |
-
|
129 |
-
// チェックボックスに反映
|
130 |
-
selectedUsers.forEach(user => {
|
131 |
-
const checkbox = document.getElementById(`user-${user}`);
|
132 |
-
if (checkbox) checkbox.checked = true;
|
133 |
-
});
|
134 |
-
|
135 |
-
updateSelectedCount();
|
136 |
-
updateProceedButton();
|
137 |
-
} catch (e) {
|
138 |
-
console.error('保存された選択の読み込みに失敗しました', e);
|
139 |
-
selectedUsers = [];
|
140 |
-
}
|
141 |
-
}
|
142 |
-
}
|
143 |
-
|
144 |
-
// エラー表示
|
145 |
-
function showError(message) {
|
146 |
-
const userListElement = document.getElementById('userList');
|
147 |
-
userListElement.innerHTML = `
|
148 |
-
<div class="no-users">
|
149 |
-
<p>${message}</p>
|
150 |
-
<button class="select-button" onclick="fetchUserList()">再読み込み</button>
|
151 |
-
</div>
|
152 |
-
`;
|
153 |
-
}
|
154 |
-
|
155 |
-
// 選択されたユーザーでサーバーに送信して次のページに進む
|
156 |
-
function proceedWithSelectedUsers() {
|
157 |
-
if (selectedUsers.length === 0) {
|
158 |
-
alert('少なくとも1人のメンバーを選択してください');
|
159 |
-
return;
|
160 |
-
}
|
161 |
-
|
162 |
-
// 選択したユーザーをサーバーに送信
|
163 |
-
fetch('/select_users', {
|
164 |
-
method: 'POST',
|
165 |
-
headers: {
|
166 |
-
'Content-Type': 'application/json',
|
167 |
-
},
|
168 |
-
body: JSON.stringify({
|
169 |
-
users: selectedUsers
|
170 |
-
})
|
171 |
-
})
|
172 |
-
.then(response => response.json())
|
173 |
-
.then(data => {
|
174 |
-
if (data.status === 'success') {
|
175 |
-
// 成功したらインデックスページに進む
|
176 |
-
window.location.href = '/index';
|
177 |
-
} else {
|
178 |
-
alert('エラーが発生しました: ' + (data.error || 'Unknown error'));
|
179 |
-
}
|
180 |
-
})
|
181 |
-
.catch(error => {
|
182 |
-
console.error('Error selecting users:', error);
|
183 |
-
alert('サーバーとの通信中にエラーが発生しました');
|
184 |
-
});
|
185 |
-
}
|
186 |
-
|
187 |
-
// 削除確認モーダルを表示
|
188 |
-
function showDeleteModal(username) {
|
189 |
-
userToDelete = username;
|
190 |
-
document.getElementById('deleteModalText').textContent = `メンバー「${username}」を削除しますか?削除すると元に戻せません。`;
|
191 |
-
document.getElementById('deleteModal').style.display = 'flex';
|
192 |
-
}
|
193 |
-
|
194 |
-
// 削除確認モーダルを非表示
|
195 |
-
function hideDeleteModal() {
|
196 |
-
document.getElementById('deleteModal').style.display = 'none';
|
197 |
-
userToDelete = null;
|
198 |
-
}
|
199 |
-
|
200 |
-
// メンバーの削除を実行
|
201 |
-
function confirmDelete() {
|
202 |
-
if (!userToDelete) return;
|
203 |
-
|
204 |
-
// 削除中の表示
|
205 |
-
document.getElementById('deleteModalText').innerHTML = `
|
206 |
-
<div class="loading">
|
207 |
-
<div class="spinner"></div>
|
208 |
-
<p>メンバー「${userToDelete}」を削除中...</p>
|
209 |
-
</div>
|
210 |
-
`;
|
211 |
-
|
212 |
-
fetch('/reset_member', {
|
213 |
-
method: 'POST',
|
214 |
-
headers: {
|
215 |
-
'Content-Type': 'application/json',
|
216 |
-
},
|
217 |
-
body: JSON.stringify({
|
218 |
-
names: [userToDelete]
|
219 |
-
})
|
220 |
-
})
|
221 |
-
.then(response => response.json())
|
222 |
-
.then(data => {
|
223 |
-
if (data.status === 'success') {
|
224 |
-
// 選択リストからも削除
|
225 |
-
const index = selectedUsers.indexOf(userToDelete);
|
226 |
-
if (index !== -1) {
|
227 |
-
selectedUsers.splice(index, 1);
|
228 |
-
saveSelections();
|
229 |
-
}
|
230 |
-
|
231 |
-
// リストから削除して再表示
|
232 |
-
allUsers = allUsers.filter(user => user !== userToDelete);
|
233 |
-
renderUserList(allUsers);
|
234 |
-
|
235 |
-
// モーダルを閉じる
|
236 |
-
hideDeleteModal();
|
237 |
-
|
238 |
-
// 成功メッセージ表示(オプション)
|
239 |
-
const successMessage = document.createElement('div');
|
240 |
-
successMessage.className = 'success-message';
|
241 |
-
successMessage.innerHTML = `<div style="background: rgba(39, 174, 96, 0.2); color: white; padding: 10px; border-radius: 6px; margin-bottom: 10px; text-align: center;">メンバーを削除しました</div>`;
|
242 |
-
document.querySelector('.container').prepend(successMessage);
|
243 |
-
|
244 |
-
// 数秒後にメッセージを消す
|
245 |
-
setTimeout(() => {
|
246 |
-
successMessage.remove();
|
247 |
-
}, 3000);
|
248 |
-
} else {
|
249 |
-
alert('削除に失敗しました: ' + (data.message || 'Unknown error'));
|
250 |
-
hideDeleteModal();
|
251 |
-
}
|
252 |
-
})
|
253 |
-
.catch(error => {
|
254 |
-
console.error('Error deleting user:', error);
|
255 |
-
alert('サーバーとの通信中にエラーが発生しました');
|
256 |
-
hideDeleteModal();
|
257 |
-
});
|
258 |
-
}
|
259 |
-
|
260 |
-
// ハンバーガーメニュー表示/非表示の切り替え
|
261 |
-
function toggleMenu(event) {
|
262 |
-
event.stopPropagation();
|
263 |
-
const menu = document.getElementById('menu');
|
264 |
-
menu.classList.toggle('open');
|
265 |
-
}
|
266 |
-
|
267 |
-
// メニュー外クリックでメニューを閉じる
|
268 |
-
function closeMenu(event) {
|
269 |
-
const menu = document.getElementById('menu');
|
270 |
-
if (menu.classList.contains('open') && !menu.contains(event.target) && event.target.id !== 'menuButton') {
|
271 |
-
menu.classList.remove('open');
|
272 |
-
}
|
273 |
-
}
|
274 |
-
|
275 |
-
// 各画面へのナビゲーション関数
|
276 |
-
function showUserRegister() {
|
277 |
-
window.location.href = '/userregister';
|
278 |
-
}
|
279 |
-
|
280 |
-
function showIndex() {
|
281 |
-
window.location.href = '/index';
|
282 |
-
}
|
283 |
-
|
284 |
-
function showResults() {
|
285 |
-
window.location.href = '/feedback';
|
286 |
-
}
|
287 |
-
|
288 |
-
function showTalkDetail() {
|
289 |
-
window.location.href = '/talk_detail';
|
290 |
-
}
|
291 |
-
|
292 |
-
function resetAction() {
|
293 |
-
window.location.href = '/reset_html';
|
294 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/register_record.js
DELETED
@@ -1,150 +0,0 @@
|
|
1 |
-
let mediaRecorder;
|
2 |
-
let audioChunks = [];
|
3 |
-
let userCount = 0; // 追加されたメンバー数を保持
|
4 |
-
let isRecording = false; // 録音中かどうかを判定するフラグ
|
5 |
-
let currentRecordingButton = null; // 現在録音中のボタンを保持
|
6 |
-
let userNames = [];
|
7 |
-
|
8 |
-
function toggleRecording(button) {
|
9 |
-
button.classList.toggle("recording");
|
10 |
-
}
|
11 |
-
|
12 |
-
async function startRecording(button) {
|
13 |
-
if (isRecording && currentRecordingButton !== button) return; // 他の人が録音中なら何もしない
|
14 |
-
isRecording = true; // 録音中に設定
|
15 |
-
currentRecordingButton = button; // 録音中のボタンを記録
|
16 |
-
|
17 |
-
try {
|
18 |
-
const stream = await navigator.mediaDevices.getUserMedia({
|
19 |
-
audio: true,
|
20 |
-
});
|
21 |
-
mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
|
22 |
-
audioChunks = [];
|
23 |
-
mediaRecorder.ondataavailable = (e) => audioChunks.push(e.data);
|
24 |
-
mediaRecorder.onstop = () => {
|
25 |
-
sendAudioChunks(audioChunks, button); // ボタン情報を渡す
|
26 |
-
audioChunks = [];
|
27 |
-
isRecording = false; // 録音停止後はフラグを戻す
|
28 |
-
currentRecordingButton = null; // 録音ボタンを解除
|
29 |
-
};
|
30 |
-
mediaRecorder.start();
|
31 |
-
toggleRecording(button);
|
32 |
-
} catch (err) {
|
33 |
-
console.error("マイクアクセスに失敗しました:", err);
|
34 |
-
isRecording = false; // エラー発生時もフラグを戻す
|
35 |
-
currentRecordingButton = null;
|
36 |
-
}
|
37 |
-
}
|
38 |
-
|
39 |
-
function stopRecording(button) {
|
40 |
-
if (!isRecording) return; // 録音中でない場合は停止しない
|
41 |
-
mediaRecorder.stop();
|
42 |
-
toggleRecording(button);
|
43 |
-
}
|
44 |
-
|
45 |
-
function handleRecording(e) {
|
46 |
-
const button = e.target.closest(".record-button");
|
47 |
-
if (button) {
|
48 |
-
if (isRecording && currentRecordingButton !== button) {
|
49 |
-
// 他の人が録音中なら反応しない
|
50 |
-
return;
|
51 |
-
}
|
52 |
-
if (mediaRecorder && mediaRecorder.state === "recording") {
|
53 |
-
stopRecording(button);
|
54 |
-
} else {
|
55 |
-
startRecording(button);
|
56 |
-
}
|
57 |
-
}
|
58 |
-
}
|
59 |
-
|
60 |
-
function sendAudioChunks(chunks, button) {
|
61 |
-
// 引数に button を追加
|
62 |
-
const audioBlob = new Blob(chunks, { type: "audio/wav" });
|
63 |
-
const reader = new FileReader();
|
64 |
-
reader.onloadend = () => {
|
65 |
-
const base64String = reader.result.split(",")[1]; // Base64エンコードされた音声データ
|
66 |
-
// フォームの取得方法を変更
|
67 |
-
const form = button.closest(".user-item")?.querySelector("form")
|
68 |
-
const nameInput = form?.querySelector('input[name="name"]');
|
69 |
-
const name = nameInput ? nameInput.value : "unknown"; // 名前がない
|
70 |
-
fetch("/upload_base_audio", {
|
71 |
-
method: "POST",
|
72 |
-
headers: {
|
73 |
-
"Content-Type": "application/json",
|
74 |
-
},
|
75 |
-
body: JSON.stringify({ audio_data: base64String, name: name }),
|
76 |
-
})
|
77 |
-
.then((response) => response.json())
|
78 |
-
.then((data) => {
|
79 |
-
// エラー処理のみ残す
|
80 |
-
if (data.error) {
|
81 |
-
alert("エラー: " + data.error);
|
82 |
-
console.error(data.details);
|
83 |
-
}
|
84 |
-
// 成功時の処理(ボタンの有効化など)
|
85 |
-
else {
|
86 |
-
console.log("音声データ送信成功:", data);
|
87 |
-
userNames.push(name);
|
88 |
-
// 必要に応じて、ここでUIの変更(ボタンの有効化など)を行う
|
89 |
-
// 例: button.disabled = true; // 送信ボタンを無効化
|
90 |
-
// 例: button.classList.remove("recording"); //録音中のスタイルを解除
|
91 |
-
}
|
92 |
-
})
|
93 |
-
.catch((error) => {
|
94 |
-
console.error("エラー:", error);
|
95 |
-
});
|
96 |
-
};
|
97 |
-
reader.readAsDataURL(audioBlob);
|
98 |
-
}
|
99 |
-
|
100 |
-
// メンバー選択画面表示
|
101 |
-
function showUserSelect() {
|
102 |
-
window.location.href = "/userselect";
|
103 |
-
}
|
104 |
-
|
105 |
-
// Add user function
|
106 |
-
function addUser() {
|
107 |
-
const userName = prompt("ユーザー名を入力してください");
|
108 |
-
if (userName) {
|
109 |
-
const userList = document.getElementById("people-list");
|
110 |
-
const userDiv = document.createElement("div");
|
111 |
-
userDiv.classList.add(
|
112 |
-
"user-item", // 追加
|
113 |
-
"bg-gray-700",
|
114 |
-
"p-4",
|
115 |
-
"rounded-lg",
|
116 |
-
"text-white",
|
117 |
-
"flex",
|
118 |
-
"justify-between",
|
119 |
-
"items-center",
|
120 |
-
"flex-wrap", // 追加
|
121 |
-
"gap-3" // 追加
|
122 |
-
);
|
123 |
-
userDiv.innerHTML = `
|
124 |
-
<form
|
125 |
-
action="/submit"
|
126 |
-
method="POST"
|
127 |
-
class="flex items-center space-x-2 w-full sm:w-auto"
|
128 |
-
onsubmit="event.preventDefault();"
|
129 |
-
>
|
130 |
-
<input
|
131 |
-
type="text"
|
132 |
-
name="name"
|
133 |
-
placeholder="名前を入力"
|
134 |
-
value="${userName}"
|
135 |
-
class="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-700 text-white"
|
136 |
-
/>
|
137 |
-
<button type="button" class="record-button" aria-label="音声録音開始">
|
138 |
-
<div class="record-icon"></div>
|
139 |
-
</button>
|
140 |
-
</form>
|
141 |
-
`;
|
142 |
-
userDiv
|
143 |
-
.querySelector(".record-button")
|
144 |
-
.addEventListener("click", handleRecording);
|
145 |
-
userList.appendChild(userDiv);
|
146 |
-
userCount++;
|
147 |
-
}
|
148 |
-
}
|
149 |
-
|
150 |
-
document.getElementById("add-btn").addEventListener("click", addUser);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/reset.js
DELETED
@@ -1,97 +0,0 @@
|
|
1 |
-
async function fetchAndRenderMembers() {
|
2 |
-
try {
|
3 |
-
const response = await fetch("/confirm");
|
4 |
-
if (!response.ok) {
|
5 |
-
throw new Error(`HTTP error! status: ${response.status}`);
|
6 |
-
}
|
7 |
-
|
8 |
-
const data = await response.json();
|
9 |
-
if (!data || !data.members || !Array.isArray(data.members)) {
|
10 |
-
console.error("Invalid member data received:", data);
|
11 |
-
return;
|
12 |
-
}
|
13 |
-
|
14 |
-
const members = data.members;
|
15 |
-
console.log(members);
|
16 |
-
const container = document.getElementById("memberCheckboxes");
|
17 |
-
container.innerHTML = ""; // 既存の中身を消去
|
18 |
-
|
19 |
-
members.forEach((name) => {
|
20 |
-
const newItem = document.createElement("div");
|
21 |
-
newItem.className = "flex items-center gap-3 mb-2";
|
22 |
-
console.log(name);
|
23 |
-
newItem.innerHTML = `
|
24 |
-
<input
|
25 |
-
type="checkbox"
|
26 |
-
name="members"
|
27 |
-
value="${name}"
|
28 |
-
id="checkbox-${name}"
|
29 |
-
class="px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-700 text-white"
|
30 |
-
/>
|
31 |
-
<lavel for="checkbox-${name}" class="text-lg">${name}</label>
|
32 |
-
`;
|
33 |
-
|
34 |
-
container.appendChild(newItem);
|
35 |
-
});
|
36 |
-
} catch (error) {
|
37 |
-
console.error("Error fetching members:", error);
|
38 |
-
}
|
39 |
-
}
|
40 |
-
|
41 |
-
// メンバー削除ボタンのイベントリスナー(修正箇所)
|
42 |
-
document.getElementById("reset_btn").addEventListener("click", () => {
|
43 |
-
// メンバー送信処理
|
44 |
-
|
45 |
-
const checkboxes = document.querySelectorAll(
|
46 |
-
'#memberCheckboxes input[type="checkbox"]:checked'
|
47 |
-
);
|
48 |
-
|
49 |
-
const selectedNames = Array.from(checkboxes).map((cb) => cb.value);
|
50 |
-
|
51 |
-
if (selectedNames.length === 0) {
|
52 |
-
alert("メンバーを1人以上選択してください。");
|
53 |
-
return;
|
54 |
-
}
|
55 |
-
|
56 |
-
fetch("/reset_member", {
|
57 |
-
method: "POST",
|
58 |
-
headers: {
|
59 |
-
"Content-Type": "application/json",
|
60 |
-
},
|
61 |
-
body: JSON.stringify({ names: selectedNames }),
|
62 |
-
})
|
63 |
-
.then((response) => {
|
64 |
-
if (!response.ok) {
|
65 |
-
throw new Error("送信に失敗しました");
|
66 |
-
}
|
67 |
-
// サーバーからのJSONレスポンスを期待する
|
68 |
-
return response.json();
|
69 |
-
})
|
70 |
-
.then((data) => {
|
71 |
-
alert("選択されたメンバーを削除しました。");
|
72 |
-
fetchAndRenderMembers(); // 再描画
|
73 |
-
})
|
74 |
-
.catch((error) => {
|
75 |
-
console.error("送信エラー:", error);
|
76 |
-
// エラーメッセージを表示する
|
77 |
-
alert(`送信エラー: ${error.message}`);
|
78 |
-
});
|
79 |
-
});
|
80 |
-
|
81 |
-
// ページが表示されたときにチェックボックスを生成
|
82 |
-
window.addEventListener("DOMContentLoaded", fetchAndRenderMembers);
|
83 |
-
|
84 |
-
// 「全選択」ボタン処理
|
85 |
-
document.getElementById("select-all").addEventListener("click", () => {
|
86 |
-
const checkboxes = document.querySelectorAll(
|
87 |
-
'#memberCheckboxes input[type="checkbox"]'
|
88 |
-
);
|
89 |
-
checkboxes.forEach((checkbox) => {
|
90 |
-
checkbox.checked = true;
|
91 |
-
});
|
92 |
-
});
|
93 |
-
|
94 |
-
// 他のページに移動する関数
|
95 |
-
function showRecorder() {
|
96 |
-
window.location.href = "/index";
|
97 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/style.css
DELETED
@@ -1,128 +0,0 @@
|
|
1 |
-
@charset "UTF-8";
|
2 |
-
body {
|
3 |
-
font-family: Arial, sans-serif;
|
4 |
-
padding: 20px;
|
5 |
-
background-color: #f4f4f4;
|
6 |
-
height: 100vh;
|
7 |
-
display: flex;
|
8 |
-
justify-content: center;
|
9 |
-
align-items: center;
|
10 |
-
}
|
11 |
-
|
12 |
-
h2 {
|
13 |
-
margin-bottom: 20px;
|
14 |
-
text-align: center;
|
15 |
-
}
|
16 |
-
|
17 |
-
a {
|
18 |
-
text-decoration: none;
|
19 |
-
color: #000000cc;
|
20 |
-
}
|
21 |
-
a:hover {
|
22 |
-
text-decoration: underline;
|
23 |
-
}
|
24 |
-
.container {
|
25 |
-
max-width: 800px;
|
26 |
-
|
27 |
-
background-color: #fff;
|
28 |
-
padding: 20px 80px;
|
29 |
-
border-radius: 8px;
|
30 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
31 |
-
}
|
32 |
-
|
33 |
-
#transcription {
|
34 |
-
white-space: pre-wrap;
|
35 |
-
padding: 10px;
|
36 |
-
background-color: #e9e9e9;
|
37 |
-
border-radius: 4px;
|
38 |
-
margin-bottom: 20px;
|
39 |
-
max-height: 400px;
|
40 |
-
overflow-y: auto;
|
41 |
-
}
|
42 |
-
button {
|
43 |
-
margin: 5px;
|
44 |
-
padding: 10px 10px;
|
45 |
-
border: none;
|
46 |
-
border-radius: 4px;
|
47 |
-
background-color: #007bff;
|
48 |
-
color: #fff;
|
49 |
-
cursor: pointer;
|
50 |
-
}
|
51 |
-
.history-button {
|
52 |
-
margin-top: 20px;
|
53 |
-
|
54 |
-
padding: 10px 20px;
|
55 |
-
background-color: #007bff;
|
56 |
-
color: white;
|
57 |
-
border: none;
|
58 |
-
border-radius: 5px;
|
59 |
-
cursor: pointer;
|
60 |
-
}
|
61 |
-
history-button:hover {
|
62 |
-
background-color: #0056b3;
|
63 |
-
}
|
64 |
-
|
65 |
-
.flex {
|
66 |
-
display: flex;
|
67 |
-
justify-content: center;
|
68 |
-
}
|
69 |
-
.new-person {
|
70 |
-
text-align: center;
|
71 |
-
}
|
72 |
-
|
73 |
-
.controls {
|
74 |
-
display: flex;
|
75 |
-
flex-direction: column;
|
76 |
-
align-items: center;
|
77 |
-
}
|
78 |
-
.record-button {
|
79 |
-
width: 80px;
|
80 |
-
height: 80px;
|
81 |
-
background-color: transparent;
|
82 |
-
border-radius: 50%;
|
83 |
-
|
84 |
-
display: flex;
|
85 |
-
justify-content: center;
|
86 |
-
align-items: center;
|
87 |
-
cursor: pointer;
|
88 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
|
89 |
-
transition: all 0.2s ease;
|
90 |
-
}
|
91 |
-
|
92 |
-
.record-icon {
|
93 |
-
width: 60px;
|
94 |
-
height: 60px;
|
95 |
-
background-color: #d32f2f;
|
96 |
-
border-radius: 50%;
|
97 |
-
transition: all 0.2s ease;
|
98 |
-
}
|
99 |
-
|
100 |
-
.recording .record-icon {
|
101 |
-
width: 40px;
|
102 |
-
height: 40px;
|
103 |
-
border-radius: 10%;
|
104 |
-
}
|
105 |
-
|
106 |
-
.record-p {
|
107 |
-
border: 2px dashed #0000008c;
|
108 |
-
}
|
109 |
-
|
110 |
-
.disabled {
|
111 |
-
background-color: gray;
|
112 |
-
cursor: not-allowed;
|
113 |
-
}
|
114 |
-
|
115 |
-
.record-icon.recording {
|
116 |
-
width: 40px;
|
117 |
-
height: 40px;
|
118 |
-
border-radius: 0;
|
119 |
-
}
|
120 |
-
|
121 |
-
.new-person-right-container {
|
122 |
-
padding-left: 20px;
|
123 |
-
}
|
124 |
-
|
125 |
-
.record-container {
|
126 |
-
display: flex;
|
127 |
-
justify-content: center;
|
128 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/talk_detail.js
DELETED
@@ -1,35 +0,0 @@
|
|
1 |
-
async function displayTranscription() {
|
2 |
-
const transcriptionElement = document.getElementById("transcription");
|
3 |
-
const loader = document.getElementById("loader");
|
4 |
-
loader.style.display = "block";
|
5 |
-
|
6 |
-
try {
|
7 |
-
const response = await fetch("/transcription");
|
8 |
-
if (!response.ok) throw new Error("データ取得に失敗しました。");
|
9 |
-
|
10 |
-
const data = await response.json();
|
11 |
-
const conversations = data.transcription;
|
12 |
-
|
13 |
-
if (!data || !data.transcription) {
|
14 |
-
throw new Error("会話データが見つかりませんでした。");
|
15 |
-
}
|
16 |
-
|
17 |
-
transcriptionElement.innerHTML = conversations;
|
18 |
-
loader.style.display = "none";
|
19 |
-
console.log(conversations);
|
20 |
-
} catch (error) {
|
21 |
-
loader.style.display = "none";
|
22 |
-
transcriptionElement.textContent = `エラー: ${error.message}`;
|
23 |
-
console.error("データ取得エラー:", error);
|
24 |
-
}
|
25 |
-
}
|
26 |
-
|
27 |
-
displayTranscription();
|
28 |
-
|
29 |
-
function showRecorder() {
|
30 |
-
window.location.href = "/index";
|
31 |
-
}
|
32 |
-
|
33 |
-
function showFeedback() {
|
34 |
-
window.location.href = "/feedback";
|
35 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tailwind.config.js
DELETED
@@ -1,27 +0,0 @@
|
|
1 |
-
/** @type {import('tailwindcss').Config} */
|
2 |
-
module.exports = {
|
3 |
-
content: [
|
4 |
-
"./templates/index.html", // HTMLファイルのパス(Flaskなどのテンプレートエンジンを考慮)
|
5 |
-
"./templates/feedback.html",
|
6 |
-
"./templates/reset.html",
|
7 |
-
"./templates/talkDetail.html",
|
8 |
-
"./templates/userRegister.html",
|
9 |
-
"./templates/userSelect.html",
|
10 |
-
],
|
11 |
-
darkMode: 'class', // ダークモードをクラスベースで適用
|
12 |
-
theme: {
|
13 |
-
extend: {
|
14 |
-
colors: {
|
15 |
-
primary: '#1f2937', // メインカラー
|
16 |
-
secondary: '#2c3e50', // サブカラー
|
17 |
-
},
|
18 |
-
fontFamily: {
|
19 |
-
sans: ['Arial', 'sans-serif'], // デフォルトフォント
|
20 |
-
},
|
21 |
-
borderRadius: {
|
22 |
-
'xl': '1rem', // 角丸の拡張
|
23 |
-
},
|
24 |
-
},
|
25 |
-
},
|
26 |
-
plugins: [],
|
27 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
talkDetail.html
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="ja">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>会話表示画面</title>
|
7 |
+
<style>
|
8 |
+
body {
|
9 |
+
font-family: Arial, sans-serif;
|
10 |
+
margin: 0;
|
11 |
+
padding: 20px;
|
12 |
+
background-color: #f4f4f4;
|
13 |
+
height: 100vh;
|
14 |
+
display: flex;
|
15 |
+
justify-content: center;
|
16 |
+
align-items: center;
|
17 |
+
}
|
18 |
+
.container {
|
19 |
+
max-width: 800px;
|
20 |
+
background-color: #fff;
|
21 |
+
padding: 20px;
|
22 |
+
border-radius: 8px;
|
23 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
24 |
+
}
|
25 |
+
h2 {
|
26 |
+
margin-bottom: 20px;
|
27 |
+
}
|
28 |
+
#transcription {
|
29 |
+
white-space: pre-wrap;
|
30 |
+
padding: 10px;
|
31 |
+
background-color: #e9e9e9;
|
32 |
+
border-radius: 4px;
|
33 |
+
margin-bottom: 20px;
|
34 |
+
max-height: 400px;
|
35 |
+
overflow-y: auto;
|
36 |
+
}
|
37 |
+
button {
|
38 |
+
margin: 5px;
|
39 |
+
padding: 10px 20px;
|
40 |
+
border: none;
|
41 |
+
border-radius: 4px;
|
42 |
+
background-color: #007bff;
|
43 |
+
color: #fff;
|
44 |
+
cursor: pointer;
|
45 |
+
}
|
46 |
+
button:hover {
|
47 |
+
background-color: #0056b3;
|
48 |
+
}
|
49 |
+
</style>
|
50 |
+
</head>
|
51 |
+
<body>
|
52 |
+
<div class="container">
|
53 |
+
<h2>会話の文字起こし表示</h2>
|
54 |
+
<div id="transcription">ここに会話内容が表示されます。</div>
|
55 |
+
<button onclick="showRecorder()">録音画面を表示</button>
|
56 |
+
<button onclick="showHistory()">フィードバック画面を表示</button>
|
57 |
+
</div>
|
58 |
+
|
59 |
+
<script>
|
60 |
+
// 会話データを表示
|
61 |
+
async function displayTranscription() {
|
62 |
+
const transcriptionElement = document.getElementById('transcription');
|
63 |
+
|
64 |
+
try {
|
65 |
+
// バックエンドからデータを取得(デモ用のURLを指定)
|
66 |
+
const response = await fetch('/api/transcription');
|
67 |
+
if (!response.ok) throw new Error('データ取得に失敗しました。');
|
68 |
+
|
69 |
+
const data = await response.json();
|
70 |
+
|
71 |
+
// 会話内容を整形して表示
|
72 |
+
const formattedText = data.conversations.map((conv, index) =>
|
73 |
+
`【${conv.speaker}】 ${conv.text}`
|
74 |
+
).join('\n');
|
75 |
+
|
76 |
+
transcriptionElement.textContent = formattedText;
|
77 |
+
} catch (error) {
|
78 |
+
transcriptionElement.textContent = `エラー: ${error.message}`;
|
79 |
+
console.error('データ取得エラー:', error);
|
80 |
+
}
|
81 |
+
}
|
82 |
+
|
83 |
+
// 録音画面に戻る
|
84 |
+
function showRecorder() {
|
85 |
+
window.location.href = 'index.html';
|
86 |
+
}
|
87 |
+
|
88 |
+
// フィードバック画面に移動
|
89 |
+
function showHistory() {
|
90 |
+
window.location.href = 'feedback.html';
|
91 |
+
}
|
92 |
+
|
93 |
+
// 初期化処理
|
94 |
+
displayTranscription();
|
95 |
+
</script>
|
96 |
+
</body>
|
97 |
+
</html>
|
templates/feedback.html
DELETED
@@ -1,81 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="ja" class="dark">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8" />
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
-
<title>会話フィードバック画面</title>
|
7 |
-
<link
|
8 |
-
rel="stylesheet"
|
9 |
-
href="{{ url_for('static', filename='loading.css') }}"
|
10 |
-
/>
|
11 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
12 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
13 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='menu.css') }}">
|
14 |
-
<script src="https://use.fontawesome.com/releases/v5.10.0/js/all.js"></script>
|
15 |
-
</head>
|
16 |
-
<body>
|
17 |
-
<div class="loader" id="loader">
|
18 |
-
<div class="one"></div>
|
19 |
-
<div class="two"></div>
|
20 |
-
<div class="three"></div>
|
21 |
-
<div class="four"></div>
|
22 |
-
</div>
|
23 |
-
<div class="main-content relative">
|
24 |
-
<!-- Title -->
|
25 |
-
<div class="main-title">会話フィードバック</div>
|
26 |
-
|
27 |
-
<!-- Hamburger Menu -->
|
28 |
-
<div class="absolute top-4 left-4">
|
29 |
-
<button
|
30 |
-
id="menuButton"
|
31 |
-
class="text-white text-2xl focus:outline-none"
|
32 |
-
onclick="toggleMenu(event)"
|
33 |
-
>
|
34 |
-
<i class="fas fa-bars"></i>
|
35 |
-
</button>
|
36 |
-
|
37 |
-
<!-- Menu Content -->
|
38 |
-
<div
|
39 |
-
id="menu"
|
40 |
-
class="absolute top-0 left-0 h-full w-64 bg-gray-800 text-white transform -translate-x-full transition-transform duration-300 ease-in-out opacity-0 visibility-hidden"
|
41 |
-
>
|
42 |
-
<div class="px-4 py-2 text-lg font-semibold">メニュー</div>
|
43 |
-
<button onclick="showUserRegister()">
|
44 |
-
<i class="fas fa-user-plus"></i> メンバーを追加
|
45 |
-
</button>
|
46 |
-
<button onclick="showUserSelect()">
|
47 |
-
<i class="fas fa-users"></i> メンバーを選択
|
48 |
-
</button>
|
49 |
-
<button onclick="showRecorder()">
|
50 |
-
<i class="fas fa-microphone"></i> 録音画面を表示
|
51 |
-
</button>
|
52 |
-
<button onclick="showResults()">
|
53 |
-
<i class="fas fa-chart-bar"></i> フィードバックを表示
|
54 |
-
</button>
|
55 |
-
<button onclick="showTalkDetail()">
|
56 |
-
<i class="fas fa-comments"></i> 会話詳細を表示
|
57 |
-
</button>
|
58 |
-
<button onclick="resetAction()">
|
59 |
-
<i class="fas fa-redo"></i> リセット
|
60 |
-
</button>
|
61 |
-
<button onclick="toggleMenu(event)">
|
62 |
-
<i class="fas fa-times"></i> 閉じる
|
63 |
-
</button>
|
64 |
-
</div>
|
65 |
-
</div>
|
66 |
-
|
67 |
-
<!-- Feedback Details -->
|
68 |
-
<div class="text-xl font-semibold mb-6" id="level">会話レベル:</div>
|
69 |
-
<div class="text-lg mb-2" id="Harassment_bool">ハラスメントの有無:</div>
|
70 |
-
<div class="text-lg mb-2" id="Harassment_type">ハラスメントの種類:</div>
|
71 |
-
<div class="text-lg mb-2" id="Harassment_loop">繰り返しの程度:</div>
|
72 |
-
<div class="text-lg mb-2" id="Harassment_comfort">会話の心地よさ:</div>
|
73 |
-
<div class="text-lg mb-2" id="Harassment_volume">
|
74 |
-
非難またはハラスメントの程度:
|
75 |
-
</div>
|
76 |
-
</div>
|
77 |
-
|
78 |
-
<script src="{{ url_for('static', filename='menu.js') }}"></script>
|
79 |
-
<script src="{{ url_for('static', filename='feedback.js') }}"></script>
|
80 |
-
</body>
|
81 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/history.html
DELETED
@@ -1,125 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="ja">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8" />
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
-
<title>会話履歴</title>
|
7 |
-
<link
|
8 |
-
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
|
9 |
-
rel="stylesheet"
|
10 |
-
/>
|
11 |
-
<style>
|
12 |
-
body {
|
13 |
-
margin: 0;
|
14 |
-
padding: 0;
|
15 |
-
font-family: Arial, sans-serif;
|
16 |
-
background-color: #fff;
|
17 |
-
color: #000;
|
18 |
-
}
|
19 |
-
header {
|
20 |
-
padding: 16px;
|
21 |
-
background-color: #f5f5f5;
|
22 |
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
23 |
-
font-size: 20px;
|
24 |
-
font-weight: bold;
|
25 |
-
text-align: center;
|
26 |
-
}
|
27 |
-
.recording-list {
|
28 |
-
padding: 16px;
|
29 |
-
}
|
30 |
-
.record-item {
|
31 |
-
display: flex;
|
32 |
-
justify-content: space-between;
|
33 |
-
align-items: center;
|
34 |
-
padding: 12px;
|
35 |
-
margin: 8px 0;
|
36 |
-
border-radius: 8px;
|
37 |
-
background-color: #e9e9e9;
|
38 |
-
transition: background-color 0.2s ease;
|
39 |
-
cursor: pointer;
|
40 |
-
}
|
41 |
-
.record-item:hover {
|
42 |
-
background-color: #d3d3d3;
|
43 |
-
}
|
44 |
-
.title {
|
45 |
-
font-size: 18px;
|
46 |
-
font-weight: bold;
|
47 |
-
}
|
48 |
-
.timestamp {
|
49 |
-
font-size: 14px;
|
50 |
-
color: #555;
|
51 |
-
}
|
52 |
-
.record-item-template {
|
53 |
-
display: none;
|
54 |
-
}
|
55 |
-
button {
|
56 |
-
margin: 5px;
|
57 |
-
padding: 10px 20px;
|
58 |
-
border: none;
|
59 |
-
border-radius: 4px; /* 4pxに統一 */
|
60 |
-
background-color: #007bff;
|
61 |
-
color: #fff;
|
62 |
-
cursor: pointer;
|
63 |
-
position: fixed; /* 画面に固定 */
|
64 |
-
left: 50%; /* 水平方向の中央 */
|
65 |
-
transform: translateX(-50%); /* 中央に配置 */
|
66 |
-
bottom: 20px; /* 画面下に配置 */
|
67 |
-
}
|
68 |
-
.history-button:hover {
|
69 |
-
background-color: #0056b3;
|
70 |
-
}
|
71 |
-
button:hover {
|
72 |
-
background-color: #0056b3;
|
73 |
-
}
|
74 |
-
</style>
|
75 |
-
<script>
|
76 |
-
const recordings = [
|
77 |
-
{ title: "Recording 1", time: "01:15:35", date: "2/26/2025" },
|
78 |
-
{ title: "Recording 2", time: "00:16:41", date: "2/10/2025" },
|
79 |
-
];
|
80 |
-
|
81 |
-
function createRecordItem(title, time, date) {
|
82 |
-
const template = document.querySelector(".record-item-template");
|
83 |
-
const item = template.cloneNode(true);
|
84 |
-
item.classList.remove("record-item-template");
|
85 |
-
item.style.display = "flex";
|
86 |
-
item.querySelector(".title").textContent = title;
|
87 |
-
item.querySelector(".timestamp").textContent = `${time} | ${date}`;
|
88 |
-
item.onclick = () => (location.href = "talkDetail");
|
89 |
-
return item;
|
90 |
-
}
|
91 |
-
|
92 |
-
function renderRecordings() {
|
93 |
-
const list = document.querySelector(".recording-list");
|
94 |
-
list.innerHTML = "";
|
95 |
-
recordings.forEach((rec) => {
|
96 |
-
const item = createRecordItem(rec.title, rec.time, rec.date);
|
97 |
-
list.appendChild(item);
|
98 |
-
});
|
99 |
-
}
|
100 |
-
|
101 |
-
window.onload = renderRecordings;
|
102 |
-
|
103 |
-
//画面遷移
|
104 |
-
function showRecorder() {
|
105 |
-
// 録音画面へ遷移
|
106 |
-
window.location.href = "/index";
|
107 |
-
}
|
108 |
-
</script>
|
109 |
-
</head>
|
110 |
-
<body>
|
111 |
-
<header>All Recordings</header>
|
112 |
-
|
113 |
-
<div class="recording-list">
|
114 |
-
<div class="record-item record-item-template">
|
115 |
-
<div>
|
116 |
-
<div class="title">Recording Title</div>
|
117 |
-
<div class="timestamp">00:00:00 | 1/1/2025</div>
|
118 |
-
</div>
|
119 |
-
</div>
|
120 |
-
</div>
|
121 |
-
<button class="history-button" id="detailButton" onclick="showRecorder()">
|
122 |
-
録音画面を表示
|
123 |
-
</button>
|
124 |
-
</body>
|
125 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/index.html
DELETED
@@ -1,219 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="ja">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8" />
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
-
<title>JustTalk - Voice Analysis</title>
|
7 |
-
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
8 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
9 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
10 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='menu.css') }}">
|
11 |
-
<script src="https://use.fontawesome.com/releases/v5.10.0/js/all.js"></script>
|
12 |
-
<style>
|
13 |
-
/* Custom Chart.js Styles */
|
14 |
-
#speechChart {
|
15 |
-
background-color: rgba(255, 255, 255, 0.05);
|
16 |
-
border-radius: 10px;
|
17 |
-
padding: 10px;
|
18 |
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
19 |
-
}
|
20 |
-
|
21 |
-
/* Record Button Styles */
|
22 |
-
.record-button {
|
23 |
-
width: 90px;
|
24 |
-
height: 90px;
|
25 |
-
background-color: transparent;
|
26 |
-
border-radius: 50%;
|
27 |
-
border: 5px solid white;
|
28 |
-
display: flex;
|
29 |
-
justify-content: center;
|
30 |
-
align-items: center;
|
31 |
-
cursor: pointer;
|
32 |
-
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.5);
|
33 |
-
transition: all 0.2s ease;
|
34 |
-
}
|
35 |
-
|
36 |
-
.record-icon {
|
37 |
-
width: 70px;
|
38 |
-
height: 70px;
|
39 |
-
background-color: #e53e3e;
|
40 |
-
border-radius: 50%;
|
41 |
-
transition: all 0.2s ease;
|
42 |
-
}
|
43 |
-
|
44 |
-
.recording .record-icon {
|
45 |
-
width: 50px;
|
46 |
-
height: 50px;
|
47 |
-
border-radius: 15%;
|
48 |
-
background-color: #c53030;
|
49 |
-
}
|
50 |
-
|
51 |
-
.icon i {
|
52 |
-
font-size: 24px;
|
53 |
-
}
|
54 |
-
|
55 |
-
/* Member Chips Style */
|
56 |
-
.member-chips {
|
57 |
-
display: flex;
|
58 |
-
flex-wrap: wrap;
|
59 |
-
justify-content: center;
|
60 |
-
gap: 0.5rem;
|
61 |
-
margin-bottom: 1.5rem;
|
62 |
-
}
|
63 |
-
|
64 |
-
.member-chip {
|
65 |
-
background: rgba(255, 255, 255, 0.2);
|
66 |
-
border-radius: 999px;
|
67 |
-
padding: 0.4rem 0.8rem;
|
68 |
-
font-size: 0.8rem;
|
69 |
-
color: white;
|
70 |
-
display: inline-flex;
|
71 |
-
align-items: center;
|
72 |
-
gap: 0.5rem;
|
73 |
-
}
|
74 |
-
|
75 |
-
.member-avatar {
|
76 |
-
width: 1.5rem;
|
77 |
-
height: 1.5rem;
|
78 |
-
background: rgba(255, 255, 255, 0.3);
|
79 |
-
border-radius: 50%;
|
80 |
-
display: flex;
|
81 |
-
align-items: center;
|
82 |
-
justify-content: center;
|
83 |
-
font-size: 0.75rem;
|
84 |
-
}
|
85 |
-
</style>
|
86 |
-
</head>
|
87 |
-
<body onclick="closeMenu(event)">
|
88 |
-
<!-- Main Content Wrapper -->
|
89 |
-
<div class="main-content relative">
|
90 |
-
<!-- Title -->
|
91 |
-
<div class="main-title">JustTalk</div>
|
92 |
-
|
93 |
-
<!-- Hamburger Menu -->
|
94 |
-
<div class="absolute top-4 left-4">
|
95 |
-
<button
|
96 |
-
id="menuButton"
|
97 |
-
class="text-white text-2xl focus:outline-none"
|
98 |
-
onclick="toggleMenu(event)"
|
99 |
-
>
|
100 |
-
<i class="fas fa-bars"></i>
|
101 |
-
</button>
|
102 |
-
|
103 |
-
<!-- Menu Content -->
|
104 |
-
<div
|
105 |
-
id="menu"
|
106 |
-
class="absolute top-0 left-0 h-full w-64 bg-gray-800 text-white transform -translate-x-full transition-transform duration-300 ease-in-out opacity-0 visibility-hidden"
|
107 |
-
>
|
108 |
-
<div class="px-4 py-2 text-lg font-semibold">メニュー</div>
|
109 |
-
<button onclick="showUserRegister()">
|
110 |
-
<i class="fas fa-user-plus"></i> メンバーを追加
|
111 |
-
</button>
|
112 |
-
<button onclick="showUserSelect()">
|
113 |
-
<i class="fas fa-users"></i> メンバーを選択
|
114 |
-
</button>
|
115 |
-
<button onclick="showRecorder()">
|
116 |
-
<i class="fas fa-microphone"></i> 録音画面を表示
|
117 |
-
</button>
|
118 |
-
<button onclick="showResults()">
|
119 |
-
<i class="fas fa-chart-bar"></i> フィードバックを表示
|
120 |
-
</button>
|
121 |
-
<button onclick="showTalkDetail()">
|
122 |
-
<i class="fas fa-comments"></i> 会話詳細を表示
|
123 |
-
</button>
|
124 |
-
<button onclick="resetAction()">
|
125 |
-
<i class="fas fa-redo"></i> リセット
|
126 |
-
</button>
|
127 |
-
<button onclick="toggleMenu(event)">
|
128 |
-
<i class="fas fa-times"></i> 閉じる
|
129 |
-
</button>
|
130 |
-
</div>
|
131 |
-
</div>
|
132 |
-
|
133 |
-
<!-- Selected Member Chips -->
|
134 |
-
<div class="member-chips" id="memberChips">
|
135 |
-
<!-- Member chips will be dynamically added here -->
|
136 |
-
</div>
|
137 |
-
|
138 |
-
<!-- Chart Display -->
|
139 |
-
<div class="chart w-72 h-72 mb-5 mx-auto">
|
140 |
-
<canvas id="speechChart"></canvas>
|
141 |
-
</div>
|
142 |
-
|
143 |
-
<!-- Record Form -->
|
144 |
-
<form
|
145 |
-
id="recordForm"
|
146 |
-
action="/submit"
|
147 |
-
method="POST"
|
148 |
-
class="flex items-center justify-center space-x-2 w-full sm:w-auto"
|
149 |
-
onsubmit="event.preventDefault();"
|
150 |
-
>
|
151 |
-
<!-- Record Button -->
|
152 |
-
<button
|
153 |
-
type="button"
|
154 |
-
class="record-button"
|
155 |
-
id="recordButton"
|
156 |
-
onclick="toggleRecording()"
|
157 |
-
>
|
158 |
-
<div class="record-icon" id="recordIcon"></div>
|
159 |
-
</button>
|
160 |
-
</form>
|
161 |
-
</div>
|
162 |
-
|
163 |
-
<script src="{{ url_for('static', filename='process.js') }}"></script>
|
164 |
-
<script src="{{ url_for('static', filename='menu.js') }}"></script>
|
165 |
-
<script>
|
166 |
-
// 選択されたメンバーの表示を更新する関数
|
167 |
-
function updateSelectedMembers() {
|
168 |
-
// ローカルストレージから選択されたメンバーを取得
|
169 |
-
let selectedUsers = [];
|
170 |
-
try {
|
171 |
-
const stored = localStorage.getItem("selectedUsers");
|
172 |
-
if (stored) {
|
173 |
-
selectedUsers = JSON.parse(stored);
|
174 |
-
}
|
175 |
-
} catch (e) {
|
176 |
-
console.error("選択メンバーの読み込みエラー:", e);
|
177 |
-
}
|
178 |
-
|
179 |
-
// メンバーチップを表示
|
180 |
-
const memberChipsContainer = document.getElementById("memberChips");
|
181 |
-
memberChipsContainer.innerHTML = "";
|
182 |
-
|
183 |
-
if (selectedUsers.length === 0) {
|
184 |
-
// メンバーがいない場合の表示
|
185 |
-
const noMembers = document.createElement("div");
|
186 |
-
noMembers.className = "text-white opacity-50 text-sm";
|
187 |
-
noMembers.textContent = "メンバーが選択されていません";
|
188 |
-
memberChipsContainer.appendChild(noMembers);
|
189 |
-
return;
|
190 |
-
}
|
191 |
-
|
192 |
-
// 現在選択されているメンバー数を表示
|
193 |
-
const countChip = document.createElement("div");
|
194 |
-
countChip.className = "member-chip";
|
195 |
-
countChip.style.backgroundColor = "rgba(66, 153, 225, 0.5)"; // 青っぽい背景
|
196 |
-
countChip.innerHTML = `<i class="fas fa-users"></i> ${selectedUsers.length}人のメンバーを選択中`;
|
197 |
-
memberChipsContainer.appendChild(countChip);
|
198 |
-
|
199 |
-
// 各メンバーをチップとして表示
|
200 |
-
selectedUsers.forEach((member) => {
|
201 |
-
const chip = document.createElement("div");
|
202 |
-
chip.className = "member-chip";
|
203 |
-
|
204 |
-
const avatar = document.createElement("div");
|
205 |
-
avatar.className = "member-avatar";
|
206 |
-
avatar.textContent = member.substr(0, 1).toUpperCase();
|
207 |
-
|
208 |
-
chip.appendChild(avatar);
|
209 |
-
chip.appendChild(document.createTextNode(member));
|
210 |
-
|
211 |
-
memberChipsContainer.appendChild(chip);
|
212 |
-
});
|
213 |
-
}
|
214 |
-
|
215 |
-
// ページ読み込み時にメンバー表示を更新
|
216 |
-
document.addEventListener("DOMContentLoaded", updateSelectedMembers);
|
217 |
-
</script>
|
218 |
-
</body>
|
219 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/reset.html
DELETED
@@ -1,85 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="ja" class="dark">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8" />
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
-
<title>リセット画面</title>
|
7 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
-
<script src="https://use.fontawesome.com/releases/v5.10.0/js/all.js"></script>
|
9 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
10 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='menu.css') }}">
|
11 |
-
</head>
|
12 |
-
<body>
|
13 |
-
<div class="main-content relative">
|
14 |
-
<div class="p-6 dark:bg-gray-800 shadow-lg rounded-2xl">
|
15 |
-
<h2 class="text-2xl font-semibold mb-4 text-center">
|
16 |
-
メンバーを消去しますか?
|
17 |
-
</h2>
|
18 |
-
|
19 |
-
<!-- Hamburger Menu -->
|
20 |
-
<div class="absolute top-4 left-4">
|
21 |
-
<button
|
22 |
-
id="menuButton"
|
23 |
-
class="text-white text-2xl focus:outline-none"
|
24 |
-
onclick="toggleMenu(event)"
|
25 |
-
>
|
26 |
-
<i class="fas fa-bars"></i>
|
27 |
-
</button>
|
28 |
-
|
29 |
-
<!-- Menu Content -->
|
30 |
-
<div
|
31 |
-
id="menu"
|
32 |
-
class="absolute top-0 left-0 h-full w-64 bg-gray-800 text-white transform -translate-x-full transition-transform duration-300 ease-in-out opacity-0 visibility-hidden"
|
33 |
-
>
|
34 |
-
<div class="px-4 py-2 text-lg font-semibold">メニュー</div>
|
35 |
-
<button onclick="showUserRegister()">
|
36 |
-
<i class="fas fa-user-plus"></i> メンバーを追加
|
37 |
-
</button>
|
38 |
-
<button onclick="showUserSelect()">
|
39 |
-
<i class="fas fa-users"></i> メンバーを選択
|
40 |
-
</button>
|
41 |
-
<button onclick="showRecorder()">
|
42 |
-
<i class="fas fa-microphone"></i> 録音画面を表示
|
43 |
-
</button>
|
44 |
-
<button onclick="showResults()">
|
45 |
-
<i class="fas fa-chart-bar"></i> フィードバックを表示
|
46 |
-
</button>
|
47 |
-
<button onclick="showTalkDetail()">
|
48 |
-
<i class="fas fa-comments"></i> 会話詳細を表示
|
49 |
-
</button>
|
50 |
-
<button onclick="resetAction()">
|
51 |
-
<i class="fas fa-redo"></i> リセット
|
52 |
-
</button>
|
53 |
-
<button onclick="toggleMenu(event)">
|
54 |
-
<i class="fas fa-times"></i> 閉じる
|
55 |
-
</button>
|
56 |
-
</div>
|
57 |
-
</div>
|
58 |
-
<!-- Hamburger Menu End -->
|
59 |
-
|
60 |
-
<input type="button" id="select-all" value="全選択" />
|
61 |
-
<div id="memberCheckboxes">
|
62 |
-
<!--ここにチャックボックスを表示してほしい-->
|
63 |
-
</div>
|
64 |
-
|
65 |
-
<div class="flex justify-center gap-4">
|
66 |
-
<button
|
67 |
-
id="reset_btn"
|
68 |
-
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
|
69 |
-
>
|
70 |
-
メンバー削除
|
71 |
-
</button>
|
72 |
-
|
73 |
-
<button
|
74 |
-
class="px-6 py-2 bg-[#607d8b] text-white rounded-lg hover:bg-[#546e7a] transition-colors"
|
75 |
-
onclick="showRecorder()"
|
76 |
-
>
|
77 |
-
録音画面を表示
|
78 |
-
</button>
|
79 |
-
</div>
|
80 |
-
</div>
|
81 |
-
</div>
|
82 |
-
<script src="{{ url_for('static', filename='reset.js') }}"></script>
|
83 |
-
<script src="{{ url_for('static', filename='menu.js') }}"></script>
|
84 |
-
</body>
|
85 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/talkDetail.html
DELETED
@@ -1,83 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="ja" class="dark">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8" />
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
-
<title>会話詳細画面</title>
|
7 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
-
<script src="https://use.fontawesome.com/releases/v5.10.0/js/all.js"></script>
|
9 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
10 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='menu.css') }}">
|
11 |
-
<link
|
12 |
-
rel="stylesheet"
|
13 |
-
href="{{ url_for('static', filename='loading.css') }}"
|
14 |
-
/>
|
15 |
-
</head>
|
16 |
-
<body>
|
17 |
-
<div class="main-content relative">
|
18 |
-
<div class="loader" id="loader">
|
19 |
-
<div class="one"></div>
|
20 |
-
<div class="two"></div>
|
21 |
-
<div class="three"></div>
|
22 |
-
<div class="four"></div>
|
23 |
-
</div>
|
24 |
-
<div
|
25 |
-
class="container mx-auto p-6 dark:bg-gray-800 shadow-lg rounded-2xl w-full max-w-none"
|
26 |
-
>
|
27 |
-
<h2 class="text-2xl font-semibold mb-4 text-center">
|
28 |
-
会話の文字起こし表示
|
29 |
-
</h2>
|
30 |
-
|
31 |
-
<!-- Hamburger Menu -->
|
32 |
-
<div class="absolute top-4 left-4">
|
33 |
-
<button
|
34 |
-
id="menuButton"
|
35 |
-
class="text-white text-2xl focus:outline-none"
|
36 |
-
onclick="toggleMenu(event)"
|
37 |
-
>
|
38 |
-
<i class="fas fa-bars"></i>
|
39 |
-
</button>
|
40 |
-
|
41 |
-
<!-- Menu Content -->
|
42 |
-
<div
|
43 |
-
id="menu"
|
44 |
-
class="absolute top-0 left-0 h-full w-64 bg-gray-800 text-white transform -translate-x-full transition-transform duration-300 ease-in-out opacity-0 visibility-hidden"
|
45 |
-
>
|
46 |
-
<div class="px-4 py-2 text-lg font-semibold">メニュー</div>
|
47 |
-
<button onclick="showUserRegister()">
|
48 |
-
<i class="fas fa-user-plus"></i> メンバーを追加
|
49 |
-
</button>
|
50 |
-
<button onclick="showUserSelect()">
|
51 |
-
<i class="fas fa-users"></i> メンバーを選択
|
52 |
-
</button>
|
53 |
-
<button onclick="showRecorder()">
|
54 |
-
<i class="fas fa-microphone"></i> 録音画面を表示
|
55 |
-
</button>
|
56 |
-
<button onclick="showResults()">
|
57 |
-
<i class="fas fa-chart-bar"></i> フィードバックを表示
|
58 |
-
</button>
|
59 |
-
<button onclick="showTalkDetail()">
|
60 |
-
<i class="fas fa-comments"></i> 会話詳細を表示
|
61 |
-
</button>
|
62 |
-
<button onclick="resetAction()">
|
63 |
-
<i class="fas fa-redo"></i> リセット
|
64 |
-
</button>
|
65 |
-
<button onclick="toggleMenu(event)">
|
66 |
-
<i class="fas fa-times"></i> 閉じる
|
67 |
-
</button>
|
68 |
-
</div>
|
69 |
-
</div>
|
70 |
-
<!-- Hamburger Menu End -->
|
71 |
-
|
72 |
-
<div
|
73 |
-
id="transcription"
|
74 |
-
class="p-4 bg-gray-700 dark:bg-gray-700 rounded-lg mb-4 max-h-96 overflow-y-auto"
|
75 |
-
>
|
76 |
-
ここに会話内容が表示されます。
|
77 |
-
</div>
|
78 |
-
</div>
|
79 |
-
<script src="{{ url_for('static', filename='talk_detail.js') }}"></script>
|
80 |
-
<script src="{{ url_for('static', filename='menu.js') }}"></script>
|
81 |
-
</div>
|
82 |
-
</body>
|
83 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/test
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
//test
|
|
|
|
templates/userRegister.html
DELETED
@@ -1,426 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="ja">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8" />
|
5 |
-
<title>ユーザー音声登録</title>
|
6 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
7 |
-
<script src="https://use.fontawesome.com/releases/v5.10.0/js/all.js"></script>
|
8 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
9 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='menu.css') }}">
|
10 |
-
<style>
|
11 |
-
@keyframes pulse-scale {
|
12 |
-
0%,
|
13 |
-
100% {
|
14 |
-
transform: scale(1);
|
15 |
-
}
|
16 |
-
50% {
|
17 |
-
transform: scale(1.1);
|
18 |
-
}
|
19 |
-
}
|
20 |
-
.animate-pulse-scale {
|
21 |
-
animation: pulse-scale 1s infinite;
|
22 |
-
}
|
23 |
-
/* Record Button Styles */
|
24 |
-
.record-button {
|
25 |
-
width: 50px;
|
26 |
-
height: 50px;
|
27 |
-
background-color: transparent;
|
28 |
-
border-radius: 50%;
|
29 |
-
border: 2px solid white;
|
30 |
-
display: flex;
|
31 |
-
justify-content: center;
|
32 |
-
align-items: center;
|
33 |
-
cursor: pointer;
|
34 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
|
35 |
-
transition: all 0.3s ease;
|
36 |
-
}
|
37 |
-
.record-icon {
|
38 |
-
width: 35px;
|
39 |
-
height: 35px;
|
40 |
-
background-color: #d32f2f;
|
41 |
-
border-radius: 50%;
|
42 |
-
transition: all 0.3s ease;
|
43 |
-
}
|
44 |
-
.record-button.recording .record-icon {
|
45 |
-
background-color: #f44336; /* 録音中は赤色 */
|
46 |
-
border-radius: 4px; /* 録音時に赤い部分だけ四角にする */
|
47 |
-
}
|
48 |
-
.recording .record-icon {
|
49 |
-
width: 20px;
|
50 |
-
height: 20px;
|
51 |
-
border-radius: 50%;
|
52 |
-
}
|
53 |
-
/* Title */
|
54 |
-
.main-title {
|
55 |
-
font-size: 2.5rem;
|
56 |
-
font-weight: bold;
|
57 |
-
margin-bottom: 1.5rem;
|
58 |
-
color: #fff;
|
59 |
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
60 |
-
}
|
61 |
-
/* Buttons */
|
62 |
-
.action-button {
|
63 |
-
margin-top: 1rem;
|
64 |
-
padding: 0.75rem 1.5rem;
|
65 |
-
border-radius: 0.5rem;
|
66 |
-
cursor: pointer;
|
67 |
-
transition: background-color 0.2s ease;
|
68 |
-
width: 100%;
|
69 |
-
}
|
70 |
-
.action-button:hover {
|
71 |
-
background-color: rgba(55, 65, 81, 0.7);
|
72 |
-
}
|
73 |
-
.back-button {
|
74 |
-
background-color: #607d8b; /* 落ち着いたグレー */
|
75 |
-
color: white;
|
76 |
-
}
|
77 |
-
.add-button {
|
78 |
-
background-color: #4caf50; /* 落ち着いた緑色 */
|
79 |
-
color: white;
|
80 |
-
}
|
81 |
-
/* Disabled State */
|
82 |
-
.disabled {
|
83 |
-
opacity: 0.5;
|
84 |
-
pointer-events: none;
|
85 |
-
}
|
86 |
-
|
87 |
-
/* Modal Styles */
|
88 |
-
.modal {
|
89 |
-
display: none;
|
90 |
-
position: fixed;
|
91 |
-
top: 0;
|
92 |
-
left: 0;
|
93 |
-
width: 100%;
|
94 |
-
height: 100%;
|
95 |
-
background-color: rgba(0, 0, 0, 0.7);
|
96 |
-
z-index: 1000;
|
97 |
-
justify-content: center;
|
98 |
-
align-items: center;
|
99 |
-
}
|
100 |
-
|
101 |
-
.modal-content {
|
102 |
-
background-color: #2d3748;
|
103 |
-
color: white;
|
104 |
-
padding: 2rem;
|
105 |
-
border-radius: 1rem;
|
106 |
-
width: 90%;
|
107 |
-
max-width: 500px;
|
108 |
-
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
109 |
-
}
|
110 |
-
|
111 |
-
.input-field {
|
112 |
-
width: 100%;
|
113 |
-
padding: 0.75rem;
|
114 |
-
border-radius: 0.5rem;
|
115 |
-
background-color: #1a202c;
|
116 |
-
border: 1px solid #4a5568;
|
117 |
-
color: white;
|
118 |
-
margin-bottom: 1rem;
|
119 |
-
}
|
120 |
-
|
121 |
-
.modal-buttons {
|
122 |
-
display: flex;
|
123 |
-
justify-content: space-between;
|
124 |
-
margin-top: 1rem;
|
125 |
-
}
|
126 |
-
|
127 |
-
.modal-button {
|
128 |
-
padding: 0.75rem 1.5rem;
|
129 |
-
border-radius: 0.5rem;
|
130 |
-
cursor: pointer;
|
131 |
-
min-width: 100px;
|
132 |
-
text-align: center;
|
133 |
-
}
|
134 |
-
|
135 |
-
.record-modal-button {
|
136 |
-
background-color: #d32f2f;
|
137 |
-
color: white;
|
138 |
-
}
|
139 |
-
|
140 |
-
.cancel-button {
|
141 |
-
background-color: #64748b;
|
142 |
-
color: white;
|
143 |
-
}
|
144 |
-
</style>
|
145 |
-
</head>
|
146 |
-
<body>
|
147 |
-
<!-- Main Content Wrapper -->
|
148 |
-
<div class="main-content relative">
|
149 |
-
<!-- Title -->
|
150 |
-
<div class="main-title">JustTalk</div>
|
151 |
-
<!-- User List -->
|
152 |
-
<div id="people-list" class="space-y-4"></div>
|
153 |
-
<!-- Add Button -->
|
154 |
-
<button id="add-btn" class="action-button add-button">
|
155 |
-
<i class="fas fa-user-plus"></i> メンバーを追加
|
156 |
-
</button>
|
157 |
-
<!-- 録音画面へ移動ボタン(Back Buttonから変更) -->
|
158 |
-
<button
|
159 |
-
id="backButton"
|
160 |
-
onclick="showUserSelect()"
|
161 |
-
class="action-button back-button"
|
162 |
-
>
|
163 |
-
<i class="fas fa-users"></i> メンバー選択画面を表示
|
164 |
-
</button>
|
165 |
-
</div>
|
166 |
-
|
167 |
-
<!-- Modal for Adding New User -->
|
168 |
-
<div id="add-modal" class="modal">
|
169 |
-
<div class="modal-content">
|
170 |
-
<h2 class="text-xl font-bold mb-4">新しいメンバーを追加</h2>
|
171 |
-
<input id="user-name" type="text" placeholder="名前を入力" class="input-field">
|
172 |
-
|
173 |
-
<div class="flex justify-center my-4">
|
174 |
-
<div id="record-button" class="record-button">
|
175 |
-
<div class="record-icon"></div>
|
176 |
-
</div>
|
177 |
-
</div>
|
178 |
-
<div id="recording-status" class="text-center mb-4">録音をクリックして開始</div>
|
179 |
-
|
180 |
-
<div class="modal-buttons">
|
181 |
-
<button id="cancel-button" class="modal-button cancel-button">キャンセル</button>
|
182 |
-
<button id="save-button" class="modal-button record-modal-button" disabled>保存</button>
|
183 |
-
</div>
|
184 |
-
</div>
|
185 |
-
</div>
|
186 |
-
|
187 |
-
<script>
|
188 |
-
// グローバル変数
|
189 |
-
let mediaRecorder;
|
190 |
-
let audioChunks = [];
|
191 |
-
let registeredUsers = [];
|
192 |
-
let isRecording = false;
|
193 |
-
|
194 |
-
// ページ読み込み時に実行
|
195 |
-
document.addEventListener('DOMContentLoaded', () => {
|
196 |
-
loadUsers();
|
197 |
-
setupEventListeners();
|
198 |
-
});
|
199 |
-
|
200 |
-
// イベントリスナーの設定
|
201 |
-
function setupEventListeners() {
|
202 |
-
// 追加ボタン
|
203 |
-
document.getElementById('add-btn').addEventListener('click', () => {
|
204 |
-
openModal();
|
205 |
-
});
|
206 |
-
|
207 |
-
// 録音ボタン
|
208 |
-
document.getElementById('record-button').addEventListener('click', toggleRecording);
|
209 |
-
|
210 |
-
// キャンセルボタン
|
211 |
-
document.getElementById('cancel-button').addEventListener('click', closeModal);
|
212 |
-
|
213 |
-
// 保存ボタン
|
214 |
-
document.getElementById('save-button').addEventListener('click', saveRecording);
|
215 |
-
}
|
216 |
-
|
217 |
-
// ユーザーリストを読み込む
|
218 |
-
async function loadUsers() {
|
219 |
-
try {
|
220 |
-
const response = await fetch('/list_base_audio');
|
221 |
-
const data = await response.json();
|
222 |
-
|
223 |
-
if (data.status === "success" && Array.isArray(data.id)) {
|
224 |
-
registeredUsers = data.id;
|
225 |
-
displayUsers(registeredUsers);
|
226 |
-
} else {
|
227 |
-
console.error('Unexpected response format:', data);
|
228 |
-
}
|
229 |
-
} catch (error) {
|
230 |
-
console.error('Error loading users:', error);
|
231 |
-
}
|
232 |
-
}
|
233 |
-
|
234 |
-
// ユーザーリストを表示
|
235 |
-
function displayUsers(users) {
|
236 |
-
const peopleList = document.getElementById('people-list');
|
237 |
-
peopleList.innerHTML = '';
|
238 |
-
|
239 |
-
if (users.length === 0) {
|
240 |
-
peopleList.innerHTML = '<p class="text-gray-400 text-center">登録されたメンバーはいません</p>';
|
241 |
-
return;
|
242 |
-
}
|
243 |
-
|
244 |
-
users.forEach(user => {
|
245 |
-
const userDiv = document.createElement('div');
|
246 |
-
userDiv.className = 'bg-gray-700 rounded-lg p-4 flex justify-between items-center';
|
247 |
-
userDiv.innerHTML = `
|
248 |
-
<div class="flex items-center">
|
249 |
-
<i class="fas fa-user-circle text-2xl mr-3"></i>
|
250 |
-
<span>${user}</span>
|
251 |
-
</div>
|
252 |
-
<button class="delete-btn text-red-500 hover:text-red-300" data-name="${user}">
|
253 |
-
<i class="fas fa-trash"></i>
|
254 |
-
</button>
|
255 |
-
`;
|
256 |
-
peopleList.appendChild(userDiv);
|
257 |
-
});
|
258 |
-
|
259 |
-
// 削除ボタンのイベントリスナーを追加
|
260 |
-
document.querySelectorAll('.delete-btn').forEach(button => {
|
261 |
-
button.addEventListener('click', function() {
|
262 |
-
const name = this.getAttribute('data-name');
|
263 |
-
deleteUser(name);
|
264 |
-
});
|
265 |
-
});
|
266 |
-
}
|
267 |
-
|
268 |
-
// ユーザーを削除
|
269 |
-
async function deleteUser(name) {
|
270 |
-
if (!confirm(`${name}を削除してもよろしいですか?`)) return;
|
271 |
-
|
272 |
-
try {
|
273 |
-
const response = await fetch('/reset_member', {
|
274 |
-
method: 'POST',
|
275 |
-
headers: {
|
276 |
-
'Content-Type': 'application/json'
|
277 |
-
},
|
278 |
-
body: JSON.stringify({
|
279 |
-
names: [name]
|
280 |
-
})
|
281 |
-
});
|
282 |
-
|
283 |
-
const data = await response.json();
|
284 |
-
|
285 |
-
if (data.status === "success") {
|
286 |
-
// 削除成功時、ユーザーリストを更新
|
287 |
-
loadUsers();
|
288 |
-
} else {
|
289 |
-
console.error('Error deleting user:', data.message);
|
290 |
-
alert('ユーザーの削除に失敗しました');
|
291 |
-
}
|
292 |
-
} catch (error) {
|
293 |
-
console.error('Error:', error);
|
294 |
-
alert('エラーが発生しました');
|
295 |
-
}
|
296 |
-
}
|
297 |
-
|
298 |
-
// モーダルを開く
|
299 |
-
function openModal() {
|
300 |
-
document.getElementById('add-modal').style.display = 'flex';
|
301 |
-
document.getElementById('user-name').value = '';
|
302 |
-
document.getElementById('save-button').disabled = true;
|
303 |
-
document.getElementById('recording-status').textContent = '録音をクリックして開始';
|
304 |
-
|
305 |
-
// 録音状態をリセット
|
306 |
-
isRecording = false;
|
307 |
-
audioChunks = [];
|
308 |
-
const recordButton = document.getElementById('record-button');
|
309 |
-
recordButton.classList.remove('recording');
|
310 |
-
}
|
311 |
-
|
312 |
-
// モーダルを閉じる
|
313 |
-
function closeModal() {
|
314 |
-
document.getElementById('add-modal').style.display = 'none';
|
315 |
-
|
316 |
-
// 録音中なら停止
|
317 |
-
if (mediaRecorder && isRecording) {
|
318 |
-
mediaRecorder.stop();
|
319 |
-
isRecording = false;
|
320 |
-
}
|
321 |
-
}
|
322 |
-
|
323 |
-
// 録音の開始/停止を切り替え
|
324 |
-
async function toggleRecording() {
|
325 |
-
const recordButton = document.getElementById('record-button');
|
326 |
-
const statusText = document.getElementById('recording-status');
|
327 |
-
|
328 |
-
if (!isRecording) {
|
329 |
-
// 録音開始
|
330 |
-
try {
|
331 |
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
332 |
-
mediaRecorder = new MediaRecorder(stream);
|
333 |
-
audioChunks = [];
|
334 |
-
|
335 |
-
mediaRecorder.ondataavailable = event => {
|
336 |
-
audioChunks.push(event.data);
|
337 |
-
};
|
338 |
-
|
339 |
-
mediaRecorder.onstop = () => {
|
340 |
-
document.getElementById('save-button').disabled = false;
|
341 |
-
statusText.textContent = '録音完了!';
|
342 |
-
recordButton.classList.remove('recording');
|
343 |
-
};
|
344 |
-
|
345 |
-
mediaRecorder.start();
|
346 |
-
isRecording = true;
|
347 |
-
statusText.textContent = '録音中...';
|
348 |
-
recordButton.classList.add('recording');
|
349 |
-
} catch (error) {
|
350 |
-
console.error('録音の開始に失敗しました:', error);
|
351 |
-
statusText.textContent = '録音の開始に失敗しました';
|
352 |
-
}
|
353 |
-
} else {
|
354 |
-
// 録音停止
|
355 |
-
mediaRecorder.stop();
|
356 |
-
isRecording = false;
|
357 |
-
}
|
358 |
-
}
|
359 |
-
|
360 |
-
// 録音を保存
|
361 |
-
async function saveRecording() {
|
362 |
-
const userName = document.getElementById('user-name').value.trim();
|
363 |
-
|
364 |
-
if (!userName) {
|
365 |
-
alert('名前を入力してください');
|
366 |
-
return;
|
367 |
-
}
|
368 |
-
|
369 |
-
if (registeredUsers.includes(userName)) {
|
370 |
-
if (!confirm(`${userName}は既に登録されています。上書きしますか?`)) {
|
371 |
-
return;
|
372 |
-
}
|
373 |
-
}
|
374 |
-
|
375 |
-
if (audioChunks.length === 0) {
|
376 |
-
alert('録音データがありません');
|
377 |
-
return;
|
378 |
-
}
|
379 |
-
|
380 |
-
try {
|
381 |
-
// 録音データをBlobに変換
|
382 |
-
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
383 |
-
|
384 |
-
// Base64に変換
|
385 |
-
const reader = new FileReader();
|
386 |
-
reader.readAsDataURL(audioBlob);
|
387 |
-
reader.onloadend = async () => {
|
388 |
-
// Base64文字列から先頭の "data:audio/wav;base64," を削除
|
389 |
-
const base64Audio = reader.result.split(',')[1];
|
390 |
-
|
391 |
-
// サーバーに送信
|
392 |
-
const response = await fetch('/upload_base_audio', {
|
393 |
-
method: 'POST',
|
394 |
-
headers: {
|
395 |
-
'Content-Type': 'application/json'
|
396 |
-
},
|
397 |
-
body: JSON.stringify({
|
398 |
-
name: userName,
|
399 |
-
audio_data: base64Audio
|
400 |
-
})
|
401 |
-
});
|
402 |
-
|
403 |
-
const data = await response.json();
|
404 |
-
|
405 |
-
if (data.state === "Registration Success!") {
|
406 |
-
closeModal();
|
407 |
-
loadUsers(); // ユーザーリストを更新
|
408 |
-
alert(`${userName}を登録しました!`);
|
409 |
-
} else {
|
410 |
-
console.error('Error saving recording:', data);
|
411 |
-
alert('録音の保存に失敗しました');
|
412 |
-
}
|
413 |
-
};
|
414 |
-
} catch (error) {
|
415 |
-
console.error('Error:', error);
|
416 |
-
alert('エラーが発生しました');
|
417 |
-
}
|
418 |
-
}
|
419 |
-
|
420 |
-
// ユーザー選択画面に戻る
|
421 |
-
function showUserSelect() {
|
422 |
-
window.location.href = "/userselect";
|
423 |
-
}
|
424 |
-
</script>
|
425 |
-
</body>
|
426 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/userSelect.html
DELETED
@@ -1,354 +0,0 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="ja">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8" />
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
-
<title>メンバー選択 - JustTalk</title>
|
7 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
8 |
-
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='menu.css') }}">
|
9 |
-
<script src="https://use.fontawesome.com/releases/v5.10.0/js/all.js"></script>
|
10 |
-
<style>
|
11 |
-
.container {
|
12 |
-
max-width: 500px;
|
13 |
-
margin: 0 auto;
|
14 |
-
padding: 30px;
|
15 |
-
border: 5px solid rgba(255, 255, 255, 0.2);
|
16 |
-
border-radius: 1rem;
|
17 |
-
background-color: rgba(0, 0, 0, 0.3);
|
18 |
-
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
|
19 |
-
width: 90%;
|
20 |
-
position: relative;
|
21 |
-
}
|
22 |
-
|
23 |
-
h1 {
|
24 |
-
color: white;
|
25 |
-
text-align: center;
|
26 |
-
margin-bottom: 30px;
|
27 |
-
font-size: 1.8rem;
|
28 |
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
29 |
-
}
|
30 |
-
|
31 |
-
.user-list {
|
32 |
-
background-color: rgba(255, 255, 255, 0.1);
|
33 |
-
border-radius: 10px;
|
34 |
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
35 |
-
padding: 15px;
|
36 |
-
margin-bottom: 25px;
|
37 |
-
max-height: 350px;
|
38 |
-
overflow-y: auto;
|
39 |
-
}
|
40 |
-
|
41 |
-
.user-item {
|
42 |
-
display: flex;
|
43 |
-
align-items: center;
|
44 |
-
padding: 12px 15px;
|
45 |
-
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
46 |
-
transition: background-color 0.2s;
|
47 |
-
}
|
48 |
-
|
49 |
-
.user-item:last-child {
|
50 |
-
border-bottom: none;
|
51 |
-
}
|
52 |
-
|
53 |
-
.user-item:hover {
|
54 |
-
background-color: rgba(255, 255, 255, 0.05);
|
55 |
-
}
|
56 |
-
|
57 |
-
.user-item label {
|
58 |
-
margin-left: 12px;
|
59 |
-
font-size: 16px;
|
60 |
-
cursor: pointer;
|
61 |
-
flex-grow: 1;
|
62 |
-
color: white;
|
63 |
-
}
|
64 |
-
|
65 |
-
input[type="checkbox"] {
|
66 |
-
cursor: pointer;
|
67 |
-
width: 18px;
|
68 |
-
height: 18px;
|
69 |
-
accent-color: #3498db;
|
70 |
-
}
|
71 |
-
|
72 |
-
.button-container {
|
73 |
-
display: flex;
|
74 |
-
justify-content: space-between;
|
75 |
-
margin-top: 25px;
|
76 |
-
}
|
77 |
-
|
78 |
-
button {
|
79 |
-
background-color: #3498db;
|
80 |
-
color: white;
|
81 |
-
border: none;
|
82 |
-
border-radius: 8px;
|
83 |
-
padding: 12px 20px;
|
84 |
-
font-size: 16px;
|
85 |
-
cursor: pointer;
|
86 |
-
transition: background-color 0.3s, transform 0.1s;
|
87 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
88 |
-
}
|
89 |
-
|
90 |
-
button:hover {
|
91 |
-
background-color: #2980b9;
|
92 |
-
}
|
93 |
-
|
94 |
-
button:active {
|
95 |
-
transform: translateY(1px);
|
96 |
-
}
|
97 |
-
|
98 |
-
button:disabled {
|
99 |
-
background-color: #95a5a6;
|
100 |
-
cursor: not-allowed;
|
101 |
-
opacity: 0.7;
|
102 |
-
}
|
103 |
-
|
104 |
-
button.secondary {
|
105 |
-
background-color: rgba(255, 255, 255, 0.2);
|
106 |
-
}
|
107 |
-
|
108 |
-
button.secondary:hover {
|
109 |
-
background-color: rgba(255, 255, 255, 0.3);
|
110 |
-
}
|
111 |
-
|
112 |
-
.selected-count {
|
113 |
-
margin: 15px 0;
|
114 |
-
text-align: center;
|
115 |
-
font-weight: bold;
|
116 |
-
color: #3498db;
|
117 |
-
font-size: 18px;
|
118 |
-
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
119 |
-
}
|
120 |
-
|
121 |
-
.no-users {
|
122 |
-
text-align: center;
|
123 |
-
padding: 30px;
|
124 |
-
color: rgba(255, 255, 255, 0.7);
|
125 |
-
font-style: italic;
|
126 |
-
}
|
127 |
-
|
128 |
-
.loading {
|
129 |
-
text-align: center;
|
130 |
-
padding: 30px;
|
131 |
-
color: white;
|
132 |
-
}
|
133 |
-
|
134 |
-
.spinner {
|
135 |
-
border: 4px solid rgba(255, 255, 255, 0.1);
|
136 |
-
width: 36px;
|
137 |
-
height: 36px;
|
138 |
-
border-radius: 50%;
|
139 |
-
border-left-color: #3498db;
|
140 |
-
animation: spin 1s linear infinite;
|
141 |
-
margin: 0 auto 15px;
|
142 |
-
}
|
143 |
-
|
144 |
-
@keyframes spin {
|
145 |
-
0% {
|
146 |
-
transform: rotate(0deg);
|
147 |
-
}
|
148 |
-
100% {
|
149 |
-
transform: rotate(360deg);
|
150 |
-
}
|
151 |
-
}
|
152 |
-
|
153 |
-
/* User Avatar */
|
154 |
-
.user-avatar {
|
155 |
-
width: 30px;
|
156 |
-
height: 30px;
|
157 |
-
background: rgba(255, 255, 255, 0.2);
|
158 |
-
border-radius: 50%;
|
159 |
-
display: flex;
|
160 |
-
align-items: center;
|
161 |
-
justify-content: center;
|
162 |
-
font-size: 14px;
|
163 |
-
margin-left: 10px;
|
164 |
-
}
|
165 |
-
|
166 |
-
/* Select All Button */
|
167 |
-
.select-controls {
|
168 |
-
display: flex;
|
169 |
-
justify-content: center;
|
170 |
-
margin-bottom: 15px;
|
171 |
-
gap: 10px;
|
172 |
-
}
|
173 |
-
|
174 |
-
.select-button {
|
175 |
-
background-color: rgba(255, 255, 255, 0.15);
|
176 |
-
color: white;
|
177 |
-
border: none;
|
178 |
-
border-radius: 5px;
|
179 |
-
padding: 8px 15px;
|
180 |
-
font-size: 14px;
|
181 |
-
cursor: pointer;
|
182 |
-
transition: background-color 0.2s;
|
183 |
-
}
|
184 |
-
|
185 |
-
.select-button:hover {
|
186 |
-
background-color: rgba(255, 255, 255, 0.25);
|
187 |
-
}
|
188 |
-
|
189 |
-
/* Delete Button */
|
190 |
-
.delete-button {
|
191 |
-
background-color: transparent;
|
192 |
-
color: #e74c3c;
|
193 |
-
border: none;
|
194 |
-
border-radius: 50%;
|
195 |
-
width: 30px;
|
196 |
-
height: 30px;
|
197 |
-
display: flex;
|
198 |
-
align-items: center;
|
199 |
-
justify-content: center;
|
200 |
-
cursor: pointer;
|
201 |
-
transition: background-color 0.2s;
|
202 |
-
padding: 0;
|
203 |
-
margin-left: 5px;
|
204 |
-
box-shadow: none;
|
205 |
-
}
|
206 |
-
|
207 |
-
.delete-button:hover {
|
208 |
-
background-color: rgba(231, 76, 60, 0.2);
|
209 |
-
}
|
210 |
-
|
211 |
-
/* Modal Dialog */
|
212 |
-
.modal {
|
213 |
-
display: none;
|
214 |
-
position: fixed;
|
215 |
-
z-index: 100;
|
216 |
-
left: 0;
|
217 |
-
top: 0;
|
218 |
-
width: 100%;
|
219 |
-
height: 100%;
|
220 |
-
background-color: rgba(0, 0, 0, 0.5);
|
221 |
-
align-items: center;
|
222 |
-
justify-content: center;
|
223 |
-
}
|
224 |
-
|
225 |
-
.modal-content {
|
226 |
-
background-color: rgb(31, 41, 55);
|
227 |
-
border-radius: 10px;
|
228 |
-
padding: 20px;
|
229 |
-
width: 90%;
|
230 |
-
max-width: 400px;
|
231 |
-
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
232 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
233 |
-
}
|
234 |
-
|
235 |
-
.modal-title {
|
236 |
-
font-size: 18px;
|
237 |
-
margin-bottom: 15px;
|
238 |
-
color: white;
|
239 |
-
}
|
240 |
-
|
241 |
-
.modal-text {
|
242 |
-
margin-bottom: 20px;
|
243 |
-
color: rgba(255, 255, 255, 0.9);
|
244 |
-
}
|
245 |
-
|
246 |
-
.modal-buttons {
|
247 |
-
display: flex;
|
248 |
-
justify-content: flex-end;
|
249 |
-
gap: 10px;
|
250 |
-
}
|
251 |
-
|
252 |
-
.modal-cancel {
|
253 |
-
background-color: rgba(255, 255, 255, 0.2);
|
254 |
-
color: white;
|
255 |
-
}
|
256 |
-
|
257 |
-
.modal-delete {
|
258 |
-
background-color: #e74c3c;
|
259 |
-
}
|
260 |
-
|
261 |
-
.modal-delete:hover {
|
262 |
-
background-color: #c0392b;
|
263 |
-
}
|
264 |
-
</style>
|
265 |
-
</head>
|
266 |
-
<body onclick="closeMenu(event)">
|
267 |
-
<div class="container">
|
268 |
-
<!-- Hamburger Menu Button -->
|
269 |
-
<button
|
270 |
-
id="menuButton"
|
271 |
-
class="text-white focus:outline-none"
|
272 |
-
onclick="toggleMenu(event)"
|
273 |
-
>
|
274 |
-
<i class="fas fa-bars"></i>
|
275 |
-
</button>
|
276 |
-
|
277 |
-
<!-- Menu Content -->
|
278 |
-
<div
|
279 |
-
id="menu"
|
280 |
-
class="absolute top-0 left-0 h-full w-64 bg-gray-800 text-white transform -translate-x-full transition-transform duration-300 ease-in-out opacity-0 visibility-hidden"
|
281 |
-
>
|
282 |
-
<div class="px-4 py-2 text-lg font-semibold">メニュー</div>
|
283 |
-
<button onclick="showUserRegister()">
|
284 |
-
<i class="fas fa-user-plus"></i> メンバーを追加
|
285 |
-
</button>
|
286 |
-
<button onclick="showIndex()">
|
287 |
-
<i class="fas fa-home"></i> ホーム画面
|
288 |
-
</button>
|
289 |
-
<button onclick="showResults()">
|
290 |
-
<i class="fas fa-chart-bar"></i> フィードバックを表示
|
291 |
-
</button>
|
292 |
-
<button onclick="showTalkDetail()">
|
293 |
-
<i class="fas fa-comments"></i> 会話詳細を表示
|
294 |
-
</button>
|
295 |
-
<button onclick="resetAction()">
|
296 |
-
<i class="fas fa-redo"></i> リセット
|
297 |
-
</button>
|
298 |
-
<button onclick="toggleMenu(event)">
|
299 |
-
<i class="fas fa-times"></i> 閉じる
|
300 |
-
</button>
|
301 |
-
</div>
|
302 |
-
|
303 |
-
<h1>会話分析に使用するメンバーを選択</h1>
|
304 |
-
|
305 |
-
<div class="select-controls">
|
306 |
-
<button class="select-button" onclick="selectAllUsers()">
|
307 |
-
すべて選択
|
308 |
-
</button>
|
309 |
-
<button class="select-button" onclick="deselectAllUsers()">
|
310 |
-
選択解除
|
311 |
-
</button>
|
312 |
-
</div>
|
313 |
-
|
314 |
-
<div class="user-list" id="userList">
|
315 |
-
<div class="loading">
|
316 |
-
<div class="spinner"></div>
|
317 |
-
<p>メンバーリストを読み込み中...</p>
|
318 |
-
</div>
|
319 |
-
</div>
|
320 |
-
|
321 |
-
<div class="selected-count" id="selectedCount">選択中: 0人</div>
|
322 |
-
|
323 |
-
<div class="button-container">
|
324 |
-
<button class="secondary" onclick="location.href='/userregister'">
|
325 |
-
新規登録
|
326 |
-
</button>
|
327 |
-
<button
|
328 |
-
id="proceedButton"
|
329 |
-
onclick="proceedWithSelectedUsers()"
|
330 |
-
disabled
|
331 |
-
>
|
332 |
-
選択して次へ
|
333 |
-
</button>
|
334 |
-
</div>
|
335 |
-
</div>
|
336 |
-
|
337 |
-
<!-- 削除確認モーダル -->
|
338 |
-
<div id="deleteModal" class="modal">
|
339 |
-
<div class="modal-content">
|
340 |
-
<div class="modal-title">メンバーの削除</div>
|
341 |
-
<div class="modal-text" id="deleteModalText">
|
342 |
-
このメンバーを削除しますか?
|
343 |
-
</div>
|
344 |
-
<div class="modal-buttons">
|
345 |
-
<button class="modal-cancel" onclick="hideDeleteModal()">
|
346 |
-
キャンセル
|
347 |
-
</button>
|
348 |
-
<button class="modal-delete" onclick="confirmDelete()">削除</button>
|
349 |
-
</div>
|
350 |
-
</div>
|
351 |
-
</div>
|
352 |
-
<script src="{{ url_for('static', filename='process1.js') }}"></script>
|
353 |
-
</body>
|
354 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transcription.py
DELETED
@@ -1,147 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
from faster_whisper import WhisperModel
|
3 |
-
from pydub import AudioSegment
|
4 |
-
import string
|
5 |
-
import random
|
6 |
-
from datetime import datetime
|
7 |
-
|
8 |
-
# Matplotlibのキャッシュディレクトリを変更
|
9 |
-
os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
|
10 |
-
|
11 |
-
# Hugging Faceのキャッシュディレクトリを変更
|
12 |
-
os.environ["HF_HOME"] = "/tmp/huggingface"
|
13 |
-
os.environ["HUGGINGFACE_HUB_CACHE"] = "/tmp/huggingface"
|
14 |
-
|
15 |
-
class TranscriptionMaker():
|
16 |
-
# 書き起こしファイルを吐き出すディレクトリを指定
|
17 |
-
def __init__(self, output_dir="/tmp/data/transcriptions"):
|
18 |
-
self.model = WhisperModel("base", device="cpu", download_root="/tmp/huggingface")
|
19 |
-
self.output_dir = output_dir
|
20 |
-
os.makedirs(self.output_dir, exist_ok=True)
|
21 |
-
|
22 |
-
|
23 |
-
#音声ファイルのディレクトリを受け取り、書き起こしファイルを作成する
|
24 |
-
def create_transcription(self,audio_directory):
|
25 |
-
conversation = []
|
26 |
-
|
27 |
-
#ディレクトリ内のファイルを全て取得
|
28 |
-
if not os.path.isdir(audio_directory):
|
29 |
-
raise ValueError(f"The specified path is not a valid directory: {audio_directory}")
|
30 |
-
audio_files = self.sort_audio_files_in_directory(audio_directory)
|
31 |
-
merged_segments = self.combine_audio(audio_files)
|
32 |
-
merged_audio_directory = self.save_marged_segments(merged_segments, output_directory='/tmp/data/transcription_audio')
|
33 |
-
merged_files = self.sort_audio_files_in_directory(merged_audio_directory)
|
34 |
-
|
35 |
-
for audio_file in merged_files:
|
36 |
-
if os.path.splitext(audio_file)[-1].lower() != '.wav':
|
37 |
-
continue
|
38 |
-
audio_path = os.path.join(merged_audio_directory, audio_file)
|
39 |
-
try:
|
40 |
-
segments,info = list(self.model.transcribe(audio_path))
|
41 |
-
except Exception as e:
|
42 |
-
print(f"Error transcripting file {audio_path}: {e}")
|
43 |
-
raise
|
44 |
-
sorted_segments = sorted(segments, key=lambda s: s.start)
|
45 |
-
results = []
|
46 |
-
for segment in sorted_segments:
|
47 |
-
results.append({
|
48 |
-
"start": segment.start,
|
49 |
-
"end": segment.end,
|
50 |
-
"text": segment.text
|
51 |
-
})
|
52 |
-
combined_text = "".join([result["text"] for result in results])
|
53 |
-
speaker = os.path.basename(audio_file).split("_")[0]
|
54 |
-
# 無音ならスキップ
|
55 |
-
if not combined_text:
|
56 |
-
continue
|
57 |
-
conversation.append(f"{speaker}: {combined_text}<br>")
|
58 |
-
|
59 |
-
#ファイルの書き込み。ファイル名は"transcription.txt"
|
60 |
-
output_file=os.path.join(self.output_dir,"transcription.txt")
|
61 |
-
print(conversation)
|
62 |
-
try:
|
63 |
-
with open(output_file,"w",encoding="utf-8") as f:
|
64 |
-
for result in conversation:
|
65 |
-
f.write(result)
|
66 |
-
except OSError as e:
|
67 |
-
print(f"Error writing transcription file: {e}")
|
68 |
-
raise
|
69 |
-
return output_file
|
70 |
-
|
71 |
-
# 受け取った音声ファイルを話者ごとに整理する
|
72 |
-
def combine_audio(self,audio_files):
|
73 |
-
if not audio_files:
|
74 |
-
raise
|
75 |
-
merged_segments = []
|
76 |
-
current_speaker = None
|
77 |
-
current_segment = []
|
78 |
-
for segment in audio_files:
|
79 |
-
speaker = os.path.basename(segment).split("_")[0]
|
80 |
-
if speaker != current_speaker:
|
81 |
-
# 話者が変わった場合はセグメントを保存
|
82 |
-
if current_segment:
|
83 |
-
merged_segments.append((current_speaker, current_segment))
|
84 |
-
current_speaker = speaker
|
85 |
-
current_segment = [segment]
|
86 |
-
else:
|
87 |
-
# 話者が同一の場合はセグメントを結合
|
88 |
-
current_segment.append(segment)
|
89 |
-
# 最後のセグメントを保存
|
90 |
-
if current_segment:
|
91 |
-
merged_segments.append((current_speaker, current_segment))
|
92 |
-
|
93 |
-
return merged_segments
|
94 |
-
|
95 |
-
# ディレクトリ内の音声ファイルを並べ替える
|
96 |
-
def sort_audio_files_in_directory(self, directory):
|
97 |
-
files = os.listdir(directory)
|
98 |
-
audio_files = [f for f in files if f.endswith(".wav")]
|
99 |
-
|
100 |
-
audio_files.sort(key=lambda x: datetime.strptime(x.split("_")[1].split(".")[0], "%Y%m%d%H%M%S"))
|
101 |
-
return [os.path.join(directory, f) for f in audio_files]
|
102 |
-
|
103 |
-
def save_marged_segments(self,merged_segments,output_directory='/tmp/data/conversations'):
|
104 |
-
if not merged_segments:
|
105 |
-
print("merged_segmentsが見つかりませんでした。")
|
106 |
-
raise
|
107 |
-
|
108 |
-
conversation = []
|
109 |
-
for speaker, segments in merged_segments:
|
110 |
-
combined_audio = self.merge_segments(segments)
|
111 |
-
conversation.append((speaker,combined_audio))
|
112 |
-
if not os.path.exists(output_directory):
|
113 |
-
os.makedirs(output_directory)
|
114 |
-
|
115 |
-
for i, (speaker, combined_audio) in enumerate(conversation):
|
116 |
-
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
|
117 |
-
filename = f"{speaker}_{current_time}.wav"
|
118 |
-
file_path = os.path.join(output_directory,filename)
|
119 |
-
combined_audio.export(file_path,format = "wav")
|
120 |
-
print(f"Saved: {file_path}")
|
121 |
-
|
122 |
-
return output_directory
|
123 |
-
|
124 |
-
def merge_segments(self,segments):
|
125 |
-
combined = AudioSegment.empty() # 空のAudioSegmentを初期化
|
126 |
-
|
127 |
-
for segment in segments:
|
128 |
-
if isinstance(segment, str):
|
129 |
-
# セグメントがファイルパスの場合、読み込む
|
130 |
-
audio = AudioSegment.from_file(segment)
|
131 |
-
elif isinstance(segment, AudioSegment):
|
132 |
-
# セグメントがすでにAudioSegmentの場合、そのまま使用
|
133 |
-
audio = segment
|
134 |
-
else:
|
135 |
-
raise ValueError("Invalid segment type. Must be file path or AudioSegment.")
|
136 |
-
|
137 |
-
combined += audio
|
138 |
-
return combined
|
139 |
-
|
140 |
-
def generate_random_string(self,length):
|
141 |
-
letters = string.ascii_letters + string.digits
|
142 |
-
return ''.join(random.choice(letters) for i in range(length))
|
143 |
-
|
144 |
-
def generate_filename(self,random_length):
|
145 |
-
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
|
146 |
-
filename = f"{current_time}.wav"
|
147 |
-
return filename
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|