refactor:영업/매니저 시나리오 음성 인식 STT 개선

- onresult에서 event.resultIndex부터 순회 (중복 처리 방지)
- finalizedSegments[] 배열로 확정 텍스트 영구 관리
- 다크 프리뷰 패널(bg-gray-900)로 UI 통일
- 확정=흰색 일반체, 미확정=회색 이탤릭 스타일 적용
- 고정 line-height(1.6)으로 텍스트 전환 시 흔들림 방지
- 인식 중/완료 상태 표시 추가
- 공사현장 사진대지 VoiceInputButton과 동일 규칙 적용

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-10 09:19:14 +09:00
parent 8b0f78f364
commit 16309c5f61

View File

@@ -14,6 +14,7 @@
timer: 0,
transcript: '',
interimTranscript: '',
finalizedSegments: [],
status: '마이크 버튼을 눌러 녹음을 시작하세요',
saving: false,
saveProgress: 0,
@@ -127,27 +128,35 @@
this.transcript = '';
this.interimTranscript = '';
this.finalizedSegments = [];
let confirmedResults = [];
// 규칙: interim=이탤릭+회색(교정 허용), final=일반체+진한색(삭제 불가)
this.recognition.onresult = (event) => {
let interimTranscript = '';
let currentInterim = '';
for (let i = 0; i < event.results.length; i++) {
const result = event.results[i];
const text = result[0].transcript;
for (let i = event.resultIndex; i < event.results.length; i++) {
const text = event.results[i][0].transcript;
if (result.isFinal) {
if (!confirmedResults[i]) {
confirmedResults[i] = text;
}
if (event.results[i].isFinal) {
// 확정: finalizedSegments에 영구 저장 (삭제 불가)
this.finalizedSegments.push(text);
currentInterim = '';
} else {
interimTranscript += text;
// 미확정: 교정은 허용하되 이전 확정분은 보존
currentInterim = text;
}
}
this.transcript = confirmedResults.filter(Boolean).join(' ');
this.interimTranscript = interimTranscript;
// transcript 합산 (서버 저장용)
this.transcript = this.finalizedSegments.join(' ');
this.interimTranscript = currentInterim;
// 자동 스크롤
this.$nextTick(() => {
if (this.$refs.transcriptContainer) {
this.$refs.transcriptContainer.scrollTop = this.$refs.transcriptContainer.scrollHeight;
}
});
};
this.recognition.onerror = (event) => {
@@ -287,6 +296,7 @@
this.timer = 0;
this.transcript = '';
this.interimTranscript = '';
this.finalizedSegments = [];
this.saving = false;
this.saveProgress = 0;
this.status = '마이크 버튼을 눌러 녹음을 시작하세요';
@@ -373,17 +383,37 @@ class="w-full h-20 bg-gray-50 rounded-lg border border-gray-200"
</div>
</div>
{{-- 실시간 텍스트 변환 표시 --}}
<div x-show="transcript || interimTranscript" class="bg-gray-50 rounded-lg border border-gray-200">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200">
<p class="text-xs font-medium text-gray-500">음성 인식 결과</p>
<p class="text-xs text-gray-400" x-text="transcript.length + ' 자'"></p>
{{-- 실시간 텍스트 변환 표시 (interim=이탤릭+회색, final=일반체+진한색) --}}
<div x-show="finalizedSegments.length > 0 || interimTranscript" class="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-700">
<div class="flex items-center gap-2">
<p class="text-xs font-medium text-gray-400">음성 인식 결과</p>
<template x-if="isRecording">
<span class="flex items-center gap-1 text-xs text-red-400">
<span class="w-1.5 h-1.5 bg-red-400 rounded-full animate-pulse"></span>
인식
</span>
</template>
<template x-if="!isRecording && finalizedSegments.length > 0 && !interimTranscript">
<span class="text-green-400 text-xs">&#10003; 완료</span>
</template>
</div>
<p class="text-xs text-gray-500" x-text="transcript.length + ' 자'"></p>
</div>
<div class="p-3 max-h-32 overflow-y-auto" x-ref="transcriptContainer" x-effect="if(transcript || interimTranscript) { $nextTick(() => $refs.transcriptContainer.scrollTop = $refs.transcriptContainer.scrollHeight) }">
<p class="text-sm text-gray-700 whitespace-pre-wrap leading-relaxed">
<span x-text="transcript"></span>
<span class="text-gray-400 italic" x-text="interimTranscript"></span>
</p>
<div class="p-3 max-h-32 overflow-y-auto" x-ref="transcriptContainer" style="line-height: 1.6;">
{{-- 확정 텍스트: 일반체 + 흰색 (삭제 불가) --}}
<template x-for="(seg, i) in finalizedSegments" :key="i">
<span class="text-white text-sm font-normal transition-colors duration-300" x-text="seg"></span>
</template>
{{-- 미확정 텍스트: 이탤릭 + 연한 회색 (교정 가능) --}}
<span x-show="interimTranscript" class="text-gray-400 text-sm italic transition-colors duration-200" x-text="interimTranscript"></span>
{{-- 녹음 + 텍스트 없음: 대기 표시 --}}
<span x-show="isRecording && finalizedSegments.length === 0 && !interimTranscript" class="text-gray-500 text-sm flex items-center gap-1.5">
<span class="inline-block w-1.5 h-1.5 bg-red-400 rounded-full animate-pulse"></span>
말씀하세요...
</span>
</div>
</div>