@dssp/supervision 1.0.0-alpha.52 → 1.0.0-alpha.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.d.ts +2 -0
- package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.js +43 -3
- package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.js.map +1 -1
- package/dist-client/pages/building-inspection-grid/building-inspection-grid-detail.d.ts +1 -1
- package/dist-client/pages/checklist/checklist-view.js +14 -5
- package/dist-client/pages/checklist/checklist-view.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/stt/speech-to-text.d.ts +13 -8
- package/dist-client/stt/speech-to-text.js +282 -40
- package/dist-client/stt/speech-to-text.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { __decorate } from "tslib";
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import { LitElement } from 'lit';
|
|
3
|
-
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
4
4
|
/**
|
|
5
5
|
* SpeechToText (음성 인식) 웹 컴포넌트
|
|
6
6
|
*
|
|
7
7
|
* - 음성 인식 시작/중지 메서드 제공
|
|
8
8
|
* - 인식 결과/에러/상태를 커스텀 이벤트(stt-result, stt-error, stt-status)로 외부에 전달
|
|
9
9
|
* - 버튼 등 UI는 포함하지 않음 (컨트롤은 외부에서)
|
|
10
|
+
* - Google Cloud Speech API 사용
|
|
10
11
|
*
|
|
11
12
|
* 사용 예시:
|
|
12
|
-
* <speech-to-text id="stt"></speech-to-text>
|
|
13
|
+
* <speech-to-text id="stt" api-key="YOUR_API_KEY"></speech-to-text>
|
|
13
14
|
*
|
|
14
15
|
* // JS에서
|
|
15
16
|
* const stt = document.getElementById('stt') as SpeechToText;
|
|
@@ -22,66 +23,307 @@ import { customElement } from 'lit/decorators.js';
|
|
|
22
23
|
let SpeechToText = class SpeechToText extends LitElement {
|
|
23
24
|
constructor() {
|
|
24
25
|
super(...arguments);
|
|
25
|
-
this.
|
|
26
|
+
this.apiKey = 'AIzaSyCH3FPpY5ZCpcfUMjePH2Nhtueu6chgBDk';
|
|
27
|
+
this.mediaRecorder = null;
|
|
28
|
+
this.audioChunks = [];
|
|
29
|
+
this.isRecording = false;
|
|
30
|
+
this.stream = null;
|
|
31
|
+
this.isProcessing = false;
|
|
26
32
|
}
|
|
27
33
|
/**
|
|
28
34
|
* 음성 인식 시작
|
|
29
35
|
*/
|
|
30
|
-
startListening() {
|
|
31
|
-
if (!
|
|
32
|
-
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
36
|
+
async startListening() {
|
|
37
|
+
if (!this.apiKey) {
|
|
38
|
+
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
39
|
+
detail: { error: 'Google Cloud API key가 설정되지 않았습니다.' }
|
|
40
|
+
}));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
44
|
+
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
45
|
+
detail: { error: '지원되지 않는 브라우저입니다.' }
|
|
46
|
+
}));
|
|
33
47
|
return;
|
|
34
48
|
}
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
if (this.isRecording || this.isProcessing) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// 이전 상태 완전 정리
|
|
53
|
+
this.cleanup();
|
|
54
|
+
try {
|
|
55
|
+
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
56
|
+
audio: {
|
|
57
|
+
echoCancellation: true,
|
|
58
|
+
noiseSuppression: true,
|
|
59
|
+
autoGainControl: true,
|
|
60
|
+
sampleRate: 48000
|
|
45
61
|
}
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
});
|
|
63
|
+
let mimeType = 'audio/webm;codecs=opus';
|
|
64
|
+
if (!MediaRecorder.isTypeSupported(mimeType)) {
|
|
65
|
+
mimeType = 'audio/webm';
|
|
66
|
+
if (!MediaRecorder.isTypeSupported(mimeType)) {
|
|
67
|
+
if (MediaRecorder.isTypeSupported('audio/mp4')) {
|
|
68
|
+
mimeType = 'audio/mp4';
|
|
69
|
+
}
|
|
70
|
+
else if (MediaRecorder.isTypeSupported('audio/wav')) {
|
|
71
|
+
mimeType = 'audio/wav';
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
mimeType = '';
|
|
75
|
+
}
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
const recorderOptions = mimeType ? { mimeType } : {};
|
|
79
|
+
this.mediaRecorder = new MediaRecorder(this.stream, recorderOptions);
|
|
80
|
+
this.audioChunks = [];
|
|
81
|
+
this.isRecording = true;
|
|
82
|
+
this.mediaRecorder.ondataavailable = event => {
|
|
83
|
+
if (event.data.size > 0) {
|
|
84
|
+
this.audioChunks.push(event.data);
|
|
54
85
|
}
|
|
55
|
-
}
|
|
86
|
+
};
|
|
87
|
+
this.mediaRecorder.onstop = async () => {
|
|
88
|
+
const audioBlob = new Blob(this.audioChunks, { type: mimeType || 'audio/webm' });
|
|
89
|
+
if (audioBlob.size > 5000) {
|
|
90
|
+
await this.processAudio(audioBlob);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
94
|
+
detail: { error: '녹음이 너무 짧습니다.' }
|
|
95
|
+
}));
|
|
96
|
+
// 녹음이 짧을 때도 완료 처리
|
|
97
|
+
this.finishRecording();
|
|
98
|
+
}
|
|
99
|
+
this.cleanup();
|
|
100
|
+
};
|
|
101
|
+
this.mediaRecorder.onerror = () => {
|
|
102
|
+
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
103
|
+
detail: { error: '녹음 오류 발생' }
|
|
104
|
+
}));
|
|
105
|
+
this.isRecording = false;
|
|
106
|
+
};
|
|
107
|
+
this.mediaRecorder.start();
|
|
56
108
|
this.dispatchEvent(new CustomEvent('stt-status', {
|
|
57
|
-
detail: { status:
|
|
109
|
+
detail: { status: '🎙️ Listening...' }
|
|
58
110
|
}));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
}
|
|
66
|
-
this.recognition.onstart = () => {
|
|
67
|
-
this.dispatchEvent(new CustomEvent('stt-status', { detail: { status: '🎙️ Listening...' } }));
|
|
68
|
-
};
|
|
69
|
-
this.recognition.start();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
114
|
+
detail: { error: `마이크 접근 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}` }
|
|
115
|
+
}));
|
|
116
|
+
this.isRecording = false;
|
|
117
|
+
}
|
|
70
118
|
}
|
|
71
119
|
/**
|
|
72
120
|
* 음성 인식 중지
|
|
73
121
|
*/
|
|
74
122
|
stopListening() {
|
|
75
|
-
if (this.
|
|
76
|
-
this.
|
|
77
|
-
|
|
123
|
+
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
124
|
+
this.mediaRecorder.stop();
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// MediaRecorder가 없거나 이미 중지된 경우 즉시 정리
|
|
128
|
+
this.cleanup();
|
|
129
|
+
this.dispatchEvent(new CustomEvent('stt-status', {
|
|
130
|
+
detail: { status: '음성 인식 종료' }
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async processAudio(audioBlob) {
|
|
135
|
+
this.isProcessing = true;
|
|
136
|
+
this.dispatchEvent(new CustomEvent('stt-status', {
|
|
137
|
+
detail: { status: '🔄 음성을 텍스트로 변환 중...' }
|
|
138
|
+
}));
|
|
139
|
+
try {
|
|
140
|
+
const reader = new FileReader();
|
|
141
|
+
reader.readAsDataURL(audioBlob);
|
|
142
|
+
reader.onloadend = async () => {
|
|
143
|
+
const base64Audio = reader.result.split(',')[1];
|
|
144
|
+
await this.sendToGoogleSpeech(base64Audio);
|
|
145
|
+
};
|
|
146
|
+
reader.onerror = () => {
|
|
147
|
+
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
148
|
+
detail: { error: '오디오 처리 실패' }
|
|
149
|
+
}));
|
|
150
|
+
this.finishRecording();
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
155
|
+
detail: { error: '처리 중 오류 발생' }
|
|
156
|
+
}));
|
|
157
|
+
this.finishRecording();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async sendToGoogleSpeech(base64Audio) {
|
|
161
|
+
let encoding = 'WEBM_OPUS';
|
|
162
|
+
let sampleRate = 48000;
|
|
163
|
+
if (this.mediaRecorder && this.mediaRecorder.mimeType) {
|
|
164
|
+
const mimeType = this.mediaRecorder.mimeType.toLowerCase();
|
|
165
|
+
if (mimeType.includes('opus')) {
|
|
166
|
+
encoding = 'WEBM_OPUS';
|
|
167
|
+
}
|
|
168
|
+
else if (mimeType.includes('webm')) {
|
|
169
|
+
encoding = 'WEBM_OPUS';
|
|
170
|
+
}
|
|
171
|
+
else if (mimeType.includes('mp4')) {
|
|
172
|
+
encoding = 'MP3';
|
|
173
|
+
}
|
|
174
|
+
else if (mimeType.includes('wav')) {
|
|
175
|
+
encoding = 'LINEAR16';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const requestBody = {
|
|
179
|
+
config: {
|
|
180
|
+
encoding: encoding,
|
|
181
|
+
sampleRateHertz: sampleRate,
|
|
182
|
+
languageCode: 'ko-KR',
|
|
183
|
+
enableAutomaticPunctuation: true,
|
|
184
|
+
model: 'latest_long',
|
|
185
|
+
useEnhanced: true,
|
|
186
|
+
enableWordTimeOffsets: false,
|
|
187
|
+
enableWordConfidence: false,
|
|
188
|
+
maxAlternatives: 1,
|
|
189
|
+
speechContexts: [
|
|
190
|
+
{
|
|
191
|
+
phrases: [
|
|
192
|
+
'1번',
|
|
193
|
+
'2번',
|
|
194
|
+
'3번',
|
|
195
|
+
'4번',
|
|
196
|
+
'5번',
|
|
197
|
+
'6번',
|
|
198
|
+
'7번',
|
|
199
|
+
'합격',
|
|
200
|
+
'불합격',
|
|
201
|
+
'적합',
|
|
202
|
+
'부적합',
|
|
203
|
+
'기본업무',
|
|
204
|
+
'시공',
|
|
205
|
+
'자재',
|
|
206
|
+
'도서',
|
|
207
|
+
'모두',
|
|
208
|
+
'전체',
|
|
209
|
+
'초기화',
|
|
210
|
+
'조치사항',
|
|
211
|
+
'확인 필요',
|
|
212
|
+
'기본 외 업무',
|
|
213
|
+
'부터',
|
|
214
|
+
'까지'
|
|
215
|
+
],
|
|
216
|
+
boost: 20
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
audio: {
|
|
221
|
+
content: base64Audio
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
try {
|
|
225
|
+
const response = await fetch(`https://speech.googleapis.com/v1/speech:recognize?key=${this.apiKey}`, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: {
|
|
228
|
+
'Content-Type': 'application/json'
|
|
229
|
+
},
|
|
230
|
+
body: JSON.stringify(requestBody)
|
|
231
|
+
});
|
|
232
|
+
if (response.ok) {
|
|
233
|
+
const data = await response.json();
|
|
234
|
+
if (data.results && data.results.length > 0) {
|
|
235
|
+
const finalTranscript = data.results.map((result) => result.alternatives[0].transcript).join(' ');
|
|
236
|
+
this.dispatchEvent(new CustomEvent('stt-result', {
|
|
237
|
+
detail: {
|
|
238
|
+
finalTranscript: finalTranscript.trim(),
|
|
239
|
+
interimTranscript: ''
|
|
240
|
+
}
|
|
241
|
+
}));
|
|
242
|
+
this.dispatchEvent(new CustomEvent('stt-status', {
|
|
243
|
+
detail: { status: `📝 Recognized: ${finalTranscript}` }
|
|
244
|
+
}));
|
|
245
|
+
// 음성 인식 완료 후 즉시 완료 처리
|
|
246
|
+
this.finishRecording();
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
this.dispatchEvent(new CustomEvent('stt-result', {
|
|
250
|
+
detail: {
|
|
251
|
+
finalTranscript: '',
|
|
252
|
+
interimTranscript: ''
|
|
253
|
+
}
|
|
254
|
+
}));
|
|
255
|
+
this.dispatchEvent(new CustomEvent('stt-status', {
|
|
256
|
+
detail: { status: '음성을 인식하지 못했습니다.' }
|
|
257
|
+
}));
|
|
258
|
+
// 인식 실패 시에도 즉시 완료 처리
|
|
259
|
+
this.finishRecording();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const error = await response.text();
|
|
264
|
+
let errorMessage = 'API 오류 발생';
|
|
265
|
+
try {
|
|
266
|
+
const errorObj = JSON.parse(error);
|
|
267
|
+
if (errorObj.error && errorObj.error.message) {
|
|
268
|
+
errorMessage = errorObj.error.message;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
errorMessage = `HTTP ${response.status}`;
|
|
273
|
+
}
|
|
274
|
+
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
275
|
+
detail: { error: errorMessage }
|
|
276
|
+
}));
|
|
277
|
+
// API 오류 시에도 즉시 완료 처리
|
|
278
|
+
this.finishRecording();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
this.dispatchEvent(new CustomEvent('stt-error', {
|
|
283
|
+
detail: { error: `네트워크 오류: ${error instanceof Error ? error.message : '알 수 없는 오류'}` }
|
|
284
|
+
}));
|
|
285
|
+
// 네트워크 오류 시에도 즉시 완료 처리
|
|
286
|
+
this.finishRecording();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
finishRecording() {
|
|
290
|
+
this.cleanup();
|
|
291
|
+
this.dispatchEvent(new CustomEvent('stt-status', {
|
|
292
|
+
detail: { status: '음성 인식 종료' }
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
cleanup() {
|
|
296
|
+
// 모든 상태 완전 초기화
|
|
297
|
+
if (this.stream) {
|
|
298
|
+
this.stream.getTracks().forEach(track => {
|
|
299
|
+
track.stop();
|
|
300
|
+
track.enabled = false;
|
|
301
|
+
});
|
|
302
|
+
this.stream = null;
|
|
303
|
+
}
|
|
304
|
+
if (this.mediaRecorder) {
|
|
305
|
+
try {
|
|
306
|
+
if (this.mediaRecorder.state !== 'inactive') {
|
|
307
|
+
this.mediaRecorder.stop();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
// MediaRecorder stop 에러 무시
|
|
312
|
+
}
|
|
313
|
+
this.mediaRecorder = null;
|
|
78
314
|
}
|
|
315
|
+
this.audioChunks = [];
|
|
316
|
+
this.isRecording = false;
|
|
317
|
+
this.isProcessing = false;
|
|
79
318
|
}
|
|
80
|
-
// UI 없음 (렌더링 X)
|
|
81
319
|
render() {
|
|
82
320
|
return null;
|
|
83
321
|
}
|
|
84
322
|
};
|
|
323
|
+
__decorate([
|
|
324
|
+
property({ type: String, attribute: 'api-key' }),
|
|
325
|
+
__metadata("design:type", String)
|
|
326
|
+
], SpeechToText.prototype, "apiKey", void 0);
|
|
85
327
|
SpeechToText = __decorate([
|
|
86
328
|
customElement('speech-to-text')
|
|
87
329
|
], SpeechToText);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"speech-to-text.js","sourceRoot":"","sources":["../../client/stt/speech-to-text.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement } from 'lit'\nimport { customElement } from 'lit/decorators.js'\n\n/**\n * SpeechToText (음성 인식) 웹 컴포넌트\n *\n * - 음성 인식 시작/중지 메서드 제공\n * - 인식 결과/에러/상태를 커스텀 이벤트(stt-result, stt-error, stt-status)로 외부에 전달\n * - 버튼 등 UI는 포함하지 않음 (컨트롤은 외부에서)\n *\n * 사용 예시:\n * <speech-to-text id=\"stt\"></speech-to-text>\n *\n * // JS에서\n * const stt = document.getElementById('stt') as SpeechToText;\n * stt.startListening();\n * stt.stopListening();\n * stt.addEventListener('stt-result', e => { ... });\n * stt.addEventListener('stt-error', e => { ... });\n * stt.addEventListener('stt-status', e => { ... });\n */\n@customElement('speech-to-text')\nexport class SpeechToText extends LitElement {\n private recognition: any = null\n\n /**\n * 음성 인식 시작\n */\n public startListening() {\n if (!('webkitSpeechRecognition' in window)) {\n this.dispatchEvent(new CustomEvent('stt-error', { detail: { error: '지원되지 않는 브라우저입니다.' } }))\n return\n }\n this.recognition = new window.webkitSpeechRecognition()\n this.recognition.lang = 'ko-KR'\n this.recognition.continuous = false\n this.recognition.interimResults = true\n\n this.recognition.onresult = (event: any) => {\n let finalTranscript = ''\n let interimTranscript = ''\n for (let i = event.resultIndex; i < event.results.length; i++) {\n if (event.results[i].isFinal) {\n finalTranscript += event.results[i][0].transcript\n } else {\n interimTranscript += event.results[i][0].transcript\n }\n }\n this.dispatchEvent(\n new CustomEvent('stt-result', {\n detail: {\n finalTranscript: finalTranscript.trim(),\n interimTranscript: interimTranscript.trim()\n }\n })\n )\n this.dispatchEvent(\n new CustomEvent('stt-status', {\n detail: { status: `📝 Recognized: ${finalTranscript} ${interimTranscript}` }\n })\n )\n }\n\n this.recognition.onerror = (event: any) => {\n this.dispatchEvent(new CustomEvent('stt-error', { detail: { error: event.error } }))\n }\n\n this.recognition.onend = () => {\n this.dispatchEvent(new CustomEvent('stt-status', { detail: { status: '음성 인식 종료' } }))\n }\n\n this.recognition.onstart = () => {\n this.dispatchEvent(new CustomEvent('stt-status', { detail: { status: '🎙️ Listening...' } }))\n }\n\n this.recognition.start()\n }\n\n /**\n * 음성 인식 중지\n */\n public stopListening() {\n if (this.recognition) {\n this.recognition.stop()\n this.dispatchEvent(new CustomEvent('stt-status', { detail: { status: '🛑 Stopped listening.' } }))\n }\n }\n\n // UI 없음 (렌더링 X)\n protected render() {\n return null\n }\n}\n\n// 타입 정의 (window에 webkitSpeechRecognition이 있는 경우)\ndeclare global {\n interface Window {\n webkitSpeechRecognition?: any\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"speech-to-text.js","sourceRoot":"","sources":["../../client/stt/speech-to-text.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n/**\n * SpeechToText (음성 인식) 웹 컴포넌트\n *\n * - 음성 인식 시작/중지 메서드 제공\n * - 인식 결과/에러/상태를 커스텀 이벤트(stt-result, stt-error, stt-status)로 외부에 전달\n * - 버튼 등 UI는 포함하지 않음 (컨트롤은 외부에서)\n * - Google Cloud Speech API 사용\n *\n * 사용 예시:\n * <speech-to-text id=\"stt\" api-key=\"YOUR_API_KEY\"></speech-to-text>\n *\n * // JS에서\n * const stt = document.getElementById('stt') as SpeechToText;\n * stt.startListening();\n * stt.stopListening();\n * stt.addEventListener('stt-result', e => { ... });\n * stt.addEventListener('stt-error', e => { ... });\n * stt.addEventListener('stt-status', e => { ... });\n */\n@customElement('speech-to-text')\nexport class SpeechToText extends LitElement {\n @property({ type: String, attribute: 'api-key' })\n apiKey: string = 'AIzaSyCH3FPpY5ZCpcfUMjePH2Nhtueu6chgBDk'\n\n private mediaRecorder: MediaRecorder | null = null\n private audioChunks: Blob[] = []\n private isRecording: boolean = false\n private stream: MediaStream | null = null\n private isProcessing: boolean = false\n\n /**\n * 음성 인식 시작\n */\n public async startListening() {\n if (!this.apiKey) {\n this.dispatchEvent(\n new CustomEvent('stt-error', {\n detail: { error: 'Google Cloud API key가 설정되지 않았습니다.' }\n })\n )\n return\n }\n\n if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {\n this.dispatchEvent(\n new CustomEvent('stt-error', {\n detail: { error: '지원되지 않는 브라우저입니다.' }\n })\n )\n return\n }\n\n if (this.isRecording || this.isProcessing) {\n return\n }\n\n // 이전 상태 완전 정리\n this.cleanup()\n\n try {\n this.stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n sampleRate: 48000\n }\n })\n\n let mimeType = 'audio/webm;codecs=opus'\n if (!MediaRecorder.isTypeSupported(mimeType)) {\n mimeType = 'audio/webm'\n if (!MediaRecorder.isTypeSupported(mimeType)) {\n if (MediaRecorder.isTypeSupported('audio/mp4')) {\n mimeType = 'audio/mp4'\n } else if (MediaRecorder.isTypeSupported('audio/wav')) {\n mimeType = 'audio/wav'\n } else {\n mimeType = ''\n }\n }\n }\n\n const recorderOptions = mimeType ? { mimeType } : {}\n this.mediaRecorder = new MediaRecorder(this.stream, recorderOptions)\n\n this.audioChunks = []\n this.isRecording = true\n\n this.mediaRecorder.ondataavailable = event => {\n if (event.data.size > 0) {\n this.audioChunks.push(event.data)\n }\n }\n\n this.mediaRecorder.onstop = async () => {\n const audioBlob = new Blob(this.audioChunks, { type: mimeType || 'audio/webm' })\n\n if (audioBlob.size > 5000) {\n await this.processAudio(audioBlob)\n } else {\n this.dispatchEvent(\n new CustomEvent('stt-error', {\n detail: { error: '녹음이 너무 짧습니다.' }\n })\n )\n // 녹음이 짧을 때도 완료 처리\n this.finishRecording()\n }\n\n this.cleanup()\n }\n\n this.mediaRecorder.onerror = () => {\n this.dispatchEvent(\n new CustomEvent('stt-error', {\n detail: { error: '녹음 오류 발생' }\n })\n )\n this.isRecording = false\n }\n\n this.mediaRecorder.start()\n this.dispatchEvent(\n new CustomEvent('stt-status', {\n detail: { status: '🎙️ Listening...' }\n })\n )\n } catch (error) {\n this.dispatchEvent(\n new CustomEvent('stt-error', {\n detail: { error: `마이크 접근 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}` }\n })\n )\n this.isRecording = false\n }\n }\n\n /**\n * 음성 인식 중지\n */\n public stopListening() {\n if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {\n this.mediaRecorder.stop()\n } else {\n // MediaRecorder가 없거나 이미 중지된 경우 즉시 정리\n this.cleanup()\n this.dispatchEvent(\n new CustomEvent('stt-status', {\n detail: { status: '음성 인식 종료' }\n })\n )\n }\n }\n\n private async processAudio(audioBlob: Blob): Promise<void> {\n this.isProcessing = true\n\n this.dispatchEvent(\n new CustomEvent('stt-status', {\n detail: { status: '🔄 음성을 텍스트로 변환 중...' }\n })\n )\n\n try {\n const reader = new FileReader()\n reader.readAsDataURL(audioBlob)\n\n reader.onloadend = async () => {\n const base64Audio = (reader.result as string).split(',')[1]\n await this.sendToGoogleSpeech(base64Audio)\n }\n\n reader.onerror = () => {\n this.dispatchEvent(\n new CustomEvent('stt-error', {\n detail: { error: '오디오 처리 실패' }\n })\n )\n this.finishRecording()\n }\n } catch (error) {\n this.dispatchEvent(\n new CustomEvent('stt-error', {\n detail: { error: '처리 중 오류 발생' }\n })\n )\n this.finishRecording()\n }\n }\n\n private async sendToGoogleSpeech(base64Audio: string): Promise<void> {\n let encoding = 'WEBM_OPUS'\n let sampleRate = 48000\n\n if (this.mediaRecorder && this.mediaRecorder.mimeType) {\n const mimeType = this.mediaRecorder.mimeType.toLowerCase()\n\n if (mimeType.includes('opus')) {\n encoding = 'WEBM_OPUS'\n } else if (mimeType.includes('webm')) {\n encoding = 'WEBM_OPUS'\n } else if (mimeType.includes('mp4')) {\n encoding = 'MP3'\n } else if (mimeType.includes('wav')) {\n encoding = 'LINEAR16'\n }\n }\n\n const requestBody = {\n config: {\n encoding: encoding,\n sampleRateHertz: sampleRate,\n languageCode: 'ko-KR',\n enableAutomaticPunctuation: true,\n model: 'latest_long',\n useEnhanced: true,\n enableWordTimeOffsets: false,\n enableWordConfidence: false,\n maxAlternatives: 1,\n speechContexts: [\n {\n phrases: [\n '1번',\n '2번',\n '3번',\n '4번',\n '5번',\n '6번',\n '7번',\n '합격',\n '불합격',\n '적합',\n '부적합',\n '기본업무',\n '시공',\n '자재',\n '도서',\n '모두',\n '전체',\n '초기화',\n '조치사항',\n '확인 필요',\n '기본 외 업무',\n '부터',\n '까지'\n ],\n boost: 20\n }\n ]\n },\n audio: {\n content: base64Audio\n }\n }\n\n try {\n const response = await fetch(`https://speech.googleapis.com/v1/speech:recognize?key=${this.apiKey}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(requestBody)\n })\n\n if (response.ok) {\n const data = await response.json()\n\n if (data.results && data.results.length > 0) {\n const finalTranscript = data.results.map((result: any) => result.alternatives[0].transcript).join(' ')\n\n this.dispatchEvent(\n new CustomEvent('stt-result', {\n detail: {\n finalTranscript: finalTranscript.trim(),\n interimTranscript: ''\n }\n })\n )\n\n this.dispatchEvent(\n new CustomEvent('stt-status', {\n detail: { status: `📝 Recognized: ${finalTranscript}` }\n })\n )\n\n // 음성 인식 완료 후 즉시 완료 처리\n this.finishRecording()\n } else {\n this.dispatchEvent(\n new CustomEvent('stt-result', {\n detail: {\n finalTranscript: '',\n interimTranscript: ''\n }\n })\n )\n\n this.dispatchEvent(\n new CustomEvent('stt-status', {\n detail: { status: '음성을 인식하지 못했습니다.' }\n })\n )\n\n // 인식 실패 시에도 즉시 완료 처리\n this.finishRecording()\n }\n } else {\n const error = await response.text()\n let errorMessage = 'API 오류 발생'\n\n try {\n const errorObj = JSON.parse(error)\n if (errorObj.error && errorObj.error.message) {\n errorMessage = errorObj.error.message\n }\n } catch (e) {\n errorMessage = `HTTP ${response.status}`\n }\n\n this.dispatchEvent(\n new CustomEvent('stt-error', {\n detail: { error: errorMessage }\n })\n )\n\n // API 오류 시에도 즉시 완료 처리\n this.finishRecording()\n }\n } catch (error) {\n this.dispatchEvent(\n new CustomEvent('stt-error', {\n detail: { error: `네트워크 오류: ${error instanceof Error ? error.message : '알 수 없는 오류'}` }\n })\n )\n\n // 네트워크 오류 시에도 즉시 완료 처리\n this.finishRecording()\n }\n }\n\n private finishRecording() {\n this.cleanup()\n\n this.dispatchEvent(\n new CustomEvent('stt-status', {\n detail: { status: '음성 인식 종료' }\n })\n )\n }\n\n private cleanup() {\n // 모든 상태 완전 초기화\n if (this.stream) {\n this.stream.getTracks().forEach(track => {\n track.stop()\n track.enabled = false\n })\n this.stream = null\n }\n\n if (this.mediaRecorder) {\n try {\n if (this.mediaRecorder.state !== 'inactive') {\n this.mediaRecorder.stop()\n }\n } catch (e) {\n // MediaRecorder stop 에러 무시\n }\n this.mediaRecorder = null\n }\n\n this.audioChunks = []\n this.isRecording = false\n this.isProcessing = false\n }\n\n protected render() {\n return null\n }\n}\n"]}
|