티스토리 뷰

반응형

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가 된다.

반응형