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:
@@ -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">✓ 완료</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user