Skip to content

Commit 924454b

Browse files
authored
main.js
1 parent 8fb4489 commit 924454b

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed

code/main.js

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
const STTalk = (() => {
2+
let mediaRecorder;
3+
let audioChunks = [];
4+
let audioBlob = null;
5+
let recognition;
6+
let finalTranscript = "";
7+
let textCallback = () => {};
8+
let startTime = null;
9+
let timerId = null;
10+
let audioContext;
11+
let analyser;
12+
let sourceNode;
13+
let volumeCallback = () => {};
14+
let volumeLevel = 0;
15+
let volumeMeterId;
16+
var ST_error_level = 0;
17+
var ST_alert_enabled = true;
18+
const DB_NAME = "STTalkDB";
19+
const STORE_NAME = "sessions";
20+
let db;
21+
22+
function openDB() {
23+
return new Promise((resolve, reject) => {
24+
if (db) return resolve(db);
25+
const request = indexedDB.open(DB_NAME, 1);
26+
request.onerror = e => reject(e.target.error);
27+
request.onsuccess = e => {
28+
db = e.target.result;
29+
resolve(db);
30+
};
31+
request.onupgradeneeded = e => {
32+
const db = e.target.result;
33+
if (!db.objectStoreNames.contains(STORE_NAME)) {
34+
db.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true });
35+
}
36+
};
37+
});
38+
}
39+
40+
async function addSession(audioBlob, text) {
41+
if (!audioBlob) throw new Error("録音音声がありません");
42+
const db = await openDB();
43+
return new Promise((resolve, reject) => {
44+
const tx = db.transaction(STORE_NAME, "readwrite");
45+
const store = tx.objectStore(STORE_NAME);
46+
const data = {
47+
audio: audioBlob,
48+
text: text || "",
49+
date: new Date().toISOString()
50+
};
51+
const request = store.add(data);
52+
request.onsuccess = () => resolve(request.result);
53+
request.onerror = e => reject(e.target.error);
54+
});
55+
}
56+
57+
async function getSessions() {
58+
const db = await openDB();
59+
return new Promise((resolve, reject) => {
60+
const tx = db.transaction(STORE_NAME, "readonly");
61+
const store = tx.objectStore(STORE_NAME);
62+
const request = store.getAll();
63+
request.onsuccess = () => resolve(request.result);
64+
request.onerror = e => reject(e.target.error);
65+
});
66+
}
67+
68+
async function deleteSession(id) {
69+
const db = await openDB();
70+
return new Promise((resolve, reject) => {
71+
const tx = db.transaction(STORE_NAME, "readwrite");
72+
const store = tx.objectStore(STORE_NAME);
73+
const request = store.delete(id);
74+
request.onsuccess = () => resolve();
75+
request.onerror = e => reject(e.target.error);
76+
});
77+
}
78+
79+
async function playSessionAudio(id) {
80+
const db = await openDB();
81+
const session = await new Promise((resolve, reject) => {
82+
const tx = db.transaction(STORE_NAME, "readonly");
83+
const store = tx.objectStore(STORE_NAME);
84+
const request = store.get(id);
85+
request.onsuccess = () => resolve(request.result);
86+
request.onerror = e => reject(e.target.error);
87+
});
88+
if (!session || !session.audio) {
89+
showAlert("音声が見つかりません");
90+
ST_error_level = 1;
91+
return;
92+
}
93+
const audio = new Audio(URL.createObjectURL(session.audio));
94+
audio.play();
95+
}
96+
97+
async function getSessionText(id) {
98+
const db = await openDB();
99+
const session = await new Promise((resolve, reject) => {
100+
const tx = db.transaction(STORE_NAME, "readonly");
101+
const store = tx.objectStore(STORE_NAME);
102+
const request = store.get(id);
103+
request.onsuccess = () => resolve(request.result);
104+
request.onerror = e => reject(e.target.error);
105+
});
106+
return session ? session.text : null;
107+
}
108+
109+
function startVolumeMeter(stream) {
110+
audioContext = new AudioContext();
111+
sourceNode = audioContext.createMediaStreamSource(stream);
112+
analyser = audioContext.createAnalyser();
113+
analyser.fftSize = 256;
114+
sourceNode.connect(analyser);
115+
const dataArray = new Uint8Array(analyser.frequencyBinCount);
116+
function update() {
117+
analyser.getByteFrequencyData(dataArray);
118+
const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
119+
volumeLevel = avg / 255;
120+
volumeCallback(volumeLevel);
121+
volumeMeterId = requestAnimationFrame(update);
122+
}
123+
update();
124+
}
125+
126+
function stopVolumeMeter() {
127+
if (volumeMeterId) cancelAnimationFrame(volumeMeterId);
128+
if (audioContext) audioContext.close();
129+
}
130+
131+
function getRecordingTime() {
132+
if (!startTime) return 0;
133+
return Math.floor((Date.now() - startTime) / 1000);
134+
}
135+
136+
function startRecording() {
137+
return navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
138+
mediaRecorder = new MediaRecorder(stream);
139+
audioChunks = [];
140+
audioBlob = null;
141+
mediaRecorder.ondataavailable = e => {
142+
if (e.data.size > 0) audioChunks.push(e.data);
143+
};
144+
mediaRecorder.onstop = () => {
145+
audioBlob = new Blob(audioChunks, { type: "audio/webm" });
146+
stopVolumeMeter();
147+
};
148+
mediaRecorder.start();
149+
startTime = Date.now();
150+
startVolumeMeter(stream);
151+
});
152+
}
153+
154+
function startRecognition() {
155+
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
156+
if (!SR) {
157+
showAlert("このブラウザは音声認識に対応していません");
158+
ST_error_level = -1;
159+
return;
160+
}
161+
recognition = new SR();
162+
recognition.lang = "ja-JP";
163+
recognition.interimResults = true;
164+
recognition.continuous = true;
165+
recognition.onresult = event => {
166+
let interim = "";
167+
for (let i = event.resultIndex; i < event.results.length; i++) {
168+
const transcript = event.results[i][0].transcript;
169+
if (event.results[i].isFinal) {
170+
finalTranscript += transcript + "\n";
171+
} else {
172+
interim += transcript;
173+
}
174+
}
175+
textCallback(finalTranscript + interim);
176+
};
177+
recognition.start();
178+
}
179+
180+
async function stopAll() {
181+
if (mediaRecorder && mediaRecorder.state !== "inactive") {
182+
mediaRecorder.stop();
183+
}
184+
if (recognition) recognition.stop();
185+
clearInterval(timerId);
186+
stopVolumeMeter();
187+
setTimeout(async () => {
188+
if (audioBlob) {
189+
try {
190+
await addSession(audioBlob, finalTranscript);
191+
console.log("録音セッションを履歴に保存しました");
192+
ST_error_level = 2;
193+
} catch (e) {
194+
console.error("履歴保存エラー", e);
195+
ST_error_level = 3;
196+
}
197+
}
198+
}, 300);
199+
}
200+
201+
function saveAudio(filename = "録音音声.webm") {
202+
if (!audioBlob) {
203+
showAlert("保存できる録音データがありません");
204+
ST_error_level = 4;
205+
return;
206+
}
207+
const url = URL.createObjectURL(audioBlob);
208+
const a = document.createElement("a");
209+
a.href = url;
210+
a.download = filename;
211+
a.click();
212+
URL.revokeObjectURL(url);
213+
}
214+
215+
function saveText(filename = "文字起こし.txt") {
216+
const blob = new Blob([finalTranscript], { type: "text/plain" });
217+
const url = URL.createObjectURL(blob);
218+
const a = document.createElement("a");
219+
a.href = url;
220+
a.download = filename;
221+
a.click();
222+
URL.revokeObjectURL(url);
223+
}
224+
225+
function playAudio() {
226+
if (!audioBlob) {
227+
showAlert("再生できる音声がありません");
228+
ST_error_level = 5;
229+
return;
230+
}
231+
const audio = new Audio(URL.createObjectURL(audioBlob));
232+
audio.play();
233+
}
234+
235+
function setAlertEnabled(enabled) {
236+
ST_alert_enabled = !!enabled;
237+
}
238+
function getAlertEnabled() {
239+
return ST_alert_enabled;
240+
}
241+
function showAlert(msg) {
242+
if (ST_alert_enabled) alert(msg);
243+
}
244+
245+
async function stopAndSaveAudio() {
246+
await stopAll();
247+
saveAudio();
248+
}
249+
250+
async function stopAndSaveText() {
251+
await stopAll();
252+
saveText();
253+
}
254+
255+
return {
256+
start: () => {
257+
finalTranscript = "";
258+
return startRecording().then(() => startRecognition());
259+
},
260+
stop: stopAll,
261+
stopAndSaveAudio,
262+
stopAndSaveText,
263+
saveAudio,
264+
saveText,
265+
playAudio,
266+
onText: callback => (textCallback = callback),
267+
getFinalText: () => finalTranscript,
268+
setFinalText: text => (finalTranscript = text),
269+
getRecordingTime,
270+
getVolume: () => volumeLevel,
271+
onVolume: callback => (volumeCallback = callback),
272+
addSession,
273+
getSessions,
274+
deleteSession,
275+
playSessionAudio,
276+
getSessionText,
277+
setAlertEnabled,
278+
getAlertEnabled,
279+
};
280+
})();
281+

0 commit comments

Comments
 (0)