Spaces:
Running
Running
buletomato25
commited on
Commit
·
093dde1
1
Parent(s):
7e8400a
upload
Browse files- __pycache__/new_record.cpython-310.pyc +0 -0
- app.py +24 -6
- new_record.py +43 -0
- templates/feedback.html +0 -1
- templates/history.html +1 -1
- templates/index.html +17 -1
- templates/new_person.html +25 -31
- templates/talkDetail.html +0 -1
__pycache__/new_record.cpython-310.pyc
ADDED
Binary file (1.4 kB). View file
|
|
app.py
CHANGED
@@ -14,6 +14,7 @@ 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 |
|
19 |
# Hugging Face のトークン取得(環境変数 HF に設定)
|
@@ -39,7 +40,9 @@ app.config['SECRET_KEY'] = os.urandom(24)
|
|
39 |
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
40 |
GOOGLE_CLIENT_ID = "228160683186-6u7986qsfhcv3kd9iqtv08iphpl4gdk2.apps.googleusercontent.com"
|
41 |
GOOGLE_CLIENT_SECRET = "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"
|
|
|
42 |
#REDIRECT_URI = "https://huggingface.co/spaces/Justtalk/JusTalk/callback"
|
|
|
43 |
REDIRECT_URI = "http://127.0.0.1:7860/callback"
|
44 |
|
45 |
flow = Flow.from_client_secrets_file(
|
@@ -105,10 +108,12 @@ def generate_filename(random_length):
|
|
105 |
filename = f"{current_time}_{random_string}.wav"
|
106 |
return filename
|
107 |
|
|
|
|
|
108 |
# トップページ(テンプレート: index.html)
|
109 |
@app.route('/')
|
110 |
def top():
|
111 |
-
return redirect('
|
112 |
|
113 |
# ログイン画面(テンプレート: login.html)
|
114 |
@app.route('/login')
|
@@ -123,7 +128,12 @@ def login():
|
|
123 |
def callback():
|
124 |
flow.fetch_token(authorization_response=request.url)
|
125 |
|
126 |
-
|
|
|
|
|
|
|
|
|
|
|
127 |
return 'State mismatch error', 400
|
128 |
|
129 |
credentials = flow.credentials
|
@@ -137,40 +147,48 @@ def callback():
|
|
137 |
session['email'] = id_info.get("email")
|
138 |
session['name'] = id_info.get("name")
|
139 |
|
140 |
-
return redirect(url_for('
|
|
|
141 |
|
142 |
# フィードバック画面(テンプレート: feedback.html)
|
143 |
@app.route('/feedback', methods=['GET', 'POST'])
|
144 |
def feedback():
|
|
|
|
|
145 |
if 'google_id' not in session:
|
146 |
return redirect(url_for('login'))
|
147 |
user_info = {
|
148 |
'name': session.get('name'),
|
149 |
'email': session.get('email')
|
150 |
}
|
151 |
-
|
|
|
152 |
|
153 |
# 会話詳細画面(テンプレート: talkDetail.html)
|
154 |
@app.route('/talk_detail', methods=['GET', 'POST'])
|
155 |
def talk_detail():
|
|
|
156 |
if 'google_id' not in session:
|
157 |
return redirect(url_for('login'))
|
158 |
user_info = {
|
159 |
'name': session.get('name'),
|
160 |
'email': session.get('email')
|
161 |
}
|
162 |
-
|
|
|
163 |
|
164 |
# インデックス画面(テンプレート: index.html)
|
165 |
@app.route('/index', methods=['GET', 'POST'])
|
166 |
def index():
|
|
|
167 |
if 'google_id' not in session:
|
168 |
return redirect(url_for('login'))
|
169 |
user_info = {
|
170 |
'name': session.get('name'),
|
171 |
'email': session.get('email')
|
172 |
}
|
173 |
-
|
|
|
174 |
|
175 |
# 登録画面(テンプレート: new_person.html)
|
176 |
@app.route('/new_person', methods=['GET', 'POST'])
|
|
|
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 |
+
from new_record import record_bp
|
18 |
|
19 |
|
20 |
# Hugging Face のトークン取得(環境変数 HF に設定)
|
|
|
40 |
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
41 |
GOOGLE_CLIENT_ID = "228160683186-6u7986qsfhcv3kd9iqtv08iphpl4gdk2.apps.googleusercontent.com"
|
42 |
GOOGLE_CLIENT_SECRET = "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"
|
43 |
+
#HFにpushするときは下記のコメントアウトを外してください
|
44 |
#REDIRECT_URI = "https://huggingface.co/spaces/Justtalk/JusTalk/callback"
|
45 |
+
#ローカルの時はこちら
|
46 |
REDIRECT_URI = "http://127.0.0.1:7860/callback"
|
47 |
|
48 |
flow = Flow.from_client_secrets_file(
|
|
|
108 |
filename = f"{current_time}_{random_string}.wav"
|
109 |
return filename
|
110 |
|
111 |
+
app.register_blueprint(record_bp)
|
112 |
+
|
113 |
# トップページ(テンプレート: index.html)
|
114 |
@app.route('/')
|
115 |
def top():
|
116 |
+
return redirect('index')
|
117 |
|
118 |
# ログイン画面(テンプレート: login.html)
|
119 |
@app.route('/login')
|
|
|
128 |
def callback():
|
129 |
flow.fetch_token(authorization_response=request.url)
|
130 |
|
131 |
+
# `session.get('state')` を使用し、エラーを防ぐ
|
132 |
+
session_state = session.get('state')
|
133 |
+
request_state = request.args.get('state')
|
134 |
+
|
135 |
+
if session_state is None or session_state != request_state:
|
136 |
+
print(f"State mismatch error: session_state={session_state}, request_state={request_state}")
|
137 |
return 'State mismatch error', 400
|
138 |
|
139 |
credentials = flow.credentials
|
|
|
147 |
session['email'] = id_info.get("email")
|
148 |
session['name'] = id_info.get("name")
|
149 |
|
150 |
+
return redirect(url_for('new_person'))
|
151 |
+
|
152 |
|
153 |
# フィードバック画面(テンプレート: feedback.html)
|
154 |
@app.route('/feedback', methods=['GET', 'POST'])
|
155 |
def feedback():
|
156 |
+
#ログイン問題解決しだい戻す
|
157 |
+
"""
|
158 |
if 'google_id' not in session:
|
159 |
return redirect(url_for('login'))
|
160 |
user_info = {
|
161 |
'name': session.get('name'),
|
162 |
'email': session.get('email')
|
163 |
}
|
164 |
+
"""
|
165 |
+
return render_template('feedback.html')
|
166 |
|
167 |
# 会話詳細画面(テンプレート: talkDetail.html)
|
168 |
@app.route('/talk_detail', methods=['GET', 'POST'])
|
169 |
def talk_detail():
|
170 |
+
"""
|
171 |
if 'google_id' not in session:
|
172 |
return redirect(url_for('login'))
|
173 |
user_info = {
|
174 |
'name': session.get('name'),
|
175 |
'email': session.get('email')
|
176 |
}
|
177 |
+
"""
|
178 |
+
return render_template('talkDetail.html')
|
179 |
|
180 |
# インデックス画面(テンプレート: index.html)
|
181 |
@app.route('/index', methods=['GET', 'POST'])
|
182 |
def index():
|
183 |
+
"""
|
184 |
if 'google_id' not in session:
|
185 |
return redirect(url_for('login'))
|
186 |
user_info = {
|
187 |
'name': session.get('name'),
|
188 |
'email': session.get('email')
|
189 |
}
|
190 |
+
"""
|
191 |
+
return render_template('index.html')
|
192 |
|
193 |
# 登録画面(テンプレート: new_person.html)
|
194 |
@app.route('/new_person', methods=['GET', 'POST'])
|
new_record.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint, request, jsonify
|
2 |
+
import os
|
3 |
+
import base64
|
4 |
+
from pydub import AudioSegment
|
5 |
+
|
6 |
+
record_bp = Blueprint('record', __name__)
|
7 |
+
|
8 |
+
# 録音データの保存先ディレクトリ
|
9 |
+
record_data_dir = "record_data"
|
10 |
+
os.makedirs(record_data_dir, exist_ok=True)
|
11 |
+
|
12 |
+
@record_bp.route('/upload_audio', methods=['POST'])
|
13 |
+
def upload_audio():
|
14 |
+
try:
|
15 |
+
data = request.get_json()
|
16 |
+
if not data or 'audio_data' not in data or 'user_name' not in data:
|
17 |
+
return jsonify({"error": "音声データまたはユーザー名がありません"}), 400
|
18 |
+
|
19 |
+
user_name = data['user_name'].replace(" ", "_") # 空白をアンダースコアに変換
|
20 |
+
audio_binary = base64.b64decode(data['audio_data'])
|
21 |
+
|
22 |
+
# 保存先のファイルパス
|
23 |
+
audio_path = os.path.join(record_data_dir, f"{user_name}.wav")
|
24 |
+
|
25 |
+
# 一時ファイルとして保存
|
26 |
+
temp_audio_path = os.path.join(record_data_dir, "temp_audio")
|
27 |
+
with open(temp_audio_path, 'wb') as f:
|
28 |
+
f.write(audio_binary)
|
29 |
+
|
30 |
+
# pydub を使って WAV に変換
|
31 |
+
try:
|
32 |
+
audio = AudioSegment.from_file(temp_audio_path, format="webm")
|
33 |
+
except Exception:
|
34 |
+
audio = AudioSegment.from_file(temp_audio_path)
|
35 |
+
|
36 |
+
audio.export(audio_path, format="wav")
|
37 |
+
os.remove(temp_audio_path)
|
38 |
+
|
39 |
+
return jsonify({"success": True, "message": f"音声が {audio_path} に保存されました"}), 200
|
40 |
+
|
41 |
+
except Exception as e:
|
42 |
+
print("Error in /upload_audio:", str(e))
|
43 |
+
return jsonify({"error": "サーバーエラー", "details": str(e)}), 500
|
templates/feedback.html
CHANGED
@@ -115,7 +115,6 @@
|
|
115 |
</script>
|
116 |
</head>
|
117 |
<body>
|
118 |
-
<h1 class="mb-4">ようこそ, {{ user.name }} さん!</h1>
|
119 |
<div class="card">
|
120 |
<div class="level" id="level">話者Lv: 85</div>
|
121 |
<div class="message" id="message">素晴らしい</div>
|
|
|
115 |
</script>
|
116 |
</head>
|
117 |
<body>
|
|
|
118 |
<div class="card">
|
119 |
<div class="level" id="level">話者Lv: 85</div>
|
120 |
<div class="message" id="message">素晴らしい</div>
|
templates/history.html
CHANGED
@@ -109,7 +109,7 @@
|
|
109 |
</head>
|
110 |
<body>
|
111 |
<header>All Recordings</header>
|
112 |
-
|
113 |
<div class="recording-list">
|
114 |
<div class="record-item record-item-template">
|
115 |
<div>
|
|
|
109 |
</head>
|
110 |
<body>
|
111 |
<header>All Recordings</header>
|
112 |
+
|
113 |
<div class="recording-list">
|
114 |
<div class="record-item record-item-template">
|
115 |
<div>
|
templates/index.html
CHANGED
@@ -121,11 +121,22 @@
|
|
121 |
.result-button:hover {
|
122 |
background-color: #388e3c;
|
123 |
}
|
|
|
|
|
|
|
124 |
</style>
|
125 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
126 |
</head>
|
127 |
<body>
|
128 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
<!-- トグルスイッチ:基準音声保存モード -->
|
130 |
<div class="toggle-container">
|
131 |
<span class="toggle-label">基準音声を保存</span>
|
@@ -352,6 +363,11 @@
|
|
352 |
// フィードバック画面へ遷移
|
353 |
window.location.href = "feedback";
|
354 |
}
|
|
|
|
|
|
|
|
|
|
|
355 |
</script>
|
356 |
</body>
|
357 |
</html>
|
|
|
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 |
+
<header>
|
132 |
+
<div></div>
|
133 |
+
<div>
|
134 |
+
<button class="result-button" id="historyButton" onclick="showLogin()">
|
135 |
+
ログイン
|
136 |
+
</button>
|
137 |
+
</div>
|
138 |
+
</header>
|
139 |
+
|
140 |
<!-- トグルスイッチ:基準音声保存モード -->
|
141 |
<div class="toggle-container">
|
142 |
<span class="toggle-label">基準音声を保存</span>
|
|
|
363 |
// フィードバック画面へ遷移
|
364 |
window.location.href = "feedback";
|
365 |
}
|
366 |
+
|
367 |
+
function showLogin() {
|
368 |
+
// フィードバック画面へ遷移
|
369 |
+
window.location.href = "login";
|
370 |
+
}
|
371 |
</script>
|
372 |
</body>
|
373 |
</html>
|
templates/new_person.html
CHANGED
@@ -188,16 +188,14 @@
|
|
188 |
let audioChunks = [];
|
189 |
let countdownTimer;
|
190 |
let isAudioRecorded = false;
|
191 |
-
|
192 |
async function toggleRecording() {
|
193 |
const recordButton = document.getElementById("recordButton");
|
194 |
-
|
195 |
const countdownDisplay = document.getElementById("countdownDisplay");
|
196 |
|
197 |
recordButton.disabled = true; // ボタンを無効化(連打防止)
|
198 |
countdownDisplay.textContent = "録音中…"; // 初期表示
|
199 |
isAudioRecorded = false;
|
200 |
-
recordIcon.classList.add("recording"); // アイコンを正方形に変更
|
201 |
|
202 |
try {
|
203 |
const stream = await navigator.mediaDevices.getUserMedia({
|
@@ -215,17 +213,15 @@
|
|
215 |
mediaRecorder.onstop = () => {
|
216 |
sendAudioChunks([...audioChunks]); // 録音データをサーバーに送信
|
217 |
audioChunks = [];
|
218 |
-
recordButton.disabled = false;
|
219 |
-
countdownDisplay.textContent = "";
|
220 |
-
isAudioRecorded = true;
|
221 |
-
|
222 |
-
validateForm(); // フォームの状態を更新
|
223 |
};
|
224 |
|
225 |
mediaRecorder.start();
|
226 |
-
startCountdown(10, countdownDisplay);
|
227 |
|
228 |
-
// 10秒後に録音を自動停止
|
229 |
setTimeout(() => {
|
230 |
if (mediaRecorder.state === "recording") {
|
231 |
mediaRecorder.stop();
|
@@ -233,9 +229,8 @@
|
|
233 |
}, 10000);
|
234 |
} catch (error) {
|
235 |
console.error("マイクのアクセスに失敗しました:", error);
|
236 |
-
recordButton.disabled = false;
|
237 |
-
countdownDisplay.textContent = "";
|
238 |
-
recordIcon.classList.remove("recording"); // アイコンを元の丸に戻す
|
239 |
}
|
240 |
}
|
241 |
|
@@ -248,29 +243,36 @@
|
|
248 |
displayElement.textContent = `録音終了まで: ${remaining}秒`;
|
249 |
|
250 |
if (remaining <= 0) {
|
251 |
-
clearInterval(countdownTimer);
|
252 |
}
|
253 |
}, 1000);
|
254 |
}
|
255 |
|
256 |
function sendAudioChunks(chunks) {
|
|
|
|
|
|
|
|
|
|
|
|
|
257 |
const audioBlob = new Blob(chunks, { type: "audio/wav" });
|
258 |
const reader = new FileReader();
|
259 |
reader.onloadend = () => {
|
260 |
const base64String = reader.result.split(",")[1]; // Base64 に変換
|
261 |
fetch("/upload_audio", {
|
262 |
method: "POST",
|
263 |
-
headers: {
|
264 |
-
|
265 |
-
|
266 |
-
|
|
|
267 |
})
|
268 |
.then((response) => response.json())
|
269 |
.then((data) => {
|
270 |
if (data.success) {
|
271 |
alert("音声がサーバーに送信されました。");
|
272 |
-
isAudioRecorded = true;
|
273 |
-
validateForm();
|
274 |
} else {
|
275 |
alert("音声の送信に失敗しました。");
|
276 |
}
|
@@ -283,17 +285,9 @@
|
|
283 |
reader.readAsDataURL(audioBlob);
|
284 |
}
|
285 |
|
286 |
-
//
|
287 |
-
function
|
288 |
-
|
289 |
-
|
290 |
-
if (isAudioRecorded) {
|
291 |
-
submitButton.classList.remove("disabled");
|
292 |
-
submitButton.disabled = false;
|
293 |
-
} else {
|
294 |
-
submitButton.classList.add("disabled");
|
295 |
-
submitButton.disabled = true;
|
296 |
-
}
|
297 |
}
|
298 |
|
299 |
// 初期化処理
|
|
|
188 |
let audioChunks = [];
|
189 |
let countdownTimer;
|
190 |
let isAudioRecorded = false;
|
191 |
+
sessionStorage.setItem("userName", "{{ user.name }}");
|
192 |
async function toggleRecording() {
|
193 |
const recordButton = document.getElementById("recordButton");
|
|
|
194 |
const countdownDisplay = document.getElementById("countdownDisplay");
|
195 |
|
196 |
recordButton.disabled = true; // ボタンを無効化(連打防止)
|
197 |
countdownDisplay.textContent = "録音中…"; // 初期表示
|
198 |
isAudioRecorded = false;
|
|
|
199 |
|
200 |
try {
|
201 |
const stream = await navigator.mediaDevices.getUserMedia({
|
|
|
213 |
mediaRecorder.onstop = () => {
|
214 |
sendAudioChunks([...audioChunks]); // 録音データをサーバーに送信
|
215 |
audioChunks = [];
|
216 |
+
recordButton.disabled = false;
|
217 |
+
countdownDisplay.textContent = "";
|
218 |
+
isAudioRecorded = true;
|
219 |
+
validateForm();
|
|
|
220 |
};
|
221 |
|
222 |
mediaRecorder.start();
|
223 |
+
startCountdown(10, countdownDisplay);
|
224 |
|
|
|
225 |
setTimeout(() => {
|
226 |
if (mediaRecorder.state === "recording") {
|
227 |
mediaRecorder.stop();
|
|
|
229 |
}, 10000);
|
230 |
} catch (error) {
|
231 |
console.error("マイクのアクセスに失敗しました:", error);
|
232 |
+
recordButton.disabled = false;
|
233 |
+
countdownDisplay.textContent = "";
|
|
|
234 |
}
|
235 |
}
|
236 |
|
|
|
243 |
displayElement.textContent = `録音終了まで: ${remaining}秒`;
|
244 |
|
245 |
if (remaining <= 0) {
|
246 |
+
clearInterval(countdownTimer);
|
247 |
}
|
248 |
}, 1000);
|
249 |
}
|
250 |
|
251 |
function sendAudioChunks(chunks) {
|
252 |
+
const userName = sessionStorage.getItem("userName"); // ユーザー名を取得
|
253 |
+
if (!userName) {
|
254 |
+
alert("ユーザー名が取得できません。ログインしてください。");
|
255 |
+
return;
|
256 |
+
}
|
257 |
+
|
258 |
const audioBlob = new Blob(chunks, { type: "audio/wav" });
|
259 |
const reader = new FileReader();
|
260 |
reader.onloadend = () => {
|
261 |
const base64String = reader.result.split(",")[1]; // Base64 に変換
|
262 |
fetch("/upload_audio", {
|
263 |
method: "POST",
|
264 |
+
headers: { "Content-Type": "application/json" },
|
265 |
+
body: JSON.stringify({
|
266 |
+
audio_data: base64String,
|
267 |
+
user_name: userName,
|
268 |
+
}),
|
269 |
})
|
270 |
.then((response) => response.json())
|
271 |
.then((data) => {
|
272 |
if (data.success) {
|
273 |
alert("音声がサーバーに送信されました。");
|
274 |
+
isAudioRecorded = true;
|
275 |
+
validateForm();
|
276 |
} else {
|
277 |
alert("音声の送信に失敗しました。");
|
278 |
}
|
|
|
285 |
reader.readAsDataURL(audioBlob);
|
286 |
}
|
287 |
|
288 |
+
// ユーザー名をセッションに保存(ログイン後に実行)
|
289 |
+
function setUserName(userName) {
|
290 |
+
sessionStorage.setItem("userName", userName);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
291 |
}
|
292 |
|
293 |
// 初期化処理
|
templates/talkDetail.html
CHANGED
@@ -70,7 +70,6 @@
|
|
70 |
</style>
|
71 |
</head>
|
72 |
<body>
|
73 |
-
<h1>ようこそ, {{ user.name }} さん!</h1>
|
74 |
<div class="container">
|
75 |
<h2>会話の文字起こし表示</h2>
|
76 |
<div id="transcription">ここに会話内容が表示されます。</div>
|
|
|
70 |
</style>
|
71 |
</head>
|
72 |
<body>
|
|
|
73 |
<div class="container">
|
74 |
<h2>会話の文字起こし表示</h2>
|
75 |
<div id="transcription">ここに会話内容が表示されます。</div>
|