Spaces:
Running
Running
pull0323
Browse files- README.md +10 -329
- app.py +319 -85
- static/feedback.js +2 -1
- static/menu.js +4 -1
- static/process1.js +293 -0
- templates/index.html +95 -1
- templates/reset.html +1 -1
- templates/userRegister.html +4 -1
- templates/userSelect.html +416 -0
- transcription.py +108 -89
README.md
CHANGED
@@ -1,329 +1,10 @@
|
|
1 |
-
---
|
2 |
-
title: JusTalk
|
3 |
-
emoji: ⚡
|
4 |
-
colorFrom: gray
|
5 |
-
colorTo: blue
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
---
|
9 |
-
|
10 |
-
|
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 |
-
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
@@ -1,30 +1,36 @@
|
|
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 |
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 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
17 |
harassment_keywords = [
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
total_audio = ""
|
23 |
|
24 |
-
|
25 |
@app.route('/index', methods=['GET', 'POST'])
|
26 |
def index():
|
27 |
-
return render_template('index.html', users
|
28 |
|
29 |
# フィードバック画面(テンプレート: feedback.html)
|
30 |
@app.route('/feedback', methods=['GET', 'POST'])
|
@@ -37,112 +43,118 @@ 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'])
|
47 |
def confirm():
|
48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
-
|
51 |
@app.route('/reset_html', methods=['GET', 'POST'])
|
52 |
def reset_html():
|
53 |
return render_template('reset.html')
|
54 |
|
55 |
-
|
56 |
-
|
57 |
-
#メンバー削除&累積音声削除
|
58 |
@app.route('/reset_member', methods=['GET', 'POST'])
|
59 |
def reset_member():
|
60 |
global users
|
61 |
global total_audio
|
62 |
global transcription_text
|
63 |
-
|
|
|
|
|
|
|
64 |
process.delete_files_in_directory('/tmp/data/transcription_audio')
|
65 |
-
|
66 |
-
|
|
|
|
|
67 |
os.remove(transcription_text)
|
68 |
print(f"{transcription_text} を削除しました。")
|
69 |
-
|
70 |
-
print(f"{
|
71 |
-
|
72 |
-
print(f"エラーが発生しました: {e}")
|
73 |
transcription_text = ""
|
|
|
74 |
try:
|
75 |
data = request.get_json()
|
76 |
if not data or "names" not in data:
|
77 |
-
return jsonify({"status": "error", "message": "Invalid request body"}), 400
|
78 |
|
79 |
names = data.get("names", [])
|
80 |
-
|
81 |
-
|
82 |
for name in names:
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
return jsonify({"status": "error", "message": f"Failed to delete {name}: {e}"}), 500
|
92 |
-
|
93 |
-
else:
|
94 |
-
print(f"ファイルが存在しません: {file_path}")
|
95 |
-
|
96 |
-
# usersリストを更新
|
97 |
users = [u for u in users if u not in names]
|
|
|
|
|
|
|
98 |
|
99 |
-
|
100 |
-
return jsonify({"status": "success", "message": "Members deleted successfully", "users": users}), 200 # 200 OK
|
101 |
|
102 |
except Exception as e:
|
103 |
print(f"An unexpected error occurred: {e}")
|
104 |
-
return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500
|
105 |
|
106 |
# 書き起こし作成エンドポイント
|
107 |
-
@app.route('/transcription',methods
|
108 |
def transcription():
|
109 |
global transcription_text
|
110 |
global total_audio
|
|
|
111 |
if not os.path.exists(transcription_text) or not transcription_text:
|
112 |
try:
|
113 |
if not total_audio or not os.path.exists(total_audio):
|
114 |
-
return jsonify({"error": "No audio segments provided"}),400
|
115 |
-
audio_directory = transcripter.merge_segments(total_audio,'/tmp/data/transcription_audio')
|
116 |
transcription_text = transcripter.create_transcription(audio_directory)
|
117 |
print("transcription")
|
118 |
print(transcription_text)
|
119 |
except Exception as e:
|
120 |
-
return jsonify({"error": str(e)}),500
|
|
|
121 |
try:
|
122 |
-
with open(transcription_text,'r',encoding='utf-8') as file:
|
123 |
file_content = file.read()
|
124 |
print(file_content)
|
125 |
-
return jsonify({'transcription': file_content}),200
|
126 |
except FileNotFoundError:
|
127 |
return jsonify({"error": "Transcription file not found"}), 404
|
128 |
except Exception as e:
|
129 |
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
130 |
|
131 |
-
|
132 |
# AI分析エンドポイント
|
133 |
-
@app.route('/analyze',methods
|
134 |
def analyze():
|
135 |
global transcription_text
|
136 |
global total_audio
|
|
|
137 |
if not os.path.exists(transcription_text) or not transcription_text:
|
138 |
try:
|
139 |
if not total_audio:
|
140 |
-
return jsonify({"error": "No audio segments provided"}),400
|
141 |
-
audio_directory = transcripter.merge_segments(total_audio,'/tmp/data/transcription_audio')
|
142 |
transcription_text = transcripter.create_transcription(audio_directory)
|
143 |
except Exception as e:
|
144 |
-
return jsonify({"error": str(e)}),500
|
145 |
-
|
146 |
analyzer = TextAnalyzer(transcription_text, harassment_keywords)
|
147 |
api_key = os.environ.get("DEEPSEEK")
|
148 |
if api_key is None:
|
@@ -168,18 +180,115 @@ def analyze():
|
|
168 |
print(f"繰り返しの程度: {repetition}")
|
169 |
print(f"会話の心地よさ: {pleasantConversation}")
|
170 |
print(f"非難またはハラスメントの程度: {blameOrHarassment}")
|
171 |
-
|
|
|
172 |
|
173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
174 |
# 音声アップロード&解析エンドポイント
|
175 |
@app.route('/upload_audio', methods=['POST'])
|
176 |
def upload_audio():
|
177 |
global total_audio
|
|
|
|
|
178 |
try:
|
179 |
data = request.get_json()
|
180 |
-
|
181 |
-
|
182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
183 |
|
184 |
# Base64デコードして音声バイナリを取得
|
185 |
audio_binary = base64.b64decode(data['audio_data'])
|
@@ -190,70 +299,195 @@ def upload_audio():
|
|
190 |
audio_path = os.path.join(audio_dir, f"{upload_name}.wav")
|
191 |
with open(audio_path, 'wb') as f:
|
192 |
f.write(audio_binary)
|
193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
194 |
# 各ユーザーの参照音声ファイルのパスをリストに格納
|
195 |
reference_paths = []
|
196 |
-
base_audio_dir = "/tmp/data/base_audio"
|
197 |
for user in users:
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
|
203 |
# 複数人の場合は参照パスのリストを、1人の場合は単一のパスを渡す
|
204 |
if len(users) > 1:
|
205 |
print("複数人の場合の処理")
|
206 |
matched_times, segments_dir = process.process_multi_audio(reference_paths, audio_path, threshold=0.05)
|
207 |
-
total_audio =
|
208 |
# 各メンバーのrateを計算
|
209 |
total_time = sum(matched_times)
|
210 |
rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times]
|
211 |
-
|
|
|
|
|
|
|
212 |
else:
|
213 |
matched_time, unmatched_time, segments_dir = process.process_audio(reference_paths[0], audio_path, threshold=0.05)
|
214 |
-
total_audio =
|
215 |
-
print("
|
216 |
print(total_audio)
|
217 |
total_time = matched_time + unmatched_time
|
218 |
rate = (matched_time / total_time) * 100 if total_time > 0 else 0
|
219 |
-
return jsonify({"rate": rate}), 200
|
|
|
220 |
except Exception as e:
|
221 |
print("Error in /upload_audio:", str(e))
|
222 |
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
@app.route('/reset', methods=['GET'])
|
225 |
def reset():
|
226 |
global users
|
227 |
users = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
return jsonify({"status": "success", "message": "Users reset"}), 200
|
229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
230 |
|
231 |
@app.route('/upload_base_audio', methods=['POST'])
|
232 |
def upload_base_audio():
|
233 |
-
global
|
|
|
234 |
try:
|
235 |
data = request.get_json()
|
236 |
if not data or 'audio_data' not in data or 'name' not in data:
|
237 |
return jsonify({"error": "音声データまたは名前がありません"}), 400
|
238 |
-
name = data['name']
|
239 |
-
print(name)
|
240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
241 |
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
|
|
|
|
|
|
|
246 |
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
)
|
252 |
-
return jsonify({"state": "Registration Success!", "path": audio_path}), 200
|
253 |
except Exception as e:
|
254 |
print("Error in /upload_base_audio:", str(e))
|
255 |
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
256 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
257 |
if __name__ == '__main__':
|
258 |
port = int(os.environ.get("PORT", 7860))
|
259 |
app.run(debug=True, host="0.0.0.0", port=port)
|
|
|
1 |
from flask import Flask, request, jsonify, render_template, send_from_directory
|
2 |
import base64
|
3 |
+
from pydub import AudioSegment
|
4 |
import os
|
5 |
import shutil
|
6 |
+
import requests
|
7 |
+
import tempfile
|
8 |
+
import json
|
9 |
from process import AudioProcessor
|
10 |
from transcription import TranscriptionMaker
|
11 |
from analyze import TextAnalyzer
|
|
|
12 |
|
13 |
+
process = AudioProcessor()
|
14 |
transcripter = TranscriptionMaker()
|
15 |
app = Flask(__name__)
|
16 |
|
17 |
+
# GASのエンドポイントURL
|
18 |
+
GAS_URL = "https://script.google.com/macros/s/AKfycbwR2cnMKVU1AxoT9NDaeZaNUaTiwRX64Ul0sH0AU4ccP49Byph-TpxtM_Lwm4G9zLnuYA/exec"
|
19 |
+
|
20 |
+
users = [] # 選択されたユーザーのリスト
|
21 |
+
all_users = [] # 利用可能なすべてのユーザーのリスト
|
22 |
+
transcription_text = ""
|
23 |
harassment_keywords = [
|
24 |
+
"バカ", "馬鹿", "アホ", "死ね", "クソ", "うざい",
|
25 |
+
"きもい", "キモい", "ブス", "デブ", "ハゲ",
|
26 |
+
"セクハラ", "パワハラ", "モラハラ"
|
27 |
+
]
|
28 |
total_audio = ""
|
29 |
|
30 |
+
|
31 |
@app.route('/index', methods=['GET', 'POST'])
|
32 |
def index():
|
33 |
+
return render_template('index.html', users=users)
|
34 |
|
35 |
# フィードバック画面(テンプレート: feedback.html)
|
36 |
@app.route('/feedback', methods=['GET', 'POST'])
|
|
|
43 |
return render_template('talkDetail.html')
|
44 |
|
45 |
# 音声登録画面(テンプレート: userRegister.html)
|
|
|
46 |
@app.route('/userregister', methods=['GET', 'POST'])
|
47 |
def userregister():
|
48 |
return render_template('userRegister.html')
|
49 |
|
50 |
+
# 人数確認
|
51 |
+
@app.route('/confirm', methods=['GET'])
|
52 |
def confirm():
|
53 |
+
global all_users
|
54 |
+
# 最新のユーザーリストを取得
|
55 |
+
try:
|
56 |
+
update_all_users()
|
57 |
+
except Exception as e:
|
58 |
+
print(f"ユーザーリストの更新エラー: {str(e)}")
|
59 |
+
return jsonify({'members': users, 'all_members': all_users}), 200
|
60 |
|
61 |
+
# リセット画面(テンプレート: reset.html)
|
62 |
@app.route('/reset_html', methods=['GET', 'POST'])
|
63 |
def reset_html():
|
64 |
return render_template('reset.html')
|
65 |
|
66 |
+
# メンバー削除&累積音声削除
|
|
|
|
|
67 |
@app.route('/reset_member', methods=['GET', 'POST'])
|
68 |
def reset_member():
|
69 |
global users
|
70 |
global total_audio
|
71 |
global transcription_text
|
72 |
+
|
73 |
+
# 一時ディレクトリのクリーンアップ
|
74 |
+
if total_audio:
|
75 |
+
process.delete_files_in_directory(total_audio)
|
76 |
process.delete_files_in_directory('/tmp/data/transcription_audio')
|
77 |
+
|
78 |
+
# 書き起こしテキストの削除
|
79 |
+
if os.path.exists(transcription_text):
|
80 |
+
try:
|
81 |
os.remove(transcription_text)
|
82 |
print(f"{transcription_text} を削除しました。")
|
83 |
+
except Exception as e:
|
84 |
+
print(f"ファイル削除中にエラーが発生しました: {e}")
|
85 |
+
|
|
|
86 |
transcription_text = ""
|
87 |
+
|
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
|
92 |
|
93 |
names = data.get("names", [])
|
94 |
+
|
95 |
+
# GASからファイルを削除
|
96 |
for name in names:
|
97 |
+
try:
|
98 |
+
delete_from_cloud(f"{name}.wav")
|
99 |
+
print(f"クラウドから {name}.wav を削除しました。")
|
100 |
+
except Exception as e:
|
101 |
+
print(f"クラウド削除中にエラーが発生しました: {e}")
|
102 |
+
return jsonify({"status": "error", "message": f"Failed to delete {name} from cloud: {e}"}), 500
|
103 |
+
|
104 |
+
# usersリストから削除するユーザーを除外
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
users = [u for u in users if u not in names]
|
106 |
+
|
107 |
+
# 全ユーザーリストの更新
|
108 |
+
update_all_users()
|
109 |
|
110 |
+
return jsonify({"status": "success", "message": "Members deleted successfully", "users": users}), 200
|
|
|
111 |
|
112 |
except Exception as e:
|
113 |
print(f"An unexpected error occurred: {e}")
|
114 |
+
return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500
|
115 |
|
116 |
# 書き起こし作成エンドポイント
|
117 |
+
@app.route('/transcription', methods=['GET', 'POST'])
|
118 |
def transcription():
|
119 |
global transcription_text
|
120 |
global total_audio
|
121 |
+
|
122 |
if not os.path.exists(transcription_text) or not transcription_text:
|
123 |
try:
|
124 |
if not total_audio or not os.path.exists(total_audio):
|
125 |
+
return jsonify({"error": "No audio segments provided"}), 400
|
126 |
+
audio_directory = transcripter.merge_segments(total_audio, '/tmp/data/transcription_audio')
|
127 |
transcription_text = transcripter.create_transcription(audio_directory)
|
128 |
print("transcription")
|
129 |
print(transcription_text)
|
130 |
except Exception as e:
|
131 |
+
return jsonify({"error": str(e)}), 500
|
132 |
+
|
133 |
try:
|
134 |
+
with open(transcription_text, 'r', encoding='utf-8') as file:
|
135 |
file_content = file.read()
|
136 |
print(file_content)
|
137 |
+
return jsonify({'transcription': file_content}), 200
|
138 |
except FileNotFoundError:
|
139 |
return jsonify({"error": "Transcription file not found"}), 404
|
140 |
except Exception as e:
|
141 |
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
142 |
|
|
|
143 |
# AI分析エンドポイント
|
144 |
+
@app.route('/analyze', methods=['GET', 'POST'])
|
145 |
def analyze():
|
146 |
global transcription_text
|
147 |
global total_audio
|
148 |
+
|
149 |
if not os.path.exists(transcription_text) or not transcription_text:
|
150 |
try:
|
151 |
if not total_audio:
|
152 |
+
return jsonify({"error": "No audio segments provided"}), 400
|
153 |
+
audio_directory = transcripter.merge_segments(total_audio, '/tmp/data/transcription_audio')
|
154 |
transcription_text = transcripter.create_transcription(audio_directory)
|
155 |
except Exception as e:
|
156 |
+
return jsonify({"error": str(e)}), 500
|
157 |
+
|
158 |
analyzer = TextAnalyzer(transcription_text, harassment_keywords)
|
159 |
api_key = os.environ.get("DEEPSEEK")
|
160 |
if api_key is None:
|
|
|
180 |
print(f"繰り返しの程度: {repetition}")
|
181 |
print(f"会話の心地よさ: {pleasantConversation}")
|
182 |
print(f"非難またはハラスメントの程度: {blameOrHarassment}")
|
183 |
+
|
184 |
+
return jsonify({"results": results}), 200
|
185 |
|
186 |
|
187 |
+
# クラウドから音声を取得してローカルに保存する関数
|
188 |
+
def download_from_cloud(filename, local_path):
|
189 |
+
try:
|
190 |
+
payload = {
|
191 |
+
"action": "download",
|
192 |
+
"fileName": filename
|
193 |
+
}
|
194 |
+
|
195 |
+
print(f"クラウドから {filename} をダウンロード中...")
|
196 |
+
response = requests.post(GAS_URL, json=payload)
|
197 |
+
if response.status_code != 200:
|
198 |
+
print(f"ダウンロードエラー: ステータスコード {response.status_code}")
|
199 |
+
print(f"レスポンス: {response.text}")
|
200 |
+
raise Exception(f"クラウドからのダウンロードに失敗しました: {response.text}")
|
201 |
+
|
202 |
+
try:
|
203 |
+
res_json = response.json()
|
204 |
+
except:
|
205 |
+
print("JSONデコードエラー、レスポン��内容:")
|
206 |
+
print(response.text[:500]) # 最初の500文字だけ表示
|
207 |
+
raise Exception("サーバーからの応答をJSONとして解析できませんでした")
|
208 |
+
|
209 |
+
if res_json.get("status") != "success":
|
210 |
+
print(f"ダウンロードステータスエラー: {res_json.get('message')}")
|
211 |
+
raise Exception(f"クラウドからのダウンロードに失敗しました: {res_json.get('message')}")
|
212 |
+
|
213 |
+
# Base64文字列をデコード
|
214 |
+
base64_data = res_json.get("base64Data")
|
215 |
+
if not base64_data:
|
216 |
+
print("Base64データが存在しません")
|
217 |
+
raise Exception("応答にBase64データが含まれていません")
|
218 |
+
|
219 |
+
try:
|
220 |
+
audio_binary = base64.b64decode(base64_data)
|
221 |
+
except Exception as e:
|
222 |
+
print(f"Base64デコードエラー: {str(e)}")
|
223 |
+
raise Exception(f"音声データのデコードに失敗しました: {str(e)}")
|
224 |
+
|
225 |
+
# 指定パスに保存
|
226 |
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
227 |
+
with open(local_path, 'wb') as f:
|
228 |
+
f.write(audio_binary)
|
229 |
+
|
230 |
+
print(f"{filename} をローカルに保存しました: {local_path}")
|
231 |
+
|
232 |
+
# データの整合性チェック(ファイルサイズが0より大きいかなど)
|
233 |
+
if os.path.getsize(local_path) <= 0:
|
234 |
+
raise Exception(f"保存されたファイル {local_path} のサイズが0バイトです")
|
235 |
+
|
236 |
+
return local_path
|
237 |
+
except Exception as e:
|
238 |
+
print(f"ダウンロード中にエラーが発生しました: {str(e)}")
|
239 |
+
# エラーを上位に伝播させる
|
240 |
+
raise
|
241 |
+
|
242 |
+
# クラウドからファイルを削除する関数
|
243 |
+
def delete_from_cloud(filename):
|
244 |
+
payload = {
|
245 |
+
"action": "delete",
|
246 |
+
"fileName": filename
|
247 |
+
}
|
248 |
+
response = requests.post(GAS_URL, json=payload)
|
249 |
+
if response.status_code != 200:
|
250 |
+
raise Exception(f"クラウドからの削除に失敗しました: {response.text}")
|
251 |
+
|
252 |
+
res_json = response.json()
|
253 |
+
if res_json.get("status") != "success":
|
254 |
+
raise Exception(f"クラウドからの削除に失敗しました: {res_json.get('message')}")
|
255 |
+
|
256 |
+
return True
|
257 |
+
# すべてのベース音声ユーザーリストを更新する関数
|
258 |
+
def update_all_users():
|
259 |
+
global all_users
|
260 |
+
|
261 |
+
payload = {"action": "list"}
|
262 |
+
response = requests.post(GAS_URL, json=payload)
|
263 |
+
if response.status_code != 200:
|
264 |
+
raise Exception(f"GAS一覧取得エラー: {response.text}")
|
265 |
+
|
266 |
+
res_json = response.json()
|
267 |
+
if res_json.get("status") != "success":
|
268 |
+
raise Exception(f"GAS一覧取得失敗: {res_json.get('message')}")
|
269 |
+
|
270 |
+
# ファイル名から拡張子を除去してユーザーリストを作成
|
271 |
+
all_users = [os.path.splitext(filename)[0] for filename in res_json.get("fileNames", [])]
|
272 |
+
return all_users
|
273 |
+
|
274 |
# 音声アップロード&解析エンドポイント
|
275 |
@app.route('/upload_audio', methods=['POST'])
|
276 |
def upload_audio():
|
277 |
global total_audio
|
278 |
+
global users
|
279 |
+
|
280 |
try:
|
281 |
data = request.get_json()
|
282 |
+
if not data or 'audio_data' not in data:
|
283 |
+
return jsonify({"error": "音声データがありません"}), 400
|
284 |
+
|
285 |
+
# リクエストからユーザーリストを取得(指定がなければ現在のusersを使用)
|
286 |
+
if 'selected_users' in data and data['selected_users']:
|
287 |
+
users = data['selected_users']
|
288 |
+
print(f"選択されたユーザー: {users}")
|
289 |
+
|
290 |
+
if not users:
|
291 |
+
return jsonify({"error": "選択されたユーザーがいません"}), 400
|
292 |
|
293 |
# Base64デコードして音声バイナリを取得
|
294 |
audio_binary = base64.b64decode(data['audio_data'])
|
|
|
299 |
audio_path = os.path.join(audio_dir, f"{upload_name}.wav")
|
300 |
with open(audio_path, 'wb') as f:
|
301 |
f.write(audio_binary)
|
302 |
+
|
303 |
+
print(f"処理を行うユーザー: {users}")
|
304 |
+
|
305 |
+
# ベース音声を一時ディレクトリにダウンロード
|
306 |
+
temp_dir = "/tmp/data/base_audio"
|
307 |
+
os.makedirs(temp_dir, exist_ok=True)
|
308 |
+
|
309 |
# 各ユーザーの参照音声ファイルのパスをリストに格納
|
310 |
reference_paths = []
|
|
|
311 |
for user in users:
|
312 |
+
try:
|
313 |
+
ref_path = os.path.join(temp_dir, f"{user}.wav")
|
314 |
+
if not os.path.exists(ref_path):
|
315 |
+
# クラウドから取得
|
316 |
+
download_from_cloud(f"{user}.wav", ref_path)
|
317 |
+
print(f"クラウドから {user}.wav をダウンロードしました")
|
318 |
+
|
319 |
+
if not os.path.exists(ref_path):
|
320 |
+
return jsonify({"error": "参照音声ファイルが見つかりません", "details": ref_path}), 500
|
321 |
+
|
322 |
+
reference_paths.append(ref_path)
|
323 |
+
except Exception as e:
|
324 |
+
return jsonify({"error": f"ユーザー {user} の音声取得に失敗しました", "details": str(e)}), 500
|
325 |
|
326 |
# 複数人の場合は参照パスのリストを、1人の場合は単一のパスを渡す
|
327 |
if len(users) > 1:
|
328 |
print("複数人の場合の処理")
|
329 |
matched_times, segments_dir = process.process_multi_audio(reference_paths, audio_path, threshold=0.05)
|
330 |
+
total_audio = segments_dir
|
331 |
# 各メンバーのrateを計算
|
332 |
total_time = sum(matched_times)
|
333 |
rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times]
|
334 |
+
|
335 |
+
# ユーザー名と話した割合をマッピング
|
336 |
+
user_rates = {users[i]: rates[i] for i in range(len(users))}
|
337 |
+
return jsonify({"rates": rates, "user_rates": user_rates}), 200
|
338 |
else:
|
339 |
matched_time, unmatched_time, segments_dir = process.process_audio(reference_paths[0], audio_path, threshold=0.05)
|
340 |
+
total_audio = segments_dir
|
341 |
+
print("単一ユーザーの処理")
|
342 |
print(total_audio)
|
343 |
total_time = matched_time + unmatched_time
|
344 |
rate = (matched_time / total_time) * 100 if total_time > 0 else 0
|
345 |
+
return jsonify({"rate": rate, "user": users[0]}), 200
|
346 |
+
|
347 |
except Exception as e:
|
348 |
print("Error in /upload_audio:", str(e))
|
349 |
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
350 |
+
|
351 |
+
# ユーザー選択画面(テンプレート: userSelect.html)
|
352 |
+
@app.route('/')
|
353 |
+
@app.route('/userselect', methods=['GET'])
|
354 |
+
def userselect():
|
355 |
+
return render_template('userSelect.html')
|
356 |
+
|
357 |
+
# 選択したユーザーを設定するエンドポイント
|
358 |
+
@app.route('/select_users', methods=['POST'])
|
359 |
+
def select_users():
|
360 |
+
global users
|
361 |
|
362 |
+
try:
|
363 |
+
data = request.get_json()
|
364 |
+
if not data or 'users' not in data:
|
365 |
+
return jsonify({"error": "ユーザーリストがありません"}), 400
|
366 |
+
|
367 |
+
users = data['users']
|
368 |
+
print(f"選択されたユーザー: {users}")
|
369 |
+
|
370 |
+
return jsonify({"status": "success", "selected_users": users}), 200
|
371 |
+
except Exception as e:
|
372 |
+
print("Error in /select_users:", str(e))
|
373 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
374 |
+
|
375 |
@app.route('/reset', methods=['GET'])
|
376 |
def reset():
|
377 |
global users
|
378 |
users = []
|
379 |
+
global total_audio
|
380 |
+
global transcription_text
|
381 |
+
|
382 |
+
# 一時ディレクトリのクリーンアップ
|
383 |
+
if total_audio:
|
384 |
+
process.delete_files_in_directory(total_audio)
|
385 |
+
process.delete_files_in_directory('/tmp/data/transcription_audio')
|
386 |
+
|
387 |
+
# 書き起こしテキストの削除
|
388 |
+
if os.path.exists(transcription_text):
|
389 |
+
try:
|
390 |
+
os.remove(transcription_text)
|
391 |
+
print(f"{transcription_text} を削除しました。")
|
392 |
+
except Exception as e:
|
393 |
+
print(f"ファイル削除中にエラーが発生しました: {e}")
|
394 |
+
|
395 |
+
transcription_text = ""
|
396 |
+
|
397 |
return jsonify({"status": "success", "message": "Users reset"}), 200
|
398 |
|
399 |
+
@app.route('/copy_selected_files', methods=['POST'])
|
400 |
+
def copy_selected_files():
|
401 |
+
try:
|
402 |
+
data = request.get_json()
|
403 |
+
if not data or "names" not in data:
|
404 |
+
return jsonify({"error": "namesパラメータが存在しません"}), 400
|
405 |
+
|
406 |
+
names = data["names"]
|
407 |
+
dest_dir = "/tmp/data/selected_audio" # コピー先のフォルダ
|
408 |
+
os.makedirs(dest_dir, exist_ok=True)
|
409 |
+
|
410 |
+
copied_files = []
|
411 |
+
for name in names:
|
412 |
+
dest_path = os.path.join(dest_dir, f"{name}.wav")
|
413 |
+
try:
|
414 |
+
# クラウドから直接ダウンロード
|
415 |
+
download_from_cloud(f"{name}.wav", dest_path)
|
416 |
+
copied_files.append(name)
|
417 |
+
print(f"{name}.wav を {dest_path} にダウンロードしました。")
|
418 |
+
except Exception as e:
|
419 |
+
print(f"ダウンロード中にエラーが発生しました: {e}")
|
420 |
+
continue
|
421 |
+
|
422 |
+
return jsonify({"status": "success", "copied": copied_files}), 200
|
423 |
+
|
424 |
+
except Exception as e:
|
425 |
+
print("Error in /copy_selected_files:", str(e))
|
426 |
+
return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500
|
427 |
+
|
428 |
+
@app.route('/clear_tmp', methods=['GET'])
|
429 |
+
def clear_tmp():
|
430 |
+
try:
|
431 |
+
tmp_dir = "/tmp/data" # アプリケーションが使用しているtmpフォルダ
|
432 |
+
# ファイルのみの削除
|
433 |
+
process.delete_files_in_directory(tmp_dir)
|
434 |
+
# フォルダがあれば再帰的に削除
|
435 |
+
for item in os.listdir(tmp_dir):
|
436 |
+
item_path = os.path.join(tmp_dir, item)
|
437 |
+
if os.path.isdir(item_path):
|
438 |
+
shutil.rmtree(item_path)
|
439 |
+
print(f"ディレクトリを削除しました: {item_path}")
|
440 |
+
|
441 |
+
return jsonify({"status": "success", "message": "tmp配下がすべて削除されました"}), 200
|
442 |
+
|
443 |
+
except Exception as e:
|
444 |
+
print("Error in /clear_tmp:", str(e))
|
445 |
+
return jsonify({"error": "サーバー内部エラー", "details": str(e)}), 500
|
446 |
|
447 |
@app.route('/upload_base_audio', methods=['POST'])
|
448 |
def upload_base_audio():
|
449 |
+
global all_users
|
450 |
+
|
451 |
try:
|
452 |
data = request.get_json()
|
453 |
if not data or 'audio_data' not in data or 'name' not in data:
|
454 |
return jsonify({"error": "音声データまたは名前がありません"}), 400
|
455 |
+
name = data['name']
|
456 |
+
print(f"登録名: {name}")
|
457 |
|
458 |
+
# GASのアップロードエンドポイントにリクエスト
|
459 |
+
payload = {
|
460 |
+
"action": "upload",
|
461 |
+
"fileName": f"{name}.wav",
|
462 |
+
"base64Data": data['audio_data']
|
463 |
+
}
|
464 |
|
465 |
+
response = requests.post(GAS_URL, json=payload)
|
466 |
+
if response.status_code != 200:
|
467 |
+
return jsonify({"error": "GASアップロードエラー", "details": response.text}), 500
|
468 |
|
469 |
+
res_json = response.json()
|
470 |
+
if res_json.get("status") != "success":
|
471 |
+
return jsonify({"error": "GASアップロード失敗", "details": res_json.get("message")}), 500
|
472 |
|
473 |
+
# 全ユーザーリストを更新
|
474 |
+
update_all_users()
|
475 |
+
|
476 |
+
return jsonify({"state": "Registration Success!", "driveFileId": res_json.get("fileId")}), 200
|
|
|
|
|
477 |
except Exception as e:
|
478 |
print("Error in /upload_base_audio:", str(e))
|
479 |
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
480 |
|
481 |
+
@app.route('/list_base_audio', methods=['GET'])
|
482 |
+
def list_base_audio():
|
483 |
+
try:
|
484 |
+
global all_users
|
485 |
+
all_users = update_all_users()
|
486 |
+
return jsonify({"status": "success", "fileNames": all_users}), 200
|
487 |
+
except Exception as e:
|
488 |
+
print("Error in /list_base_audio:", str(e))
|
489 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
490 |
+
|
491 |
if __name__ == '__main__':
|
492 |
port = int(os.environ.get("PORT", 7860))
|
493 |
app.run(debug=True, host="0.0.0.0", port=port)
|
static/feedback.js
CHANGED
@@ -15,8 +15,9 @@ async function getAnalysis() {
|
|
15 |
const loader = document.getElementById("loader");
|
16 |
loader.style.display = "block";
|
17 |
try {
|
18 |
-
|
19 |
|
|
|
20 |
if (!response.ok) {
|
21 |
throw new Error(`HTTP error! status: ${response.status}`);
|
22 |
}
|
|
|
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 |
}
|
static/menu.js
CHANGED
@@ -3,7 +3,10 @@
|
|
3 |
fetch("/reset");
|
4 |
window.location.href = "userregister";
|
5 |
}
|
6 |
-
|
|
|
|
|
|
|
7 |
// Show recorder page
|
8 |
function showRecorder() {
|
9 |
window.location.href = "index";
|
|
|
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";
|
static/process1.js
ADDED
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
let allUsers = [];
|
2 |
+
let selectedUsers = [];
|
3 |
+
let userToDelete = null;
|
4 |
+
|
5 |
+
// ページ読み込み時にユーザーリストを取得
|
6 |
+
document.addEventListener('DOMContentLoaded', fetchUserList);
|
7 |
+
|
8 |
+
// ユーザーリスト取得
|
9 |
+
function fetchUserList() {
|
10 |
+
fetch('/list_base_audio')
|
11 |
+
.then(response => response.json())
|
12 |
+
.then(data => {
|
13 |
+
if (data.status === 'success' && data.fileNames) {
|
14 |
+
allUsers = data.fileNames;
|
15 |
+
renderUserList(allUsers);
|
16 |
+
} else {
|
17 |
+
showError('メンバーリストの取得に失敗しました');
|
18 |
+
}
|
19 |
+
})
|
20 |
+
.catch(error => {
|
21 |
+
console.error('Error fetching user list:', error);
|
22 |
+
showError('サーバーとの通信中にエラーが発生しました');
|
23 |
+
});
|
24 |
+
}
|
25 |
+
|
26 |
+
// ユーザーリストの表示
|
27 |
+
function renderUserList(users) {
|
28 |
+
const userListElement = document.getElementById('userList');
|
29 |
+
|
30 |
+
if (!users || users.length === 0) {
|
31 |
+
userListElement.innerHTML = `
|
32 |
+
<div class="no-users">
|
33 |
+
<p>登録されているメンバーがいません。</p>
|
34 |
+
<p>「新規登録」から音声を登録してください。</p>
|
35 |
+
</div>
|
36 |
+
`;
|
37 |
+
return;
|
38 |
+
}
|
39 |
+
|
40 |
+
let html = '';
|
41 |
+
users.forEach(user => {
|
42 |
+
const firstLetter = user.substr(0, 1).toUpperCase();
|
43 |
+
html += `
|
44 |
+
<div class="user-item">
|
45 |
+
<input type="checkbox" id="user-${user}" value="${user}" onchange="toggleUserSelection('${user}')">
|
46 |
+
<label for="user-${user}">${user}</label>
|
47 |
+
<div class="user-avatar">${firstLetter}</div>
|
48 |
+
<button class="delete-button" onclick="showDeleteModal('${user}')">
|
49 |
+
<i class="fas fa-trash"></i>
|
50 |
+
</button>
|
51 |
+
</div>
|
52 |
+
`;
|
53 |
+
});
|
54 |
+
|
55 |
+
userListElement.innerHTML = html;
|
56 |
+
|
57 |
+
// 既に選択済みのユーザーがあればチェックを入れる
|
58 |
+
checkStoredSelections();
|
59 |
+
}
|
60 |
+
|
61 |
+
// ユーザー選択の切り替え
|
62 |
+
function toggleUserSelection(username) {
|
63 |
+
const index = selectedUsers.indexOf(username);
|
64 |
+
if (index === -1) {
|
65 |
+
selectedUsers.push(username);
|
66 |
+
} else {
|
67 |
+
selectedUsers.splice(index, 1);
|
68 |
+
}
|
69 |
+
|
70 |
+
updateSelectedCount();
|
71 |
+
updateProceedButton();
|
72 |
+
saveSelections();
|
73 |
+
}
|
74 |
+
|
75 |
+
// すべてのユーザーを選択
|
76 |
+
function selectAllUsers() {
|
77 |
+
selectedUsers = [...allUsers];
|
78 |
+
|
79 |
+
// チェックボックスを更新
|
80 |
+
allUsers.forEach(user => {
|
81 |
+
const checkbox = document.getElementById(`user-${user}`);
|
82 |
+
if (checkbox) checkbox.checked = true;
|
83 |
+
});
|
84 |
+
|
85 |
+
updateSelectedCount();
|
86 |
+
updateProceedButton();
|
87 |
+
saveSelections();
|
88 |
+
}
|
89 |
+
|
90 |
+
// すべての選択を解除
|
91 |
+
function deselectAllUsers() {
|
92 |
+
selectedUsers = [];
|
93 |
+
|
94 |
+
// チェックボックスを更新
|
95 |
+
allUsers.forEach(user => {
|
96 |
+
const checkbox = document.getElementById(`user-${user}`);
|
97 |
+
if (checkbox) checkbox.checked = false;
|
98 |
+
});
|
99 |
+
|
100 |
+
updateSelectedCount();
|
101 |
+
updateProceedButton();
|
102 |
+
saveSelections();
|
103 |
+
}
|
104 |
+
|
105 |
+
// 選択数の表示を更新
|
106 |
+
function updateSelectedCount() {
|
107 |
+
document.getElementById('selectedCount').textContent = `選択中: ${selectedUsers.length}人`;
|
108 |
+
}
|
109 |
+
|
110 |
+
// 進むボタンの有効/無効を更新
|
111 |
+
function updateProceedButton() {
|
112 |
+
document.getElementById('proceedButton').disabled = selectedUsers.length === 0;
|
113 |
+
}
|
114 |
+
|
115 |
+
// 選択を保存
|
116 |
+
function saveSelections() {
|
117 |
+
localStorage.setItem('selectedUsers', JSON.stringify(selectedUsers));
|
118 |
+
}
|
119 |
+
|
120 |
+
// 保存されている選択を読み込み
|
121 |
+
function checkStoredSelections() {
|
122 |
+
const storedSelections = localStorage.getItem('selectedUsers');
|
123 |
+
if (storedSelections) {
|
124 |
+
try {
|
125 |
+
selectedUsers = JSON.parse(storedSelections);
|
126 |
+
selectedUsers = selectedUsers.filter(user => allUsers.includes(user)); // 存在するユーザーのみ選択
|
127 |
+
|
128 |
+
// チェックボックスに反映
|
129 |
+
selectedUsers.forEach(user => {
|
130 |
+
const checkbox = document.getElementById(`user-${user}`);
|
131 |
+
if (checkbox) checkbox.checked = true;
|
132 |
+
});
|
133 |
+
|
134 |
+
updateSelectedCount();
|
135 |
+
updateProceedButton();
|
136 |
+
} catch (e) {
|
137 |
+
console.error('保存された選択の読み込みに失敗しました', e);
|
138 |
+
selectedUsers = [];
|
139 |
+
}
|
140 |
+
}
|
141 |
+
}
|
142 |
+
|
143 |
+
// エラー表示
|
144 |
+
function showError(message) {
|
145 |
+
const userListElement = document.getElementById('userList');
|
146 |
+
userListElement.innerHTML = `
|
147 |
+
<div class="no-users">
|
148 |
+
<p>${message}</p>
|
149 |
+
<button class="select-button" onclick="fetchUserList()">再読み込み</button>
|
150 |
+
</div>
|
151 |
+
`;
|
152 |
+
}
|
153 |
+
|
154 |
+
// 選択されたユーザーでサーバーに送信して次のページに進む
|
155 |
+
function proceedWithSelectedUsers() {
|
156 |
+
if (selectedUsers.length === 0) {
|
157 |
+
alert('少なくとも1人のメンバーを選択してください');
|
158 |
+
return;
|
159 |
+
}
|
160 |
+
|
161 |
+
// 選択したユーザーをサーバーに送信
|
162 |
+
fetch('/select_users', {
|
163 |
+
method: 'POST',
|
164 |
+
headers: {
|
165 |
+
'Content-Type': 'application/json',
|
166 |
+
},
|
167 |
+
body: JSON.stringify({
|
168 |
+
users: selectedUsers
|
169 |
+
})
|
170 |
+
})
|
171 |
+
.then(response => response.json())
|
172 |
+
.then(data => {
|
173 |
+
if (data.status === 'success') {
|
174 |
+
// 成功したらインデックスページに進む
|
175 |
+
window.location.href = '/index';
|
176 |
+
} else {
|
177 |
+
alert('エラーが発生しました: ' + (data.error || 'Unknown error'));
|
178 |
+
}
|
179 |
+
})
|
180 |
+
.catch(error => {
|
181 |
+
console.error('Error selecting users:', error);
|
182 |
+
alert('サーバーとの通信中にエラーが発生しました');
|
183 |
+
});
|
184 |
+
}
|
185 |
+
|
186 |
+
// 削除確認モーダルを表示
|
187 |
+
function showDeleteModal(username) {
|
188 |
+
userToDelete = username;
|
189 |
+
document.getElementById('deleteModalText').textContent = `メンバー「${username}」を削除しますか?削除すると元に戻せません。`;
|
190 |
+
document.getElementById('deleteModal').style.display = 'flex';
|
191 |
+
}
|
192 |
+
|
193 |
+
// 削除確認モーダルを非表示
|
194 |
+
function hideDeleteModal() {
|
195 |
+
document.getElementById('deleteModal').style.display = 'none';
|
196 |
+
userToDelete = null;
|
197 |
+
}
|
198 |
+
|
199 |
+
// メンバーの削除を実行
|
200 |
+
function confirmDelete() {
|
201 |
+
if (!userToDelete) return;
|
202 |
+
|
203 |
+
// 削除中の表示
|
204 |
+
document.getElementById('deleteModalText').innerHTML = `
|
205 |
+
<div class="loading">
|
206 |
+
<div class="spinner"></div>
|
207 |
+
<p>メンバー「${userToDelete}」を削除中...</p>
|
208 |
+
</div>
|
209 |
+
`;
|
210 |
+
|
211 |
+
fetch('/reset_member', {
|
212 |
+
method: 'POST',
|
213 |
+
headers: {
|
214 |
+
'Content-Type': 'application/json',
|
215 |
+
},
|
216 |
+
body: JSON.stringify({
|
217 |
+
names: [userToDelete]
|
218 |
+
})
|
219 |
+
})
|
220 |
+
.then(response => response.json())
|
221 |
+
.then(data => {
|
222 |
+
if (data.status === 'success') {
|
223 |
+
// 選択リストからも削除
|
224 |
+
const index = selectedUsers.indexOf(userToDelete);
|
225 |
+
if (index !== -1) {
|
226 |
+
selectedUsers.splice(index, 1);
|
227 |
+
saveSelections();
|
228 |
+
}
|
229 |
+
|
230 |
+
// リストから削除して再表示
|
231 |
+
allUsers = allUsers.filter(user => user !== userToDelete);
|
232 |
+
renderUserList(allUsers);
|
233 |
+
|
234 |
+
// モーダルを閉じる
|
235 |
+
hideDeleteModal();
|
236 |
+
|
237 |
+
// 成功メッセージ表示(オプション)
|
238 |
+
const successMessage = document.createElement('div');
|
239 |
+
successMessage.className = 'success-message';
|
240 |
+
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>`;
|
241 |
+
document.querySelector('.container').prepend(successMessage);
|
242 |
+
|
243 |
+
// 数秒後にメッセージを消す
|
244 |
+
setTimeout(() => {
|
245 |
+
successMessage.remove();
|
246 |
+
}, 3000);
|
247 |
+
} else {
|
248 |
+
alert('削除に失敗しました: ' + (data.message || 'Unknown error'));
|
249 |
+
hideDeleteModal();
|
250 |
+
}
|
251 |
+
})
|
252 |
+
.catch(error => {
|
253 |
+
console.error('Error deleting user:', error);
|
254 |
+
alert('サーバーとの通信中にエラーが発生しました');
|
255 |
+
hideDeleteModal();
|
256 |
+
});
|
257 |
+
}
|
258 |
+
|
259 |
+
// ハンバーガーメニュー表示/非表示の切り替え
|
260 |
+
function toggleMenu(event) {
|
261 |
+
event.stopPropagation();
|
262 |
+
const menu = document.getElementById('menu');
|
263 |
+
menu.classList.toggle('open');
|
264 |
+
}
|
265 |
+
|
266 |
+
// メニュー外クリックでメニューを閉じる
|
267 |
+
function closeMenu(event) {
|
268 |
+
const menu = document.getElementById('menu');
|
269 |
+
if (menu.classList.contains('open') && !menu.contains(event.target) && event.target.id !== 'menuButton') {
|
270 |
+
menu.classList.remove('open');
|
271 |
+
}
|
272 |
+
}
|
273 |
+
|
274 |
+
// 各画面へのナビゲーション関数
|
275 |
+
function showUserRegister() {
|
276 |
+
window.location.href = '/userregister';
|
277 |
+
}
|
278 |
+
|
279 |
+
function showIndex() {
|
280 |
+
window.location.href = '/index';
|
281 |
+
}
|
282 |
+
|
283 |
+
function showResults() {
|
284 |
+
window.location.href = '/feedback';
|
285 |
+
}
|
286 |
+
|
287 |
+
function showTalkDetail() {
|
288 |
+
window.location.href = '/talk_detail';
|
289 |
+
}
|
290 |
+
|
291 |
+
function resetAction() {
|
292 |
+
window.location.href = '/reset_html';
|
293 |
+
}
|
templates/index.html
CHANGED
@@ -152,6 +152,37 @@
|
|
152 |
#menuButton:hover {
|
153 |
background-color: rgba(255, 255, 255, 0.2);
|
154 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
</style>
|
156 |
</head>
|
157 |
<body onclick="closeMenu(event)">
|
@@ -179,6 +210,9 @@
|
|
179 |
<button onclick="showUserRegister()">
|
180 |
<i class="fas fa-user-plus"></i> メンバーを追加
|
181 |
</button>
|
|
|
|
|
|
|
182 |
<button onclick="showRecorder()">
|
183 |
<i class="fas fa-microphone"></i> 録音画面を表示
|
184 |
</button>
|
@@ -197,6 +231,11 @@
|
|
197 |
</div>
|
198 |
</div>
|
199 |
|
|
|
|
|
|
|
|
|
|
|
200 |
<!-- Chart Display -->
|
201 |
<div class="chart w-72 h-72 mb-5 mx-auto">
|
202 |
<canvas id="speechChart"></canvas>
|
@@ -224,5 +263,60 @@
|
|
224 |
|
225 |
<script src="{{ url_for('static', filename='process.js') }}"></script>
|
226 |
<script src="{{ url_for('static', filename='menu.js') }}"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
227 |
</body>
|
228 |
-
</html>
|
|
|
152 |
#menuButton:hover {
|
153 |
background-color: rgba(255, 255, 255, 0.2);
|
154 |
}
|
155 |
+
|
156 |
+
/* Member Chips Style */
|
157 |
+
.member-chips {
|
158 |
+
display: flex;
|
159 |
+
flex-wrap: wrap;
|
160 |
+
justify-content: center;
|
161 |
+
gap: 0.5rem;
|
162 |
+
margin-bottom: 1.5rem;
|
163 |
+
}
|
164 |
+
|
165 |
+
.member-chip {
|
166 |
+
background: rgba(255, 255, 255, 0.2);
|
167 |
+
border-radius: 999px;
|
168 |
+
padding: 0.4rem 0.8rem;
|
169 |
+
font-size: 0.8rem;
|
170 |
+
color: white;
|
171 |
+
display: inline-flex;
|
172 |
+
align-items: center;
|
173 |
+
gap: 0.5rem;
|
174 |
+
}
|
175 |
+
|
176 |
+
.member-avatar {
|
177 |
+
width: 1.5rem;
|
178 |
+
height: 1.5rem;
|
179 |
+
background: rgba(255, 255, 255, 0.3);
|
180 |
+
border-radius: 50%;
|
181 |
+
display: flex;
|
182 |
+
align-items: center;
|
183 |
+
justify-content: center;
|
184 |
+
font-size: 0.75rem;
|
185 |
+
}
|
186 |
</style>
|
187 |
</head>
|
188 |
<body onclick="closeMenu(event)">
|
|
|
210 |
<button onclick="showUserRegister()">
|
211 |
<i class="fas fa-user-plus"></i> メンバーを追加
|
212 |
</button>
|
213 |
+
<button onclick="showUserSelect()">
|
214 |
+
<i class="fas fa-users"></i> メンバーを選択
|
215 |
+
</button>
|
216 |
<button onclick="showRecorder()">
|
217 |
<i class="fas fa-microphone"></i> 録音画面を表示
|
218 |
</button>
|
|
|
231 |
</div>
|
232 |
</div>
|
233 |
|
234 |
+
<!-- Selected Member Chips -->
|
235 |
+
<div class="member-chips" id="memberChips">
|
236 |
+
<!-- Member chips will be dynamically added here -->
|
237 |
+
</div>
|
238 |
+
|
239 |
<!-- Chart Display -->
|
240 |
<div class="chart w-72 h-72 mb-5 mx-auto">
|
241 |
<canvas id="speechChart"></canvas>
|
|
|
263 |
|
264 |
<script src="{{ url_for('static', filename='process.js') }}"></script>
|
265 |
<script src="{{ url_for('static', filename='menu.js') }}"></script>
|
266 |
+
<script>
|
267 |
+
|
268 |
+
|
269 |
+
// 選択されたメンバーの表示を更新する関数
|
270 |
+
function updateSelectedMembers() {
|
271 |
+
// ローカルストレージから選択されたメンバーを取得
|
272 |
+
let selectedUsers = [];
|
273 |
+
try {
|
274 |
+
const stored = localStorage.getItem('selectedUsers');
|
275 |
+
if (stored) {
|
276 |
+
selectedUsers = JSON.parse(stored);
|
277 |
+
}
|
278 |
+
} catch (e) {
|
279 |
+
console.error('選択メンバーの読み込みエラー:', e);
|
280 |
+
}
|
281 |
+
|
282 |
+
// メンバーチップを表示
|
283 |
+
const memberChipsContainer = document.getElementById('memberChips');
|
284 |
+
memberChipsContainer.innerHTML = '';
|
285 |
+
|
286 |
+
if (selectedUsers.length === 0) {
|
287 |
+
// メンバーがいない場合の表示
|
288 |
+
const noMembers = document.createElement('div');
|
289 |
+
noMembers.className = 'text-white opacity-50 text-sm';
|
290 |
+
noMembers.textContent = 'メンバーが選択されていません';
|
291 |
+
memberChipsContainer.appendChild(noMembers);
|
292 |
+
return;
|
293 |
+
}
|
294 |
+
|
295 |
+
// 現在選択されているメンバー数を表示
|
296 |
+
const countChip = document.createElement('div');
|
297 |
+
countChip.className = 'member-chip';
|
298 |
+
countChip.style.backgroundColor = 'rgba(66, 153, 225, 0.5)'; // 青っぽい背景
|
299 |
+
countChip.innerHTML = `<i class="fas fa-users"></i> ${selectedUsers.length}人のメンバーを選択中`;
|
300 |
+
memberChipsContainer.appendChild(countChip);
|
301 |
+
|
302 |
+
// 各メンバーをチップとして表示
|
303 |
+
selectedUsers.forEach(member => {
|
304 |
+
const chip = document.createElement('div');
|
305 |
+
chip.className = 'member-chip';
|
306 |
+
|
307 |
+
const avatar = document.createElement('div');
|
308 |
+
avatar.className = 'member-avatar';
|
309 |
+
avatar.textContent = member.substr(0, 1).toUpperCase();
|
310 |
+
|
311 |
+
chip.appendChild(avatar);
|
312 |
+
chip.appendChild(document.createTextNode(member));
|
313 |
+
|
314 |
+
memberChipsContainer.appendChild(chip);
|
315 |
+
});
|
316 |
+
}
|
317 |
+
|
318 |
+
// ページ読み込み時にメンバー表示を更新
|
319 |
+
document.addEventListener('DOMContentLoaded', updateSelectedMembers);
|
320 |
+
</script>
|
321 |
</body>
|
322 |
+
</html>
|
templates/reset.html
CHANGED
@@ -17,7 +17,7 @@
|
|
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;
|
|
|
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;
|
templates/userRegister.html
CHANGED
@@ -133,7 +133,7 @@
|
|
133 |
<i class="fas fa-user-plus"></i> メンバーを追加
|
134 |
</button>
|
135 |
|
136 |
-
<!--
|
137 |
<button
|
138 |
id="backButton"
|
139 |
onclick="showRecorder()"
|
@@ -144,5 +144,8 @@
|
|
144 |
</div>
|
145 |
|
146 |
<script src="{{ url_for('static', filename='register_record.js') }}"></script>
|
|
|
|
|
|
|
147 |
</body>
|
148 |
</html>
|
|
|
133 |
<i class="fas fa-user-plus"></i> メンバーを追加
|
134 |
</button>
|
135 |
|
136 |
+
<!-- Back Button -->
|
137 |
<button
|
138 |
id="backButton"
|
139 |
onclick="showRecorder()"
|
|
|
144 |
</div>
|
145 |
|
146 |
<script src="{{ url_for('static', filename='register_record.js') }}"></script>
|
147 |
+
|
148 |
+
|
149 |
+
|
150 |
</body>
|
151 |
</html>
|
templates/userSelect.html
ADDED
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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" href="https://use.fontawesome.com/releases/v5.10.0/css/all.css" />
|
8 |
+
<style>
|
9 |
+
body {
|
10 |
+
font-family: 'Helvetica Neue', Arial, sans-serif;
|
11 |
+
margin: 0;
|
12 |
+
padding: 0;
|
13 |
+
background: linear-gradient(135deg, #2c3e50, #1f2937);
|
14 |
+
color: white;
|
15 |
+
min-height: 100vh;
|
16 |
+
display: flex;
|
17 |
+
align-items: center;
|
18 |
+
justify-content: center;
|
19 |
+
}
|
20 |
+
|
21 |
+
.container {
|
22 |
+
max-width: 500px;
|
23 |
+
margin: 0 auto;
|
24 |
+
padding: 30px;
|
25 |
+
border: 5px solid rgba(255, 255, 255, 0.2);
|
26 |
+
border-radius: 1rem;
|
27 |
+
background-color: rgba(0, 0, 0, 0.3);
|
28 |
+
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
|
29 |
+
width: 90%;
|
30 |
+
position: relative;
|
31 |
+
}
|
32 |
+
|
33 |
+
h1 {
|
34 |
+
color: white;
|
35 |
+
text-align: center;
|
36 |
+
margin-bottom: 30px;
|
37 |
+
font-size: 1.8rem;
|
38 |
+
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
39 |
+
}
|
40 |
+
|
41 |
+
.user-list {
|
42 |
+
background-color: rgba(255, 255, 255, 0.1);
|
43 |
+
border-radius: 10px;
|
44 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
45 |
+
padding: 15px;
|
46 |
+
margin-bottom: 25px;
|
47 |
+
max-height: 350px;
|
48 |
+
overflow-y: auto;
|
49 |
+
}
|
50 |
+
|
51 |
+
.user-item {
|
52 |
+
display: flex;
|
53 |
+
align-items: center;
|
54 |
+
padding: 12px 15px;
|
55 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
56 |
+
transition: background-color 0.2s;
|
57 |
+
}
|
58 |
+
|
59 |
+
.user-item:last-child {
|
60 |
+
border-bottom: none;
|
61 |
+
}
|
62 |
+
|
63 |
+
.user-item:hover {
|
64 |
+
background-color: rgba(255, 255, 255, 0.05);
|
65 |
+
}
|
66 |
+
|
67 |
+
.user-item label {
|
68 |
+
margin-left: 12px;
|
69 |
+
font-size: 16px;
|
70 |
+
cursor: pointer;
|
71 |
+
flex-grow: 1;
|
72 |
+
color: white;
|
73 |
+
}
|
74 |
+
|
75 |
+
input[type="checkbox"] {
|
76 |
+
cursor: pointer;
|
77 |
+
width: 18px;
|
78 |
+
height: 18px;
|
79 |
+
accent-color: #3498db;
|
80 |
+
}
|
81 |
+
|
82 |
+
.button-container {
|
83 |
+
display: flex;
|
84 |
+
justify-content: space-between;
|
85 |
+
margin-top: 25px;
|
86 |
+
}
|
87 |
+
|
88 |
+
button {
|
89 |
+
background-color: #3498db;
|
90 |
+
color: white;
|
91 |
+
border: none;
|
92 |
+
border-radius: 8px;
|
93 |
+
padding: 12px 20px;
|
94 |
+
font-size: 16px;
|
95 |
+
cursor: pointer;
|
96 |
+
transition: background-color 0.3s, transform 0.1s;
|
97 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
98 |
+
}
|
99 |
+
|
100 |
+
button:hover {
|
101 |
+
background-color: #2980b9;
|
102 |
+
}
|
103 |
+
|
104 |
+
button:active {
|
105 |
+
transform: translateY(1px);
|
106 |
+
}
|
107 |
+
|
108 |
+
button:disabled {
|
109 |
+
background-color: #95a5a6;
|
110 |
+
cursor: not-allowed;
|
111 |
+
opacity: 0.7;
|
112 |
+
}
|
113 |
+
|
114 |
+
button.secondary {
|
115 |
+
background-color: rgba(255, 255, 255, 0.2);
|
116 |
+
}
|
117 |
+
|
118 |
+
button.secondary:hover {
|
119 |
+
background-color: rgba(255, 255, 255, 0.3);
|
120 |
+
}
|
121 |
+
|
122 |
+
.selected-count {
|
123 |
+
margin: 15px 0;
|
124 |
+
text-align: center;
|
125 |
+
font-weight: bold;
|
126 |
+
color: #3498db;
|
127 |
+
font-size: 18px;
|
128 |
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
129 |
+
}
|
130 |
+
|
131 |
+
.no-users {
|
132 |
+
text-align: center;
|
133 |
+
padding: 30px;
|
134 |
+
color: rgba(255, 255, 255, 0.7);
|
135 |
+
font-style: italic;
|
136 |
+
}
|
137 |
+
|
138 |
+
.loading {
|
139 |
+
text-align: center;
|
140 |
+
padding: 30px;
|
141 |
+
color: white;
|
142 |
+
}
|
143 |
+
|
144 |
+
.spinner {
|
145 |
+
border: 4px solid rgba(255, 255, 255, 0.1);
|
146 |
+
width: 36px;
|
147 |
+
height: 36px;
|
148 |
+
border-radius: 50%;
|
149 |
+
border-left-color: #3498db;
|
150 |
+
animation: spin 1s linear infinite;
|
151 |
+
margin: 0 auto 15px;
|
152 |
+
}
|
153 |
+
|
154 |
+
@keyframes spin {
|
155 |
+
0% { transform: rotate(0deg); }
|
156 |
+
100% { transform: rotate(360deg); }
|
157 |
+
}
|
158 |
+
|
159 |
+
/* User Avatar */
|
160 |
+
.user-avatar {
|
161 |
+
width: 30px;
|
162 |
+
height: 30px;
|
163 |
+
background: rgba(255, 255, 255, 0.2);
|
164 |
+
border-radius: 50%;
|
165 |
+
display: flex;
|
166 |
+
align-items: center;
|
167 |
+
justify-content: center;
|
168 |
+
font-size: 14px;
|
169 |
+
margin-left: 10px;
|
170 |
+
}
|
171 |
+
|
172 |
+
/* Hamburger Menu Styles */
|
173 |
+
#menu {
|
174 |
+
position: absolute;
|
175 |
+
top: 0;
|
176 |
+
left: 0;
|
177 |
+
z-index: 10;
|
178 |
+
transform: translateX(-100%);
|
179 |
+
visibility: hidden;
|
180 |
+
opacity: 0;
|
181 |
+
background-color: rgb(31, 41, 55);
|
182 |
+
transition: transform 0.3s ease-in-out, visibility 0s 0.3s,
|
183 |
+
opacity 0.3s ease-in-out;
|
184 |
+
backdrop-filter: blur(10px);
|
185 |
+
border-right: 1px solid rgba(255, 255, 255, 0.2);
|
186 |
+
height: 100%;
|
187 |
+
}
|
188 |
+
|
189 |
+
#menu.open {
|
190 |
+
transform: translateX(0);
|
191 |
+
visibility: visible;
|
192 |
+
opacity: 1;
|
193 |
+
transition: transform 0.3s ease-in-out, visibility 0s 0s,
|
194 |
+
opacity 0.3s ease-in-out;
|
195 |
+
}
|
196 |
+
|
197 |
+
#menu button {
|
198 |
+
transition: background-color 0.2s ease;
|
199 |
+
background-color: rgba(0, 0, 0, 0.1);
|
200 |
+
margin: 2px;
|
201 |
+
border-radius: 5px;
|
202 |
+
display: flex;
|
203 |
+
align-items: center;
|
204 |
+
justify-content: flex-start;
|
205 |
+
gap: 10px;
|
206 |
+
padding: 0.75rem 1rem;
|
207 |
+
width: 100%;
|
208 |
+
text-align: left;
|
209 |
+
border: none;
|
210 |
+
color: #fff;
|
211 |
+
font-size: 1rem;
|
212 |
+
cursor: pointer;
|
213 |
+
box-shadow: none;
|
214 |
+
}
|
215 |
+
|
216 |
+
#menu button:hover {
|
217 |
+
background-color: rgba(55, 65, 81, 0.7);
|
218 |
+
}
|
219 |
+
|
220 |
+
#menuButton {
|
221 |
+
background-color: rgba(255, 255, 255, 0.1);
|
222 |
+
border: none;
|
223 |
+
border-radius: 50%;
|
224 |
+
padding: 0.5rem;
|
225 |
+
cursor: pointer;
|
226 |
+
transition: background-color 0.2s ease;
|
227 |
+
position: absolute;
|
228 |
+
top: 20px;
|
229 |
+
left: 20px;
|
230 |
+
box-shadow: none;
|
231 |
+
font-size: 18px;
|
232 |
+
width: 40px;
|
233 |
+
height: 40px;
|
234 |
+
display: flex;
|
235 |
+
align-items: center;
|
236 |
+
justify-content: center;
|
237 |
+
}
|
238 |
+
|
239 |
+
#menuButton:hover {
|
240 |
+
background-color: rgba(255, 255, 255, 0.2);
|
241 |
+
}
|
242 |
+
|
243 |
+
/* Select All Button */
|
244 |
+
.select-controls {
|
245 |
+
display: flex;
|
246 |
+
justify-content: center;
|
247 |
+
margin-bottom: 15px;
|
248 |
+
gap: 10px;
|
249 |
+
}
|
250 |
+
|
251 |
+
.select-button {
|
252 |
+
background-color: rgba(255, 255, 255, 0.15);
|
253 |
+
color: white;
|
254 |
+
border: none;
|
255 |
+
border-radius: 5px;
|
256 |
+
padding: 8px 15px;
|
257 |
+
font-size: 14px;
|
258 |
+
cursor: pointer;
|
259 |
+
transition: background-color 0.2s;
|
260 |
+
}
|
261 |
+
|
262 |
+
.select-button:hover {
|
263 |
+
background-color: rgba(255, 255, 255, 0.25);
|
264 |
+
}
|
265 |
+
|
266 |
+
/* Delete Button */
|
267 |
+
.delete-button {
|
268 |
+
background-color: transparent;
|
269 |
+
color: #e74c3c;
|
270 |
+
border: none;
|
271 |
+
border-radius: 50%;
|
272 |
+
width: 30px;
|
273 |
+
height: 30px;
|
274 |
+
display: flex;
|
275 |
+
align-items: center;
|
276 |
+
justify-content: center;
|
277 |
+
cursor: pointer;
|
278 |
+
transition: background-color 0.2s;
|
279 |
+
padding: 0;
|
280 |
+
margin-left: 5px;
|
281 |
+
box-shadow: none;
|
282 |
+
}
|
283 |
+
|
284 |
+
.delete-button:hover {
|
285 |
+
background-color: rgba(231, 76, 60, 0.2);
|
286 |
+
}
|
287 |
+
|
288 |
+
/* Modal Dialog */
|
289 |
+
.modal {
|
290 |
+
display: none;
|
291 |
+
position: fixed;
|
292 |
+
z-index: 100;
|
293 |
+
left: 0;
|
294 |
+
top: 0;
|
295 |
+
width: 100%;
|
296 |
+
height: 100%;
|
297 |
+
background-color: rgba(0, 0, 0, 0.5);
|
298 |
+
align-items: center;
|
299 |
+
justify-content: center;
|
300 |
+
}
|
301 |
+
|
302 |
+
.modal-content {
|
303 |
+
background-color: rgb(31, 41, 55);
|
304 |
+
border-radius: 10px;
|
305 |
+
padding: 20px;
|
306 |
+
width: 90%;
|
307 |
+
max-width: 400px;
|
308 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
309 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
310 |
+
}
|
311 |
+
|
312 |
+
.modal-title {
|
313 |
+
font-size: 18px;
|
314 |
+
margin-bottom: 15px;
|
315 |
+
color: white;
|
316 |
+
}
|
317 |
+
|
318 |
+
.modal-text {
|
319 |
+
margin-bottom: 20px;
|
320 |
+
color: rgba(255, 255, 255, 0.9);
|
321 |
+
}
|
322 |
+
|
323 |
+
.modal-buttons {
|
324 |
+
display: flex;
|
325 |
+
justify-content: flex-end;
|
326 |
+
gap: 10px;
|
327 |
+
}
|
328 |
+
|
329 |
+
.modal-cancel {
|
330 |
+
background-color: rgba(255, 255, 255, 0.2);
|
331 |
+
color: white;
|
332 |
+
}
|
333 |
+
|
334 |
+
.modal-delete {
|
335 |
+
background-color: #e74c3c;
|
336 |
+
}
|
337 |
+
|
338 |
+
.modal-delete:hover {
|
339 |
+
background-color: #c0392b;
|
340 |
+
}
|
341 |
+
</style>
|
342 |
+
</head>
|
343 |
+
<body onclick="closeMenu(event)">
|
344 |
+
<div class="container">
|
345 |
+
<!-- Hamburger Menu Button -->
|
346 |
+
<button
|
347 |
+
id="menuButton"
|
348 |
+
class="text-white focus:outline-none"
|
349 |
+
onclick="toggleMenu(event)"
|
350 |
+
>
|
351 |
+
<i class="fas fa-bars"></i>
|
352 |
+
</button>
|
353 |
+
|
354 |
+
<!-- Menu Content -->
|
355 |
+
<div
|
356 |
+
id="menu"
|
357 |
+
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"
|
358 |
+
>
|
359 |
+
<div class="px-4 py-2 text-lg font-semibold">メニュー</div>
|
360 |
+
<button onclick="showUserRegister()">
|
361 |
+
<i class="fas fa-user-plus"></i> メンバーを追加
|
362 |
+
</button>
|
363 |
+
<button onclick="showIndex()">
|
364 |
+
<i class="fas fa-home"></i> ホーム画面
|
365 |
+
</button>
|
366 |
+
<button onclick="showResults()">
|
367 |
+
<i class="fas fa-chart-bar"></i> フィードバックを表示
|
368 |
+
</button>
|
369 |
+
<button onclick="showTalkDetail()">
|
370 |
+
<i class="fas fa-comments"></i> 会話詳細を表示
|
371 |
+
</button>
|
372 |
+
<button onclick="resetAction()">
|
373 |
+
<i class="fas fa-redo"></i> リセット
|
374 |
+
</button>
|
375 |
+
<button onclick="toggleMenu(event)">
|
376 |
+
<i class="fas fa-times"></i> 閉じる
|
377 |
+
</button>
|
378 |
+
</div>
|
379 |
+
|
380 |
+
<h1>会話分析に使用するメンバーを選択</h1>
|
381 |
+
|
382 |
+
<div class="select-controls">
|
383 |
+
<button class="select-button" onclick="selectAllUsers()">すべて選択</button>
|
384 |
+
<button class="select-button" onclick="deselectAllUsers()">選択解除</button>
|
385 |
+
</div>
|
386 |
+
|
387 |
+
<div class="user-list" id="userList">
|
388 |
+
<div class="loading">
|
389 |
+
<div class="spinner"></div>
|
390 |
+
<p>メンバーリストを読み込み中...</p>
|
391 |
+
</div>
|
392 |
+
</div>
|
393 |
+
|
394 |
+
<div class="selected-count" id="selectedCount">選択中: 0人</div>
|
395 |
+
|
396 |
+
<div class="button-container">
|
397 |
+
<button class="secondary" onclick="location.href='/userregister'">新規登録</button>
|
398 |
+
<button id="proceedButton" onclick="proceedWithSelectedUsers()" disabled>選択して次へ</button>
|
399 |
+
</div>
|
400 |
+
</div>
|
401 |
+
|
402 |
+
<!-- 削除確認モーダル -->
|
403 |
+
<div id="deleteModal" class="modal">
|
404 |
+
<div class="modal-content">
|
405 |
+
<div class="modal-title">メンバーの削除</div>
|
406 |
+
<div class="modal-text" id="deleteModalText">このメンバーを削除しますか?</div>
|
407 |
+
<div class="modal-buttons">
|
408 |
+
<button class="modal-cancel" onclick="hideDeleteModal()">キャンセル</button>
|
409 |
+
<button class="modal-delete" onclick="confirmDelete()">削除</button>
|
410 |
+
</div>
|
411 |
+
</div>
|
412 |
+
</div>
|
413 |
+
<script src="{{ url_for('static', filename='process1.js') }}"></script>
|
414 |
+
|
415 |
+
</body>
|
416 |
+
</html>
|
transcription.py
CHANGED
@@ -1,90 +1,109 @@
|
|
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 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
os.environ["
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
self.
|
20 |
-
self.output_dir =
|
21 |
-
|
22 |
-
|
23 |
-
#音声ファイルのディレクトリを受け取り、書き起こしファイルを作成する
|
24 |
-
def create_transcription(self,audio_directory):
|
25 |
-
results = []
|
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 = os.listdir(audio_directory)
|
31 |
-
audio_files = sorted(os.listdir(audio_directory))
|
32 |
-
for audio_file in audio_files:
|
33 |
-
if os.path.splitext(audio_file)[-1].lower() != '.wav':
|
34 |
-
continue
|
35 |
-
audio_path = os.path.join(audio_directory, audio_file)
|
36 |
-
try:
|
37 |
-
segments,info = list(self.model.transcribe(audio_path))
|
38 |
-
except Exception as e:
|
39 |
-
print(f"Error transcripting file {audio_path}: {e}")
|
40 |
-
raise
|
41 |
-
sorted_segments = sorted(segments, key=lambda s: s.start)
|
42 |
-
for segment in sorted_segments:
|
43 |
-
results.append({
|
44 |
-
"start": segment.start,
|
45 |
-
"end": segment.end,
|
46 |
-
"text": segment.text
|
47 |
-
})
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
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 |
return filename
|
|
|
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 |
+
results = []
|
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 = os.listdir(audio_directory)
|
31 |
+
audio_files = sorted(os.listdir(audio_directory))
|
32 |
+
for audio_file in audio_files:
|
33 |
+
if os.path.splitext(audio_file)[-1].lower() != '.wav':
|
34 |
+
continue
|
35 |
+
audio_path = os.path.join(audio_directory, audio_file)
|
36 |
+
try:
|
37 |
+
segments,info = list(self.model.transcribe(audio_path))
|
38 |
+
except Exception as e:
|
39 |
+
print(f"Error transcripting file {audio_path}: {e}")
|
40 |
+
raise
|
41 |
+
sorted_segments = sorted(segments, key=lambda s: s.start)
|
42 |
+
for segment in sorted_segments:
|
43 |
+
results.append({
|
44 |
+
"start": segment.start,
|
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:
|
51 |
+
with open(output_file,"w",encoding="utf-8") as f:
|
52 |
+
for result in results:
|
53 |
+
f.write(f"{result['text']}\n")
|
54 |
+
except OSError as e:
|
55 |
+
print(f"Error writing transcription file: {e}")
|
56 |
+
raise
|
57 |
+
return output_file
|
58 |
+
|
59 |
+
#ファイル名が連続しているならくっつける
|
60 |
+
def merge_segments(self,segments_dir,output_dir = "/tmp/data/merged_segment"):
|
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 |
+
merged_files = []
|
67 |
+
current_group = []
|
68 |
+
previous_index = None
|
69 |
+
|
70 |
+
for file in files:
|
71 |
+
# ファイル名から番号を抽出(例: "0.wav" -> 0)
|
72 |
+
file_index = int(file.split('.')[0])
|
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,random_length):
|
107 |
+
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
|
108 |
+
filename = f"{current_time}.wav"
|
109 |
return filename
|