fix:화자분리 SentencePiece 토큰 깨짐 수정
- ▁(U+2581) 문자를 _(U+005F)와 별도로 처리 - SentencePiece 토큰 결합 로직 추가 (joinSentencePieceTokens) - ▁로 시작하는 토큰: 새 단어 → 공백 추가 - ▁없는 토큰: 이전 단어에 직접 붙임 - cleanSttText에서 ▁→공백 변환 추가 - 프론트엔드에서도 ▁ 문자 정제 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -432,9 +432,10 @@ private function parseDiarizationResult(array $operationResult): ?array
|
||||
}
|
||||
|
||||
// word-level 화자 정보를 세그먼트로 그룹핑
|
||||
// Google STT의 SentencePiece 토크나이저: ▁(U+2581)는 새 단어 시작 표시
|
||||
$segments = [];
|
||||
$currentSpeaker = null;
|
||||
$currentWords = [];
|
||||
$currentTokens = [];
|
||||
$segmentStartMs = 0;
|
||||
|
||||
foreach ($words as $word) {
|
||||
@@ -443,36 +444,39 @@ private function parseDiarizationResult(array $operationResult): ?array
|
||||
$startMs = $this->parseGoogleTimeToMs($word['startTime'] ?? '0s');
|
||||
$endMs = $this->parseGoogleTimeToMs($word['endTime'] ?? '0s');
|
||||
|
||||
// 언더스코어 노이즈 제거 (단어 앞뒤/내부 모두)
|
||||
$cleanWord = str_replace('_', '', $wordText);
|
||||
if (trim($cleanWord) === '') {
|
||||
// SentencePiece: ▁(U+2581) 또는 _로 시작하면 새 단어
|
||||
$isNewWord = preg_match('/^[\x{2581}_]/u', $wordText);
|
||||
|
||||
// 모든 구분자 문자 제거: _(U+005F), ▁(U+2581)
|
||||
$cleanToken = preg_replace('/[\x{2581}_]/u', '', $wordText);
|
||||
if (trim($cleanToken) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($speakerTag !== $currentSpeaker && $currentSpeaker !== null && ! empty($currentWords)) {
|
||||
if ($speakerTag !== $currentSpeaker && $currentSpeaker !== null && ! empty($currentTokens)) {
|
||||
$segments[] = [
|
||||
'speaker_name' => '화자 ' . $currentSpeaker,
|
||||
'speaker_label' => (string) $currentSpeaker,
|
||||
'text' => $this->cleanSttText(implode(' ', $currentWords)),
|
||||
'text' => $this->joinSentencePieceTokens($currentTokens),
|
||||
'start_time_ms' => $segmentStartMs,
|
||||
'end_time_ms' => $startMs,
|
||||
'is_manual_speaker' => false,
|
||||
];
|
||||
$currentWords = [];
|
||||
$currentTokens = [];
|
||||
$segmentStartMs = $startMs;
|
||||
}
|
||||
|
||||
$currentSpeaker = $speakerTag;
|
||||
$currentWords[] = $cleanWord;
|
||||
$currentTokens[] = ['text' => $cleanToken, 'new_word' => (bool) $isNewWord];
|
||||
}
|
||||
|
||||
// 마지막 세그먼트
|
||||
if (! empty($currentWords)) {
|
||||
if (! empty($currentTokens)) {
|
||||
$lastWord = end($words);
|
||||
$segments[] = [
|
||||
'speaker_name' => '화자 ' . $currentSpeaker,
|
||||
'speaker_label' => (string) $currentSpeaker,
|
||||
'text' => $this->cleanSttText(implode(' ', $currentWords)),
|
||||
'text' => $this->joinSentencePieceTokens($currentTokens),
|
||||
'start_time_ms' => $segmentStartMs,
|
||||
'end_time_ms' => $this->parseGoogleTimeToMs($lastWord['endTime'] ?? '0s'),
|
||||
'is_manual_speaker' => false,
|
||||
@@ -508,12 +512,35 @@ private function parseGoogleTimeToMs(string $timeStr): int
|
||||
}
|
||||
|
||||
/**
|
||||
* STT 텍스트에서 언더스코어 노이즈 제거
|
||||
* SentencePiece 토큰 배열을 자연스러운 텍스트로 결합
|
||||
*
|
||||
* ▁(U+2581)가 있던 토큰은 새 단어 시작 → 앞에 공백 추가
|
||||
* ▁가 없던 토큰은 이전 단어에 바로 붙임
|
||||
*/
|
||||
private function joinSentencePieceTokens(array $tokens): string
|
||||
{
|
||||
$result = '';
|
||||
foreach ($tokens as $i => $token) {
|
||||
if ($i === 0) {
|
||||
$result = $token['text'];
|
||||
} elseif ($token['new_word']) {
|
||||
$result .= ' ' . $token['text'];
|
||||
} else {
|
||||
$result .= $token['text'];
|
||||
}
|
||||
}
|
||||
|
||||
return trim(preg_replace('/\s{2,}/', ' ', $result));
|
||||
}
|
||||
|
||||
/**
|
||||
* STT 텍스트에서 SentencePiece/언더스코어 노이즈 제거
|
||||
*/
|
||||
private function cleanSttText(string $text): string
|
||||
{
|
||||
// 언더스코어 제거 후 연속 공백 정리
|
||||
$cleaned = str_replace('_', '', $text);
|
||||
// ▁(U+2581)를 공백으로, _(U+005F)는 제거, 연속 공백 정리
|
||||
$cleaned = preg_replace('/\x{2581}/u', ' ', $text);
|
||||
$cleaned = str_replace('_', '', $cleaned);
|
||||
|
||||
return trim(preg_replace('/\s{2,}/', ' ', $cleaned));
|
||||
}
|
||||
|
||||
@@ -947,7 +947,7 @@ className="w-full text-sm text-gray-800 leading-relaxed bg-white/70 border borde
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-800 leading-relaxed">
|
||||
{group.texts.map((t, ti) => <span key={ti}>{ti > 0 ? ' ' : ''}{t.text.replace(/_/g, '').replace(/\s{2,}/g, ' ').trim()}</span>)}
|
||||
{group.texts.map((t, ti) => <span key={ti}>{ti > 0 ? ' ' : ''}{t.text.replace(/[_\u2581]/g, ' ').replace(/\s{2,}/g, ' ').trim()}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -979,7 +979,7 @@ function ScriptView({ segments, interimText, isRecording }) {
|
||||
return (
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<div className="text-sm text-gray-800 leading-relaxed whitespace-pre-wrap">
|
||||
{segments.map((s, i) => <span key={i}>{s.text.replace(/_/g, '').replace(/\s{2,}/g, ' ').trim()}{i < segments.length - 1 ? ' ' : ''}</span>)}
|
||||
{segments.map((s, i) => <span key={i}>{s.text.replace(/[_\u2581]/g, ' ').replace(/\s{2,}/g, ' ').trim()}{i < segments.length - 1 ? ' ' : ''}</span>)}
|
||||
{isRecording && interimText && <span className="text-gray-400 italic"> {interimText}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user