File size: 7,978 Bytes
d6fcf92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9cbfee0
d6fcf92
 
 
c8c7d92
d6fcf92
 
 
 
 
 
 
 
9cbfee0
2e0855f
c8c7d92
9cbfee0
d6fcf92
 
c8c7d92
2e0855f
d6fcf92
 
 
 
 
 
 
f0821d0
 
 
9cbfee0
f0821d0
 
 
d6fcf92
 
9cbfee0
 
 
 
 
d6fcf92
 
 
 
2e0855f
c8c7d92
9cbfee0
 
d6fcf92
 
 
2e0855f
c8c7d92
d6fcf92
 
 
f0821d0
d6fcf92
 
f0821d0
 
 
 
 
 
 
 
 
 
 
d6fcf92
f0821d0
 
 
 
d6fcf92
 
 
f0821d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6fcf92
2e0855f
c8c7d92
9cbfee0
 
 
d6fcf92
 
 
2e0855f
d6fcf92
 
 
 
 
 
 
 
 
2e0855f
d6fcf92
 
 
c8c7d92
d6fcf92
 
2e0855f
c8c7d92
d6fcf92
 
 
 
2e0855f
d6fcf92
 
 
9cbfee0
 
d6fcf92
 
c8c7d92
d6fcf92
 
 
2e0855f
9cbfee0
d6fcf92
9cbfee0
d6fcf92
 
 
 
 
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Voice Translator</title>
    <style>
        body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background-color: #f0f0f0; }
        #controls { margin-bottom: 20px; }
        button { font-size: 1.2em; padding: 10px 20px; cursor: pointer; }
        #status { font-size: 1.1em; color: #333; }
    </style>
</head>
<body>
    <h1>Real-Time Voice Translator</h1>
    <div id="controls">
        <button id="startButton">Start Translation</button>
        <button id="stopButton" disabled>Stop Translation</button>
    </div>
    <p id="status">Status: Not connected</p>
    <div id="log"></div>
    <script>
        const startButton = document.getElementById('startButton');
        const stopButton = document.getElementById('stopButton');
        const status = document.getElementById('status');
        let socket;
        let mediaRecorder;
        let audioContext;
        let audioQueue = [];
        let isPlaying = false;

        const connectWebSocket = () => {
            const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
            const wsUri = `${proto}//${window.location.host}/ws`;
            console.log("[CLIENT] Attempting to connect to WebSocket:", wsUri);
            status.textContent = `Status: Connecting to ${wsUri}...`;
            socket = new WebSocket(wsUri);

            socket.onopen = () => {
                status.textContent = 'Status: Connected. Ready to start.';
                console.log("[CLIENT] WebSocket connection opened. Enabling start button.");
                startButton.disabled = false;
            };

            socket.onmessage = (event) => {
                if (event.data instanceof Blob) {
                    const reader = new FileReader();
                    reader.onload = function() {
                        // The server sends raw PCM; we need to wrap it in a WAV header
                        const pcmData = new Int16Array(this.result);
                        const wavBlob = createWavBlob(pcmData, 1, 16000);
                        if (audioContext) {
                            audioQueue.push(wavBlob);
                            if (!isPlaying) playNextInQueue();
                        }
                    };
                    reader.readAsArrayBuffer(event.data);
                } else {
                    // Handle text messages from server (e.g., for logging)
                    const logElement = document.createElement('p');
                    logElement.textContent = event.data;
                    document.getElementById('log').prepend(logElement);
                }
            };

            socket.onclose = () => {
                console.log("[CLIENT] WebSocket connection closed.");
                status.textContent = 'Status: Disconnected. Please refresh the page.';
                startButton.disabled = false; // Allow user to try starting again
                stopButton.disabled = true; 
            };

            socket.onerror = (error) => {
                console.error("[CLIENT] WebSocket Error:", error);
                status.textContent = 'Status: Connection error. Check console for details.';
            };
        };

        const playNextInQueue = async () => {
            if (audioQueue.length > 0) {
                isPlaying = true;
                const blob = audioQueue.shift();
                try {
                    const arrayBuffer = await blob.arrayBuffer();
                    const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
                    const source = audioContext.createBufferSource();
                    source.buffer = audioBuffer;
                    source.connect(audioContext.destination);
                    source.onended = playNextInQueue; // Chain the next playback
                    source.start();
                } catch (e) {
                    console.error("Error decoding or playing audio:", e);
                    isPlaying = false;
                    playNextInQueue(); // Try the next one
                }
            } else {
                isPlaying = false;
            }
        };

        // Helper function to create a WAV blob from raw PCM data
        const createWavBlob = (pcmData, numChannels, sampleRate) => {
            const header = new ArrayBuffer(44);
            const view = new DataView(header);
            const pcmLength = pcmData.length * 2; // 16-bit samples

            // RIFF header
            view.setUint32(0, 0x52494646, false); // "RIFF"
            view.setUint32(4, 36 + pcmLength, true);
            view.setUint32(8, 0x57415645, false); // "WAVE"
            // "fmt " sub-chunk
            view.setUint32(12, 0x666d7420, false); // "fmt "
            view.setUint32(16, 16, true); // Sub-chunk size
            view.setUint16(20, 1, true); // Audio format (1 for PCM)
            view.setUint16(22, numChannels, true);
            view.setUint32(24, sampleRate, true);
            view.setUint32(28, sampleRate * numChannels * 2, true); // Byte rate
            view.setUint16(32, numChannels * 2, true); // Block align
            view.setUint16(34, 16, true); // Bits per sample
            view.setUint32(36, 0x64617461, false); // "data"
            view.setUint32(40, pcmLength, true);

            return new Blob([header, pcmData], { type: 'audio/wav' });
        };

        startButton.onclick = async () => {
            console.log("[CLIENT] Start button clicked.");
            // AudioContext must be created or resumed by a user gesture.
            if (!audioContext) {
                audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
            } else if (audioContext.state === 'suspended') {
                await audioContext.resume();
            }

            console.log("[CLIENT] Requesting microphone access...");
            navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000, channelCount: 1 } })
                .then(stream => {
                    mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm; codecs=opus' });
                    mediaRecorder.ondataavailable = event => {
                        if (event.data.size > 0 && socket.readyState === WebSocket.OPEN) {
                            socket.send(event.data);
                        }
                    };
                    mediaRecorder.start(250); // Send data every 250ms
                    console.log("[CLIENT] Microphone access granted. MediaRecorder started.");

                    startButton.disabled = true;
                    stopButton.disabled = false;
                    status.textContent = 'Status: Translating...';
                })
                .catch(err => {
                    console.error('[CLIENT] Error getting user media:', err);
                    status.textContent = 'Error: Could not access microphone.';
                });
        };

        stopButton.onclick = () => {
            console.log("[CLIENT] Stop button clicked.");
            if (mediaRecorder) {
                mediaRecorder.stop();
            }
            // Don't close the socket, just stop sending data.
            // The user might want to start and stop multiple times in one session.
            startButton.disabled = false;
            stopButton.disabled = true;
            status.textContent = 'Status: Stopped. Press Start to translate again.';
        };

        window.onload = () => {
            console.log("[CLIENT] Page loaded. Initializing...");
            startButton.disabled = true;
            stopButton.disabled = true;
            connectWebSocket(); // Connect automatically on page load
        };

    </script>
</body>
</html>