123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Browser STT Client</title>
- <style>
- body {
- background-color: #f4f4f9;
- color: #333;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100vh;
- margin: 0;
- }
- #container {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 100%;
- max-width: 700px;
- padding: 20px;
- box-sizing: border-box;
- gap: 20px; /* Add more vertical space between items */
- height: 90%; /* Fixed height to prevent layout shift */
- }
- #status {
- color: #0056b3;
- font-size: 20px;
- text-align: center;
- }
- #transcriptionContainer {
- height: 90px; /* Fixed height for approximately 3 lines of text */
- overflow-y: auto;
- width: 100%;
- padding: 10px;
- box-sizing: border-box;
- background-color: #f9f9f9;
- border: 1px solid #ddd;
- border-radius: 5px;
- }
- #transcription {
- font-size: 18px;
- line-height: 1.6;
- color: #333;
- word-wrap: break-word;
- }
- #fullTextContainer {
- height: 150px; /* Fixed height to prevent layout shift */
- overflow-y: auto;
- width: 100%;
- padding: 10px;
- box-sizing: border-box;
- background-color: #f9f9f9;
- border: 1px solid #ddd;
- border-radius: 5px;
- }
- #fullText {
- color: #4CAF50;
- font-size: 18px;
- font-weight: 600;
- word-wrap: break-word;
- }
- .last-word {
- color: #007bff;
- font-weight: 600;
- }
- button {
- padding: 12px 24px;
- font-size: 16px;
- cursor: pointer;
- border: none;
- border-radius: 5px;
- margin: 5px;
- transition: background-color 0.3s ease;
- color: #fff;
- background-color: #0056b3;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- }
- button:hover {
- background-color: #007bff;
- }
- button:disabled {
- background-color: #cccccc;
- cursor: not-allowed;
- }
- </style>
- </head>
- <body>
- <div id="container">
- <div id="status">Press "Start Recording"...</div>
- <button id="startButton" onclick="startRecording()">Start Recording</button>
- <button id="stopButton" onclick="stopRecording()" disabled>Stop Recording</button>
- <div id="transcriptionContainer">
- <div id="transcription" class="realtime"></div>
- </div>
- <div id="fullTextContainer">
- <div id="fullText"></div>
- </div>
- </div>
- <script>
- const statusDiv = document.getElementById("status");
- const transcriptionDiv = document.getElementById("transcription");
- const fullTextDiv = document.getElementById("fullText");
- const startButton = document.getElementById("startButton");
- const stopButton = document.getElementById("stopButton");
- const controlURL = "ws://127.0.0.1:8011";
- const dataURL = "ws://127.0.0.1:8012";
- let dataSocket;
- let audioContext;
- let mediaStream;
- let mediaProcessor;
- // Connect to the data WebSocket
- function connectToDataSocket() {
- dataSocket = new WebSocket(dataURL);
- dataSocket.onopen = () => {
- statusDiv.textContent = "Connected to STT server.";
- console.log("Connected to data WebSocket.");
- };
- dataSocket.onmessage = (event) => {
- try {
- const message = JSON.parse(event.data);
- if (message.type === "realtime") {
- // Show real-time transcription with the last word in bold, orange
- let words = message.text.split(" ");
- let lastWord = words.pop();
- transcriptionDiv.innerHTML = `${words.join(" ")} <span class="last-word">${lastWord}</span>`;
- // Auto-scroll to the bottom of the transcription container
- const transcriptionContainer = document.getElementById("transcriptionContainer");
- transcriptionContainer.scrollTop = transcriptionContainer.scrollHeight;
- } else if (message.type === "fullSentence") {
- // Accumulate the final transcription in green
- fullTextDiv.innerHTML += message.text + " ";
- transcriptionDiv.innerHTML = message.text;
- // Scroll to the bottom of fullTextContainer when new text is added
- const fullTextContainer = document.getElementById("fullTextContainer");
- fullTextContainer.scrollTop = fullTextContainer.scrollHeight;
- }
- } catch (e) {
- console.error("Error parsing message:", e);
- }
- };
- dataSocket.onclose = () => {
- statusDiv.textContent = "Disconnected from STT server.";
- };
- dataSocket.onerror = (error) => {
- console.error("WebSocket error:", error);
- statusDiv.textContent = "Error connecting to the STT server.";
- };
- }
- // Start recording audio from the microphone
- async function startRecording() {
- try {
- startButton.disabled = true;
- stopButton.disabled = false;
- statusDiv.textContent = "Recording...";
- transcriptionDiv.textContent = "";
- fullTextDiv.textContent = "";
- audioContext = new AudioContext();
- mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
- const input = audioContext.createMediaStreamSource(mediaStream);
- // Set up processor for audio chunks
- mediaProcessor = audioContext.createScriptProcessor(1024, 1, 1);
- mediaProcessor.onaudioprocess = (event) => {
- const audioData = event.inputBuffer.getChannelData(0);
- sendAudioChunk(audioData, audioContext.sampleRate);
- };
- input.connect(mediaProcessor);
- mediaProcessor.connect(audioContext.destination);
- connectToDataSocket();
- } catch (error) {
- console.error("Error accessing microphone:", error);
- statusDiv.textContent = "Error accessing microphone.";
- stopRecording();
- }
- }
- // Stop recording audio and close resources
- function stopRecording() {
- if (mediaProcessor && audioContext) {
- mediaProcessor.disconnect();
- audioContext.close();
- }
- if (mediaStream) {
- mediaStream.getTracks().forEach(track => track.stop());
- }
- if (dataSocket) {
- dataSocket.close();
- }
- startButton.disabled = false;
- stopButton.disabled = true;
- statusDiv.textContent = "Stopped recording.";
- }
- // Send an audio chunk to the server
- function sendAudioChunk(audioData, sampleRate) {
- if (dataSocket && dataSocket.readyState === WebSocket.OPEN) {
- const float32Array = new Float32Array(audioData);
- const pcm16Data = new Int16Array(float32Array.length);
- for (let i = 0; i < float32Array.length; i++) {
- pcm16Data[i] = Math.max(-1, Math.min(1, float32Array[i])) * 0x7FFF;
- }
- const metadata = JSON.stringify({ sampleRate });
- const metadataLength = new Uint32Array([metadata.length]);
- const metadataBuffer = new TextEncoder().encode(metadata);
- const message = new Uint8Array(
- metadataLength.byteLength + metadataBuffer.byteLength + pcm16Data.byteLength
- );
-
- message.set(new Uint8Array(metadataLength.buffer), 0);
- message.set(metadataBuffer, metadataLength.byteLength);
- message.set(new Uint8Array(pcm16Data.buffer), metadataLength.byteLength + metadataBuffer.byteLength);
- dataSocket.send(message);
- }
- }
- </script>
- </body>
- </html>
|