티스토리 뷰
반응형
Web Speech API는 WAV 파일을 넣고 자동적으로 돌아갈 수 없는 시스템이다.
프론트엔드에서 직접적으로 부를 수 있는 API여서 그렇다.
CER이 궁금했던 나는 전에 해봤던 Whisper 파인튜닝 실험과 같이 성능 측정을 시도했다.
방법은 이렇다. 자동화를 위해서 source를 가져와 스피커로 틀고, 그 스피커로 튼 걸 마이크로 인식시킨다.
그렇게 지난번 포스팅과 똑같은 1000개 정도의 샘플 파일을 다음과 같은 코드로 시도했다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Speech API CER 측정</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
</head>
<body>
<h1>Web Speech API CER 측정</h1>
<input type="file" id="csvInput" accept=".csv">
<input type="file" id="folderInput" webkitdirectory directory multiple>
<button id="startButton" disabled>시작</button>
<div id="results"></div>
<script>
let audioFiles = [];
let currentIndex = 0;
let csvData = [];
let cerList = []; // CER 값을 저장하는 배열
let totalCER = 0; // 총 CER 값을 저장
const recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.lang = 'ko-KR';
document.getElementById('csvInput').addEventListener('change', handleCSVFile);
document.getElementById('folderInput').addEventListener('change', handleFolderSelect);
document.getElementById('startButton').addEventListener('click', startProcessing);
function handleCSVFile(event) {
const file = event.target.files[0];
Papa.parse(file, {
complete: function(results) {
csvData = results.data;
console.log("CSV 데이터 로드 완료", csvData);
checkStartButton();
},
header: true,
});
}
function handleFolderSelect(event) {
const files = event.target.files;
const wavFiles = [];
for (let file of files) {
if (file.name.endsWith('.wav')) {
wavFiles.push(file);
}
}
const limitedFiles = wavFiles.slice(0, 1000);
audioFiles = limitedFiles.map(wavFile => {
const matchingRow = csvData.find(row => row.audio_file === wavFile.name);
if (matchingRow) {
return { file: wavFile, script: removeWhitespace(matchingRow.stt_text) };
}
}).filter(Boolean);
checkStartButton();
}
function checkStartButton() {
if (csvData.length > 0 && audioFiles.length > 0) {
document.getElementById('startButton').disabled = false;
}
}
function startProcessing() {
currentIndex = 0;
cerList = []; // 초기화
totalCER = 0; // 초기화
processNextFile();
}
function processNextFile() {
if (currentIndex >= audioFiles.length) {
console.log('모든 파일 처리 완료');
return;
}
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const currentFile = audioFiles[currentIndex];
const reader = new FileReader();
reader.onload = function(e) {
audioContext.decodeAudioData(e.target.result, function(audioBuffer) {
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
recognition.start();
source.start();
source.onended = () => {
};
});
};
reader.readAsArrayBuffer(currentFile.file);
}
recognition.onresult = (event) => {
const result = removeWhitespace(event.results[0][0].transcript);
const currentFile = audioFiles[currentIndex];
const cer = calculateCER(result, currentFile.script);
// CER 값을 배열과 총합에 추가
cerList.push(cer);
totalCER += cer;
// 처리 결과 출력
displayResult(currentFile.file.name, result, currentFile.script, cer);
currentIndex++;
// 10개의 파일이 처리될 때마다 평균 CER 출력
if (currentIndex % 10 === 0) {
const averageCER = totalCER / currentIndex;
displayAverageCER(averageCER);
}
};
recognition.onend = () => {
if (currentIndex < audioFiles.length) {
recognition.stop();
setTimeout(processNextFile, 1000);
}
};
function displayResult(file, sttResult, originalScript, cer) {
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML += `
<p><strong>파일:</strong> ${file}</p>
<p><strong>STT 결과:</strong> ${sttResult}</p>
<p><strong>원본 텍스트:</strong> ${originalScript}</p>
<p><strong>CER:</strong> ${cer}</p>
<hr>`;
}
// 평균 CER 출력
function displayAverageCER(averageCER) {
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML += `
<p><strong>현재까지의 평균 CER (10개마다):</strong> ${averageCER}</p>
<hr>`;
}
function calculateCER(recognized, actual) {
const recognizedLength = recognized.length;
const actualLength = actual.length;
const dp = Array.from(Array(recognizedLength + 1), () => Array(actualLength + 1).fill(0));
for (let i = 0; i <= recognizedLength; i++) dp[i][0] = i;
for (let j = 0; j <= actualLength; j++) dp[0][j] = j;
for (let i = 1; i <= recognizedLength; i++) {
for (let j = 1; j <= actualLength; j++) {
if (recognized[i - 1] === actual[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
dp[i - 1][j - 1] + 1,
dp[i - 1][j] + 1,
dp[i][j - 1] + 1
);
}
}
}
const levenshteinDistance = dp[recognizedLength][actualLength];
return levenshteinDistance / actualLength;
}
function removeWhitespace(text) {
return text.replace(/\s+/g, '');
}
</script>
</body>
</html>
결과는 어떻게 나왔을까?
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07250.wav
STT 결과: 저사람이나한테저렇게행동을하는구나
원본 텍스트: 저사람이나한테저렇게행동을하는구나
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07251.wav
STT 결과: 그러고내가더그사람한테저사람이모자란게무엇이고
원본 텍스트: 그러고내가더그사람한테저사람이모자란게무엇이고
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07252.wav
STT 결과: 저사람이원하는게무엇인가를내가생각해봐야지
원본 텍스트: 저사람이원하는게무엇인가를내가생각해봐야지
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07253.wav
STT 결과: 그렇게사람한테베풀어주면싸울일이없다고생각해
원본 텍스트: 그렇게사람한테베풀어주면싸울일이없다고생각해
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07254.wav
STT 결과: 아무리그렇게잘해서화가났어도내가좀참고
원본 텍스트: 아무리극에달해서화가났어도내가좀참고
CER: 0.2222222222222222
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07255.wav
STT 결과: 아여보내가미안해미안해소리를그말을입에달고
원본 텍스트: 아여보내가미안해미안해소리를그말을입에달고
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07256.wav
STT 결과: 또돌아서면고마워감사해그말이라그마음만
원본 텍스트: 또돌아서면고마워감사해그말이랑그마음만
CER: 0.05263157894736842
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07257.wav
STT 결과: 가지고살면은가정이평화롭다고난생각해
원본 텍스트: 가지고살면은가정이평화롭다고난생각해
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07258.wav
STT 결과: 맞다우리휴일날로망이뭐지
원본 텍스트: 맞다우리휴일날로망이뭐지
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07259.wav
STT 결과: 아침에늦잠자는게휴일날우리의특권인데
원본 텍스트: 아침에늦잠자는게휴일날우리의특권인데
CER: 0
현재까지의 평균 CER (10개마다): 0.027485380116959064
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07260.wav
STT 결과: 사실이그것까지도제대로안되잖아식구들챙기고
원본 텍스트: 사실그거까지도제대로안되잖아식구들챙기고
CER: 0.1
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07261.wav
STT 결과: 아침부터정신없잖아나는이제휴일날은아침에조금
원본 텍스트: 아침부터정신없잖아나는이제휴일날은아침에조금
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07262.wav
STT 결과: 늦게일어나도되고토요일에도늦잠자도돼
원본 텍스트: 늦게일어나도되고토요일에도늦잠자도돼
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07263.wav
STT 결과: 이제오후에손자들오거든그럼손자들오면
원본 텍스트: 이제오후에손자들오거든그럼손자들오면
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07264.wav
STT 결과: 애들오전에교회가는게있어서가서데려다주고
원본 텍스트: 애들오전에교회가는게있어서가서데려다주고
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07265.wav
STT 결과: 그다음에1시나2시이정도면교회가끝나
원본 텍스트: 그다음에한시나두시이정도면교회가끝나
CER: 0.1111111111111111
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07266.wav
STT 결과: 교회끝나면다시집에또가서애들하고같이
원본 텍스트: 교회끝나면다시집에또가서애들하고같이
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07267.wav
STT 결과: 가서밥먹고이제또애들을유치원가야돼
원본 텍스트: 가서밥먹고이제또애들을유치원가야돼
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07268.wav
STT 결과: 유치원도서관인데거기에애들데리고가면
원본 텍스트: 유치원도서관인데거기에애들데리고가면
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07269.wav
STT 결과: 2시부터그영화를해아이들만화영화야
원본 텍스트: 두시부터그영화를해아이들만화영화야
CER: 0.058823529411764705
현재까지의 평균 CER (10개마다): 0.02723942208462332
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07270.wav
STT 결과: 이제토요일은법인안된거하고일요일은법인된거하는데
원본 텍스트: 이제토요일은더빙안된거하고일요일은더빙된거하는데
CER: 0.16666666666666666
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07271.wav
STT 결과: 겨울왕국같은유명한애니메이션영화있잖아
원본 텍스트: 겨울왕국같은유명한애니메이션영화있잖아
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07272.wav
STT 결과: 그런영화들상영해주고하여튼영화좋은거많이하는데
원본 텍스트: 그런영화들상영해주고하여튼영화좋은거많이하는데
CER: 0
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07273.wav
STT 결과: 우리애들그거참좋아하고재밌어하더라
원본 텍스트: 우리애들그거참좋아하고재밌어하더라
CER: 0
이 결과는 초반의 결과이다.
대략적으로 Web speech api는 약간씩 원본이랑 틀릴 수밖에 없는게 존재한다.
예를 들어 티비를 TV라고 한다던지, 아니면 한시 를 1시라고 한다던지 말이다.
사실 더 정확한 교정이긴 하다. 그렇게 생각하고, 마이크 품질까지 생각해서 CER을 보정해보면 오차율은 10%~15% 정도 될 것이다.
파일: 노인남여_노인대화07_F_CSO00_62_수도권_녹음실_07644.wav
STT 결과: TV틀면은채널맛있는녀석들틀어
원본 텍스트: 티비틀면은채널맛있는녀석들틀어
CER: 0.13333333333333333
결론적으로 1000개 샘플 파일의 CER은 다음과 같다.
현재까지의 평균 CER (10개마다): 0.03554398899504777
Whisper-v3 large의 CER이 0.021,
Whisper v3 large-turbo 가 0.0239였다.
대략적으로 CER이 0.03 ~0.033정도로 취급된다고 할 수 있겠다.
무료로 사용할 거라면 Whisper large나 다른 버전들보다도 좋은 수치기에, 프론트엔드에서 활용할 수 있는 좋은 API가 된다.
반응형