Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature]: STT 테스트 페이지 및 웹소켓 설정 #37

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/components/common/MicrophoneButton/MicrophoneButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,21 @@ import {
} from "./MicrophoneButton.styles";

export interface MicrophoneButtonProps {
onClick?: () => void;
onStart?: () => void; // 녹음 시작 콜백
onStop?: () => void; // 녹음 중지 콜백
isRecording: boolean; // 추가된 부분
}

const MicrophoneButton: React.FC<MicrophoneButtonProps> = ({ onClick }) => {
const MicrophoneButton: React.FC<MicrophoneButtonProps> = ({ onStart, onStop }) => {
const [isRecording, setIsRecording] = useState(false);

const handleClick = () => {
setIsRecording(!isRecording);
if (onClick) onClick();
if (!isRecording && onStart) {
onStart(); // 녹음 시작 콜백 호출
} else if (isRecording && onStop) {
onStop(); // 녹음 중지 콜백 호출
}
};

return (
Expand Down
156 changes: 156 additions & 0 deletions src/pages/Speech/SpeechRecognitionPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React, { useState, useRef } from 'react';
import MicrophoneButton from '../../components/common/MicrophoneButton/MicrophoneButton';

const SpeechRecognitionPage = () => {
const [transcript, setTranscript] = useState('');
const socketRef = useRef<WebSocket | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const processorRef = useRef<ScriptProcessorNode | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const [isRecording, setIsRecording] = useState(false);

const handleStartRecording = async () => {
setIsRecording(true);

// WebSocket 연결
console.log('WebSocket 연결 시도 중...');
socketRef.current = new WebSocket('ws://localhost:8080/ws/stt');
socketRef.current.binaryType = 'arraybuffer';

socketRef.current.onopen = () => {
console.log('WebSocket 연결이 열렸습니다.');
};

socketRef.current.onmessage = (event) => {
const text = event.data;
console.log('인식된 텍스트:', text);
setTranscript((prev) => prev + text);
};

socketRef.current.onerror = (error) => {
console.error('WebSocket 오류 발생:', error);
};

socketRef.current.onclose = (event) => {
console.log(`WebSocket이 닫혔습니다. 코드: ${event.code}, 이유: ${event.reason}`);
setIsRecording(false);
};

// 마이크 접근 및 AudioContext 설정
try {
console.log('마이크 접근 요청 중...');
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log('마이크 접근 성공');

// AudioContext 생성
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
const audioContext = new AudioContext({ sampleRate: 16000 });
audioContextRef.current = audioContext;
streamRef.current = stream;

// 입력 소스 생성
const input = audioContext.createMediaStreamSource(stream);

// ScriptProcessorNode 생성
const processor = audioContext.createScriptProcessor(4096, 1, 1);
processorRef.current = processor;

// 오디오 처리
processor.onaudioprocess = (e) => {
const inputData = e.inputBuffer.getChannelData(0);
const buffer = downsampleBuffer(inputData, audioContext.sampleRate, 16000);

if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
socketRef.current.send(buffer);
}
};

input.connect(processor);
processor.connect(audioContext.destination);
} catch (error) {
console.error('마이크 접근 오류:', error);
}
};

const handleStopRecording = () => {
setIsRecording(false);

// 오디오 스트림 및 프로세서 종료
if (processorRef.current) {
processorRef.current.disconnect();
processorRef.current = null;
}

if (audioContextRef.current) {
audioContextRef.current.close();
audioContextRef.current = null;
}

if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop());
streamRef.current = null;
}

// WebSocket 종료
if (socketRef.current) {
if (socketRef.current.readyState === WebSocket.OPEN) {
console.log('WebSocket 연결 닫기 시도 중...');
socketRef.current.close();
}
socketRef.current = null;
}
};

// 오디오 데이터를 16kHz로 다운샘플링하는 함수
function downsampleBuffer(buffer: Float32Array, inputSampleRate: number, outputSampleRate: number) {
if (outputSampleRate === inputSampleRate) {
return convertFloat32ToInt16(buffer);
}
const sampleRateRatio = inputSampleRate / outputSampleRate;
const newLength = Math.round(buffer.length / sampleRateRatio);
const result = new Float32Array(newLength);
let offsetResult = 0;
let offsetBuffer = 0;
while (offsetResult < result.length) {
const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
let accum = 0,
count = 0;
for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
accum += buffer[i];
count++;
}
result[offsetResult] = accum / count;
offsetResult++;
offsetBuffer = nextOffsetBuffer;
}
return convertFloat32ToInt16(result);
}

// Float32Array를 Int16Array로 변환하는 함수
function convertFloat32ToInt16(buffer: Float32Array) {
const l = buffer.length;
const result = new Int16Array(l);
for (let i = 0; i < l; i++) {
const s = Math.max(-1, Math.min(1, buffer[i]));
result[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
return result.buffer;
}

return (
<div>
<h1>실시간 음성 인식 테스트</h1>
<MicrophoneButton
onStart={handleStartRecording}
onStop={handleStopRecording}
isRecording={isRecording}
/>
<div>
<h2>인식된 텍스트:</h2>
<p>{transcript}</p>
</div>
</div>
);
};

export default SpeechRecognitionPage;
6 changes: 6 additions & 0 deletions src/router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import MyPage from "@/pages/Mypage/Mypage";
import Layout from "@/components/features/Layout/Layout";
import MainPage from "@/pages/Main/MainPage";
import PrivateRoute from "./PrivateRoute";
import SpeechRecognitionPage from "@/pages/Speech/SpeechRecognitionPage"; // SpeechRecognitionPage 임포트


import CheckAuth from "./CheckAuth";

Expand Down Expand Up @@ -73,6 +75,10 @@ function Router() {
</PrivateRoute>
),
},
{
path: RouterPath.speechRecognition, // 새로운 경로 추가
element: <SpeechRecognitionPage />, // 해당 경로에 페이지 등록
},
],
},
]);
Expand Down
1 change: 1 addition & 0 deletions src/router/RouterPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const RouterPath = {
plan: "/plan",
friend: "/friend",
myPage: "/mypage",
speechRecognition: "/speech-recognition" // 새로운 경로 추가
};

export default RouterPath;