buletomato25 commited on
Commit
1dfab95
·
2 Parent(s): 98c4a68 86d99b4

docker conflict

Browse files
__pycache__/app.cpython-311.pyc ADDED
Binary file (13.8 kB). View file
 
__pycache__/database.cpython-311.pyc ADDED
Binary file (275 Bytes). View file
 
__pycache__/users.cpython-311.pyc ADDED
Binary file (1.17 kB). View file
 
app.py CHANGED
@@ -1,14 +1,112 @@
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 dotenv import load_dotenv
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- process=AudioProcessor()
10
  app = Flask(__name__)
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  load_dotenv() # .env ファイルを読み込む
13
  HUGGINGFACE_HUB_TOKEN = os.getenv("HUGGINGFACE_HUB_TOKEN")
14
  if not HUGGINGFACE_HUB_TOKEN:
@@ -16,19 +114,71 @@ if not HUGGINGFACE_HUB_TOKEN:
16
 
17
  # トップページ(テンプレート: index.html)
18
  @app.route('/')
19
- @app.route('/index', methods=['GET', 'POST'])
 
 
 
 
 
20
  def index():
21
- return render_template('index.html')
 
 
 
 
 
 
22
 
23
  # フィードバック画面(テンプレート: feedback.html)
24
  @app.route('/feedback', methods=['GET', 'POST'])
25
  def feedback():
26
- return render_template('feedback.html')
 
27
 
28
  # 会話詳細画面(テンプレート: talkDetail.html)
29
  @app.route('/talk_detail', methods=['GET', 'POST'])
30
  def talk_detail():
31
- return render_template('talkDetail.html')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  # 音声アップロード&解析エンドポイント
34
  @app.route('/upload_audio', methods=['POST'])
 
1
+ from flask import Flask, request, jsonify, render_template, send_from_directory,redirect, make_response, Response, session
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 flask_login import LoginManager, UserMixin, login_user, logout_user, login_required
14
+ from database import db
15
+ from users import Users
16
+ from werkzeug.security import generate_password_hash, check_password_hash
17
+
18
+ # Hugging Face のトークン取得(環境変数 HF に設定)
19
+ #hf_token = os.environ.get("HF")
20
+ hf_token = "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"
21
+ if hf_token is None:
22
+ raise ValueError("HUGGINGFACE_HUB_TOKEN が設定されていません。")
23
+
24
+ # キャッシュディレクトリの作成(書き込み可能な /tmp を利用)
25
+ cache_dir = "/tmp/hf_cache"
26
+ os.makedirs(cache_dir, exist_ok=True)
27
+
28
+ # pyannote モデルの読み込み
29
+ model = Model.from_pretrained("pyannote/embedding", use_auth_token=hf_token, cache_dir=cache_dir)
30
+ inference = Inference(model)
31
 
 
32
  app = Flask(__name__)
33
 
34
+ app.config['SECRET_KEY'] = os.urandom(24)
35
+ # データベース設定
36
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
37
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
38
+
39
+ # db を Flask アプリに紐づける
40
+ db.init_app(app)
41
+
42
+ # Flask-Login の設定
43
+ login_manager = LoginManager()
44
+ login_manager.init_app(app)
45
+ login_manager.login_view = "login"
46
+
47
+ @login_manager.user_loader
48
+ def load_user(user_id):
49
+ return Users.query.get(int(user_id))
50
+
51
+ def cosine_similarity(vec1, vec2):
52
+ vec1 = vec1 / np.linalg.norm(vec1)
53
+ vec2 = vec2 / np.linalg.norm(vec2)
54
+ return np.dot(vec1, vec2)
55
+
56
+ def segment_audio(path, target_path='/tmp/setup_voice', seg_duration=1.0):
57
+ """
58
+ 音声を指定秒数ごとに分割する。
59
+ target_path に分割したファイルを保存し、元の音声の総長(ミリ秒)を返す。
60
+ """
61
+ os.makedirs(target_path, exist_ok=True)
62
+ base_sound = AudioSegment.from_file(path)
63
+ duration_ms = len(base_sound)
64
+ seg_duration_ms = int(seg_duration * 1000)
65
+
66
+ for i, start in enumerate(range(0, duration_ms, seg_duration_ms)):
67
+ end = min(start + seg_duration_ms, duration_ms)
68
+ segment = base_sound[start:end]
69
+ segment.export(os.path.join(target_path, f'{i}.wav'), format="wav")
70
+
71
+ return target_path, duration_ms
72
+
73
+ def calculate_similarity(path1, path2):
74
+ embedding1 = inference(path1)
75
+ embedding2 = inference(path2)
76
+ return float(cosine_similarity(embedding1.data.flatten(), embedding2.data.flatten()))
77
+
78
+ def process_audio(reference_path, input_path, output_folder='/tmp/data/matched_segments', seg_duration=1.0, threshold=0.5):
79
+ """
80
+ 入力音声ファイルを seg_duration 秒ごとに分割し、各セグメントと参照音声の類似度を計算。
81
+ 類似度が threshold を超えたセグメントを output_folder にコピーし、マッチした時間(ms)と
82
+ マッチしなかった時間(ms)を返す。
83
+ """
84
+ os.makedirs(output_folder, exist_ok=True)
85
+ segmented_path, total_duration_ms = segment_audio(input_path, seg_duration=seg_duration)
86
+
87
+ matched_time_ms = 0
88
+ for file in sorted(os.listdir(segmented_path)):
89
+ segment_file = os.path.join(segmented_path, file)
90
+ similarity = calculate_similarity(segment_file, reference_path)
91
+ if similarity > threshold:
92
+ shutil.copy(segment_file, output_folder)
93
+ matched_time_ms += len(AudioSegment.from_file(segment_file))
94
+
95
+ unmatched_time_ms = total_duration_ms - matched_time_ms
96
+ return matched_time_ms, unmatched_time_ms
97
+
98
+ def generate_random_string(length):
99
+ letters = string.ascii_letters + string.digits
100
+ return ''.join(random.choice(letters) for i in range(length))
101
+
102
+ def generate_filename(random_length):
103
+ random_string = generate_random_string(random_length)
104
+ current_time = datetime.now().strftime("%Y%m%d%H%M%S")
105
+ filename = f"{current_time}_{random_string}.wav"
106
+ return filename
107
+
108
+
109
+
110
  load_dotenv() # .env ファイルを読み込む
111
  HUGGINGFACE_HUB_TOKEN = os.getenv("HUGGINGFACE_HUB_TOKEN")
112
  if not HUGGINGFACE_HUB_TOKEN:
 
114
 
115
  # トップページ(テンプレート: index.html)
116
  @app.route('/')
117
+ def top():
118
+ return redirect('/login')
119
+
120
+ # ログイン後画面
121
+ @app.route('/after')
122
+ @login_required # ログインしているユーザのみアクセス許可
123
  def index():
124
+ users = Users.query.order_by(Users.id).all()
125
+ return render_template('index.html', users=users)
126
+
127
+ @app.route('/index', methods=['GET', 'POST'])
128
+ def index_page():
129
+ users = Users.query.order_by(Users.id).all()
130
+ return render_template('index.html', users=users)
131
 
132
  # フィードバック画面(テンプレート: feedback.html)
133
  @app.route('/feedback', methods=['GET', 'POST'])
134
  def feedback():
135
+ users = Users.query.order_by(Users.id).all()
136
+ return render_template('feedback.html', users=users)
137
 
138
  # 会話詳細画面(テンプレート: talkDetail.html)
139
  @app.route('/talk_detail', methods=['GET', 'POST'])
140
  def talk_detail():
141
+ users = Users.query.order_by(Users.id).all()
142
+ return render_template('talkDetail.html', users=users)
143
+
144
+ # ログイン画面(テンプレート: login.html)
145
+ @app.route('/login', methods=['GET', 'POST'])
146
+ def login():
147
+ if request.method == "POST":
148
+ username = request.form.get('username')
149
+ password = request.form.get('password')
150
+ # Userテーブルからusernameに一致するユーザを取得
151
+ user = Users.query.filter_by(username=username).first()
152
+ if check_password_hash(user.password, password):
153
+ login_user(user)
154
+ return redirect('after')
155
+ else:
156
+ # 入力したユーザー名のパスワードが間違っている場合
157
+ # return "<p>パスワードが間違っています。</p>"
158
+ return Response(status=404, response="ページが見つかりません。")
159
+ else:
160
+ return render_template('login.html')
161
+
162
+ # 登録画面(テンプレート: new_person.html)
163
+ @app.route('/new_person', methods=['GET', 'POST'])
164
+ def new_person():
165
+ if request.method == "POST":
166
+ username = request.form.get('username')
167
+ password = request.form.get('password')
168
+ # Userのインスタンスを作成
169
+ user = Users(username=username, password=generate_password_hash(password, method='sha256'), email=username)
170
+ db.session.add(user)
171
+ db.session.commit()
172
+ return redirect('login')
173
+ else:
174
+ return render_template('new_person.html')
175
+
176
+ @app.before_request
177
+ def before_request():
178
+ # リクエストのたびにセッションの寿命を更新する
179
+ session.permanent = True
180
+ app.permanent_session_lifetime = timedelta(minutes=15)
181
+ session.modified = True
182
 
183
  # 音声アップロード&解析エンドポイント
184
  @app.route('/upload_audio', methods=['POST'])
database.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+
3
+ db = SQLAlchemy()
init/createdatabase.sql ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ USE app;
2
+
3
+ CREATE TABLE users(
4
+ user_id INT PRIMARY KEY AUTO_INCREMENT,
5
+ username VARCHAR(255),
6
+ password VARCHAR(255),
7
+
8
+ );
9
+
10
+ INSERT INTO users(username,password) VALUES('sample','sample');
11
+ INSERT INTO users(username,password) VALUES('test','test');
12
+ INSERT INTO users(username,password) VALUES('app','app');
13
+
14
+ GRANT ALL ON app.* TO test;
instance/site.db ADDED
File without changes
requirements.txt CHANGED
@@ -3,4 +3,9 @@ pyannote.audio==2.1.1
3
  numpy==1.23.5
4
  pydub==0.25.1
5
  matplotlib==3.6.3
6
- python-dotenv
 
 
 
 
 
 
3
  numpy==1.23.5
4
  pydub==0.25.1
5
  matplotlib==3.6.3
6
+ python-dotenv
7
+ uwsgi
8
+ flask-sqlalchemy
9
+ PyMySQL
10
+ flask-login
11
+
templates/index.html CHANGED
@@ -67,12 +67,14 @@
67
  .chart {
68
  width: 300px;
69
  height: 300px;
70
- margin-bottom: 50px;
 
 
 
 
 
71
  }
72
- /* 録音ボタンのスタイル */
73
  .record-button {
74
- position: fixed;
75
- bottom: 30px;
76
  width: 80px;
77
  height: 80px;
78
  background-color: transparent;
@@ -97,13 +99,10 @@
97
  height: 40px;
98
  border-radius: 10%;
99
  }
100
- /* 結果ボタンのスタイル */
101
- .result-buttons {
102
- margin-top: 5px;
103
- display: flex;
104
- gap: 10px;
105
- }
106
  .result-button {
 
 
 
107
  padding: 10px 20px;
108
  background-color: #4caf50;
109
  border: none;
@@ -111,7 +110,9 @@
111
  color: white;
112
  cursor: pointer;
113
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
114
- transition: background-color 0.2s ease;
 
 
115
  }
116
  .result-button:hover {
117
  background-color: #388e3c;
@@ -141,8 +142,12 @@
141
 
142
  <!-- 結果ボタン -->
143
  <div class="result-buttons">
144
- <button class="result-button" id="historyButton" onclick="showHistory()">会話履歴を表示</button>
145
- <button class="result-button" id="feedbackButton" onclick="showResults()">フィードバック画面を表示</button>
 
 
 
 
146
  </div>
147
 
148
  <script>
@@ -150,51 +155,55 @@
150
  let mediaRecorder;
151
  let audioChunks = [];
152
  let recordingInterval; // 通常モードでの10秒周期用
153
- let baseTimeout; // 基準音声モード用のタイマー
154
  let count_voice = 0;
155
  let before_rate = 0;
156
 
157
  // Chart.js の初期化
158
- const ctx = document.getElementById('speechChart').getContext('2d');
159
  const speechChart = new Chart(ctx, {
160
- type: 'doughnut',
161
  data: {
162
- labels: ['自分', '他の人'],
163
- datasets: [{
164
- data: [30, 70],
165
- backgroundColor: ['#4caf50', '#757575'],
166
- }],
 
 
167
  },
168
  options: {
169
  responsive: true,
170
  plugins: {
171
  legend: {
172
  display: true,
173
- position: 'bottom',
174
- labels: { color: 'white' }
175
- }
176
- }
177
- }
178
  });
179
 
180
  // トグルの状態を取得する関数
181
  function isBaseVoiceMode() {
182
- return document.getElementById('baseVoiceToggle').checked;
183
  }
184
 
185
  async function toggleRecording() {
186
- const recordButton = document.getElementById('recordButton');
187
 
188
  if (!isRecording) {
189
  // 録音開始
190
  isRecording = true;
191
- recordButton.classList.add('recording');
192
  try {
193
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
 
 
194
  mediaRecorder = new MediaRecorder(stream);
195
  audioChunks = [];
196
 
197
- mediaRecorder.ondataavailable = event => {
198
  if (event.data.size > 0) {
199
  audioChunks.push(event.data);
200
  }
@@ -214,8 +223,8 @@
214
  mediaRecorder.stop();
215
  // 10秒経過しても録音ボタンがONなら強制的に停止&トグルをオフにする
216
  isRecording = false;
217
- recordButton.classList.remove('recording');
218
- document.getElementById('baseVoiceToggle').checked = false;
219
  }
220
  }, 10000);
221
  } else {
@@ -227,14 +236,14 @@
227
  }, 10000);
228
  }
229
  } catch (error) {
230
- console.error('マイクへのアクセスに失敗しました:', error);
231
  isRecording = false;
232
- recordButton.classList.remove('recording');
233
  }
234
  } else {
235
  // 手動停止
236
  isRecording = false;
237
- recordButton.classList.remove('recording');
238
  if (isBaseVoiceMode()) {
239
  clearTimeout(baseTimeout);
240
  } else {
@@ -249,71 +258,95 @@
249
  }
250
 
251
  function sendAudioChunks(chunks) {
252
- const audioBlob = new Blob(chunks, { type: 'audio/wav' });
253
  const reader = new FileReader();
254
  reader.onloadend = () => {
255
- const base64String = reader.result.split(',')[1]; // Base64エンコードされた音声データ
256
  // エンドポイントの選択:基準音声モードなら '/upload_base_audio'
257
- const endpoint = isBaseVoiceMode() ? '/upload_base_audio' : '/upload_audio';
 
 
258
  fetch(endpoint, {
259
- method: 'POST',
260
  headers: {
261
- 'Content-Type': 'application/json',
262
  },
263
  body: JSON.stringify({ audio_data: base64String }),
264
  })
265
- .then(response => response.json())
266
- .then(data => {
267
- if (data.error) {
268
- alert('エラー: ' + data.error);
269
- console.error(data.details);
270
- } else if (data.rate !== undefined && !isBaseVoiceMode()) {
271
- // 通常モードの場合、解析結果をチャートに反映
272
- if (count_voice === 0) {
273
- speechChart.data.datasets[0].data = [data.rate, 100 - data.rate];
274
- before_rate = data.rate;
275
- } else if (count_voice === 1) {
276
- let tmp_rate = (data.rate + before_rate) / 2;
277
- speechChart.data.datasets[0].data = [tmp_rate, 100 - tmp_rate];
278
- before_rate = tmp_rate;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  } else {
280
- let tmp_rate = (data.rate + before_rate * 2) / 3;
281
- speechChart.data.datasets[0].data = [tmp_rate, 100 - tmp_rate];
282
- before_rate = tmp_rate;
 
 
 
 
 
283
  }
284
- count_voice++;
285
- speechChart.update();
286
- } else {
287
- // 基準音声モードまたは解析結果がない場合
288
- if (isBaseVoiceMode()) {
289
- //alert('基準音声が保存されました。');
290
- // トグルをリセット
291
- document.getElementById('baseVoiceToggle').checked = false;
292
- } else {
293
- //alert('音声がバックエンドに送信されました。');
294
  }
295
- }
296
- // 通常モードの場合、録音が継続中なら次の録音を開始(自動連続録音)
297
- if (!isBaseVoiceMode() && isRecording && mediaRecorder && mediaRecorder.state === "inactive") {
298
- mediaRecorder.start();
299
- }
300
- })
301
- .catch(error => {
302
- console.error('エラー:', error);
303
- if (!isBaseVoiceMode() && isRecording && mediaRecorder && mediaRecorder.state === "inactive") {
304
- mediaRecorder.start();
305
- }
306
- });
307
  };
308
  reader.readAsDataURL(audioBlob);
309
  }
310
 
311
  function showHistory() {
312
- alert('会話履歴を表示する機能は未実装です。');
 
 
313
  }
314
 
315
  function showResults() {
316
- window.location.href = 'feedback';
 
317
  }
318
  </script>
319
  </body>
 
67
  .chart {
68
  width: 300px;
69
  height: 300px;
70
+ margin-bottom: 20px; /* 円グラフとボタンの間隔を狭く */
71
+ }
72
+ .controls {
73
+ display: flex;
74
+ flex-direction: column;
75
+ align-items: center;
76
  }
 
77
  .record-button {
 
 
78
  width: 80px;
79
  height: 80px;
80
  background-color: transparent;
 
99
  height: 40px;
100
  border-radius: 10%;
101
  }
 
 
 
 
 
 
102
  .result-button {
103
+ margin-left: 10px;
104
+
105
+ margin-top: 20px;
106
  padding: 10px 20px;
107
  background-color: #4caf50;
108
  border: none;
 
110
  color: white;
111
  cursor: pointer;
112
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
113
+ }
114
+ .result {
115
+ display: flex;
116
  }
117
  .result-button:hover {
118
  background-color: #388e3c;
 
142
 
143
  <!-- 結果ボタン -->
144
  <div class="result-buttons">
145
+ <button class="result-button" id="historyButton" onclick="showHistory()">
146
+ 会話履歴を表示
147
+ </button>
148
+ <button class="result-button" id="feedbackButton" onclick="showResults()">
149
+ フィードバック画面を表示
150
+ </button>
151
  </div>
152
 
153
  <script>
 
155
  let mediaRecorder;
156
  let audioChunks = [];
157
  let recordingInterval; // 通常モードでの10秒周期用
158
+ let baseTimeout; // 基準音声モード用のタイマー
159
  let count_voice = 0;
160
  let before_rate = 0;
161
 
162
  // Chart.js の初期化
163
+ const ctx = document.getElementById("speechChart").getContext("2d");
164
  const speechChart = new Chart(ctx, {
165
+ type: "doughnut",
166
  data: {
167
+ labels: ["自分", "他の人"],
168
+ datasets: [
169
+ {
170
+ data: [30, 70],
171
+ backgroundColor: ["#4caf50", "#757575"],
172
+ },
173
+ ],
174
  },
175
  options: {
176
  responsive: true,
177
  plugins: {
178
  legend: {
179
  display: true,
180
+ position: "bottom",
181
+ labels: { color: "white" },
182
+ },
183
+ },
184
+ },
185
  });
186
 
187
  // トグルの状態を取得する関数
188
  function isBaseVoiceMode() {
189
+ return document.getElementById("baseVoiceToggle").checked;
190
  }
191
 
192
  async function toggleRecording() {
193
+ const recordButton = document.getElementById("recordButton");
194
 
195
  if (!isRecording) {
196
  // 録音開始
197
  isRecording = true;
198
+ recordButton.classList.add("recording");
199
  try {
200
+ const stream = await navigator.mediaDevices.getUserMedia({
201
+ audio: true,
202
+ });
203
  mediaRecorder = new MediaRecorder(stream);
204
  audioChunks = [];
205
 
206
+ mediaRecorder.ondataavailable = (event) => {
207
  if (event.data.size > 0) {
208
  audioChunks.push(event.data);
209
  }
 
223
  mediaRecorder.stop();
224
  // 10秒経過しても録音ボタンがONなら強制的に停止&トグルをオフにする
225
  isRecording = false;
226
+ recordButton.classList.remove("recording");
227
+ document.getElementById("baseVoiceToggle").checked = false;
228
  }
229
  }, 10000);
230
  } else {
 
236
  }, 10000);
237
  }
238
  } catch (error) {
239
+ console.error("マイクへのアクセスに失敗しました:", error);
240
  isRecording = false;
241
+ recordButton.classList.remove("recording");
242
  }
243
  } else {
244
  // 手動停止
245
  isRecording = false;
246
+ recordButton.classList.remove("recording");
247
  if (isBaseVoiceMode()) {
248
  clearTimeout(baseTimeout);
249
  } else {
 
258
  }
259
 
260
  function sendAudioChunks(chunks) {
261
+ const audioBlob = new Blob(chunks, { type: "audio/wav" });
262
  const reader = new FileReader();
263
  reader.onloadend = () => {
264
+ const base64String = reader.result.split(",")[1]; // Base64エンコードされた音声データ
265
  // エンドポイントの選択:基準音声モードなら '/upload_base_audio'
266
+ const endpoint = isBaseVoiceMode()
267
+ ? "/upload_base_audio"
268
+ : "/upload_audio";
269
  fetch(endpoint, {
270
+ method: "POST",
271
  headers: {
272
+ "Content-Type": "application/json",
273
  },
274
  body: JSON.stringify({ audio_data: base64String }),
275
  })
276
+ .then((response) => response.json())
277
+ .then((data) => {
278
+ if (data.error) {
279
+ alert("エラー: " + data.error);
280
+ console.error(data.details);
281
+ } else if (data.rate !== undefined && !isBaseVoiceMode()) {
282
+ // 通常モードの場合、解析結果をチャートに反映
283
+ if (count_voice === 0) {
284
+ speechChart.data.datasets[0].data = [
285
+ data.rate,
286
+ 100 - data.rate,
287
+ ];
288
+ before_rate = data.rate;
289
+ } else if (count_voice === 1) {
290
+ let tmp_rate = (data.rate + before_rate) / 2;
291
+ speechChart.data.datasets[0].data = [
292
+ tmp_rate,
293
+ 100 - tmp_rate,
294
+ ];
295
+ before_rate = tmp_rate;
296
+ } else {
297
+ let tmp_rate = (data.rate + before_rate * 2) / 3;
298
+ speechChart.data.datasets[0].data = [
299
+ tmp_rate,
300
+ 100 - tmp_rate,
301
+ ];
302
+ before_rate = tmp_rate;
303
+ }
304
+ count_voice++;
305
+ speechChart.update();
306
  } else {
307
+ // 基準音声モードまたは解析結果がない場合
308
+ if (isBaseVoiceMode()) {
309
+ //alert('基準音声が保存されました。');
310
+ // トグルをリセット
311
+ document.getElementById("baseVoiceToggle").checked = false;
312
+ } else {
313
+ //alert('音声がバックエンドに送信されました。');
314
+ }
315
  }
316
+ // 通常モードの場合、録音が継続中なら次の録音を開始(自動連続録音)
317
+ if (
318
+ !isBaseVoiceMode() &&
319
+ isRecording &&
320
+ mediaRecorder &&
321
+ mediaRecorder.state === "inactive"
322
+ ) {
323
+ mediaRecorder.start();
 
 
324
  }
325
+ })
326
+ .catch((error) => {
327
+ console.error("エラー:", error);
328
+ if (
329
+ !isBaseVoiceMode() &&
330
+ isRecording &&
331
+ mediaRecorder &&
332
+ mediaRecorder.state === "inactive"
333
+ ) {
334
+ mediaRecorder.start();
335
+ }
336
+ });
337
  };
338
  reader.readAsDataURL(audioBlob);
339
  }
340
 
341
  function showHistory() {
342
+ // 会話履歴表示の画面があれば、そのページへ遷移する例
343
+ // window.location.href = 'history';
344
+ alert("会話履歴を表示する機能は未実装です。");
345
  }
346
 
347
  function showResults() {
348
+ // フィードバック画面へ遷移
349
+ window.location.href = "feedback";
350
  }
351
  </script>
352
  </body>
templates/login.html ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ログイン画面</title>
7
+ <style>
8
+ @charset "UTF-8";
9
+ body {
10
+ font-family: Arial, sans-serif;
11
+ padding: 20px;
12
+ background-color: #f4f4f4;
13
+ height: 100vh;
14
+ display: flex;
15
+ justify-content: center;
16
+ align-items: center;
17
+ }
18
+
19
+ h2 {
20
+ margin-bottom: 20px;
21
+ text-align: center;
22
+ }
23
+
24
+ a {
25
+ text-decoration: none;
26
+ color: #000000cc;
27
+ }
28
+ a:hover {
29
+ text-decoration: underline;
30
+ }
31
+ .container {
32
+ max-width: 800px;
33
+
34
+ background-color: #fff;
35
+ padding: 20px 80px;
36
+ border-radius: 8px;
37
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
38
+ }
39
+
40
+ #transcription {
41
+ white-space: pre-wrap;
42
+ padding: 10px;
43
+ background-color: #e9e9e9;
44
+ border-radius: 4px;
45
+ margin-bottom: 20px;
46
+ max-height: 400px;
47
+ overflow-y: auto;
48
+ }
49
+ button {
50
+ margin: 5px;
51
+ padding: 10px 10px;
52
+ border: none;
53
+ border-radius: 4px;
54
+ background-color: #007bff;
55
+ color: #fff;
56
+ cursor: pointer;
57
+ }
58
+ .history-button {
59
+ margin-top: 20px;
60
+
61
+ padding: 10px 20px;
62
+ background-color: #007bff;
63
+ color: white;
64
+ border: none;
65
+ border-radius: 5px;
66
+ cursor: pointer;
67
+ }
68
+ history-button:hover {
69
+ background-color: #0056b3;
70
+ }
71
+
72
+ .flex {
73
+ display: flex;
74
+ justify-content: center;
75
+ }
76
+ .new-person {
77
+ text-align: center;
78
+ }
79
+
80
+ .controls {
81
+ display: flex;
82
+ flex-direction: column;
83
+ align-items: center;
84
+ }
85
+ .record-button {
86
+ width: 80px;
87
+ height: 80px;
88
+ background-color: transparent;
89
+ border-radius: 50%;
90
+
91
+ display: flex;
92
+ justify-content: center;
93
+ align-items: center;
94
+ cursor: pointer;
95
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
96
+ transition: all 0.2s ease;
97
+ }
98
+
99
+ .record-icon {
100
+ width: 60px;
101
+ height: 60px;
102
+ background-color: #d32f2f;
103
+ border-radius: 50%;
104
+ transition: all 0.2s ease;
105
+ }
106
+
107
+ .recording .record-icon {
108
+ width: 40px;
109
+ height: 40px;
110
+ border-radius: 10%;
111
+ }
112
+
113
+ .record-p {
114
+ border: 2px dashed #0000008c;
115
+ }
116
+
117
+ .disabled {
118
+ background-color: gray;
119
+ cursor: not-allowed;
120
+ }
121
+
122
+ .record-icon.recording {
123
+ width: 40px;
124
+ height: 40px;
125
+ border-radius: 0;
126
+ }
127
+
128
+ .new-person-right-container {
129
+ padding-left: 20px;
130
+ }
131
+
132
+ .record-container {
133
+ display: flex;
134
+ justify-content: center;
135
+ }
136
+
137
+ </style>
138
+
139
+ </head>
140
+ <body>
141
+ <form method="POST">
142
+ <div class="container">
143
+ <h2>ログイン</h2>
144
+ <div>
145
+ <label for="">ユーザ名</label>
146
+ <input type="text" name="username" />
147
+ <br />
148
+ <br />
149
+ <label for="">パスワード</label>
150
+ <input type="password" name="password" />
151
+ <br />
152
+ </div>
153
+ <div class="flex">
154
+ <input
155
+ type="submit"
156
+ class="history-button"
157
+ id="loginButton"
158
+ onclick="showRecorder()"
159
+ value="ログイン"
160
+ />
161
+ </div>
162
+ </form>
163
+ <div class="new-person">
164
+ <p><a href="new_person.php" class="new-person">新規会員登録をする</a></p>
165
+ </div>
166
+ </div>
167
+
168
+
169
+
170
+ <script>
171
+ // 会話データを表示
172
+ async function displayTranscription() {
173
+ const transcriptionElement = document.getElementById("transcription");
174
+
175
+ try {
176
+ // バックエンドからデータを取得(デモ用のURLを指定)
177
+ const response = await fetch("/api/transcription");
178
+ if (!response.ok) throw new Error("データ取得に失敗しました。");
179
+
180
+ const data = await response.json();
181
+
182
+ // 会話内容を整形して表示
183
+ const formattedText = data.conversations
184
+ .map((conv, index) => `【${conv.speaker}】 ${conv.text}`)
185
+ .join("\n");
186
+
187
+ transcriptionElement.textContent = formattedText;
188
+ } catch (error) {
189
+ transcriptionElement.textContent = `エラー: ${error.message}`;
190
+ console.error("データ取得エラー:", error);
191
+ }
192
+ }
193
+
194
+ // 初期化処理
195
+ displayTranscription();
196
+
197
+ //画面遷移
198
+ function showRecorder() {
199
+ // 録音画面へ遷移
200
+ window.location.href = "/index";
201
+ }
202
+ function showFeedback() {
203
+ // フィードバック画面へ遷移
204
+ window.location.href = "/feedback";
205
+ }
206
+ </script>
207
+ </body>
208
+ </html>
templates/new_person.html CHANGED
@@ -1,215 +1,344 @@
1
- <!DOCTYPE html>
2
- <html lang="ja">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <link rel="stylesheet" href="style.css" />
7
- <title>ユーザー登録画面</title>
8
- </head>
9
- <body>
10
- <form method="POST">
11
- <div class="container">
12
- <h2>ユーザー登録</h2>
13
- <div class="flex">
14
- <div>
15
- <label for="">ユーザー名</label>
16
- <br />
17
- <input
18
- type="text"
19
- id="username"
20
- name="username"
21
- placeholder="例:技育 太郎"
22
- required
23
- oninput="validateForm()"
24
- />
25
- <br />
26
- <br />
27
- <label for="">パスワード</label>
28
- <br />
29
- <input
30
- type="password"
31
- id="password"
32
- name="password"
33
- placeholder="半角数字8文字以上20文字以内"
34
- maxlength="20"
35
- required
36
- oninput="validateForm()"
37
- />
38
- <br />
39
- <br />
40
- <label for=""> パスワード再入力</label>
41
- <br />
42
- <input
43
- type="password"
44
- id="repassword"
45
- name="repassword"
46
- placeholder="再入力してください"
47
- maxlength="20"
48
- required
49
- oninput="validateForm()"
50
- />
51
- <p id="passwordError" class="error-message"></p>
52
- <br />
53
- </div>
54
- <div class="new-person-right-container">
55
- <label for="">
56
- 録音ボタンを押した後、10秒間声を録音してください</label
57
- >
58
- <br />
59
- <p class="record-p">
60
- 例文:昔々、あるところにおばあさんとおじいさんがいました。<br />
61
- おばあさんは川に洗濯に、おじいさんは山に芝刈りに行きました。
62
- </p>
63
- <div class="record-container">
64
- <button
65
- class="record-button"
66
- id="recordButton"
67
- onclick="toggleRecording()"
68
- >
69
- <div class="record-icon" id="recordIcon"></div>
70
- </button>
71
- </div>
72
-
73
- <p id="countdownDisplay"></p>
74
- </div>
75
- </div>
76
-
77
- <div class="flex">
78
- <input
79
- type="submit"
80
- value="会員登録を行う"
81
- name="submit"
82
- id="submit"
83
- class="history-button disabled"
84
- disabled
85
- />
86
- </div>
87
- </div>
88
- </form>
89
- <script>
90
- let mediaRecorder;
91
- let audioChunks = [];
92
- let countdownTimer;
93
- let isAudioRecorded = false;
94
-
95
- async function toggleRecording() {
96
- const recordButton = document.getElementById("recordButton");
97
-
98
- const countdownDisplay = document.getElementById("countdownDisplay");
99
-
100
- recordButton.disabled = true; // ボタンを無効化(連打防止)
101
- countdownDisplay.textContent = "録音中…"; // 初期表示
102
- isAudioRecorded = false;
103
- recordIcon.classList.add("recording"); // アイコンを正方形に変更
104
-
105
- try {
106
- const stream = await navigator.mediaDevices.getUserMedia({
107
- audio: true,
108
- });
109
- mediaRecorder = new MediaRecorder(stream);
110
- audioChunks = [];
111
-
112
- mediaRecorder.ondataavailable = (event) => {
113
- if (event.data.size > 0) {
114
- audioChunks.push(event.data);
115
- }
116
- };
117
-
118
- mediaRecorder.onstop = () => {
119
- sendAudioChunks([...audioChunks]); // 録音データをサーバーに送信
120
- audioChunks = [];
121
- recordButton.disabled = false; // ボタンを再度有効化
122
- countdownDisplay.textContent = ""; // カウントダウンを消す
123
- isAudioRecorded = true; // 録音完了
124
- recordIcon.classList.remove("recording"); // アイコンを元の丸に戻す
125
- validateForm(); // フォームの状態を更新
126
- };
127
-
128
- mediaRecorder.start();
129
- startCountdown(10, countdownDisplay); // 10秒のカウントダウン開始
130
-
131
- // 10秒後に録音を自動停止
132
- setTimeout(() => {
133
- if (mediaRecorder.state === "recording") {
134
- mediaRecorder.stop();
135
- }
136
- }, 10000);
137
- } catch (error) {
138
- console.error("マイクのアクセスに失敗しました:", error);
139
- recordButton.disabled = false; // ボタンを再度有効化
140
- countdownDisplay.textContent = ""; // カウントダウンを消す
141
- recordIcon.classList.remove("recording"); // アイコンを元の丸に戻す
142
- }
143
- }
144
-
145
- function startCountdown(seconds, displayElement) {
146
- let remaining = seconds;
147
- displayElement.textContent = `録音終了まで: ${remaining}秒`;
148
-
149
- countdownTimer = setInterval(() => {
150
- remaining--;
151
- displayElement.textContent = `録音終了まで: ${remaining}秒`;
152
-
153
- if (remaining <= 0) {
154
- clearInterval(countdownTimer); // カウントダウン終了
155
- }
156
- }, 1000);
157
- }
158
-
159
- function sendAudioChunks(chunks) {
160
- const audioBlob = new Blob(chunks, { type: "audio/wav" });
161
- const reader = new FileReader();
162
- reader.onloadend = () => {
163
- const base64String = reader.result.split(",")[1]; // Base64 に変換
164
- fetch("/upload_audio", {
165
- method: "POST",
166
- headers: {
167
- "Content-Type": "application/json",
168
- },
169
- body: JSON.stringify({ audio_data: base64String }),
170
- })
171
- .then((response) => response.json())
172
- .then((data) => {
173
- alert("音声がサーバーに送信されました。");
174
- })
175
- .catch((error) => {
176
- console.error("送信エラー:", error);
177
- });
178
- };
179
- reader.readAsDataURL(audioBlob);
180
- }
181
-
182
- // フォームの入力状態をチェック
183
- function validateForm() {
184
- const username = document.getElementById("username").value.trim();
185
- const password = document.getElementById("password").value.trim();
186
- const repassword = document.getElementById("repassword").value.trim();
187
- const submitButton = document.getElementById("submit");
188
- const passwordError = document.getElementById("passwordError");
189
-
190
- if (password !== repassword) {
191
- passwordError.textContent = "パスワードが一致しません。";
192
- submitButton.classList.add("disabled");
193
- submitButton.disabled = true;
194
- return;
195
- } else {
196
- passwordError.textContent = "";
197
- }
198
-
199
- if (
200
- username !== "" &&
201
- password.length >= 8 &&
202
- password.length <= 20 &&
203
- password === repassword &&
204
- isAudioRecorded
205
- ) {
206
- submitButton.classList.remove("disabled");
207
- submitButton.disabled = false;
208
- } else {
209
- submitButton.classList.add("disabled");
210
- submitButton.disabled = true;
211
- }
212
- }
213
- </script>
214
- </body>
215
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <style>
7
+ @charset "UTF-8";
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ padding: 20px;
11
+ background-color: #f4f4f4;
12
+ height: 100vh;
13
+ display: flex;
14
+ justify-content: center;
15
+ align-items: center;
16
+ }
17
+
18
+ h2 {
19
+ margin-bottom: 20px;
20
+ text-align: center;
21
+ }
22
+
23
+ a {
24
+ text-decoration: none;
25
+ color: #000000cc;
26
+ }
27
+ a:hover {
28
+ text-decoration: underline;
29
+ }
30
+ .container {
31
+ max-width: 800px;
32
+
33
+ background-color: #fff;
34
+ padding: 20px 80px;
35
+ border-radius: 8px;
36
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
37
+ }
38
+
39
+ #transcription {
40
+ white-space: pre-wrap;
41
+ padding: 10px;
42
+ background-color: #e9e9e9;
43
+ border-radius: 4px;
44
+ margin-bottom: 20px;
45
+ max-height: 400px;
46
+ overflow-y: auto;
47
+ }
48
+ button {
49
+ margin: 5px;
50
+ padding: 10px 10px;
51
+ border: none;
52
+ border-radius: 4px;
53
+ background-color: #007bff;
54
+ color: #fff;
55
+ cursor: pointer;
56
+ }
57
+ .history-button {
58
+ margin-top: 20px;
59
+
60
+ padding: 10px 20px;
61
+ background-color: #007bff;
62
+ color: white;
63
+ border: none;
64
+ border-radius: 5px;
65
+ cursor: pointer;
66
+ }
67
+ history-button:hover {
68
+ background-color: #0056b3;
69
+ }
70
+
71
+ .flex {
72
+ display: flex;
73
+ justify-content: center;
74
+ }
75
+ .new-person {
76
+ text-align: center;
77
+ }
78
+
79
+ .controls {
80
+ display: flex;
81
+ flex-direction: column;
82
+ align-items: center;
83
+ }
84
+ .record-button {
85
+ width: 80px;
86
+ height: 80px;
87
+ background-color: transparent;
88
+ border-radius: 50%;
89
+
90
+ display: flex;
91
+ justify-content: center;
92
+ align-items: center;
93
+ cursor: pointer;
94
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
95
+ transition: all 0.2s ease;
96
+ }
97
+
98
+ .record-icon {
99
+ width: 60px;
100
+ height: 60px;
101
+ background-color: #d32f2f;
102
+ border-radius: 50%;
103
+ transition: all 0.2s ease;
104
+ }
105
+
106
+ .recording .record-icon {
107
+ width: 40px;
108
+ height: 40px;
109
+ border-radius: 10%;
110
+ }
111
+
112
+ .record-p {
113
+ border: 2px dashed #0000008c;
114
+ }
115
+
116
+ .disabled {
117
+ background-color: gray;
118
+ cursor: not-allowed;
119
+ }
120
+
121
+ .record-icon.recording {
122
+ width: 40px;
123
+ height: 40px;
124
+ border-radius: 0;
125
+ }
126
+
127
+ .new-person-right-container {
128
+ padding-left: 20px;
129
+ }
130
+
131
+ .record-container {
132
+ display: flex;
133
+ justify-content: center;
134
+ }
135
+ </style>
136
+ <title>ユーザー登録画面</title>
137
+ </head>
138
+ <body>
139
+ <form method="POST">
140
+ <div class="container">
141
+ <h2>ユーザー登録</h2>
142
+ <div class="flex">
143
+ <div>
144
+ <label for="">ユーザー名</label>
145
+ <br />
146
+ <input
147
+ type="text"
148
+ id="username"
149
+ name="username"
150
+ placeholder="例:技育 太郎"
151
+ required
152
+ oninput="validateForm()"
153
+ />
154
+ <br />
155
+ <br />
156
+ <label for="">パスワード</label>
157
+ <br />
158
+ <input
159
+ type="password"
160
+ id="password"
161
+ name="password"
162
+ placeholder="半角数字8文字以上20文字以内"
163
+ maxlength="20"
164
+ required
165
+ oninput="validateForm()"
166
+ />
167
+ <br />
168
+ <br />
169
+ <label for=""> パスワード再入力</label>
170
+ <br />
171
+ <input
172
+ type="password"
173
+ id="repassword"
174
+ name="repassword"
175
+ placeholder="再入力してください"
176
+ maxlength="20"
177
+ required
178
+ oninput="validateForm()"
179
+ />
180
+ <p id="passwordError" class="error-message"></p>
181
+ <br />
182
+ </div>
183
+ <div class="new-person-right-container">
184
+ <label for="">
185
+ 録音ボタンを押した後、10秒間声を録音してください</label
186
+ >
187
+ <br />
188
+ <p class="record-p">
189
+ 例文:昔々、あるところにおばあさんとおじいさんがいました。<br />
190
+ おばあさんは川に洗濯に、おじいさんは山に芝刈りに行きました。
191
+ </p>
192
+ <div class="record-container">
193
+ <button
194
+ class="record-button"
195
+ id="recordButton"
196
+ onclick="toggleRecording()"
197
+ >
198
+ <div class="record-icon" id="recordIcon"></div>
199
+ </button>
200
+ </div>
201
+
202
+ <p id="countdownDisplay"></p>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="flex">
207
+ <input
208
+ type="submit"
209
+ value="会員登録を行う"
210
+ name="submit"
211
+ id="submit"
212
+ class="history-button disabled"
213
+ disabled
214
+ />
215
+ </div>
216
+ </div>
217
+ </form>
218
+ <script>
219
+ let mediaRecorder;
220
+ let audioChunks = [];
221
+ let countdownTimer;
222
+ let isAudioRecorded = false;
223
+
224
+ async function toggleRecording() {
225
+ const recordButton = document.getElementById("recordButton");
226
+
227
+ const countdownDisplay = document.getElementById("countdownDisplay");
228
+
229
+ recordButton.disabled = true; // ボタンを無効化(連打防止)
230
+ countdownDisplay.textContent = "録音中…"; // 初期表示
231
+ isAudioRecorded = false;
232
+ recordIcon.classList.add("recording"); // アイコンを正方形に変更
233
+
234
+ try {
235
+ const stream = await navigator.mediaDevices.getUserMedia({
236
+ audio: true,
237
+ });
238
+ mediaRecorder = new MediaRecorder(stream);
239
+ audioChunks = [];
240
+
241
+ mediaRecorder.ondataavailable = (event) => {
242
+ if (event.data.size > 0) {
243
+ audioChunks.push(event.data);
244
+ }
245
+ };
246
+
247
+ mediaRecorder.onstop = () => {
248
+ sendAudioChunks([...audioChunks]); // 録音データをサーバーに送信
249
+ audioChunks = [];
250
+ recordButton.disabled = false; // ボタンを再度有効化
251
+ countdownDisplay.textContent = ""; // カウントダウンを消す
252
+ isAudioRecorded = true; // 録音完了
253
+ recordIcon.classList.remove("recording"); // アイコンを元の丸に戻す
254
+ validateForm(); // フォームの状態を更新
255
+ };
256
+
257
+ mediaRecorder.start();
258
+ startCountdown(10, countdownDisplay); // 10秒のカウントダウン開始
259
+
260
+ // 10秒後に録音を自動停止
261
+ setTimeout(() => {
262
+ if (mediaRecorder.state === "recording") {
263
+ mediaRecorder.stop();
264
+ }
265
+ }, 10000);
266
+ } catch (error) {
267
+ console.error("マイクのアクセスに失敗しま��た:", error);
268
+ recordButton.disabled = false; // ボタンを再度有効化
269
+ countdownDisplay.textContent = ""; // カウントダウンを消す
270
+ recordIcon.classList.remove("recording"); // アイコンを元の丸に戻す
271
+ }
272
+ }
273
+
274
+ function startCountdown(seconds, displayElement) {
275
+ let remaining = seconds;
276
+ displayElement.textContent = `録音終了まで: ${remaining}秒`;
277
+
278
+ countdownTimer = setInterval(() => {
279
+ remaining--;
280
+ displayElement.textContent = `録音終了まで: ${remaining}秒`;
281
+
282
+ if (remaining <= 0) {
283
+ clearInterval(countdownTimer); // カウントダウン終了
284
+ }
285
+ }, 1000);
286
+ }
287
+
288
+ function sendAudioChunks(chunks) {
289
+ const audioBlob = new Blob(chunks, { type: "audio/wav" });
290
+ const reader = new FileReader();
291
+ reader.onloadend = () => {
292
+ const base64String = reader.result.split(",")[1]; // Base64 に変換
293
+ fetch("/upload_audio", {
294
+ method: "POST",
295
+ headers: {
296
+ "Content-Type": "application/json",
297
+ },
298
+ body: JSON.stringify({ audio_data: base64String }),
299
+ })
300
+ .then((response) => response.json())
301
+ .then((data) => {
302
+ alert("音声がサーバーに送信されました。");
303
+ })
304
+ .catch((error) => {
305
+ console.error("送信エラー:", error);
306
+ });
307
+ };
308
+ reader.readAsDataURL(audioBlob);
309
+ }
310
+
311
+ // フォームの入力状態をチェック
312
+ function validateForm() {
313
+ const username = document.getElementById("username").value.trim();
314
+ const password = document.getElementById("password").value.trim();
315
+ const repassword = document.getElementById("repassword").value.trim();
316
+ const submitButton = document.getElementById("submit");
317
+ const passwordError = document.getElementById("passwordError");
318
+
319
+ if (password !== repassword) {
320
+ passwordError.textContent = "パスワードが一致しません。";
321
+ submitButton.classList.add("disabled");
322
+ submitButton.disabled = true;
323
+ return;
324
+ } else {
325
+ passwordError.textContent = "";
326
+ }
327
+
328
+ if (
329
+ username !== "" &&
330
+ password.length >= 8 &&
331
+ password.length <= 20 &&
332
+ password === repassword &&
333
+ isAudioRecorded
334
+ ) {
335
+ submitButton.classList.remove("disabled");
336
+ submitButton.disabled = false;
337
+ } else {
338
+ submitButton.classList.add("disabled");
339
+ submitButton.disabled = true;
340
+ }
341
+ }
342
+ </script>
343
+ </body>
344
+ </html>
templates/resetting_password.html CHANGED
@@ -36,28 +36,6 @@
36
 
37
 
38
  <script>
39
- // 会話データを表示
40
- async function displayTranscription() {
41
- const transcriptionElement = document.getElementById("transcription");
42
-
43
- try {
44
- // バックエンドからデータを取得(デモ用のURLを指定)
45
- const response = await fetch("/api/transcription");
46
- if (!response.ok) throw new Error("データ取得に失敗しました。");
47
-
48
- const data = await response.json();
49
-
50
- // 会話内容を整形して表示
51
- const formattedText = data.conversations
52
- .map((conv, index) => `【${conv.speaker}】 ${conv.text}`)
53
- .join("\n");
54
-
55
- transcriptionElement.textContent = formattedText;
56
- } catch (error) {
57
- transcriptionElement.textContent = `エラー: ${error.message}`;
58
- console.error("データ取得エラー:", error);
59
- }
60
- }
61
 
62
  // 初期化処理
63
  displayTranscription();
 
36
 
37
 
38
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  // 初期化処理
41
  displayTranscription();
users.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from flask_login import UserMixin
3
+ from database import db
4
+
5
+ class Users(UserMixin, db.Model):
6
+ '''
7
+ Users Table Model
8
+ '''
9
+ __tablename__ = 'users'
10
+ id = db.Column(db.Integer, primary_key=True)
11
+ username = db.Column(db.String(255), nullable=False, unique=True)
12
+ password = db.Column(db.String(255))
13
+
14
+ def __init__(self,username,password):
15
+ self.username = username
16
+ self.password = password
17
+