File size: 4,196 Bytes
1ff9fd2
f56b852
 
 
 
 
 
 
1ff9fd2
 
f56b852
 
1ff9fd2
f56b852
 
 
 
 
 
1ff9fd2
 
f56b852
 
 
 
1ff9fd2
 
f56b852
 
 
1ff9fd2
 
 
f56b852
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1ff9fd2
 
 
f56b852
 
 
 
 
 
 
 
 
 
 
1ff9fd2
 
 
f56b852
 
1ff9fd2
 
 
f56b852
 
 
 
 
 
 
1ff9fd2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

if (!SpeechRecognition) {
    document.getElementById("statusMessage").innerText =
        "❌ This browser does NOT support speech recognition. Use Chrome Desktop.";
    throw new Error("SpeechRecognition unsupported");
}

let recognition = null;
let isRecording = false;
let lastFinalTimestamp = null;
let pauseTimer = null;

const languageSelect = document.getElementById("languageSelect");
const toggleButton = document.getElementById("toggleButton");
const transcriptionOutput = document.getElementById("transcriptionOutput");
const statusMessage = document.getElementById("statusMessage");
const clearButton = document.getElementById("clearButton");
const downloadButton = document.getElementById("downloadButton");

function highlightDisfluencies(text) {
    return text.replace(
        /\b(um+|uh+|er+|ah+|eh+|oh+|hmm+|mmm+|like)\b/gi,
        (m) => `<span class="disfluency">${m}</span>`
    );
}

function addPauseMarker() {
    transcriptionOutput.innerHTML += " <span class='pause'>…</span> ";
    transcriptionOutput.scrollTop = transcriptionOutput.scrollHeight;
}

function initRecognition(lang) {
    recognition = new SpeechRecognition();
    recognition.lang = lang;
    recognition.continuous = true;
    recognition.interimResults = true;

    recognition.onstart = () => {
        statusMessage.innerText = `🎙️ Listening… (${lang})`;
    };

    recognition.onerror = (e) => {
        statusMessage.innerText = `⚠️ Error: ${e.error}`;

        if (e.error === "not-allowed") {
            statusMessage.innerText =
                "❌ Microphone blocked. Click the 🔒 icon → Allow microphone.";
        }
    };

    recognition.onend = () => {
        if (isRecording) recognition.start();
    };

    recognition.onresult = (event) => {
        let interimText = "";
        let finalText = "";

        for (let i = event.resultIndex; i < event.results.length; i++) {
            const text = event.results[i][0].transcript.trim();

            if (event.results[i].isFinal) {
                finalText += text + " ";
            } else {
                interimText += text + " ";
            }
        }

        // ---- INTERIM (live) ----
        if (interimText) {
            const base = transcriptionOutput.innerHTML.replace(/<span class="interim">.*?<\/span>/, "");
            transcriptionOutput.innerHTML = base + 
                `<span class="interim">${highlightDisfluencies(interimText)}</span>`;
            transcriptionOutput.scrollTop = transcriptionOutput.scrollHeight;

            clearTimeout(pauseTimer);
            pauseTimer = setTimeout(() => addPauseMarker(), 2000);
        }

        // ---- FINAL ----
        if (finalText) {
            let cleaned = transcriptionOutput.innerHTML.replace(/<span class="interim">.*?<\/span>/, "");

            const now = Date.now();
            if (lastFinalTimestamp && now - lastFinalTimestamp > 3000) {
                cleaned += "<br><span class='pause'>…</span> ";
            }

            transcriptionOutput.innerHTML =
                cleaned + highlightDisfluencies(finalText);
            transcriptionOutput.scrollTop = transcriptionOutput.scrollHeight;

            lastFinalTimestamp = now;
        }
    };
}

toggleButton.addEventListener("click", () => {
    if (isRecording) {
        isRecording = false;
        recognition.stop();
        toggleButton.textContent = "🎤 Start Listening";
        statusMessage.innerText = "Stopped.";
    } else {
        initRecognition(languageSelect.value);
        recognition.start();
        isRecording = true;
        toggleButton.textContent = "⏹ Stop Listening";
    }
});

clearButton.addEventListener("click", () => {
    transcriptionOutput.innerHTML = "";
    statusMessage.innerText = "Cleared.";
});

downloadButton.addEventListener("click", () => {
    const text = transcriptionOutput.innerText;
    const blob = new Blob([text], { type: "text/plain" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "transcription.txt";
    a.click();
});