buletomato25 commited on
Commit
91f546c
·
2 Parent(s): 96e3aa5 d59b24c

fix_feedback

Browse files
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: JusTalk
3
+ emoji: ⚡
4
+ colorFrom: gray
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ ## <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
- @app.route('/reset_member', methods=['GET', 'POST'])
57
- def reset_member():
58
- global users
59
- global total_audio
60
- print(total_audio)
61
- process.delete_files_in_directory(total_audio)
62
- try:
63
- data = request.get_json()
64
- if not data or "names" not in data:
65
- return jsonify({"status": "error", "message": "Invalid request body"}), 400 # 400 Bad Request
66
-
67
- names = data.get("names", [])
68
- base_audio_dir = "/tmp/data/base_audio"
69
-
70
- for name in names:
71
- file_path = os.path.join(base_audio_dir, f"{name}.wav")
72
- if os.path.exists(file_path):
73
- try:
74
- os.remove(file_path)
75
- print(f"{file_path} を削除しました。")
76
- except Exception as e:
77
- print(f"削除中にエラーが発生しました: {e}")
78
- # ファイル削除に失敗した場合も、エラーを返す
79
- return jsonify({"status": "error", "message": f"Failed to delete {name}: {e}"}), 500
80
-
81
- else:
82
- print(f"ファイルが存在しません: {file_path}")
83
-
84
- # usersリストを更新
85
- users = [u for u in users if u not in names]
86
-
87
- # 成功した場合のレスポンス
88
- return jsonify({"status": "success", "message": "Members deleted successfully", "users": users}), 200 # 200 OK
89
-
90
- except Exception as e:
91
- print(f"An unexpected error occurred: {e}")
92
- return jsonify({"status": "error", "message": f"Internal server error: {e}"}), 500 # 500 Internal Server Error
93
-
94
- # 書き起こし作成エンドポイント
95
- @app.route('/transcription',methods =['GET','POST'])
96
- def transcription():
97
- global transcription_text
98
- global total_audio
99
- try:
100
- audio_directory = transcripter.merge_segments(total_audio)
101
- transcription_text = transcripter.create_transcription(audio_directory)
102
- with open(transcription_text,'r',encoding='utf-8') as file:
103
- file_content = file.read()
104
- print(file_content)
105
- return jsonify({'transcription': file_content}),200
106
- except Exception as e:
107
- return jsonify({"error": str(e)}),500
108
-
109
- # AI分析エンドポイント
110
- @app.route('/analyze',methods =['GET','POST'])
111
- def analyze():
112
- global transcription_text
113
- analyzer = TextAnalyzer(transcription_text, harassment_keywords)
114
- api_key = os.environ.get("DEEPSEEK")
115
- if api_key is None:
116
- raise ValueError("DEEPSEEK_API_KEY が設定されていません。")
117
-
118
- results = analyzer.analyze(api_key=api_key)
119
-
120
- print(json.dumps(results, ensure_ascii=False, indent=2))
121
-
122
- if "deepseek_analysis" in results and results["deepseek_analysis"]:
123
- deepseek_data = results["deepseek_analysis"]
124
- conversation_level = deepseek_data.get("conversationLevel")
125
- harassment_present = deepseek_data.get("harassmentPresent")
126
- harassment_type = deepseek_data.get("harassmentType")
127
- repetition = deepseek_data.get("repetition")
128
- pleasantConversation = deepseek_data.get("pleasantConversation")
129
- blameOrHarassment = deepseek_data.get("blameOrHarassment")
130
-
131
- print("\n--- DeepSeek 分析結果 ---")
132
- print(f"会話レベル: {conversation_level}")
133
- print(f"ハラスメントの有無: {harassment_present}")
134
- print(f"ハラスメントの種類: {harassment_type}")
135
- print(f"繰り返しの程度: {repetition}")
136
- print(f"会話の心地よさ: {pleasantConversation}")
137
- print(f"非難またはハラスメントの程度: {blameOrHarassment}")
138
- return jsonify({"results": results}),200
139
-
140
-
141
- # 音声アップロード&解析エンドポイント
142
- @app.route('/upload_audio', methods=['POST'])
143
- def upload_audio():
144
- global total_audio
145
- try:
146
- data = request.get_json()
147
- # name か users のいずれかが必須。どちらも無い場合はエラー
148
- if not data or 'audio_data' not in data or ('name' not in data and 'users' not in data):
149
- return jsonify({"error": "音声データまたは名前がありません"}), 400
150
-
151
- # Base64デコードして音声バイナリを取得
152
- audio_binary = base64.b64decode(data['audio_data'])
153
-
154
- upload_name = 'tmp'
155
- audio_dir = "/tmp/data"
156
- os.makedirs(audio_dir, exist_ok=True)
157
- audio_path = os.path.join(audio_dir, f"{upload_name}.wav")
158
- with open(audio_path, 'wb') as f:
159
- f.write(audio_binary)
160
- print(users)
161
- # 各ユーザーの参照音声ファイルのパスをリストに格納
162
- reference_paths = []
163
- base_audio_dir = "/tmp/data/base_audio"
164
- for user in users:
165
- ref_path = os.path.abspath(os.path.join(base_audio_dir, f"{user}.wav"))
166
- if not os.path.exists(ref_path):
167
- return jsonify({"error": "参照音声ファイルが見つかりません", "details": ref_path}), 500
168
- reference_paths.append(ref_path)
169
-
170
- # 複数人の場合は参照パスのリストを、1人の場合は単一のパスを渡す
171
- if len(users) > 1:
172
- print("複数人の場合の処理")
173
- matched_times, segments_dir = process.process_multi_audio(reference_paths, audio_path, threshold=0.05)
174
- total_audio = transcripter.merge_segments(segments_dir)
175
- # 各メンバーのrateを計算
176
- total_time = sum(matched_times)
177
- rates = [(time / total_time) * 100 if total_time > 0 else 0 for time in matched_times]
178
- return jsonify({"rates": rates}), 200
179
- else:
180
- matched_time, unmatched_time, segments_dir = process.process_audio(reference_paths[0], audio_path, threshold=0.05)
181
- total_audio = transcripter.merge_segments(segments_dir)
182
- total_time = matched_time + unmatched_time
183
- rate = (matched_time / total_time) * 100 if total_time > 0 else 0
184
- return jsonify({"rate": rate}), 200
185
- except Exception as e:
186
- print("Error in /upload_audio:", str(e))
187
- return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
188
-
189
- @app.route('/reset', methods=['GET'])
190
- def reset():
191
- global users
192
- users = []
193
- return jsonify({"status": "success", "message": "Users reset"}), 200
194
-
195
- @app.route('/login', methods=['POST', 'GET'])
196
- def login():
197
- global users#グローバル変数を編集できるようにする
198
- try:
199
- data = request.get_json()
200
- name = data['name'] # 名前を取得
201
- if name in users:
202
- print("名前はリストに存在します。")
203
- index()
204
- else:
205
- print("名前はリストに存在しません。")
206
-
207
- except Exception as e:
208
- print("Error in /upload_base_audio:", str(e))
209
- return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
210
-
211
-
212
- @app.route('/upload_base_audio', methods=['POST'])
213
- def upload_base_audio():
214
- global users#グローバル変数を編集できるようにする
215
- try:
216
- data = request.get_json()
217
- if not data or 'audio_data' not in data or 'name' not in data:
218
- return jsonify({"error": "音声データまたは名前がありません"}), 400
219
- name = data['name'] # 名前を取得
220
- print(name)
221
-
222
-
223
- users.append(name)
224
- users=list(set(users))#重複排除
225
- print(users)
226
-
227
-
228
- audio_path=process.save_audio_from_base64(
229
- base64_audio=data['audio_data'], # 音声データ
230
- output_dir= "/tmp/data/base_audio", #保存先
231
- output_filename=f"{name}.wav" # 固定ファイル名(必要に応じて generate_filename() で一意のファイル名に変更可能)
232
- )
233
- return jsonify({"state": "Registration Success!", "path": audio_path}), 200
234
- except Exception as e:
235
- print("Error in /upload_base_audio:", str(e))
236
- return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
237
-
238
- if __name__ == '__main__':
239
- port = int(os.environ.get("PORT", 7860))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 goBack() {
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
- </head>
9
- <body
10
- class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen flex items-center justify-center"
11
- >
12
- <div
13
- class="container mx-auto p-6 bg-white dark:bg-gray-800 shadow-lg rounded-2xl"
14
- >
15
- <h2 class="text-2xl font-semibold mb-4">メンバーを消去しますか?</h2>
16
- <input type="button" id="select-all" value="全選択" />
17
- <div id="memberCheckboxes">
18
- <!--ここにチャックボックスを表示してほしい-->
19
- </div>
20
-
21
- <div class="flex justify-center gap-4">
22
- <button
23
- id="reset_btn"
24
- class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
25
- >
26
- メンバー削除
27
- </button>
28
-
29
- <button
30
- class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
31
- onclick="showRecorder()"
32
- >
33
- 録音画面を表示
34
- </button>
35
- </div>
36
- </div>
37
- <script src="{{ url_for('static', filename='reset.js') }}"></script>
38
- </body>
39
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <link
9
- rel="stylesheet"
10
- href="{{ url_for('static', filename='loading.css') }}"
11
- />
12
- </head>
13
- <body
14
- class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen flex items-center justify-center"
15
- >
16
- <div class="loader" id="loader">
17
- <div class="one"></div>
18
- <div class="two"></div>
19
- <div class="three"></div>
20
- <div class="four"></div>
21
- </div>
22
- <div
23
- class="container mx-auto p-6 bg-white dark:bg-gray-800 shadow-lg rounded-2xl"
24
- >
25
- <h2 class="text-2xl font-semibold mb-4">会話の文字起こし表示</h2>
26
- <div
27
- id="transcription"
28
- class="p-4 bg-gray-200 dark:bg-gray-700 rounded-lg mb-4 max-h-96 overflow-y-auto"
29
- >
30
- ここに会話内容が表示されます。
31
- </div>
32
- <div class="flex justify-center gap-4">
33
- <button
34
- class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
35
- onclick="showRecorder()"
36
- >
37
- 録音画面を表示
38
- </button>
39
- <button
40
- class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
41
- onclick="showFeedback()"
42
- >
43
- フィードバック画面を表示
44
- </button>
45
- </div>
46
- </div>
47
- <script src="{{ url_for('static', filename='talk_detail.js') }}"></script>
48
- </body>
49
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">ユーザー音声登録</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="goBack()"
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 = "/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
 
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