This view is limited to 50 files because it contains too many changes.  See the raw diff here.
.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
- FROM nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04
2
-
3
- # タイムゾーン設定
4
- RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
5
-
6
- # Python3、pip、ffmpegをインストール
7
- RUN apt-get update && \
8
- apt-get install -y python3 python3-pip ffmpeg && \
9
- rm -rf /var/lib/apt/lists/*
10
-
11
- # pipを最新版にアップグレード
12
- RUN python3 -m pip install --upgrade pip
13
-
14
- WORKDIR /app
15
-
16
- # requirements.txt をコンテナ内にコピーして、必要なパッケージをインストール
17
- COPY requirements.txt /app/
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, render_template, send_from_directory
2
  import base64
3
- from pydub import AudioSegment
4
  import os
5
- import shutil
6
- import requests
7
- import tempfile
8
- import json
9
- from process import AudioProcessor
10
- from transcription import TranscriptionMaker
11
- from analyze import TextAnalyzer
12
- from flask_cors import CORS
13
- process = AudioProcessor()
14
- transcripter = TranscriptionMaker()
15
- app = Flask(__name__)
16
-
17
- # CORS設定: すべてのオリジンからのリクエストを許可
18
- # 必要であれば、特定のオリジンやメソッド、ヘッダーをより厳密に指定できます
19
- # 例: CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}}, supports_credentials=True)
20
- CORS(app, origins="*", methods=["GET", "POST", "DELETE", "OPTIONS"], headers=["Content-Type", "Authorization"])
21
-
22
- # GASのエンドポイントURL
23
- GAS_URL = "https://script.google.com/macros/s/AKfycbwR2cnMKVU1AxoT9NDaeZaNUaTiwRX64Ul0sH0AU4ccP49Byph-TpxtM_Lwm4G9zLnuYA/exec"
24
-
25
- users = [] # 選択されたユーザーのリスト
26
- all_users = [] # 利用可能なすべてのユーザーのリスト
27
- transcription_text = ""
28
- harassment_keywords = [
29
- "バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい",
30
- "きもい", "キモい", "ブス", "デブ", "ハゲ",
31
- "セクハラ", "パワハラ", "モラハラ"
32
- ]
33
- total_audio = ""
34
 
 
35
 
36
- @app.route('/index', methods=['GET', 'POST'])
37
  def index():
38
- return render_template('index.html', users=users)
39
 
40
- # フィードバック画面(テンプレート: feedback.html)
41
- @app.route('/feedback', methods=['GET', 'POST'])
42
  def feedback():
43
- return render_template('feedback.html')
44
-
45
- # 会話詳細画面(テンプレート: talkDetail.html)
46
- @app.route('/talk_detail', methods=['GET', 'POST'])
47
- def talk_detail():
48
- return render_template('talkDetail.html')
49
-
50
- # 音声登録画面(テンプレート: userRegister.html)
51
- @app.route('/userregister', methods=['GET', 'POST'])
52
- def userregister():
53
- return render_template('userRegister.html')
54
-
55
- # 人数確認
56
- @app.route('/confirm', methods=['GET'])
57
- def confirm():
58
- global all_users
59
- # 最新のユーザーリストを取得
60
- try:
61
- update_all_users()
62
- except Exception as e:
63
- print(f"ユーザーリストの更新エラー: {str(e)}")
64
- return jsonify({'members': users, 'all_members': all_users}), 200
65
-
66
- # リセット画面(テンプレート: reset.html)
67
- @app.route('/reset_html', methods=['GET', 'POST'])
68
- def reset_html():
69
- return render_template('reset.html')
70
-
71
- # メンバー削除&累積音声削除
72
- @app.route('/reset_member', methods=['GET', 'POST'])
73
- def reset_member():
74
- global users
75
- global total_audio
76
- global transcription_text
77
-
78
- # 一時ディレクトリのクリーンアップ
79
- if total_audio:
80
- process.delete_files_in_directory(total_audio)
81
- process.delete_files_in_directory('/tmp/data/transcription_audio')
82
-
83
- # 書き起こしテキストの削除
84
- if os.path.exists(transcription_text):
85
- try:
86
- os.remove(transcription_text)
87
- print(f"{transcription_text} を削除しました。")
88
- except Exception as e:
89
- print(f"ファイル削除中にエラーが発生しました: {e}")
90
-
91
- transcription_text = ""
92
-
93
- try:
94
- data = request.get_json()
95
- if not data or "names" not in data:
96
- return jsonify({"status": "error", "message": "Invalid request body"}), 400
97
-
98
- names = data.get("names", [])
99
-
100
- # GASからファイルを削除
101
- for name in names:
102
- try:
103
- delete_from_cloud(f"{name}.wav")
104
- print(f"クラウドから {name}.wav を削除しました。")
105
- except Exception as e:
106
- print(f"クラウド削除中にエラーが発生しました: {e}")
107
- return jsonify({"status": "error", "message": f"Failed to delete {name} from cloud: {e}"}), 500
108
-
109
- # usersリストから削除するユーザーを除外
110
- users = [u for u in users if u not in names]
111
-
112
- # 全ユーザーリストの更新
113
- update_all_users()
114
-
115
- return jsonify({"status": "success", "message": "Members deleted successfully", "users": users}), 200
116
-
117
- except Exception as e:
118
- print(f"An unexpected error occurred: {e}")
119
- return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500
120
-
121
- # 書き起こし作成エンドポイント
122
- @app.route('/transcription', methods=['GET', 'POST'])
123
- def transcription():
124
- global transcription_text
125
- global total_audio
126
-
127
- if not os.path.exists(transcription_text) or not transcription_text:
128
- try:
129
- if not total_audio or not os.path.exists(total_audio):
130
- return jsonify({"error": "No audio segments provided"}), 400
131
- transcription_text = transcripter.create_transcription(total_audio)
132
- print("transcription")
133
- print(transcription_text)
134
- except Exception as e:
135
- return jsonify({"error": str(e)}), 500
136
-
137
- try:
138
- with open(transcription_text, 'r', encoding='utf-8') as file:
139
- file_content = file.read()
140
- print(file_content)
141
- return jsonify({'transcription': file_content}), 200
142
- except FileNotFoundError:
143
- return jsonify({"error": "Transcription file not found"}), 404
144
- except Exception as e:
145
- return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
146
-
147
- # AI分析エンドポイント
148
- @app.route('/analyze', methods=['GET', 'POST'])
149
- def analyze():
150
- global transcription_text
151
- global total_audio
152
-
153
- if not os.path.exists(transcription_text) or not transcription_text:
154
- try:
155
- if not total_audio:
156
- return jsonify({"error": "No audio segments provided"}), 400
157
- transcription_text = transcripter.create_transcription(total_audio)
158
- except Exception as e:
159
- return jsonify({"error": str(e)}), 500
160
-
161
- analyzer = TextAnalyzer(transcription_text, harassment_keywords)
162
- api_key = os.environ.get("DEEPSEEK")
163
- if api_key is None:
164
- raise ValueError("DEEPSEEK_API_KEY が設定されていません。")
165
-
166
- results = analyzer.analyze(api_key=api_key)
167
-
168
- print(json.dumps(results, ensure_ascii=False, indent=2))
169
-
170
- if "deepseek_analysis" in results and results["deepseek_analysis"]:
171
- deepseek_data = results["deepseek_analysis"]
172
- conversation_level = deepseek_data.get("conversationLevel")
173
- harassment_present = deepseek_data.get("harassmentPresent")
174
- harassment_type = deepseek_data.get("harassmentType")
175
- repetition = deepseek_data.get("repetition")
176
- pleasantConversation = deepseek_data.get("pleasantConversation")
177
- blameOrHarassment = deepseek_data.get("blameOrHarassment")
178
-
179
- print("\n--- DeepSeek 分析結果 ---")
180
- print(f"会話レベル: {conversation_level}")
181
- print(f"ハラスメントの有無: {harassment_present}")
182
- print(f"ハラスメントの種類: {harassment_type}")
183
- print(f"繰り返しの程度: {repetition}")
184
- print(f"会話の心地よさ: {pleasantConversation}")
185
- print(f"非難またはハラスメントの程度: {blameOrHarassment}")
186
-
187
- return jsonify({"results": results}), 200
188
-
189
-
190
- # クラウドから音声を取得してローカルに保存する関数
191
- def download_from_cloud(filename, local_path):
192
- try:
193
- payload = {
194
- "action": "download",
195
- "fileName": filename
196
- }
197
-
198
- print(f"クラウドから {filename} をダウンロード中...")
199
- response = requests.post(GAS_URL, json=payload)
200
- if response.status_code != 200:
201
- print(f"ダウンロードエラー: ステータスコード {response.status_code}")
202
- print(f"レスポンス: {response.text}")
203
- raise Exception(f"クラウドからのダウンロードに失敗しました: {response.text}")
204
-
205
- try:
206
- res_json = response.json()
207
- except:
208
- print("JSONデコードエラー、レスポンス内容:")
209
- print(response.text[:500]) # 最初の500文字だけ表示
210
- raise Exception("サーバーからの応答をJSONとして解析できませんでした")
211
-
212
- if res_json.get("status") != "success":
213
- print(f"ダウンロードステータスエラー: {res_json.get('message')}")
214
- raise Exception(f"クラウドからのダウンロードに失敗しました: {res_json.get('message')}")
215
-
216
- # Base64文字列をデコード
217
- base64_data = res_json.get("base64Data")
218
- if not base64_data:
219
- print("Base64データが存在しません")
220
- raise Exception("応答にBase64データが含まれていません")
221
-
222
- try:
223
- audio_binary = base64.b64decode(base64_data)
224
- except Exception as e:
225
- print(f"Base64デコードエラー: {str(e)}")
226
- raise Exception(f"音声データのデコードに失敗しました: {str(e)}")
227
-
228
- # 指定パスに保存
229
- os.makedirs(os.path.dirname(local_path), exist_ok=True)
230
- with open(local_path, 'wb') as f:
231
- f.write(audio_binary)
232
-
233
- print(f"{filename} をローカルに保存しました: {local_path}")
234
-
235
- # データの整合性チェック(ファイルサイズが0より大きいかなど)
236
- if os.path.getsize(local_path) <= 0:
237
- raise Exception(f"保存されたファイル {local_path} のサイズが0バイトです")
238
-
239
- return local_path
240
- except Exception as e:
241
- print(f"ダウンロード中にエラーが発生しました: {str(e)}")
242
- # エラーを上位に伝播させる
243
- raise
244
-
245
- # クラウドからファイルを削除する関数
246
- def delete_from_cloud(filename):
247
- payload = {
248
- "action": "delete",
249
- "fileName": filename
250
- }
251
- response = requests.post(GAS_URL, json=payload)
252
- if response.status_code != 200:
253
- raise Exception(f"クラウドからの削除に失敗しました: {response.text}")
254
-
255
- res_json = response.json()
256
- if res_json.get("status") != "success":
257
- raise Exception(f"クラウドからの削除に失敗しました: {res_json.get('message')}")
258
-
259
- return True
260
- # すべてのベース音声ユーザーリストを更新する関数
261
- def update_all_users():
262
- global all_users
263
-
264
- payload = {"action": "list"}
265
- response = requests.post(GAS_URL, json=payload)
266
- if response.status_code != 200:
267
- raise Exception(f"GAS一覧取得エラー: {response.text}")
268
-
269
- res_json = response.json()
270
- if res_json.get("status") != "success":
271
- raise Exception(f"GAS一覧取得失敗: {res_json.get('message')}")
272
-
273
- # ファイル名から拡張子を除去してユーザーリストを作成
274
- all_users = [os.path.splitext(filename)[0] for filename in res_json.get("fileNames", [])]
275
- return all_users
276
 
277
- # 音声アップロード&解析エンドポイント
278
  @app.route('/upload_audio', methods=['POST'])
279
  def upload_audio():
280
- global total_audio
281
- global users
282
-
283
  try:
284
  data = request.get_json()
285
- if not data or 'audio_data' not in data:
286
- return jsonify({"error": "音声データがありません"}), 400
287
-
288
- # リクエストからユーザーリストを取得(指定がなければ現在のusersを使用)
289
- if 'selected_users' in data and data['selected_users']:
290
- users = data['selected_users']
291
- print(f"選択されたユーザー: {users}")
292
-
293
- if not users:
294
- return jsonify({"error": "選択されたユーザーがいません"}), 400
295
-
296
- # Base64デコードして音声バイナリを取得
297
- audio_binary = base64.b64decode(data['audio_data'])
298
-
299
- upload_name = 'tmp'
300
- audio_dir = "/tmp/data"
301
- os.makedirs(audio_dir, exist_ok=True)
302
- audio_path = os.path.join(audio_dir, f"{upload_name}.wav")
303
- with open(audio_path, 'wb') as f:
304
- f.write(audio_binary)
305
-
306
- print(f"処理を行うユーザー: {users}")
307
-
308
- # ベース音声を一時ディレクトリにダウンロード
309
- temp_dir = "/tmp/data/base_audio"
310
- os.makedirs(temp_dir, exist_ok=True)
311
-
312
- # 各ユーザーの参照音声ファイルのパスをリストに格納
313
- reference_paths = []
314
- for user in users:
315
- try:
316
- ref_path = os.path.join(temp_dir, f"{user}.wav")
317
- if not os.path.exists(ref_path):
318
- # クラウドから取得
319
- download_from_cloud(f"{user}.wav", ref_path)
320
- print(f"クラウドから {user}.wav をダウンロードしました")
321
-
322
- if not os.path.exists(ref_path):
323
- return jsonify({"error": "参照音声ファイルが見つかりません", "details": ref_path}), 500
324
-
325
- reference_paths.append(ref_path)
326
- except Exception as e:
327
- return jsonify({"error": f"ユーザー {user} の音声取得に失敗しました", "details": str(e)}), 500
328
-
329
- # 複数人の場合は参照パスのリストを、1人の場合は単一のパスを渡す
330
- if len(users) > 1:
331
- print("複数人の場合の処理")
332
- matched_times, merged_segments = process.process_multi_audio(reference_paths, audio_path, users, threshold=0.05)
333
- total_audio = transcripter.save_marged_segments(merged_segments)
334
- # 各メンバーのrateを計算
335
- total_time = sum(matched_times)
336
- rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times]
337
-
338
- # ユーザー名と話した割合をマッピング
339
- user_rates = {users[i]: rates[i] for i in range(len(users))}
340
- return jsonify({"rates": rates, "user_rates": user_rates}), 200
341
- else:
342
- matched_time, unmatched_time, merged_segments = process.process_audio(reference_paths[0], audio_path, users[0], threshold=0.05)
343
- total_audio = transcripter.save_marged_segments(merged_segments)
344
- print("単一ユーザーの処理")
345
- total_time = matched_time + unmatched_time
346
- rate = (matched_time / total_time) * 100 if total_time > 0 else 0
347
- return jsonify({"rate": rate, "user": users[0]}), 200
348
-
349
- except Exception as e:
350
- print("Error in /upload_audio:", str(e))
351
- return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
352
 
353
- # ユーザー選択画面(テンプレート: userSelect.html)
 
 
354
 
355
- # ユーザー選択画面(テンプレート: userSelect.html)
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
- os.remove(transcription_text)
395
- print(f"{transcription_text} を削除しました。")
396
- except Exception as e:
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
- return jsonify({"status": "success", "copied": copied_files}), 200
 
 
427
 
428
- except Exception as e:
429
- print("Error in /copy_selected_files:", str(e))
430
- return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500
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({"status": "success", "message": "tmp配下がすべて削除されました"}), 200
446
 
447
  except Exception as e:
448
- print("Error in /clear_tmp:", str(e))
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.2.5
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