buletomato25 commited on
Commit
48f9750
·
2 Parent(s): cfeb3ec 0dcfc65

addupload_base_audio

Browse files
__pycache__/process.cpython-310.pyc CHANGED
Binary files a/__pycache__/process.cpython-310.pyc and b/__pycache__/process.cpython-310.pyc differ
 
app.py CHANGED
@@ -1,151 +1,34 @@
1
- from flask import Flask, request, jsonify, render_template, send_from_directory,redirect, make_response, Response, session, url_for
2
  import base64
3
  from pydub import AudioSegment # 変換用にpydubをインポート
4
  import os
5
  import shutil
6
- import numpy as np
7
- import string
8
- import random
9
- from datetime import datetime, timedelta
10
- from pyannote.audio import Model, Inference
11
- from pydub import AudioSegment
12
- from flask_sqlalchemy import SQLAlchemy
13
- from dotenv import load_dotenv
14
- from google.oauth2 import id_token
15
- from google_auth_oauthlib.flow import Flow
16
- from google.auth.transport import requests as google_requests
17
-
18
- # Hugging Face のトークン取得(環境変数 HF に設定)
19
- #hf_token = os.environ.get("HF")
20
- load_dotenv()
21
- hf_token = os.getenv("HF")
22
- if hf_token is None:
23
- raise ValueError("HUGGINGFACE_HUB_TOKEN が設定されていません。")
24
-
25
- # キャッシュディレクトリの作成(書き込み可能な /tmp を利用)
26
- cache_dir = "/tmp/hf_cache"
27
- os.makedirs(cache_dir, exist_ok=True)
28
-
29
- # pyannote モデルの読み込み
30
- model = Model.from_pretrained("pyannote/embedding", use_auth_token=hf_token, cache_dir=cache_dir)
31
- inference = Inference(model)
32
 
 
33
  app = Flask(__name__)
34
 
35
- app.config['SECRET_KEY'] = os.urandom(24)
36
-
37
-
38
-
39
-
40
- def cosine_similarity(vec1, vec2):
41
- vec1 = vec1 / np.linalg.norm(vec1)
42
- vec2 = vec2 / np.linalg.norm(vec2)
43
- return np.dot(vec1, vec2)
44
-
45
- def segment_audio(path, target_path='/tmp/setup_voice', seg_duration=1.0):
46
- """
47
- 音声を指定秒数ごとに分割する。
48
- target_path に分割したファイルを保存し、元の音声の総長(ミリ秒)を返す。
49
- """
50
- os.makedirs(target_path, exist_ok=True)
51
- base_sound = AudioSegment.from_file(path)
52
- duration_ms = len(base_sound)
53
- seg_duration_ms = int(seg_duration * 1000)
54
-
55
- for i, start in enumerate(range(0, duration_ms, seg_duration_ms)):
56
- end = min(start + seg_duration_ms, duration_ms)
57
- segment = base_sound[start:end]
58
- segment.export(os.path.join(target_path, f'{i}.wav'), format="wav")
59
-
60
- return target_path, duration_ms
61
-
62
- def calculate_similarity(path1, path2):
63
- embedding1 = inference(path1)
64
- embedding2 = inference(path2)
65
- return float(cosine_similarity(embedding1.data.flatten(), embedding2.data.flatten()))
66
-
67
- def process_audio(reference_path, input_path, output_folder='/tmp/data/matched_segments', seg_duration=1.0, threshold=0.5):
68
- """
69
- 入力音声ファイルを seg_duration 秒ごとに分割し、各セグメントと参照音声の類似度を計算。
70
- 類似度が threshold を超えたセグメントを output_folder にコピーし、マッチした時間(ms)と
71
- マッチしなかった時間(ms)を返す。
72
- """
73
- os.makedirs(output_folder, exist_ok=True)
74
- segmented_path, total_duration_ms = segment_audio(input_path, seg_duration=seg_duration)
75
-
76
- matched_time_ms = 0
77
- for file in sorted(os.listdir(segmented_path)):
78
- segment_file = os.path.join(segmented_path, file)
79
- similarity = calculate_similarity(segment_file, reference_path)
80
- if similarity > threshold:
81
- shutil.copy(segment_file, output_folder)
82
- matched_time_ms += len(AudioSegment.from_file(segment_file))
83
-
84
- unmatched_time_ms = total_duration_ms - matched_time_ms
85
- return matched_time_ms, unmatched_time_ms
86
-
87
- def generate_random_string(length):
88
- letters = string.ascii_letters + string.digits
89
- return ''.join(random.choice(letters) for i in range(length))
90
-
91
- def generate_filename(random_length):
92
- random_string = generate_random_string(random_length)
93
- current_time = datetime.now().strftime("%Y%m%d%H%M%S")
94
- filename = f"{current_time}_{random_string}.wav"
95
- return filename
96
-
97
  # トップページ(テンプレート: index.html)
98
  @app.route('/')
99
- def top():
100
- return redirect('index')
101
-
102
 
103
  # フィードバック画面(テンプレート: feedback.html)
104
  @app.route('/feedback', methods=['GET', 'POST'])
105
  def feedback():
106
- #ログイン問題解決しだい戻す
107
- """
108
- if 'google_id' not in session:
109
- return redirect(url_for('login'))
110
- user_info = {
111
- 'name': session.get('name'),
112
- 'email': session.get('email')
113
- }
114
- """
115
  return render_template('feedback.html')
116
 
 
117
  # 会話詳細画面(テンプレート: talkDetail.html)
118
  @app.route('/talk_detail', methods=['GET', 'POST'])
119
  def talk_detail():
120
- """
121
- if 'google_id' not in session:
122
- return redirect(url_for('login'))
123
- user_info = {
124
- 'name': session.get('name'),
125
- 'email': session.get('email')
126
- }
127
- """
128
  return render_template('talkDetail.html')
129
 
130
- # インデックス画面(テンプレート: index.html)
131
- @app.route('/index', methods=['GET', 'POST'])
132
- def index():
133
- """
134
- if 'google_id' not in session:
135
- return redirect(url_for('login'))
136
- user_info = {
137
- 'name': session.get('name'),
138
- 'email': session.get('email')
139
- }
140
- """
141
- return render_template('index.html')
142
-
143
- @app.before_request
144
- def before_request():
145
- # リクエストのたびにセッションの寿命を更新する
146
- session.permanent = True
147
- app.permanent_session_lifetime = timedelta(minutes=15)
148
- session.modified = True
149
 
150
  # 音声アップロード&解析エンドポイント
151
  @app.route('/upload_audio', methods=['POST'])
@@ -183,15 +66,23 @@ def upload_audio():
183
  def upload_base_audio():
184
  try:
185
  data = request.get_json()
186
- if not data or 'audio_data' not in data:
187
- return jsonify({"error": "音声データがありません"}), 400
188
 
189
  # Base64デコードして音声バイナリを取得
190
  audio_binary = base64.b64decode(data['audio_data'])
 
191
 
192
  # 保存先ディレクトリの作成
193
  audio_dir = "/tmp/data/base_audio"
194
  os.makedirs(audio_dir, exist_ok=True)
 
 
 
 
 
 
 
195
 
196
  # 一時ファイルに保存(実際の形式は WebM などと仮定)
197
  temp_audio_path = os.path.join(audio_dir, "temp_audio")
@@ -216,8 +107,6 @@ def upload_base_audio():
216
  print("Error in /upload_base_audio:", str(e))
217
  return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
218
 
219
-
220
-
221
  if __name__ == '__main__':
222
  port = int(os.environ.get("PORT", 7860))
223
- 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
 
8
+ process=AudioProcessor()
9
  app = Flask(__name__)
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  # トップページ(テンプレート: index.html)
12
  @app.route('/')
13
+ @app.route('/index', methods=['GET', 'POST'])
14
+ def index():
15
+ return render_template('index.html')
16
 
17
  # フィードバック画面(テンプレート: feedback.html)
18
  @app.route('/feedback', methods=['GET', 'POST'])
19
  def feedback():
 
 
 
 
 
 
 
 
 
20
  return render_template('feedback.html')
21
 
22
+
23
  # 会話詳細画面(テンプレート: talkDetail.html)
24
  @app.route('/talk_detail', methods=['GET', 'POST'])
25
  def talk_detail():
 
 
 
 
 
 
 
 
26
  return render_template('talkDetail.html')
27
 
28
+ # 音声登録画面(テンプレート: userRegister.html)
29
+ @app.route('/userregister', methods=['GET', 'POST'])
30
+ def userregister():
31
+ return render_template('userregister.html')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  # 音声アップロード&解析エンドポイント
34
  @app.route('/upload_audio', methods=['POST'])
 
66
  def upload_base_audio():
67
  try:
68
  data = request.get_json()
69
+ if not data or 'audio_data' not in data or 'name' not in data:
70
+ return jsonify({"error": "音声データまたは名前がありません"}), 400
71
 
72
  # Base64デコードして音声バイナリを取得
73
  audio_binary = base64.b64decode(data['audio_data'])
74
+ name = data['name'] # 名前を取得
75
 
76
  # 保存先ディレクトリの作成
77
  audio_dir = "/tmp/data/base_audio"
78
  os.makedirs(audio_dir, exist_ok=True)
79
+
80
+ # 辞書型を作成(音声データと名前)
81
+ audio_info = {
82
+ "name": name,
83
+ "audio_data": audio_binary # バイナリデータをそのまま格納
84
+ }
85
+
86
 
87
  # 一時ファイルに保存(実際の形式は WebM などと仮定)
88
  temp_audio_path = os.path.join(audio_dir, "temp_audio")
 
107
  print("Error in /upload_base_audio:", str(e))
108
  return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
109
 
 
 
110
  if __name__ == '__main__':
111
  port = int(os.environ.get("PORT", 7860))
112
+ app.run(debug=True, host="0.0.0.0", port=port)
templates/index.html CHANGED
@@ -4,80 +4,52 @@
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>Voice Recorder Interface</title>
7
- <link
8
- href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
9
- rel="stylesheet"
10
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  <style>
12
- body {
13
- display: flex;
14
- flex-direction: column;
15
- justify-content: center;
16
- align-items: center;
17
- height: 100vh;
18
- margin: 0;
19
- background-color: #121212;
20
- color: white;
21
- }
22
- /* トグルスイッチ(基準音声保存用) */
23
- .toggle-container {
24
- display: flex;
25
- align-items: center;
26
- margin-bottom: 20px;
27
- }
28
- .toggle-label {
29
- margin-right: 10px;
30
- }
31
- .toggle-switch {
32
- position: relative;
33
- display: inline-block;
34
- width: 50px;
35
- height: 24px;
36
- }
37
- .toggle-switch input {
38
- opacity: 0;
39
- width: 0;
40
- height: 0;
41
- }
42
- .slider {
43
- position: absolute;
44
- cursor: pointer;
45
- top: 0;
46
- left: 0;
47
- right: 0;
48
- bottom: 0;
49
- background-color: #757575;
50
- transition: 0.2s;
51
- border-radius: 34px;
52
- }
53
- .slider::before {
54
- content: "";
55
- position: absolute;
56
- height: 18px;
57
- width: 18px;
58
- left: 4px;
59
- bottom: 3px;
60
- background-color: white;
61
- transition: 0.2s;
62
- border-radius: 50%;
63
- }
64
- input:checked + .slider {
65
- background-color: #4caf50;
66
- }
67
- input:checked + .slider::before {
68
- transform: translateX(26px);
69
- }
70
- /* チャートのスタイル */
71
- .chart {
72
- width: 300px;
73
- height: 300px;
74
- margin-bottom: 20px; /* 円グラフとボタンの間隔を狭く */
75
- }
76
- .controls {
77
- display: flex;
78
- flex-direction: column;
79
- align-items: center;
80
- }
81
  .record-button {
82
  width: 80px;
83
  height: 80px;
@@ -91,6 +63,7 @@
91
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
92
  transition: all 0.2s ease;
93
  }
 
94
  .record-icon {
95
  width: 60px;
96
  height: 60px;
@@ -98,83 +71,35 @@
98
  border-radius: 50%;
99
  transition: all 0.2s ease;
100
  }
 
101
  .recording .record-icon {
102
  width: 40px;
103
  height: 40px;
104
  border-radius: 10%;
105
  }
106
- .result-button {
107
- margin-left: 10px;
108
-
109
- margin-top: 20px;
110
- padding: 10px 20px;
111
- background-color: #4caf50;
112
- border: none;
113
- border-radius: 5px;
114
- color: white;
115
- cursor: pointer;
116
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
117
- }
118
- .result {
119
- display: flex;
120
- }
121
- .result-button:hover {
122
- background-color: #388e3c;
123
- }
124
- header {
125
- display: flex;
126
- }
127
  </style>
128
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
129
- </head>
130
- <body>
131
- <!-- トグルスイッチ:基準音声保存モード -->
132
- <div class="toggle-container">
133
- <span class="toggle-label">基準音声を保存</span>
134
- <label class="toggle-switch">
135
- <input type="checkbox" id="baseVoiceToggle" />
136
- <span class="slider"></span>
137
- </label>
138
- </div>
139
-
140
- <!-- チャート表示部 -->
141
- <div class="chart">
142
- <canvas id="speechChart"></canvas>
143
- </div>
144
-
145
- <!-- 録音ボタン -->
146
- <button class="record-button" id="recordButton" onclick="toggleRecording()">
147
- <div class="record-icon" id="recordIcon"></div>
148
- </button>
149
-
150
- <!-- 結果ボタン -->
151
- <div class="result-buttons">
152
- <button class="result-button" id="historyButton" onclick="showHistory()">
153
- 会話履歴を表示
154
- </button>
155
- <button class="result-button" id="feedbackButton" onclick="showResults()">
156
- フィードバック画面を表示
157
- </button>
158
- </div>
159
 
160
  <script>
161
  let isRecording = false;
162
  let mediaRecorder;
163
  let audioChunks = [];
164
- let recordingInterval; // 通常モードでの10秒周期用
165
- let baseTimeout; // 基準音声モード用のタイマー
166
  let count_voice = 0;
167
  let before_rate = 0;
168
 
 
 
 
 
169
  // Chart.js の初期化
170
  const ctx = document.getElementById("speechChart").getContext("2d");
171
  const speechChart = new Chart(ctx, {
172
  type: "doughnut",
173
  data: {
174
- labels: ["自分", "他の人"],
175
  datasets: [
176
  {
177
- data: [30, 70],
178
  backgroundColor: ["#4caf50", "#757575"],
179
  },
180
  ],
@@ -191,9 +116,20 @@
191
  },
192
  });
193
 
194
- // トグルの状態を取得する関数
195
- function isBaseVoiceMode() {
196
- return document.getElementById("baseVoiceToggle").checked;
 
 
 
 
 
 
 
 
 
 
 
197
  }
198
 
199
  async function toggleRecording() {
@@ -222,26 +158,6 @@
222
  };
223
 
224
  mediaRecorder.start();
225
-
226
- if (isBaseVoiceMode()) {
227
- // 基準音声モード:10秒後に自動停止するタイマーをセット
228
- baseTimeout = setTimeout(() => {
229
- if (mediaRecorder && mediaRecorder.state === "recording") {
230
- mediaRecorder.stop();
231
- // 10秒経過しても録音ボタンがONなら強制的に停止&トグルをオフにする
232
- isRecording = false;
233
- recordButton.classList.remove("recording");
234
- document.getElementById("baseVoiceToggle").checked = false;
235
- }
236
- }, 10000);
237
- } else {
238
- // 通常モード:10秒ごとに自動停止して送信、継続録音する処理
239
- recordingInterval = setInterval(() => {
240
- if (mediaRecorder && mediaRecorder.state === "recording") {
241
- mediaRecorder.stop();
242
- }
243
- }, 10000);
244
- }
245
  } catch (error) {
246
  console.error("マイクへのアクセスに失敗しました:", error);
247
  isRecording = false;
@@ -251,11 +167,6 @@
251
  // 手動停止
252
  isRecording = false;
253
  recordButton.classList.remove("recording");
254
- if (isBaseVoiceMode()) {
255
- clearTimeout(baseTimeout);
256
- } else {
257
- clearInterval(recordingInterval);
258
- }
259
  if (mediaRecorder && mediaRecorder.state === "recording") {
260
  mediaRecorder.stop();
261
  count_voice = 0;
@@ -269,11 +180,7 @@
269
  const reader = new FileReader();
270
  reader.onloadend = () => {
271
  const base64String = reader.result.split(",")[1]; // Base64エンコードされた音声データ
272
- // エンドポイントの選択:基準音声モードなら '/upload_base_audio'
273
- const endpoint = isBaseVoiceMode()
274
- ? "/upload_base_audio"
275
- : "/upload_audio";
276
- fetch(endpoint, {
277
  method: "POST",
278
  headers: {
279
  "Content-Type": "application/json",
@@ -285,7 +192,7 @@
285
  if (data.error) {
286
  alert("エラー: " + data.error);
287
  console.error(data.details);
288
- } else if (data.rate !== undefined && !isBaseVoiceMode()) {
289
  // 通常モードの場合、解析結果をチャートに反映
290
  if (count_voice === 0) {
291
  speechChart.data.datasets[0].data = [
@@ -310,44 +217,17 @@
310
  }
311
  count_voice++;
312
  speechChart.update();
313
- } else {
314
- // 基準音声モードまたは解析結果がない場合
315
- if (isBaseVoiceMode()) {
316
- //alert('基準音声が保存されました。');
317
- // トグルをリセット
318
- document.getElementById("baseVoiceToggle").checked = false;
319
- } else {
320
- //alert('音声がバックエンドに送信されました。');
321
- }
322
- }
323
- // 通常モードの場合、録音が継続中なら次の録音を開始(自動連続録音)
324
- if (
325
- !isBaseVoiceMode() &&
326
- isRecording &&
327
- mediaRecorder &&
328
- mediaRecorder.state === "inactive"
329
- ) {
330
- mediaRecorder.start();
331
  }
332
  })
333
  .catch((error) => {
334
  console.error("エラー:", error);
335
- if (
336
- !isBaseVoiceMode() &&
337
- isRecording &&
338
- mediaRecorder &&
339
- mediaRecorder.state === "inactive"
340
- ) {
341
- mediaRecorder.start();
342
- }
343
  });
344
  };
345
  reader.readAsDataURL(audioBlob);
346
  }
347
 
348
- function showHistory() {
349
- window.location.href = "history";
350
- alert("会話履歴を表示する機能は未実装です。");
351
  }
352
 
353
  function showResults() {
@@ -355,9 +235,9 @@
355
  window.location.href = "feedback";
356
  }
357
 
358
- function showLogin() {
359
- // フィードバック画面へ遷移
360
- window.location.href = "login";
361
  }
362
  </script>
363
  </body>
 
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>Voice Recorder Interface</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ </head>
10
+ <body
11
+ class="flex flex-col items-center justify-center h-screen bg-gray-900 text-white"
12
+ >
13
+ <!-- メンバーを登録ボタン -->
14
+ <div class="flex items-center mb-5">
15
+ <button
16
+ id="registerButton"
17
+ onclick="showUserRegister()"
18
+ class="px-4 py-2 bg-blue-600 rounded-md hover:bg-blue-700 transition"
19
+ >
20
+ メンバーを登録
21
+ </button>
22
+ </div>
23
+
24
+ <!-- チャート表示部 -->
25
+ <div class="chart w-72 h-72 mb-5">
26
+ <canvas id="speechChart"></canvas>
27
+ </div>
28
+
29
+ <!-- 録音ボタン -->
30
+ <button class="record-button" id="recordButton" onclick="toggleRecording()">
31
+ <div class="record-icon" id="recordIcon"></div>
32
+ </button>
33
+
34
+ <!-- 結果ボタン -->
35
+ <div class="flex mt-5">
36
+ <button
37
+ id="historyButton"
38
+ onclick="showTalkdetail()"
39
+ class="result-button px-4 py-2 mx-2 bg-green-600 rounded-md hover:bg-green-700 transition"
40
+ >
41
+ 会話履歴を表示
42
+ </button>
43
+ <button
44
+ id="feedbackButton"
45
+ onclick="showResults()"
46
+ class="result-button px-4 py-2 mx-2 bg-blue-600 rounded-md hover:bg-blue-700 transition"
47
+ >
48
+ フィードバック画面を表示
49
+ </button>
50
+ </div>
51
+
52
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  .record-button {
54
  width: 80px;
55
  height: 80px;
 
63
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
64
  transition: all 0.2s ease;
65
  }
66
+
67
  .record-icon {
68
  width: 60px;
69
  height: 60px;
 
71
  border-radius: 50%;
72
  transition: all 0.2s ease;
73
  }
74
+
75
  .recording .record-icon {
76
  width: 40px;
77
  height: 40px;
78
  border-radius: 10%;
79
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
  <script>
83
  let isRecording = false;
84
  let mediaRecorder;
85
  let audioChunks = [];
86
+ let recordingInterval;
 
87
  let count_voice = 0;
88
  let before_rate = 0;
89
 
90
+ // 初期設定:人数と名前を受け取って円グラフを作成
91
+ let members = ["自分", "ahaha", "nufufu", "不明"];
92
+ let voiceData = [50, 20, 20, 10]; // 自分と不明の割合を仮設定
93
+
94
  // Chart.js の初期化
95
  const ctx = document.getElementById("speechChart").getContext("2d");
96
  const speechChart = new Chart(ctx, {
97
  type: "doughnut",
98
  data: {
99
+ labels: members,
100
  datasets: [
101
  {
102
+ data: voiceData,
103
  backgroundColor: ["#4caf50", "#757575"],
104
  },
105
  ],
 
116
  },
117
  });
118
 
119
+ //録音ボタン見た目変化
120
+ function toggleRecording() {
121
+ isRecording = !isRecording;
122
+ const recordIcon = document.getElementById("recordIcon");
123
+ if (isRecording) {
124
+ recordIcon.classList.add("w-10", "h-10", "bg-red-900", "rounded-md");
125
+ } else {
126
+ recordIcon.classList.remove(
127
+ "w-10",
128
+ "h-10",
129
+ "bg-red-900",
130
+ "rounded-md"
131
+ );
132
+ }
133
  }
134
 
135
  async function toggleRecording() {
 
158
  };
159
 
160
  mediaRecorder.start();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  } catch (error) {
162
  console.error("マイクへのアクセスに失敗しました:", error);
163
  isRecording = false;
 
167
  // 手動停止
168
  isRecording = false;
169
  recordButton.classList.remove("recording");
 
 
 
 
 
170
  if (mediaRecorder && mediaRecorder.state === "recording") {
171
  mediaRecorder.stop();
172
  count_voice = 0;
 
180
  const reader = new FileReader();
181
  reader.onloadend = () => {
182
  const base64String = reader.result.split(",")[1]; // Base64エンコードされた音声データ
183
+ fetch("/upload_audio", {
 
 
 
 
184
  method: "POST",
185
  headers: {
186
  "Content-Type": "application/json",
 
192
  if (data.error) {
193
  alert("エラー: " + data.error);
194
  console.error(data.details);
195
+ } else if (data.rate !== undefined) {
196
  // 通常モードの場合、解析結果をチャートに反映
197
  if (count_voice === 0) {
198
  speechChart.data.datasets[0].data = [
 
217
  }
218
  count_voice++;
219
  speechChart.update();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  }
221
  })
222
  .catch((error) => {
223
  console.error("エラー:", error);
 
 
 
 
 
 
 
 
224
  });
225
  };
226
  reader.readAsDataURL(audioBlob);
227
  }
228
 
229
+ function showTalkdetail() {
230
+ window.location.href = "talk_detail";
 
231
  }
232
 
233
  function showResults() {
 
235
  window.location.href = "feedback";
236
  }
237
 
238
+ function showUserRegister() {
239
+ // 音声登録画面へ遷移
240
+ window.location.href = "userregister";
241
  }
242
  </script>
243
  </body>
templates/userRegister.html ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <style>
8
+ @keyframes pulse-scale {
9
+ 0%,
10
+ 100% {
11
+ transform: scale(1);
12
+ }
13
+ 50% {
14
+ transform: scale(1.1);
15
+ }
16
+ }
17
+ .animate-pulse-scale {
18
+ animation: pulse-scale 1s infinite;
19
+ }
20
+ .record-button {
21
+ width: 50px;
22
+ height: 50px;
23
+ background-color: transparent;
24
+ border-radius: 50%;
25
+ border: 2px solid white;
26
+ display: flex;
27
+ justify-content: center;
28
+ align-items: center;
29
+ cursor: pointer;
30
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
31
+ transition: all 0.3s ease;
32
+ }
33
+ .record-icon {
34
+ width: 35px;
35
+ height: 35px;
36
+ background-color: #d32f2f;
37
+ border-radius: 50%;
38
+ transition: all 0.3s ease;
39
+ }
40
+ .record-button.recording .record-icon {
41
+ border-radius: 4px; /* 録音時に赤い部分だけ四角にする */
42
+ }
43
+ .recording .record-icon {
44
+ width: 20px;
45
+ height: 20px;
46
+ border-radius: 50%;
47
+ }
48
+ @media (max-width: 640px) {
49
+ .container {
50
+ padding: 2rem;
51
+ }
52
+ }
53
+ </style>
54
+ </head>
55
+ <body
56
+ class="bg-gray-800 text-gray-100 dark:bg-gray-900 dark:text-gray-300 transition-colors"
57
+ >
58
+ <div class="container mx-auto p-5 max-w-full sm:max-w-2xl">
59
+ <div id="people-list" class="space-y-4"></div>
60
+ <button
61
+ id="add-btn"
62
+ class="mt-6 px-6 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
63
+ >
64
+ メンバーを追加
65
+ </button>
66
+
67
+ <!-- 録音画面に戻るボタン -->
68
+ <button
69
+ id="backButton"
70
+ class="mt-6 px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
71
+ >
72
+ 録音画面に戻る
73
+ </button>
74
+ </div>
75
+
76
+ <script>
77
+ let mediaRecorder;
78
+ let audioChunks = [];
79
+ let userCount = 0; // 追加されたメンバー数を保持
80
+ let isRecording = false; // 録音中かどうかを判定するフラグ
81
+ let currentRecordingButton = null; // 現在録音中のボタンを保持
82
+
83
+ function toggleRecording(button) {
84
+ button.classList.toggle("recording");
85
+ }
86
+
87
+ async function startRecording(button) {
88
+ if (isRecording && currentRecordingButton !== button) return; // 他の人が録音中なら何もしない
89
+ isRecording = true; // 録音中に設定
90
+ currentRecordingButton = button; // 録音中のボタンを記録
91
+
92
+ try {
93
+ const stream = await navigator.mediaDevices.getUserMedia({
94
+ audio: true,
95
+ });
96
+ mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
97
+ audioChunks = [];
98
+ mediaRecorder.ondataavailable = (e) => audioChunks.push(e.data);
99
+ mediaRecorder.onstop = () => {
100
+ sendAudioChunks(audioChunks, button); // ボタン情報を渡す
101
+ audioChunks = [];
102
+ isRecording = false; // 録音停止後はフラグを戻す
103
+ currentRecordingButton = null; // 録音ボタンを解除
104
+ };
105
+ mediaRecorder.start();
106
+ toggleRecording(button);
107
+ } catch (err) {
108
+ console.error("マイクアクセスに失敗しました:", err);
109
+ isRecording = false; // エラー発生時もフラグを戻す
110
+ currentRecordingButton = null;
111
+ }
112
+ }
113
+
114
+ function stopRecording(button) {
115
+ if (!isRecording) return; // 録音中でない場合は停止しない
116
+ mediaRecorder.stop();
117
+ toggleRecording(button);
118
+ }
119
+
120
+ function handleRecording(e) {
121
+ const button = e.target.closest(".record-button");
122
+ if (button) {
123
+ if (isRecording && currentRecordingButton !== button) {
124
+ // 他の人が録音中なら反応しない
125
+ return;
126
+ }
127
+ if (mediaRecorder && mediaRecorder.state === "recording") {
128
+ stopRecording(button);
129
+ } else {
130
+ startRecording(button);
131
+ }
132
+ }
133
+ }
134
+
135
+ function sendAudioChunks(chunks, button) {
136
+ // 引数に button を追加
137
+ const audioBlob = new Blob(chunks, { type: "audio/wav" });
138
+ const reader = new FileReader();
139
+ reader.onloadend = () => {
140
+ const base64String = reader.result.split(",")[1]; // Base64エンコードされた音声データ
141
+ const form = button.closest("form");
142
+ const nameInput = form.querySelector('input[name="name"]');
143
+ const name = nameInput ? nameInput.value : "unknown"; // 名前がない
144
+ fetch("/upload_base_audio", {
145
+ method: "POST",
146
+ headers: {
147
+ "Content-Type": "application/json",
148
+ },
149
+ body: JSON.stringify({ audio_data: base64String, name: name }),
150
+ })
151
+ .then((response) => response.json())
152
+ .then((data) => {
153
+ // エラー処理のみ残す
154
+ if (data.error) {
155
+ alert("エラー: " + data.error);
156
+ console.error(data.details);
157
+ }
158
+ // 成功時の処理(ボタンの有効化など)
159
+ else {
160
+ console.log("音声データ送信成功:", data);
161
+ // 必要に応じて、ここでUIの変更(ボタンの有効化など)を行う
162
+ // 例: button.disabled = true; // 送信ボタンを無効化
163
+ // 例: button.classList.remove("recording"); //録音中のスタイルを解除
164
+ }
165
+ })
166
+ .catch((error) => {
167
+ console.error("エラー:", error);
168
+ });
169
+ };
170
+ reader.readAsDataURL(audioBlob);
171
+ }
172
+
173
+ document.getElementById("add-btn").addEventListener("click", () => {
174
+ const newItem = document.createElement("div");
175
+ newItem.className = "flex items-center gap-3 flex-wrap";
176
+ newItem.innerHTML = `
177
+ <form
178
+ action="/submit"
179
+ method="POST"
180
+ class="flex items-center space-x-2 w-full sm:w-auto"
181
+ onsubmit="event.preventDefault();"
182
+ >
183
+ <input
184
+ type="text"
185
+ name="name"
186
+ placeholder="名前を入力"
187
+ 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"
188
+ />
189
+
190
+ <button type="button" class="record-button" aria-label="音声録音開始">
191
+ <div class="record-icon"></div>
192
+ </button>
193
+
194
+ <button
195
+ type="submit"
196
+ class="submit-button px-4 py-2 border rounded-lg bg-blue-500 text-white hover:bg-blue-600"
197
+ >
198
+ 送信
199
+ </button>
200
+ </form>
201
+ `;
202
+ newItem.addEventListener("click", handleRecording);
203
+ document.getElementById("people-list").appendChild(newItem);
204
+ userCount++; // 新しいメンバーを追加するたびにカウントを増やす
205
+ });
206
+
207
+ // 「録音画面に戻る」ボタンの処理
208
+ document
209
+ .getElementById("backButton")
210
+ .addEventListener("click", function () {
211
+ // メンバーの人数を送信する
212
+ sendUserCount();
213
+
214
+ // index.htmlに戻る
215
+ window.location.href = "index.html";
216
+ });
217
+
218
+ // メンバーの人数を送信する関数
219
+ function sendUserCount() {
220
+ console.log(`追加された人数: ${userCount}`);
221
+ // ここで人数を送信する処理を実行(例: fetchを使ってサーバーに送信)
222
+ }
223
+ </script>
224
+ </body>
225
+ </html>
transcription.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  from faster_whisper import WhisperModel
3
 
4
  class TranscriptionMaker():
5
- #書き起こしファイル(ファイル名_transcription.txt)を吐き出すディレクトリを指定
6
  def __init__(self,output_dir=os.path.abspath("/tmp/data/transcriptions")):
7
  self.model = WhisperModel("base", device="cpu")
8
  self.output_dir = output_dir
@@ -13,35 +13,35 @@ class TranscriptionMaker():
13
  print(f"Error creating directory {self.output_dir}: {e}")
14
  raise
15
 
16
- #音声ファイルのパスを受け取り、書き起こしファイルを作成する
17
- def create_transcription(self,audio_path):
18
- try:
19
- if not os.path.isfile(audio_path):
20
- raise FileNotFoundError(f"The specified audio file does not exist: {audio_path}")
21
-
22
- segments, info = self.model.transcribe(audio_path)
23
- results = []
24
-
 
 
 
 
 
 
 
25
  for segment in segments:
26
  results.append({
27
  "start": segment.start,
28
  "end": segment.end,
29
  "text": segment.text
30
  })
31
-
32
- #ファイルの書き込み
33
- output_file=os.path.join(self.output_dir,os.path.basename(audio_path)+"_transcription.txt")
34
- try:
35
- with open(output_file,"w",encoding="utf-8") as f:
36
- for result in results:
37
- f.write(f"[{result['start']:.2f}s - {result['end']:.2f}s] {result['text']}\n")
38
- except OSError as e:
39
- print(f"Error writing transcription file: {e}")
40
- raise
41
- return output_file
42
- except FileNotFoundError as e:
43
- print(f"Error: {e}")
44
  raise
45
- except Exception as e:
46
- print(f"An unexpected error occurred: {e}")
47
- raise
 
2
  from faster_whisper import WhisperModel
3
 
4
  class TranscriptionMaker():
5
+ #書き起こしファイルを吐き出すディレクトリを指定
6
  def __init__(self,output_dir=os.path.abspath("/tmp/data/transcriptions")):
7
  self.model = WhisperModel("base", device="cpu")
8
  self.output_dir = output_dir
 
13
  print(f"Error creating directory {self.output_dir}: {e}")
14
  raise
15
 
16
+ #音声ファイルのディレクトリを受け取り、書き起こしファイルを作成する
17
+ def create_transcription(self,audio_directory):
18
+ results = []
19
+ #ディレクトリ内のファイルを全て取得
20
+ if not os.path.isdir(audio_directory):
21
+ raise ValueError(f"The specified path is not a valid directory: {audio_directory}")
22
+ audio_files = os.listdir(audio_directory)
23
+ for audio_file in audio_files:
24
+ if os.path.splitext(audio_file)[-1].lower() != '.wav':
25
+ continue
26
+ audio_path = os.path.join(audio_directory, audio_file)
27
+ try:
28
+ segments,info = list(self.model.transcribe(audio_path))
29
+ except Exception as e:
30
+ print(f"Error transcripting file {audio_path}: {e}")
31
+ raise
32
  for segment in segments:
33
  results.append({
34
  "start": segment.start,
35
  "end": segment.end,
36
  "text": segment.text
37
  })
38
+ #ファイルの書き込み。ファイル名は"読み込みディレクトリ名_transcription.txt"
39
+ output_file=os.path.join(self.output_dir,os.path.basename(audio_directory)+"_transcription.txt")
40
+ try:
41
+ with open(output_file,"w",encoding="utf-8") as f:
42
+ for result in results:
43
+ f.write(f"{result['text']}\n")
44
+ except OSError as e:
45
+ print(f"Error writing transcription file: {e}")
 
 
 
 
 
46
  raise
47
+ return output_file