Spaces:
Running
Running
fix_feedback
Browse files- README.md +329 -10
- __pycache__/analyze.cpython-310.pyc +0 -0
- __pycache__/process.cpython-310.pyc +0 -0
- __pycache__/transcription.cpython-310.pyc +0 -0
- app.py +272 -239
- static/feedback.js +0 -12
- static/register_record.js +150 -150
- templates/reset.html +162 -39
- templates/talkDetail.html +160 -49
- templates/userRegister.html +148 -151
- transcription.py +19 -38
README.md
CHANGED
@@ -1,10 +1,329 @@
|
|
1 |
-
---
|
2 |
-
title: JusTalk
|
3 |
-
emoji: ⚡
|
4 |
-
colorFrom: gray
|
5 |
-
colorTo: blue
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
---
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: JusTalk
|
3 |
+
emoji: ⚡
|
4 |
+
colorFrom: gray
|
5 |
+
colorTo: blue
|
6 |
+
sdk: docker
|
7 |
+
pinned: false
|
8 |
+
---
|
9 |
+
|
10 |
+
## <app.py>
|
11 |
+
---
|
12 |
+
|
13 |
+
## グローバル変数と主要な設定
|
14 |
+
|
15 |
+
- **`users`**
|
16 |
+
登録されたユーザー名のリスト。
|
17 |
+
- **`transcription_text`**
|
18 |
+
音声から作成された書き起こしのファイルパスまたは内容。
|
19 |
+
- **`total_audio`**
|
20 |
+
音声セグメントをマージした結果の音声ファイルパス。
|
21 |
+
- **`harassment_keywords`**
|
22 |
+
ハラスメントとして検出するキーワードのリスト(例:"バカ", "死ね" など)。
|
23 |
+
|
24 |
+
また、外部ライブラリとして `pydub`、および独自の `AudioProcessor`、`TranscriptionMaker`、`TextAnalyzer` を用いて音声処理や解析を行います。
|
25 |
+
|
26 |
+
---
|
27 |
+
|
28 |
+
## 各エンドポイントの概要
|
29 |
+
|
30 |
+
### 1. トップページ/ユーザー登録画面
|
31 |
+
|
32 |
+
- **URL:** `/index`
|
33 |
+
**メソッド:** GET, POST
|
34 |
+
**機能:**
|
35 |
+
- テンプレート `index.html` を表示。
|
36 |
+
- 現在の `users` リストをテンプレートに渡す。
|
37 |
+
|
38 |
+
- **URL:** `/` または `/userregister`
|
39 |
+
**メソッド:** GET, POST
|
40 |
+
**機能:**
|
41 |
+
- ユーザー登録用の画面を表示するため、`userRegister.html` テンプレートを返す。
|
42 |
+
|
43 |
+
---
|
44 |
+
|
45 |
+
### 2. フィードバックおよび会話詳細画面
|
46 |
+
|
47 |
+
- **URL:** `/feedback`
|
48 |
+
**メソッド:** GET, POST
|
49 |
+
**機能:**
|
50 |
+
- フィードバック画面(テンプレート:`feedback.html`)を表示。
|
51 |
+
|
52 |
+
- **URL:** `/talk_detail`
|
53 |
+
**メソッド:** GET, POST
|
54 |
+
**機能:**
|
55 |
+
- 会話の詳細を表示する画面(テンプレート:`talkDetail.html`)を返す。
|
56 |
+
|
57 |
+
---
|
58 |
+
|
59 |
+
### 3. ユーザーとファイルのリセット関連
|
60 |
+
|
61 |
+
- **URL:** `/reset_html`
|
62 |
+
**メソッド:** GET, POST
|
63 |
+
**機能:**
|
64 |
+
- リセット画面(テンプレート:`reset.html`)を表示。
|
65 |
+
|
66 |
+
- **URL:** `/reset_member`
|
67 |
+
**メソッド:** GET, POST
|
68 |
+
**機能:**
|
69 |
+
- 指定されたメンバー(ユーザー)とその関連の参照音声ファイルを削除する。
|
70 |
+
- リクエストボディ内で `"names"` キーが必須。
|
71 |
+
- `/tmp/data/base_audio` 内の各ユーザーの音声ファイル(`{name}.wav`)を削除。
|
72 |
+
- 削除後、`users` リストから対象ユーザーを除外する。
|
73 |
+
- 加えて、累積音声ファイル(`total_audio`)や、書き起こしファイル(`transcription_text`)も削除する処理を実施。
|
74 |
+
|
75 |
+
- **URL:** `/reset`
|
76 |
+
**メソッド:** GET
|
77 |
+
**機能:**
|
78 |
+
- `users` リストを空にリセットし、成功レスポンスを返す。
|
79 |
+
|
80 |
+
---
|
81 |
+
|
82 |
+
### 4. 状態確認エンドポイント
|
83 |
+
|
84 |
+
- **URL:** `/confirm`
|
85 |
+
**メソッド:** GET
|
86 |
+
**機能:**
|
87 |
+
- 現在登録されているメンバー(`users`)を JSON 形式で返す。
|
88 |
+
|
89 |
+
---
|
90 |
+
|
91 |
+
### 5. 音声アップロード・解析関連
|
92 |
+
|
93 |
+
#### a. 基本の音声アップロードと参照音声との照合
|
94 |
+
|
95 |
+
- **URL:** `/upload_audio`
|
96 |
+
**メソッド:** POST
|
97 |
+
**機能:**
|
98 |
+
- クライアントから受け取った Base64 形式の音声データをデコードし、一時ファイル(`/tmp/data/tmp.wav`)として保存。
|
99 |
+
- リクエストボディ内には、`audio_data` とともに、`name` または `users`(登録済みユーザーの情報)が必要。
|
100 |
+
- 各ユーザーの参照音声ファイル(`/tmp/data/base_audio/{user}.wav`)とアップロードされた音声を比較し、
|
101 |
+
- 複数ユーザーの場合:`process_multi_audio` を使用して各ユーザー毎の一致時間を計算し、その比率(パーセンテージ)を返す。
|
102 |
+
- 単一ユーザーの場合:`process_audio` を用いて一致時間と非一致時間から比率(パーセンテージ)を計算して返す。
|
103 |
+
- また、処理後に複数の音声セグメントをマージし、その結果を `total_audio` に格納する。
|
104 |
+
|
105 |
+
#### b. 基準音声のアップロード(ユーザー登録時に使用)
|
106 |
+
|
107 |
+
- **URL:** `/upload_base_audio`
|
108 |
+
**メソッド:** POST
|
109 |
+
**機能:**
|
110 |
+
- リクエストボディ内の `audio_data`(Base64形式)と `name` を受け取り、
|
111 |
+
- `users` リストに名前を追加(重複排除を実施)。
|
112 |
+
- `/tmp/data/base_audio/` 内に、`{name}.wav` という名前で保存。
|
113 |
+
- 登録成功時に、状態とファイルパスを返す。
|
114 |
+
|
115 |
+
---
|
116 |
+
|
117 |
+
### 6. 書き起こし生成およびテキスト解析
|
118 |
+
|
119 |
+
#### a. 書き起こし作成
|
120 |
+
|
121 |
+
- **URL:** `/transcription`
|
122 |
+
**メソッド:** GET, POST
|
123 |
+
**機能:**
|
124 |
+
- グローバル変数 `transcription_text` に書き起こし済みのファイルが存在しない場合、
|
125 |
+
- `total_audio`(マージ済み音声ファイル)が存在しているかをチェック。
|
126 |
+
- `TranscriptionMaker` ��� `merge_segments` メソッドでセグメントをマージし、`create_transcription` で書き起こしファイルを生成。
|
127 |
+
- 書き起こしファイルを読み込み、内容を JSON 形式で返す。
|
128 |
+
- エラー発生時には適切な HTTP ステータスコード(400, 404, 500)とエラーメッセージを返す。
|
129 |
+
|
130 |
+
#### b. AI によるテキスト解析
|
131 |
+
|
132 |
+
- **URL:** `/analyze`
|
133 |
+
**メソッド:** GET, POST
|
134 |
+
**機能:**
|
135 |
+
- `/transcription` と同様に、書き起こしファイルの存在を確認し、必要なら再生成する。
|
136 |
+
- `TextAnalyzer` クラスを使って、書き起こしテキストとハラスメントキーワードをもとに解析を実施。
|
137 |
+
- 環境変数 `DEEPSEEK` を API キーとして取得し、DeepSeek 解析を呼び出す。
|
138 |
+
- 解析結果(会話レベル、ハラスメントの有無や種類、繰り返しの程度、会話の心地よさ、非難やハラスメントの程度など)をコンソール出力し、JSON 形式で返す。
|
139 |
+
|
140 |
+
---
|
141 |
+
|
142 |
+
## エンドポイント間の流れとポイント
|
143 |
+
|
144 |
+
- **ユーザー登録:**
|
145 |
+
`/upload_base_audio` でユーザーごとの基準音声を登録し、`users` リストに名前を追加。
|
146 |
+
|
147 |
+
- **音声解析:**
|
148 |
+
`/upload_audio` によりアップロードされた音声を、登録された参照音声と照合。
|
149 |
+
- 複数ユーザーの場合、各ユーザーの一致時間を計算し、パーセンテージとして返す。
|
150 |
+
- 単一ユーザーの場合、一致時間と非一致時間から比率を返す。
|
151 |
+
- この段階で、音声セグメントをマージし、後続の書き起こし・解析に利用するために `total_audio` に格納。
|
152 |
+
|
153 |
+
- **書き起こしの生成と利用:**
|
154 |
+
`/transcription` で `total_audio` から書き起こしファイルを生成し、その内容を取得可能。
|
155 |
+
`/analyze` では、生成済みの書き起こしテキストをもとに、DeepSeek API を用いた AI 解析が実行される。
|
156 |
+
|
157 |
+
- **リセット機能:**
|
158 |
+
`/reset_member` で指定ユーザーの音声ファイル、累積音声、書き起こしファイルを削除。
|
159 |
+
`/reset` で `users` リストを完全にクリアする。
|
160 |
+
|
161 |
+
---
|
162 |
+
|
163 |
+
## <process.py>
|
164 |
+
---
|
165 |
+
|
166 |
+
## 全体概要
|
167 |
+
|
168 |
+
- **目的:**
|
169 |
+
音声ファイルの前処理、セグメント分割、音声特徴量(エンベディング)計算、類似度算出、さらに Base64 形式の音声データの保存など、音声データの様々な操作を行うための機能を提供します。
|
170 |
+
|
171 |
+
- **利用ライブラリ:**
|
172 |
+
- `pydub`:音声ファイルの読み込み、書き出し、セグメント分割に使用
|
173 |
+
- `pyannote.audio`:事前学習済みモデルを用いて音声のエンベディング(特徴量)を計算
|
174 |
+
- `numpy`:数値計算およびベクトル演算(コサイン類似度計算など)
|
175 |
+
- その他、`os`, `shutil`, `base64`, `datetime` など、ファイル操作や乱数生成に利用
|
176 |
+
|
177 |
+
- **初期設定:**
|
178 |
+
コンストラクタ (`__init__`) では、環境変数 `HF` から Hugging Face のトークンを取得し、`pyannote` のモデルをロード。標準の音声長さ(秒)も設定します。
|
179 |
+
|
180 |
+
---
|
181 |
+
|
182 |
+
## 各関数の詳細
|
183 |
+
|
184 |
+
### 1. `normalize_audio_duration`
|
185 |
+
- **目的:**
|
186 |
+
指定された音声ファイルの長さを、ターゲットの秒数に合わせて正規化(短い場合は無音でパディング、長い場合は切り詰め)します。
|
187 |
+
|
188 |
+
- **引数:**
|
189 |
+
- `input_path`:入力音声ファイルのパス
|
190 |
+
- `target_duration_seconds`:目標とする音声の長さ(秒)。未指定の場合は標準値(`self.standard_duration`)を使用
|
191 |
+
- `output_path`:出力先のパス(未指定の場合は一時ファイルとして生成)
|
192 |
+
|
193 |
+
- **戻り値:**
|
194 |
+
正規化された音声ファイルのパス
|
195 |
+
|
196 |
+
---
|
197 |
+
|
198 |
+
### 2. `batch_normalize_audio_duration`
|
199 |
+
- **目的:**
|
200 |
+
指定したディレクトリ内のすべての音声ファイルに対して、`normalize_audio_duration` を適用し、ファイルの長さを統一します。
|
201 |
+
|
202 |
+
- **引数:**
|
203 |
+
- `input_directory`:入力音声ファイルが格納されているディレクトリ
|
204 |
+
- `target_duration_seconds`:目標とする音声の長さ(秒)
|
205 |
+
- `output_directory`:出力先ディレクトリ(未指定の場合は入力ディレクトリと同じ場所)
|
206 |
+
|
207 |
+
- **戻り値:**
|
208 |
+
処理後の音声ファイルパスのリスト
|
209 |
+
|
210 |
+
---
|
211 |
+
|
212 |
+
### 3. `cosine_similarity`
|
213 |
+
- **目的:**
|
214 |
+
2つのベクトル間のコサイン類似度を計算します。
|
215 |
+
- **引数:**
|
216 |
+
- `vec1`, `vec2`:比較対象となる numpy 配列
|
217 |
+
- **処理:**
|
218 |
+
次元が一致しているかを確認し、各ベクトルを正規化した後、内積を計算
|
219 |
+
- **戻り値:**
|
220 |
+
コサイン類似度(-1~1の範囲)
|
221 |
+
|
222 |
+
---
|
223 |
+
|
224 |
+
### 4. `segment_audio`
|
225 |
+
- **目的:**
|
226 |
+
入力音声ファイルを指定した秒数のセグメントに分割し、各セグメン���を出力ディレクトリに保存します。
|
227 |
+
- **引数:**
|
228 |
+
- `path`:入力音声ファイルのパス
|
229 |
+
- `target_path`:分割されたセグメントの保存先ディレクトリ
|
230 |
+
- `seg_duration`:各セグメントの長さ(秒)
|
231 |
+
- **戻り値:**
|
232 |
+
分割されたセグメントのディレクトリパスと、元の音声の総時間(ミリ秒)
|
233 |
+
|
234 |
+
---
|
235 |
+
|
236 |
+
### 5. `calculate_embedding`
|
237 |
+
- **目的:**
|
238 |
+
指定した音声ファイルからエンベディング(特徴量)を計算します。
|
239 |
+
- **引数:**
|
240 |
+
- `audio_path`:音声ファイルのパス
|
241 |
+
- **処理:**
|
242 |
+
- まず音声の長さを `normalize_audio_duration` で標準化
|
243 |
+
- `pyannote.audio` の推論機能を使い、エンベディングを計算
|
244 |
+
- 処理後に一時ファイルを削除(必要に応じて)
|
245 |
+
- **戻り値:**
|
246 |
+
エンベディング(flattened な numpy 配列)
|
247 |
+
|
248 |
+
---
|
249 |
+
|
250 |
+
### 6. `calculate_similarity`
|
251 |
+
- **目的:**
|
252 |
+
2つの音声ファイル間の類似度(コサイン類似度)を計算します。
|
253 |
+
- **引数:**
|
254 |
+
- `path1`, `path2`:比較対象となる2つの音声ファイルのパス
|
255 |
+
- **処理:**
|
256 |
+
- 各音声のエンベディングを計算し、次元チェックを実施
|
257 |
+
- `cosine_similarity` を利用して類似度を求める
|
258 |
+
- **戻り値:**
|
259 |
+
類似度(float 値、エラー時は None)
|
260 |
+
|
261 |
+
---
|
262 |
+
|
263 |
+
### 7. `process_audio`
|
264 |
+
- **目的:**
|
265 |
+
リファレンス音声と入力音声を比較し、リファレンスに類似したセグメントを抽出します。
|
266 |
+
- **引数:**
|
267 |
+
- `reference_path`:リファレンス音声のパス
|
268 |
+
- `input_path`:入力音声のパス
|
269 |
+
- `output_folder`:マッチしたセグメントの出力先ディレクトリ
|
270 |
+
- `seg_duration`:セグメントの長さ(秒)
|
271 |
+
- `threshold`:類似度の閾値
|
272 |
+
- **処理:**
|
273 |
+
- リファレンス音声のエンベディングを計算
|
274 |
+
- 入力音声をセグメントに分割し、各セグメントのエンベディングを計算
|
275 |
+
- 各セグメントとリファレンス間のコサイン類似度を計算し、閾値以上なら出力フォルダへコピー
|
276 |
+
- マッチしたセグメントの総時間と、マッチしなかった時間を計算
|
277 |
+
- **戻り値:**
|
278 |
+
タプル (マッチ時間[ms], 非マッチ時間[ms], 出力フォルダパス)
|
279 |
+
|
280 |
+
---
|
281 |
+
|
282 |
+
### 8. `process_multi_audio`
|
283 |
+
- **目的:**
|
284 |
+
複数のリファレンス音声に対して、入力音声内の各セグメントの類似度を計算し、各リファレンスごとの一致時間を集計します。
|
285 |
+
- **引数:**
|
286 |
+
- `reference_pathes`:リファレンス音声のパスのリスト
|
287 |
+
- `input_path`:入力音声のパス
|
288 |
+
- `output_folder`:マッチしたセグメントの出力先ディレクトリ
|
289 |
+
- `seg_duration`:各セグメントの長さ(秒)
|
290 |
+
- `threshold`:類似度の閾値
|
291 |
+
- **処理:**
|
292 |
+
- 各リファレンス音声のエンベディングを先に計算
|
293 |
+
- 入力音声をセグメントに分割し、各セグメントのエンベディングを計算
|
294 |
+
- リファレンス毎に各セグメントとの類似度を算出し、最も高い類似度が閾値以上ならそのリファレンスとマッチと判断
|
295 |
+
- 各リファレンスごとにマッチしたセグメントの時間(秒)を集計
|
296 |
+
- **戻り値:**
|
297 |
+
タプル (各リファレンスごとの一致時間のリスト, セグメントが保存されたディレクトリパス)
|
298 |
+
|
299 |
+
---
|
300 |
+
|
301 |
+
### 9. `save_audio_from_base64`
|
302 |
+
- **目的:**
|
303 |
+
Base64 形式でエンコードされた音声データをデコードし、WAV ファイルとして保存します。
|
304 |
+
- **引数:**
|
305 |
+
- `base64_audio`:Base64 エンコードされた音声データ
|
306 |
+
- `output_dir`:出力先ディレクトリ
|
307 |
+
- `output_filename`:出力するファイル名
|
308 |
+
- `temp_format`:一時ファイルのフォーマット(デフォルトは 'webm')
|
309 |
+
- **処理:**
|
310 |
+
- Base64 文字列をデコードし、一時ファイルに保存
|
311 |
+
- `pydub` を用いてファイルを読み込み、WAV 形式に変換して保存
|
312 |
+
- 一時ファイルは処理後に削除
|
313 |
+
- **戻り値:**
|
314 |
+
保存された WAV ファイルのパス
|
315 |
+
|
316 |
+
---
|
317 |
+
|
318 |
+
### 10. `delete_files_in_directory`
|
319 |
+
- **目的:**
|
320 |
+
指定したディレクトリ内に存在するすべてのファイルを削除します。
|
321 |
+
- **引数:**
|
322 |
+
- `directory_path`:対象のディレクトリパス
|
323 |
+
- **処理:**
|
324 |
+
- ディレクトリ内の各ファイルを走査し、ファイルのみ削除
|
325 |
+
- 削除結果やエラーはコンソールに出力
|
326 |
+
|
327 |
+
---
|
328 |
+
|
329 |
+
|
__pycache__/analyze.cpython-310.pyc
CHANGED
Binary files a/__pycache__/analyze.cpython-310.pyc and b/__pycache__/analyze.cpython-310.pyc differ
|
|
__pycache__/process.cpython-310.pyc
CHANGED
Binary files a/__pycache__/process.cpython-310.pyc and b/__pycache__/process.cpython-310.pyc differ
|
|
__pycache__/transcription.cpython-310.pyc
CHANGED
Binary files a/__pycache__/transcription.cpython-310.pyc and b/__pycache__/transcription.cpython-310.pyc differ
|
|
app.py
CHANGED
@@ -1,240 +1,273 @@
|
|
1 |
-
from flask import Flask, request, jsonify, render_template, send_from_directory
|
2 |
-
import base64
|
3 |
-
from pydub import AudioSegment # 変換用にpydubをインポート
|
4 |
-
import os
|
5 |
-
import shutil
|
6 |
-
from process import AudioProcessor
|
7 |
-
from transcription import TranscriptionMaker
|
8 |
-
from analyze import TextAnalyzer
|
9 |
-
import json
|
10 |
-
|
11 |
-
process=AudioProcessor()
|
12 |
-
transcripter = TranscriptionMaker()
|
13 |
-
app = Flask(__name__)
|
14 |
-
|
15 |
-
users = []
|
16 |
-
transcription_text=""
|
17 |
-
harassment_keywords = [
|
18 |
-
"バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい",
|
19 |
-
"きもい", "キモい", "ブス", "デブ", "ハゲ",
|
20 |
-
"セクハラ", "パワハラ", "モラハラ"
|
21 |
-
]
|
22 |
-
total_audio = ""
|
23 |
-
|
24 |
-
# トップページ(テンプレート: index.html)
|
25 |
-
@app.route('/index', methods=['GET', 'POST'])
|
26 |
-
def index():
|
27 |
-
return render_template('index.html', users = users)
|
28 |
-
|
29 |
-
# フィードバック画面(テンプレート: feedback.html)
|
30 |
-
@app.route('/feedback', methods=['GET', 'POST'])
|
31 |
-
def feedback():
|
32 |
-
return render_template('feedback.html')
|
33 |
-
|
34 |
-
# 会話詳細画面(テンプレート: talkDetail.html)
|
35 |
-
@app.route('/talk_detail', methods=['GET', 'POST'])
|
36 |
-
def talk_detail():
|
37 |
-
return render_template('talkDetail.html')
|
38 |
-
|
39 |
-
# 音声登録画面(テンプレート: userRegister.html)
|
40 |
-
@app.route('/')
|
41 |
-
@app.route('/userregister', methods=['GET', 'POST'])
|
42 |
-
def userregister():
|
43 |
-
return render_template('userRegister.html')
|
44 |
-
|
45 |
-
#人数確認
|
46 |
-
@app.route('/confirm', methods=['GET']) # 基本的にGETで取得する想定なので、GETのみに変更
|
47 |
-
def confirm():
|
48 |
-
return jsonify({'members': users}), 200
|
49 |
-
|
50 |
-
#リセット画面(テンプレート: reset.html)
|
51 |
-
@app.route('/reset_html', methods=['GET', 'POST'])
|
52 |
-
def reset_html():
|
53 |
-
return render_template('reset.html')
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
@app.route('/
|
190 |
-
def
|
191 |
-
global
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
except Exception as e:
|
235 |
-
print("Error in /
|
236 |
-
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
237 |
-
|
238 |
-
|
239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
240 |
app.run(debug=True, host="0.0.0.0", port=port)
|
|
|
1 |
+
from flask import Flask, request, jsonify, render_template, send_from_directory
|
2 |
+
import base64
|
3 |
+
from pydub import AudioSegment # 変換用にpydubをインポート
|
4 |
+
import os
|
5 |
+
import shutil
|
6 |
+
from process import AudioProcessor
|
7 |
+
from transcription import TranscriptionMaker
|
8 |
+
from analyze import TextAnalyzer
|
9 |
+
import json
|
10 |
+
|
11 |
+
process=AudioProcessor()
|
12 |
+
transcripter = TranscriptionMaker()
|
13 |
+
app = Flask(__name__)
|
14 |
+
|
15 |
+
users = []
|
16 |
+
transcription_text=""
|
17 |
+
harassment_keywords = [
|
18 |
+
"バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい",
|
19 |
+
"きもい", "キモい", "ブス", "デブ", "ハゲ",
|
20 |
+
"セクハラ", "パワハラ", "モラハラ"
|
21 |
+
]
|
22 |
+
total_audio = ""
|
23 |
+
|
24 |
+
# トップページ(テンプレート: index.html)
|
25 |
+
@app.route('/index', methods=['GET', 'POST'])
|
26 |
+
def index():
|
27 |
+
return render_template('index.html', users = users)
|
28 |
+
|
29 |
+
# フィードバック画面(テンプレート: feedback.html)
|
30 |
+
@app.route('/feedback', methods=['GET', 'POST'])
|
31 |
+
def feedback():
|
32 |
+
return render_template('feedback.html')
|
33 |
+
|
34 |
+
# 会話詳細画面(テンプレート: talkDetail.html)
|
35 |
+
@app.route('/talk_detail', methods=['GET', 'POST'])
|
36 |
+
def talk_detail():
|
37 |
+
return render_template('talkDetail.html')
|
38 |
+
|
39 |
+
# 音声登録画面(テンプレート: userRegister.html)
|
40 |
+
@app.route('/')
|
41 |
+
@app.route('/userregister', methods=['GET', 'POST'])
|
42 |
+
def userregister():
|
43 |
+
return render_template('userRegister.html')
|
44 |
+
|
45 |
+
#人数確認
|
46 |
+
@app.route('/confirm', methods=['GET']) # 基本的にGETで取得する想定なので、GETのみに変更
|
47 |
+
def confirm():
|
48 |
+
return jsonify({'members': users}), 200
|
49 |
+
|
50 |
+
#リセット画面(テンプレート: reset.html)
|
51 |
+
@app.route('/reset_html', methods=['GET', 'POST'])
|
52 |
+
def reset_html():
|
53 |
+
return render_template('reset.html')
|
54 |
+
|
55 |
+
@app.route('/login', methods=['POST', 'GET'])
|
56 |
+
def login():
|
57 |
+
global users#グローバル変数を編集できるようにする
|
58 |
+
try:
|
59 |
+
data = request.get_json()
|
60 |
+
name = data['name'] # 名前を取得
|
61 |
+
if name in users:
|
62 |
+
print("名前はリストに存在します。")
|
63 |
+
index()
|
64 |
+
else:
|
65 |
+
print("名前はリストに存在しません。")
|
66 |
+
|
67 |
+
except Exception as e:
|
68 |
+
print("Error in /upload_base_audio:", str(e))
|
69 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
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 |
+
process.delete_files_in_directory(total_audio)
|
78 |
+
process.delete_files_in_directory('/tmp/data/transcription_audio')
|
79 |
+
try:
|
80 |
+
if os.path.exists(transcription_text):
|
81 |
+
os.remove(transcription_text)
|
82 |
+
print(f"{transcription_text} を削除しました。")
|
83 |
+
else:
|
84 |
+
print(f"{transcription_text} は存在しません。")
|
85 |
+
except Exception as e:
|
86 |
+
print(f"エラーが発生しました: {e}")
|
87 |
+
transcription_text = ""
|
88 |
+
try:
|
89 |
+
data = request.get_json()
|
90 |
+
if not data or "names" not in data:
|
91 |
+
return jsonify({"status": "error", "message": "Invalid request body"}), 400 # 400 Bad Request
|
92 |
+
|
93 |
+
names = data.get("names", [])
|
94 |
+
base_audio_dir = "/tmp/data/base_audio"
|
95 |
+
|
96 |
+
for name in names:
|
97 |
+
file_path = os.path.join(base_audio_dir, f"{name}.wav")
|
98 |
+
if os.path.exists(file_path):
|
99 |
+
try:
|
100 |
+
os.remove(file_path)
|
101 |
+
print(f"{file_path} を削除しました。")
|
102 |
+
except Exception as e:
|
103 |
+
print(f"削除中にエラーが発生しました: {e}")
|
104 |
+
# ファイル削除に失敗した場合も、エラーを返す
|
105 |
+
return jsonify({"status": "error", "message": f"Failed to delete {name}: {e}"}), 500
|
106 |
+
|
107 |
+
else:
|
108 |
+
print(f"ファイルが存在しません: {file_path}")
|
109 |
+
|
110 |
+
# usersリストを更新
|
111 |
+
users = [u for u in users if u not in names]
|
112 |
+
|
113 |
+
# 成功した場合のレスポンス
|
114 |
+
return jsonify({"status": "success", "message": "Members deleted successfully", "users": users}), 200 # 200 OK
|
115 |
+
|
116 |
+
except Exception as e:
|
117 |
+
print(f"An unexpected error occurred: {e}")
|
118 |
+
return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500 # 500 Internal Server Error
|
119 |
+
|
120 |
+
# 書き起こし作成エンドポイント
|
121 |
+
@app.route('/transcription',methods =['GET','POST'])
|
122 |
+
def transcription():
|
123 |
+
global transcription_text
|
124 |
+
global total_audio
|
125 |
+
if not os.path.exists(transcription_text) or not transcription_text:
|
126 |
+
try:
|
127 |
+
if not total_audio or not os.path.exists(total_audio):
|
128 |
+
return jsonify({"error": "No audio segments provided"}),400
|
129 |
+
audio_directory = transcripter.merge_segments(total_audio,'/tmp/data/transcription_audio')
|
130 |
+
transcription_text = transcripter.create_transcription(audio_directory)
|
131 |
+
print("transcription")
|
132 |
+
print(transcription_text)
|
133 |
+
except Exception as e:
|
134 |
+
return jsonify({"error": str(e)}),500
|
135 |
+
try:
|
136 |
+
with open(transcription_text,'r',encoding='utf-8') as file:
|
137 |
+
file_content = file.read()
|
138 |
+
print(file_content)
|
139 |
+
return jsonify({'transcription': file_content}),200
|
140 |
+
except FileNotFoundError:
|
141 |
+
return jsonify({"error": "Transcription file not found"}), 404
|
142 |
+
except Exception as e:
|
143 |
+
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
144 |
+
|
145 |
+
|
146 |
+
# AI分析エンドポイント
|
147 |
+
@app.route('/analyze',methods =['GET','POST'])
|
148 |
+
def analyze():
|
149 |
+
global transcription_text
|
150 |
+
global total_audio
|
151 |
+
if not os.path.exists(transcription_text) or not transcription_text:
|
152 |
+
try:
|
153 |
+
if not total_audio:
|
154 |
+
return jsonify({"error": "No audio segments provided"}),400
|
155 |
+
audio_directory = transcripter.merge_segments(total_audio,'/tmp/data/transcription_audio')
|
156 |
+
transcription_text = transcripter.create_transcription(audio_directory)
|
157 |
+
except Exception as e:
|
158 |
+
return jsonify({"error": str(e)}),500
|
159 |
+
|
160 |
+
analyzer = TextAnalyzer(transcription_text, harassment_keywords)
|
161 |
+
api_key = os.environ.get("DEEPSEEK")
|
162 |
+
if api_key is None:
|
163 |
+
raise ValueError("DEEPSEEK_API_KEY が設定されていません。")
|
164 |
+
|
165 |
+
results = analyzer.analyze(api_key=api_key)
|
166 |
+
|
167 |
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
168 |
+
|
169 |
+
if "deepseek_analysis" in results and results["deepseek_analysis"]:
|
170 |
+
deepseek_data = results["deepseek_analysis"]
|
171 |
+
conversation_level = deepseek_data.get("conversationLevel")
|
172 |
+
harassment_present = deepseek_data.get("harassmentPresent")
|
173 |
+
harassment_type = deepseek_data.get("harassmentType")
|
174 |
+
repetition = deepseek_data.get("repetition")
|
175 |
+
pleasantConversation = deepseek_data.get("pleasantConversation")
|
176 |
+
blameOrHarassment = deepseek_data.get("blameOrHarassment")
|
177 |
+
|
178 |
+
print("\n--- DeepSeek 分析結果 ---")
|
179 |
+
print(f"会話レベル: {conversation_level}")
|
180 |
+
print(f"ハラスメントの有無: {harassment_present}")
|
181 |
+
print(f"ハラスメントの種類: {harassment_type}")
|
182 |
+
print(f"繰り返しの程度: {repetition}")
|
183 |
+
print(f"会話の心地よさ: {pleasantConversation}")
|
184 |
+
print(f"非難またはハラスメントの程度: {blameOrHarassment}")
|
185 |
+
return jsonify({"results": results}),200
|
186 |
+
|
187 |
+
|
188 |
+
# 音声アップロード&解析エンドポイント
|
189 |
+
@app.route('/upload_audio', methods=['POST'])
|
190 |
+
def upload_audio():
|
191 |
+
global total_audio
|
192 |
+
try:
|
193 |
+
data = request.get_json()
|
194 |
+
# name か users のいずれかが必須。どちらも無い場合はエラー
|
195 |
+
if not data or 'audio_data' not in data or ('name' not in data and 'users' not in data):
|
196 |
+
return jsonify({"error": "音声データまたは名前がありません"}), 400
|
197 |
+
|
198 |
+
# Base64デコードして音声バイナリを取得
|
199 |
+
audio_binary = base64.b64decode(data['audio_data'])
|
200 |
+
|
201 |
+
upload_name = 'tmp'
|
202 |
+
audio_dir = "/tmp/data"
|
203 |
+
os.makedirs(audio_dir, exist_ok=True)
|
204 |
+
audio_path = os.path.join(audio_dir, f"{upload_name}.wav")
|
205 |
+
with open(audio_path, 'wb') as f:
|
206 |
+
f.write(audio_binary)
|
207 |
+
print(users)
|
208 |
+
# 各ユーザーの参照音声ファイルのパスをリストに格納
|
209 |
+
reference_paths = []
|
210 |
+
base_audio_dir = "/tmp/data/base_audio"
|
211 |
+
for user in users:
|
212 |
+
ref_path = os.path.abspath(os.path.join(base_audio_dir, f"{user}.wav"))
|
213 |
+
if not os.path.exists(ref_path):
|
214 |
+
return jsonify({"error": "参照音声ファイルが見つかりません", "details": ref_path}), 500
|
215 |
+
reference_paths.append(ref_path)
|
216 |
+
|
217 |
+
# 複数人の場合は参照パスのリストを、1人の場合は単一のパスを渡す
|
218 |
+
if len(users) > 1:
|
219 |
+
print("複数人の場合の処理")
|
220 |
+
matched_times, segments_dir = process.process_multi_audio(reference_paths, audio_path, threshold=0.05)
|
221 |
+
total_audio = transcripter.merge_segments(segments_dir)
|
222 |
+
# 各メンバーのrateを計算
|
223 |
+
total_time = sum(matched_times)
|
224 |
+
rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times]
|
225 |
+
return jsonify({"rates": rates}), 200
|
226 |
+
else:
|
227 |
+
matched_time, unmatched_time, segments_dir = process.process_audio(reference_paths[0], audio_path, threshold=0.05)
|
228 |
+
total_audio = transcripter.merge_segments(segments_dir)
|
229 |
+
print("solo")
|
230 |
+
print(total_audio)
|
231 |
+
total_time = matched_time + unmatched_time
|
232 |
+
rate = (matched_time / total_time) * 100 if total_time > 0 else 0
|
233 |
+
return jsonify({"rate": rate}), 200
|
234 |
+
except Exception as e:
|
235 |
+
print("Error in /upload_audio:", str(e))
|
236 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
237 |
+
|
238 |
+
@app.route('/reset', methods=['GET'])
|
239 |
+
def reset():
|
240 |
+
global users
|
241 |
+
users = []
|
242 |
+
return jsonify({"status": "success", "message": "Users reset"}), 200
|
243 |
+
|
244 |
+
|
245 |
+
@app.route('/upload_base_audio', methods=['POST'])
|
246 |
+
def upload_base_audio():
|
247 |
+
global users#グローバル変数を編集できるようにする
|
248 |
+
try:
|
249 |
+
data = request.get_json()
|
250 |
+
if not data or 'audio_data' not in data or 'name' not in data:
|
251 |
+
return jsonify({"error": "音声データまたは名前がありません"}), 400
|
252 |
+
name = data['name'] # 名前を取得
|
253 |
+
print(name)
|
254 |
+
|
255 |
+
|
256 |
+
users.append(name)
|
257 |
+
users=list(set(users))#重複排除
|
258 |
+
print(users)
|
259 |
+
|
260 |
+
|
261 |
+
audio_path=process.save_audio_from_base64(
|
262 |
+
base64_audio=data['audio_data'], # 音声データ
|
263 |
+
output_dir= "/tmp/data/base_audio", #保存先
|
264 |
+
output_filename=f"{name}.wav" # 固定ファイル名(必要に応じて generate_filename() で一意のファイル名に変更可能)
|
265 |
+
)
|
266 |
+
return jsonify({"state": "Registration Success!", "path": audio_path}), 200
|
267 |
+
except Exception as e:
|
268 |
+
print("Error in /upload_base_audio:", str(e))
|
269 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
270 |
+
|
271 |
+
if __name__ == '__main__':
|
272 |
+
port = int(os.environ.get("PORT", 7860))
|
273 |
app.run(debug=True, host="0.0.0.0", port=port)
|
static/feedback.js
CHANGED
@@ -15,8 +15,6 @@ 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 |
|
22 |
if (!response.ok) {
|
@@ -35,16 +33,6 @@ async function getAnalysis() {
|
|
35 |
const pleasantConversation = analysis.pleasantConversation;
|
36 |
const blameOrHarassment = analysis.blameOrHarassment;
|
37 |
|
38 |
-
if (harassmentPresent) {
|
39 |
-
harassmentPresent = "あり";
|
40 |
-
} else {
|
41 |
-
harassmentPresent = "なし";
|
42 |
-
}
|
43 |
-
|
44 |
-
if (harassmentType == null) {
|
45 |
-
harassmentType = "該当なし";
|
46 |
-
}
|
47 |
-
|
48 |
loader.style.display = "none";
|
49 |
// DOMに表示
|
50 |
document.getElementById(
|
|
|
15 |
const loader = document.getElementById("loader");
|
16 |
loader.style.display = "block";
|
17 |
try {
|
|
|
|
|
18 |
const response = await fetch("/analyze");
|
19 |
|
20 |
if (!response.ok) {
|
|
|
33 |
const pleasantConversation = analysis.pleasantConversation;
|
34 |
const blameOrHarassment = analysis.blameOrHarassment;
|
35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
loader.style.display = "none";
|
37 |
// DOMに表示
|
38 |
document.getElementById(
|
static/register_record.js
CHANGED
@@ -1,150 +1,150 @@
|
|
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
|
102 |
-
window.location.href = "index";
|
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);
|
|
|
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 showRecorder() {
|
102 |
+
window.location.href = "index";
|
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);
|
templates/reset.html
CHANGED
@@ -1,39 +1,162 @@
|
|
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 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
<style>
|
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 {
|
22 |
+
border: 5px solid rgba(255, 255, 255, 0.2);
|
23 |
+
border-radius: 1rem;
|
24 |
+
margin: 1rem auto; /* 中央揃え */
|
25 |
+
width: 90%;
|
26 |
+
max-width: 500px;
|
27 |
+
/*width: 100%; */ /* 枠線全体を広げる */
|
28 |
+
/*max-width: 90%;*/ /* 最大幅を画面の90%に設定 */
|
29 |
+
}
|
30 |
+
|
31 |
+
/* Hamburger Menu Button */
|
32 |
+
#menuButton {
|
33 |
+
background-color: rgba(255, 255, 255, 0.1);
|
34 |
+
border: none;
|
35 |
+
border-radius: 50%;
|
36 |
+
padding: 0.5rem;
|
37 |
+
cursor: pointer;
|
38 |
+
transition: background-color 0.2s ease;
|
39 |
+
}
|
40 |
+
|
41 |
+
#menuButton:hover {
|
42 |
+
background-color: rgba(255, 255, 255, 0.2);
|
43 |
+
}
|
44 |
+
|
45 |
+
/* Hamburger Menu Styles */
|
46 |
+
#menu {
|
47 |
+
position: absolute;
|
48 |
+
top: 0;
|
49 |
+
left: 0;
|
50 |
+
z-index: 10;
|
51 |
+
transform: translateX(-100%);
|
52 |
+
visibility: hidden;
|
53 |
+
opacity: 0;
|
54 |
+
background-color: rgb(31, 41, 55);
|
55 |
+
transition: transform 0.3s ease-in-out, visibility 0s 0.3s,
|
56 |
+
opacity 0.3s ease-in-out;
|
57 |
+
backdrop-filter: blur(10px);
|
58 |
+
border-right: 1px solid rgba(255, 255, 255, 0.2);
|
59 |
+
}
|
60 |
+
|
61 |
+
#menu.open {
|
62 |
+
transform: translateX(0);
|
63 |
+
visibility: visible;
|
64 |
+
opacity: 1;
|
65 |
+
transition: transform 0.3s ease-in-out, visibility 0s 0s,
|
66 |
+
opacity 0.3s ease-in-out;
|
67 |
+
}
|
68 |
+
|
69 |
+
#menu button {
|
70 |
+
transition: background-color 0.2s ease;
|
71 |
+
background-color: rgba(0, 0, 0, 0.1);
|
72 |
+
margin: 2px;
|
73 |
+
border-radius: 5px;
|
74 |
+
display: flex;
|
75 |
+
align-items: center;
|
76 |
+
justify-content: flex-start;
|
77 |
+
gap: 10px;
|
78 |
+
padding: 0.75rem 1rem;
|
79 |
+
width: 100%;
|
80 |
+
text-align: left;
|
81 |
+
border: none;
|
82 |
+
color: #fff;
|
83 |
+
font-size: 1rem;
|
84 |
+
cursor: pointer;
|
85 |
+
}
|
86 |
+
|
87 |
+
#menu button:hover {
|
88 |
+
background-color: rgba(55, 65, 81, 0.7);
|
89 |
+
}
|
90 |
+
</style>
|
91 |
+
</head>
|
92 |
+
<body>
|
93 |
+
<div class="main-content relative">
|
94 |
+
<div class="p-6 bg-white dark:bg-gray-800 shadow-lg rounded-2xl">
|
95 |
+
<h2 class="text-2xl font-semibold mb-4 text-center">
|
96 |
+
メンバーを消去しますか?
|
97 |
+
</h2>
|
98 |
+
|
99 |
+
<!-- Hamburger Menu -->
|
100 |
+
<div class="absolute top-4 left-4">
|
101 |
+
<button
|
102 |
+
id="menuButton"
|
103 |
+
class="text-white text-2xl focus:outline-none"
|
104 |
+
onclick="toggleMenu(event)"
|
105 |
+
>
|
106 |
+
<i class="fas fa-bars"></i>
|
107 |
+
</button>
|
108 |
+
|
109 |
+
<!-- Menu Content -->
|
110 |
+
<div
|
111 |
+
id="menu"
|
112 |
+
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"
|
113 |
+
>
|
114 |
+
<div class="px-4 py-2 text-lg font-semibold">メニュー</div>
|
115 |
+
<button onclick="showUserRegister()">
|
116 |
+
<i class="fas fa-user-plus"></i> メンバーを追加
|
117 |
+
</button>
|
118 |
+
<button onclick="showRecorder()">
|
119 |
+
<i class="fas fa-microphone"></i> 録音画面を表示
|
120 |
+
</button>
|
121 |
+
<button onclick="showResults()">
|
122 |
+
<i class="fas fa-chart-bar"></i> フィードバックを表示
|
123 |
+
</button>
|
124 |
+
<button onclick="showTalkDetail()">
|
125 |
+
<i class="fas fa-comments"></i> 会話詳細を表示
|
126 |
+
</button>
|
127 |
+
<button onclick="resetAction()">
|
128 |
+
<i class="fas fa-redo"></i> リセット
|
129 |
+
</button>
|
130 |
+
<button onclick="toggleMenu(event)">
|
131 |
+
<i class="fas fa-times"></i> 閉じる
|
132 |
+
</button>
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
<!-- Hamburger Menu End -->
|
136 |
+
|
137 |
+
<input type="button" id="select-all" value="全選択" />
|
138 |
+
<div id="memberCheckboxes">
|
139 |
+
<!--ここにチャックボックスを表示してほしい-->
|
140 |
+
</div>
|
141 |
+
|
142 |
+
<div class="flex justify-center gap-4">
|
143 |
+
<button
|
144 |
+
id="reset_btn"
|
145 |
+
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
|
146 |
+
>
|
147 |
+
メンバー削除
|
148 |
+
</button>
|
149 |
+
|
150 |
+
<button
|
151 |
+
class="px-6 py-2 bg-[#607d8b] text-white rounded-lg hover:bg-[#546e7a] transition-colors"
|
152 |
+
onclick="showRecorder()"
|
153 |
+
>
|
154 |
+
録音画面を表示
|
155 |
+
</button>
|
156 |
+
</div>
|
157 |
+
</div>
|
158 |
+
</div>
|
159 |
+
<script src="{{ url_for('static', filename='reset.js') }}"></script>
|
160 |
+
<script src="{{ url_for('static', filename='menu.js') }}"></script>
|
161 |
+
</body>
|
162 |
+
</html>
|
templates/talkDetail.html
CHANGED
@@ -1,49 +1,160 @@
|
|
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 |
-
<
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
10 |
+
rel="stylesheet"
|
11 |
+
href="{{ url_for('static', filename='loading.css') }}"
|
12 |
+
/>
|
13 |
+
<style>
|
14 |
+
/* Main Container */
|
15 |
+
body {
|
16 |
+
background: linear-gradient(135deg, #2c3e50, #1f2937);
|
17 |
+
display: flex;
|
18 |
+
align-items: center;
|
19 |
+
justify-content: center;
|
20 |
+
min-height: 100vh;
|
21 |
+
font-family: "Arial", sans-serif;
|
22 |
+
color: #fff;
|
23 |
+
}
|
24 |
+
|
25 |
+
.main-content {
|
26 |
+
border: 5px solid rgba(255, 255, 255, 0.2);
|
27 |
+
border-radius: 1rem;
|
28 |
+
margin: 1rem; /* 外側の余白 */
|
29 |
+
width: 90%;
|
30 |
+
max-width: 500px;
|
31 |
+
/*width: 100%; */ /* 枠線全体を広げる */
|
32 |
+
/*max-width: 90%;*/ /* 最大幅を画面の90%に設定 */
|
33 |
+
}
|
34 |
+
|
35 |
+
/* Hamburger Menu Button */
|
36 |
+
#menuButton {
|
37 |
+
background-color: rgba(255, 255, 255, 0.1);
|
38 |
+
border: none;
|
39 |
+
border-radius: 50%;
|
40 |
+
padding: 0.5rem;
|
41 |
+
cursor: pointer;
|
42 |
+
transition: background-color 0.2s ease;
|
43 |
+
}
|
44 |
+
|
45 |
+
#menuButton:hover {
|
46 |
+
background-color: rgba(255, 255, 255, 0.2);
|
47 |
+
}
|
48 |
+
|
49 |
+
/* Hamburger Menu Styles */
|
50 |
+
#menu {
|
51 |
+
position: absolute;
|
52 |
+
top: 0;
|
53 |
+
left: 0;
|
54 |
+
z-index: 10;
|
55 |
+
transform: translateX(-100%);
|
56 |
+
visibility: hidden;
|
57 |
+
opacity: 0;
|
58 |
+
background-color: rgb(31, 41, 55);
|
59 |
+
transition: transform 0.3s ease-in-out, visibility 0s 0.3s,
|
60 |
+
opacity 0.3s ease-in-out;
|
61 |
+
backdrop-filter: blur(10px);
|
62 |
+
border-right: 1px solid rgba(255, 255, 255, 0.2);
|
63 |
+
}
|
64 |
+
|
65 |
+
#menu.open {
|
66 |
+
transform: translateX(0);
|
67 |
+
visibility: visible;
|
68 |
+
opacity: 1;
|
69 |
+
transition: transform 0.3s ease-in-out, visibility 0s 0s,
|
70 |
+
opacity 0.3s ease-in-out;
|
71 |
+
}
|
72 |
+
|
73 |
+
#menu button {
|
74 |
+
transition: background-color 0.2s ease;
|
75 |
+
background-color: rgba(0, 0, 0, 0.1);
|
76 |
+
margin: 2px;
|
77 |
+
border-radius: 5px;
|
78 |
+
display: flex;
|
79 |
+
align-items: center;
|
80 |
+
justify-content: flex-start;
|
81 |
+
gap: 10px;
|
82 |
+
padding: 0.75rem 1rem;
|
83 |
+
width: 100%;
|
84 |
+
text-align: left;
|
85 |
+
border: none;
|
86 |
+
color: #fff;
|
87 |
+
font-size: 1rem;
|
88 |
+
cursor: pointer;
|
89 |
+
}
|
90 |
+
|
91 |
+
#menu button:hover {
|
92 |
+
background-color: rgba(55, 65, 81, 0.7);
|
93 |
+
}
|
94 |
+
</style>
|
95 |
+
</head>
|
96 |
+
<body>
|
97 |
+
<div class="main-content relative">
|
98 |
+
<div class="loader" id="loader">
|
99 |
+
<div class="one"></div>
|
100 |
+
<div class="two"></div>
|
101 |
+
<div class="three"></div>
|
102 |
+
<div class="four"></div>
|
103 |
+
</div>
|
104 |
+
<div
|
105 |
+
class="container mx-auto p-6 bg-white dark:bg-gray-800 shadow-lg rounded-2xl w-full max-w-none"
|
106 |
+
>
|
107 |
+
<h2 class="text-2xl font-semibold mb-4 text-center">
|
108 |
+
会話の文字起こし表示
|
109 |
+
</h2>
|
110 |
+
|
111 |
+
<!-- Hamburger Menu -->
|
112 |
+
<div class="absolute top-4 left-4">
|
113 |
+
<button
|
114 |
+
id="menuButton"
|
115 |
+
class="text-white text-2xl focus:outline-none"
|
116 |
+
onclick="toggleMenu(event)"
|
117 |
+
>
|
118 |
+
<i class="fas fa-bars"></i>
|
119 |
+
</button>
|
120 |
+
|
121 |
+
<!-- Menu Content -->
|
122 |
+
<div
|
123 |
+
id="menu"
|
124 |
+
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"
|
125 |
+
>
|
126 |
+
<div class="px-4 py-2 text-lg font-semibold">メニュー</div>
|
127 |
+
<button onclick="showUserRegister()">
|
128 |
+
<i class="fas fa-user-plus"></i> メンバーを追加
|
129 |
+
</button>
|
130 |
+
<button onclick="showRecorder()">
|
131 |
+
<i class="fas fa-microphone"></i> 録音画面を表示
|
132 |
+
</button>
|
133 |
+
<button onclick="showResults()">
|
134 |
+
<i class="fas fa-chart-bar"></i> フィードバックを表示
|
135 |
+
</button>
|
136 |
+
<button onclick="showTalkDetail()">
|
137 |
+
<i class="fas fa-comments"></i> 会話詳細を表示
|
138 |
+
</button>
|
139 |
+
<button onclick="resetAction()">
|
140 |
+
<i class="fas fa-redo"></i> リセット
|
141 |
+
</button>
|
142 |
+
<button onclick="toggleMenu(event)">
|
143 |
+
<i class="fas fa-times"></i> 閉じる
|
144 |
+
</button>
|
145 |
+
</div>
|
146 |
+
</div>
|
147 |
+
<!-- Hamburger Menu End -->
|
148 |
+
|
149 |
+
<div
|
150 |
+
id="transcription"
|
151 |
+
class="p-4 bg-gray-200 dark:bg-gray-700 rounded-lg mb-4 max-h-96 overflow-y-auto"
|
152 |
+
>
|
153 |
+
ここに会話内容が表示されます。
|
154 |
+
</div>
|
155 |
+
</div>
|
156 |
+
<script src="{{ url_for('static', filename='talk_detail.js') }}"></script>
|
157 |
+
<script src="{{ url_for('static', filename='menu.js') }}"></script>
|
158 |
+
</div>
|
159 |
+
</body>
|
160 |
+
</html>
|
templates/userRegister.html
CHANGED
@@ -1,151 +1,148 @@
|
|
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 |
-
<style>
|
9 |
-
@keyframes pulse-scale {
|
10 |
-
0%,
|
11 |
-
100% {
|
12 |
-
transform: scale(1);
|
13 |
-
}
|
14 |
-
50% {
|
15 |
-
transform: scale(1.1);
|
16 |
-
}
|
17 |
-
}
|
18 |
-
.animate-pulse-scale {
|
19 |
-
animation: pulse-scale 1s infinite;
|
20 |
-
}
|
21 |
-
|
22 |
-
/* Record Button Styles */
|
23 |
-
.record-button {
|
24 |
-
width: 50px;
|
25 |
-
height: 50px;
|
26 |
-
background-color: transparent;
|
27 |
-
border-radius: 50%;
|
28 |
-
border: 2px solid white;
|
29 |
-
display: flex;
|
30 |
-
justify-content: center;
|
31 |
-
align-items: center;
|
32 |
-
cursor: pointer;
|
33 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
|
34 |
-
transition: all 0.3s ease;
|
35 |
-
}
|
36 |
-
.record-icon {
|
37 |
-
width: 35px;
|
38 |
-
height: 35px;
|
39 |
-
background-color: #d32f2f;
|
40 |
-
border-radius: 50%;
|
41 |
-
transition: all 0.3s ease;
|
42 |
-
}
|
43 |
-
.record-button.recording .record-icon {
|
44 |
-
background-color: #f44336; /* 録音中は赤色 */
|
45 |
-
border-radius: 4px; /* 録音時に赤い部分だけ四角にする */
|
46 |
-
}
|
47 |
-
.recording .record-icon {
|
48 |
-
width: 20px;
|
49 |
-
height: 20px;
|
50 |
-
border-radius: 50%;
|
51 |
-
}
|
52 |
-
|
53 |
-
/* Main Container */
|
54 |
-
body {
|
55 |
-
background: linear-gradient(135deg, #2c3e50, #1f2937);
|
56 |
-
display: flex;
|
57 |
-
align-items: center;
|
58 |
-
justify-content: center;
|
59 |
-
min-height: 100vh;
|
60 |
-
font-family: "Arial", sans-serif;
|
61 |
-
}
|
62 |
-
|
63 |
-
/* Main Content Wrapper */
|
64 |
-
.main-content {
|
65 |
-
border: 5px solid rgba(255, 255, 255, 0.2);
|
66 |
-
padding: 2rem;
|
67 |
-
border-radius: 1rem;
|
68 |
-
width: 90%;
|
69 |
-
max-width: 500px;
|
70 |
-
background-color: rgba(0, 0, 0, 0.3);
|
71 |
-
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
|
72 |
-
text-align: center;
|
73 |
-
}
|
74 |
-
|
75 |
-
/* Title */
|
76 |
-
.main-title {
|
77 |
-
font-size: 2.5rem;
|
78 |
-
font-weight: bold;
|
79 |
-
margin-bottom: 1.5rem;
|
80 |
-
color: #fff;
|
81 |
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
82 |
-
}
|
83 |
-
|
84 |
-
/* Buttons */
|
85 |
-
.action-button {
|
86 |
-
margin-top: 1rem;
|
87 |
-
padding: 0.75rem 1.5rem;
|
88 |
-
border-radius: 0.5rem;
|
89 |
-
cursor: pointer;
|
90 |
-
transition: background-color 0.2s ease;
|
91 |
-
width: 100%;
|
92 |
-
}
|
93 |
-
|
94 |
-
.action-button:hover {
|
95 |
-
background-color: rgba(55, 65, 81, 0.7);
|
96 |
-
}
|
97 |
-
|
98 |
-
.back-button {
|
99 |
-
background-color: #607d8b; /* 落ち着いたグレー */
|
100 |
-
color: white;
|
101 |
-
}
|
102 |
-
|
103 |
-
.add-button {
|
104 |
-
background-color: #4caf50; /* 落ち着いた緑色 */
|
105 |
-
color: white;
|
106 |
-
}
|
107 |
-
|
108 |
-
/* Disabled State */
|
109 |
-
.disabled {
|
110 |
-
opacity: 0.5;
|
111 |
-
pointer-events: none;
|
112 |
-
}
|
113 |
-
|
114 |
-
/* Responsive Design */
|
115 |
-
@media (max-width: 640px) {
|
116 |
-
.main-content {
|
117 |
-
padding: 1.5rem;
|
118 |
-
}
|
119 |
-
}
|
120 |
-
</style>
|
121 |
-
</head>
|
122 |
-
<body>
|
123 |
-
<!-- Main Content Wrapper -->
|
124 |
-
<div class="main-content relative">
|
125 |
-
<!-- Title -->
|
126 |
-
<div class="main-title"
|
127 |
-
|
128 |
-
<!-- User List -->
|
129 |
-
<div id="people-list" class="space-y-4"></div>
|
130 |
-
|
131 |
-
<!-- Add Button -->
|
132 |
-
<button id="add-btn" class="action-button add-button">
|
133 |
-
<i class="fas fa-user-plus"></i> メンバーを追加
|
134 |
-
</button>
|
135 |
-
|
136 |
-
<!-- Back Button -->
|
137 |
-
<button
|
138 |
-
id="backButton"
|
139 |
-
onclick="
|
140 |
-
class="action-button back-button"
|
141 |
-
>
|
142 |
-
|
143 |
-
</button>
|
144 |
-
</div>
|
145 |
-
|
146 |
-
<script src="{{ url_for('static', filename='register_record.js') }}"></script>
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
</body>
|
151 |
-
</html>
|
|
|
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 |
+
<style>
|
9 |
+
@keyframes pulse-scale {
|
10 |
+
0%,
|
11 |
+
100% {
|
12 |
+
transform: scale(1);
|
13 |
+
}
|
14 |
+
50% {
|
15 |
+
transform: scale(1.1);
|
16 |
+
}
|
17 |
+
}
|
18 |
+
.animate-pulse-scale {
|
19 |
+
animation: pulse-scale 1s infinite;
|
20 |
+
}
|
21 |
+
|
22 |
+
/* Record Button Styles */
|
23 |
+
.record-button {
|
24 |
+
width: 50px;
|
25 |
+
height: 50px;
|
26 |
+
background-color: transparent;
|
27 |
+
border-radius: 50%;
|
28 |
+
border: 2px solid white;
|
29 |
+
display: flex;
|
30 |
+
justify-content: center;
|
31 |
+
align-items: center;
|
32 |
+
cursor: pointer;
|
33 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
|
34 |
+
transition: all 0.3s ease;
|
35 |
+
}
|
36 |
+
.record-icon {
|
37 |
+
width: 35px;
|
38 |
+
height: 35px;
|
39 |
+
background-color: #d32f2f;
|
40 |
+
border-radius: 50%;
|
41 |
+
transition: all 0.3s ease;
|
42 |
+
}
|
43 |
+
.record-button.recording .record-icon {
|
44 |
+
background-color: #f44336; /* 録音中は赤色 */
|
45 |
+
border-radius: 4px; /* 録音時に赤い部分だけ四角にする */
|
46 |
+
}
|
47 |
+
.recording .record-icon {
|
48 |
+
width: 20px;
|
49 |
+
height: 20px;
|
50 |
+
border-radius: 50%;
|
51 |
+
}
|
52 |
+
|
53 |
+
/* Main Container */
|
54 |
+
body {
|
55 |
+
background: linear-gradient(135deg, #2c3e50, #1f2937);
|
56 |
+
display: flex;
|
57 |
+
align-items: center;
|
58 |
+
justify-content: center;
|
59 |
+
min-height: 100vh;
|
60 |
+
font-family: "Arial", sans-serif;
|
61 |
+
}
|
62 |
+
|
63 |
+
/* Main Content Wrapper */
|
64 |
+
.main-content {
|
65 |
+
border: 5px solid rgba(255, 255, 255, 0.2);
|
66 |
+
padding: 2rem;
|
67 |
+
border-radius: 1rem;
|
68 |
+
width: 90%;
|
69 |
+
max-width: 500px;
|
70 |
+
background-color: rgba(0, 0, 0, 0.3);
|
71 |
+
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
|
72 |
+
text-align: center;
|
73 |
+
}
|
74 |
+
|
75 |
+
/* Title */
|
76 |
+
.main-title {
|
77 |
+
font-size: 2.5rem;
|
78 |
+
font-weight: bold;
|
79 |
+
margin-bottom: 1.5rem;
|
80 |
+
color: #fff;
|
81 |
+
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
82 |
+
}
|
83 |
+
|
84 |
+
/* Buttons */
|
85 |
+
.action-button {
|
86 |
+
margin-top: 1rem;
|
87 |
+
padding: 0.75rem 1.5rem;
|
88 |
+
border-radius: 0.5rem;
|
89 |
+
cursor: pointer;
|
90 |
+
transition: background-color 0.2s ease;
|
91 |
+
width: 100%;
|
92 |
+
}
|
93 |
+
|
94 |
+
.action-button:hover {
|
95 |
+
background-color: rgba(55, 65, 81, 0.7);
|
96 |
+
}
|
97 |
+
|
98 |
+
.back-button {
|
99 |
+
background-color: #607d8b; /* 落ち着いたグレー */
|
100 |
+
color: white;
|
101 |
+
}
|
102 |
+
|
103 |
+
.add-button {
|
104 |
+
background-color: #4caf50; /* 落ち着いた緑色 */
|
105 |
+
color: white;
|
106 |
+
}
|
107 |
+
|
108 |
+
/* Disabled State */
|
109 |
+
.disabled {
|
110 |
+
opacity: 0.5;
|
111 |
+
pointer-events: none;
|
112 |
+
}
|
113 |
+
|
114 |
+
/* Responsive Design */
|
115 |
+
@media (max-width: 640px) {
|
116 |
+
.main-content {
|
117 |
+
padding: 1.5rem;
|
118 |
+
}
|
119 |
+
}
|
120 |
+
</style>
|
121 |
+
</head>
|
122 |
+
<body>
|
123 |
+
<!-- Main Content Wrapper -->
|
124 |
+
<div class="main-content relative">
|
125 |
+
<!-- Title -->
|
126 |
+
<div class="main-title">JustTalk</div>
|
127 |
+
|
128 |
+
<!-- User List -->
|
129 |
+
<div id="people-list" class="space-y-4"></div>
|
130 |
+
|
131 |
+
<!-- Add Button -->
|
132 |
+
<button id="add-btn" class="action-button add-button">
|
133 |
+
<i class="fas fa-user-plus"></i> メンバーを追加
|
134 |
+
</button>
|
135 |
+
|
136 |
+
<!-- 録音画面へ移動ボタン(Back Buttonから変更) -->
|
137 |
+
<button
|
138 |
+
id="backButton"
|
139 |
+
onclick="showRecorder()"
|
140 |
+
class="action-button back-button"
|
141 |
+
>
|
142 |
+
<i class="fas fa-microphone"></i> 録音画面を表示
|
143 |
+
</button>
|
144 |
+
</div>
|
145 |
+
|
146 |
+
<script src="{{ url_for('static', filename='register_record.js') }}"></script>
|
147 |
+
</body>
|
148 |
+
</html>
|
|
|
|
|
|
transcription.py
CHANGED
@@ -4,6 +4,7 @@ from pydub import AudioSegment
|
|
4 |
import string
|
5 |
import random
|
6 |
from datetime import datetime
|
|
|
7 |
|
8 |
# Matplotlibのキャッシュディレクトリを変更
|
9 |
os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
|
@@ -19,7 +20,6 @@ class TranscriptionMaker():
|
|
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 |
results = []
|
@@ -45,6 +45,7 @@ class TranscriptionMaker():
|
|
45 |
"end": segment.end,
|
46 |
"text": segment.text
|
47 |
})
|
|
|
48 |
#ファイルの書き込み。ファイル名は"transcription.txt"
|
49 |
output_file=os.path.join(self.output_dir,"transcription.txt")
|
50 |
try:
|
@@ -56,54 +57,34 @@ class TranscriptionMaker():
|
|
56 |
raise
|
57 |
return output_file
|
58 |
|
59 |
-
|
60 |
-
def merge_segments(self,segments_dir,output_dir
|
61 |
if not os.path.exists(output_dir):
|
62 |
os.makedirs(output_dir, exist_ok=True)
|
63 |
|
64 |
files = sorted([f for f in os.listdir(segments_dir) if f.endswith('.wav')])
|
65 |
|
66 |
-
|
67 |
-
|
68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
|
70 |
for file in files:
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
# 番号が連続していない場合、新しいグループを作成
|
75 |
-
if previous_index is not None and file_index != previous_index + 1:
|
76 |
-
# 現在のグループを結合して保存
|
77 |
-
if current_group:
|
78 |
-
merged_files.append(current_group)
|
79 |
-
current_group = []
|
80 |
-
|
81 |
-
# 現在のファイルをグループに追加
|
82 |
-
current_group.append(file)
|
83 |
-
previous_index = file_index
|
84 |
|
85 |
-
|
86 |
-
if current_group:
|
87 |
-
merged_files.append(current_group)
|
88 |
-
|
89 |
-
# グループごとに結合して保存
|
90 |
-
for i, group in enumerate(merged_files):
|
91 |
-
combined_audio = AudioSegment.empty()
|
92 |
-
for file in group:
|
93 |
-
file_path = os.path.join(segments_dir, file)
|
94 |
-
segment = AudioSegment.from_file(file_path)
|
95 |
-
combined_audio += segment
|
96 |
-
# 出力ファイル名を設定して保存
|
97 |
-
output_file = os.path.join(output_dir, self.generate_filename(3))
|
98 |
-
combined_audio.export(output_file, format='wav')
|
99 |
|
|
|
100 |
return output_dir
|
101 |
-
|
102 |
-
def generate_random_string(self,length):
|
103 |
-
letters = string.ascii_letters + string.digits
|
104 |
-
return ''.join(random.choice(letters) for i in range(length))
|
105 |
|
106 |
-
def generate_filename(self
|
107 |
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
|
108 |
filename = f"{current_time}.wav"
|
109 |
return filename
|
|
|
4 |
import string
|
5 |
import random
|
6 |
from datetime import datetime
|
7 |
+
import shutil
|
8 |
|
9 |
# Matplotlibのキャッシュディレクトリを変更
|
10 |
os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
|
|
|
20 |
self.output_dir = output_dir
|
21 |
os.makedirs(self.output_dir, exist_ok=True)
|
22 |
|
|
|
23 |
#音声ファイルのディレクトリを受け取り、書き起こしファイルを作成する
|
24 |
def create_transcription(self,audio_directory):
|
25 |
results = []
|
|
|
45 |
"end": segment.end,
|
46 |
"text": segment.text
|
47 |
})
|
48 |
+
|
49 |
#ファイルの書き込み。ファイル名は"transcription.txt"
|
50 |
output_file=os.path.join(self.output_dir,"transcription.txt")
|
51 |
try:
|
|
|
57 |
raise
|
58 |
return output_file
|
59 |
|
60 |
+
#ディレクトリ内の音声ファイルをくっつける
|
61 |
+
def merge_segments(self, segments_dir, output_dir="/tmp/data/merged_segment"):
|
62 |
if not os.path.exists(output_dir):
|
63 |
os.makedirs(output_dir, exist_ok=True)
|
64 |
|
65 |
files = sorted([f for f in os.listdir(segments_dir) if f.endswith('.wav')])
|
66 |
|
67 |
+
if len(files) <= 1:
|
68 |
+
print('No need to merge')
|
69 |
+
single_file_path = os.path.join(segments_dir, files[0])
|
70 |
+
destination_path = os.path.join(output_dir, files[0])
|
71 |
+
shutil.copy(single_file_path, destination_path)
|
72 |
+
print(f"ファイル {files[0]} を {output_dir} に移動しました。")
|
73 |
+
return output_dir
|
74 |
+
|
75 |
+
combined_audio = AudioSegment.empty()
|
76 |
|
77 |
for file in files:
|
78 |
+
file_path = os.path.join(segments_dir, file)
|
79 |
+
segment = AudioSegment.from_file(file_path)
|
80 |
+
combined_audio += segment
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
|
82 |
+
output_file = os.path.join(output_dir, self.generate_filename())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
|
84 |
+
combined_audio.export(output_file, format="wav")
|
85 |
return output_dir
|
|
|
|
|
|
|
|
|
86 |
|
87 |
+
def generate_filename(self):
|
88 |
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
|
89 |
filename = f"{current_time}.wav"
|
90 |
return filename
|