Upload files to "/"
This commit is contained in:
13
Real-Time Interview Assistant.json
Normal file
13
Real-Time Interview Assistant.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "real-time-interview-assistant",
|
||||
"private_key_id": "e43ea108c231a617212cbd456d905194b345494c",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7sWxPjrwrsZSc\nS7dMZSFuXMZ7aS2HfimFdVwK0v0SaGusxJQ7fnqWZAW9EcUf/RNm4gkmYd/boztp\ngXsAuVuZogrHMgVIQW+MXWRtgImEwCQmvnKjJoitnob1SpY83DD5yddt/7pVSj9F\nN+d4TqPekQgzK+EPQRtwYjT6EuDxQIJUe2djfEdESfcWYbTHNqZOl0V5SE3jIJae\nBXsXobq7nx1ETy7qMEqADzFJIPslzehOxQxpPFIbcUElBofAm3Zho8nJg8t9AG/u\nDKC/px27lU1tSPKdExQ4bSzD4DyhQEtqc7C1GsdZr/yAqr2AbNMTQxRihUAL37sX\nn72e148LAgMBAAECggEAO3PEwiKNOi3iy+sz4W/7OfELMdYsBMoSruJwyEDyxpzq\n1mviJEI45GBEbRIu5aYNOj6I9W51MSYwUIgiBSWxfSWV0mjmwW9wvP5sLD9V0AXo\nrZkPyNwQ2SXoy9PXaOm6XbTwlzg0toVxKyS9Hh+SypIYDdVtUZ6m6V9CNqA6PlBM\nDjQ/ZotvqZegPgGLP0l4/elpew2DtkhmEdw4COY4FB5Fi1M7/XPHAyF/C1AxZWOF\nn7YXqDUxGmcMNIUSqJ6y1Dkb+6vGo5aoPTuF9yvrBIeYW4ByieWowzIF6vffu+ei\n1Hjpq22NUgfXKtpcCqcBxYfP2k/ooi6zv1Aa+PE7yQKBgQDz96ygLFYirjJXfKIk\nVEQBwlsGUte3ZQcJSMub0aGkgLkH+Nn9JI+q7xTRhlsiGkC+Y70yqEtiPkqaOCp7\nj3NjZ6/IXKN7WPD5Wtvih1UJJ5hYUzbARJITIlJCNf6T9tYcGD056JRLHriH9RBi\nUht33jsHpAL2MxCikuDuyeqlbwKBgQDE8zrPtvjhxWs/r9NBXbDfbajJwZBoRlVI\nsJcdn2EqJGBPTzjGrCPaz3U/el5l9KoLjceSWQJIxTAj5AbDcweZMucwqNmG0QNX\ny6+sg9feMD1FLB1+BeCIOOJPmk1oPvx1PgQfgkJg9QR83GIl8YuLtvEer4pz8n4M\n/OkBKHW6JQKBgQDbKAoyeKGH1ePzI0qkR+4vhmAudgzB+kcv1+zPtKj4FYoh1zI7\nbLSCYPLapU8Ie3zdistSzkupnTt2/i1rgZmuGl6WJmHVVDhkR3JvNBL3flIkRdxR\nK0ftWE98mvRuBraf1kZp1rwgHyC1QTfOmuOB8mgknPjsIUM4R6k32LqokQKBgCi6\nqDHmg+ekvP4prUV6S6aY9evrVKLL0L08j7O3jw95AFXGa1ZiqPOKLZQYCUeKZlQH\nWPtB3wAPj1oMwP1QX4TmCvt8H6gKt2dDnyvNBCpVzYXtjpfOPVXOdbbCkTl6tRjF\n33JorLOCWBA+PynbnuEgzxJqePZrcnfbIZB0vovlAoGAUGFQB2hcDEQhcS9vgHSC\n4kvjVyHbHWFNZ/5+VVpZYrhUYmIHdrtE2WNHQlEP6BZtx6puS2hOfJQw/6q6ta6x\nO3y41O5S1rYOyRUZTze5eh6dxi7Ze2noWICW1vTzjd8iOb+PBCJZevkcWKFi5R+T\nyddbdkIyEYYzx39rvjeTLlk=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "ssum-859@real-time-interview-assistant.iam.gserviceaccount.com",
|
||||
"client_id": "108038919108873159722",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ssum-859%40real-time-interview-assistant.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
109
audio_capture.py
Normal file
109
audio_capture.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# audio_capture.py
|
||||
import pyaudio
|
||||
import numpy as np
|
||||
import threading
|
||||
import queue
|
||||
|
||||
class AudioCapture:
|
||||
def __init__(self, sample_rate=16000, chunk_size=1024):
|
||||
self.sample_rate = sample_rate
|
||||
self.chunk_size = chunk_size
|
||||
self.audio_queue = queue.Queue()
|
||||
self.is_running = False
|
||||
self.thread = None
|
||||
self.p = pyaudio.PyAudio()
|
||||
|
||||
def start_capture(self):
|
||||
self.is_running = True
|
||||
self.thread = threading.Thread(target=self._capture_loop)
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def stop_capture(self):
|
||||
self.is_running = False
|
||||
if self.thread:
|
||||
self.thread.join(timeout=2.0)
|
||||
|
||||
def _capture_loop(self):
|
||||
# 사용 가능한 모든 입력 장치 출력
|
||||
print("사용 가능한 입력 장치:")
|
||||
for i in range(self.p.get_device_count()):
|
||||
info = self.p.get_device_info_by_index(i)
|
||||
print(f"장치 {i}: {info['name']}")
|
||||
|
||||
# 기본 출력 장치 찾기
|
||||
output_device_index = self._get_default_output_device()
|
||||
print(f"기본 출력 장치 인덱스: {output_device_index}")
|
||||
print(f"기본 출력 장치: {self.p.get_device_info_by_index(output_device_index)['name']}")
|
||||
|
||||
# 스테레오 믹스 찾기 (대체용)
|
||||
stereo_mix_index = None
|
||||
for i in range(self.p.get_device_count()):
|
||||
info = self.p.get_device_info_by_index(i)
|
||||
if "스테레오 믹스" in info['name'] or "Stereo Mix" in info['name']:
|
||||
stereo_mix_index = i
|
||||
break
|
||||
|
||||
# 오디오 스트림 열기 시도
|
||||
input_device_index = output_device_index # 먼저 기본 출력 장치로 시도
|
||||
if stereo_mix_index is not None:
|
||||
input_device_index = stereo_mix_index # 스테레오 믹스가 있으면 그것을 사용
|
||||
|
||||
print(f"오디오 캡처에 사용할 장치: {input_device_index} ({self.p.get_device_info_by_index(input_device_index)['name']})")
|
||||
|
||||
try:
|
||||
stream = self.p.open(
|
||||
format=pyaudio.paInt16,
|
||||
channels=1,
|
||||
rate=self.sample_rate,
|
||||
input=True,
|
||||
input_device_index=input_device_index,
|
||||
frames_per_buffer=self.chunk_size
|
||||
)
|
||||
|
||||
print(f"오디오 캡처 시작: 장치 {input_device_index}")
|
||||
|
||||
while self.is_running:
|
||||
data = stream.read(self.chunk_size, exception_on_overflow=False)
|
||||
self.audio_queue.put(data)
|
||||
|
||||
except Exception as e:
|
||||
print(f"오디오 캡처 오류: {e}")
|
||||
|
||||
# 스테레오 믹스로 시도 실패하면 사용자 지정 장치로 시도
|
||||
try:
|
||||
print("대체 방법으로 장치 12 시도...")
|
||||
stream = self.p.open(
|
||||
format=pyaudio.paInt16,
|
||||
channels=1,
|
||||
rate=self.sample_rate,
|
||||
input=True,
|
||||
input_device_index=12, # 이전에 작동했던 장치 번호
|
||||
frames_per_buffer=self.chunk_size
|
||||
)
|
||||
|
||||
print(f"오디오 캡처 시작: 장치 12")
|
||||
|
||||
while self.is_running:
|
||||
data = stream.read(self.chunk_size, exception_on_overflow=False)
|
||||
self.audio_queue.put(data)
|
||||
except Exception as e2:
|
||||
print(f"대체 방법도 실패: {e2}")
|
||||
finally:
|
||||
if 'stream' in locals() and stream:
|
||||
stream.stop_stream()
|
||||
stream.close()
|
||||
self.p.terminate()
|
||||
print("오디오 캡처 종료")
|
||||
|
||||
def _get_default_output_device(self):
|
||||
"""기본 출력 장치 찾기"""
|
||||
info = self.p.get_host_api_info_by_index(0)
|
||||
default_output = info.get('defaultOutputDevice')
|
||||
return default_output
|
||||
|
||||
def get_audio_data(self, block=True, timeout=None):
|
||||
try:
|
||||
return self.audio_queue.get(block=block, timeout=timeout)
|
||||
except queue.Empty:
|
||||
return None
|
||||
59
llm_response.py
Normal file
59
llm_response.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# llm_response.py
|
||||
from google.cloud import aiplatform
|
||||
import time
|
||||
|
||||
class ResponseGenerator:
|
||||
def __init__(self, project_id, location="us-central1"):
|
||||
self.project_id = project_id
|
||||
self.location = location
|
||||
|
||||
# Vertex AI 초기화
|
||||
aiplatform.init(project=project_id, location=location)
|
||||
|
||||
def generate_response(self, english_question, korean_translation, callback=None):
|
||||
"""면접 질문에 대한 응답 생성"""
|
||||
try:
|
||||
# 프롬프트 구성
|
||||
prompt = f"""[System] 너는 2025년 경희대학교 국제 인턴십 면접에 참가한 지원자이다. 면접관은 영어로 질문을 하고, 너는 영어로 답변한다. 친절하고 전문적인 어조로 답하라.
|
||||
|
||||
[User] 면접관: "{english_question}"
|
||||
한국어 번역: "{korean_translation}"
|
||||
|
||||
이 질문에 대한 이상적인, 간결하고 명확한 영어 답변을 생성해주세요. 1-3개 문단으로 제한하고, 첫 문장에 핵심을 담아주세요.
|
||||
"""
|
||||
# Gemini 2.5 Flash 모델 호출
|
||||
parameters = {
|
||||
"temperature": 0.5,
|
||||
"max_output_tokens": 512,
|
||||
"top_p": 0.9,
|
||||
"top_k": 40,
|
||||
"thinking_budget": 0, # 빠른 응답을 위해 thinking 모드 끄기
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# 실제 API 호출 부분
|
||||
llm_response = aiplatform.TextGenerationModel.from_pretrained(
|
||||
"gemini-2.5-flash-preview-04-17"
|
||||
).predict(
|
||||
prompt=prompt,
|
||||
**parameters
|
||||
).text
|
||||
|
||||
duration = time.time() - start_time
|
||||
print(f"LLM 응답 생성 시간: {duration:.2f}초")
|
||||
|
||||
# 콜백으로 결과 전달
|
||||
if callback:
|
||||
callback(llm_response)
|
||||
|
||||
return llm_response
|
||||
|
||||
except Exception as e:
|
||||
print(f"LLM 응답 생성 오류: {e}")
|
||||
error_msg = "답변 생성 중 오류가 발생했습니다. 자신의 경험을 바탕으로 답변해보세요."
|
||||
|
||||
if callback:
|
||||
callback(error_msg)
|
||||
|
||||
return error_msg
|
||||
170
main_app.py
Normal file
170
main_app.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# main_app.py
|
||||
import sys
|
||||
import threading
|
||||
from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout,
|
||||
QLabel, QTextEdit, QPushButton, QWidget)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QObject
|
||||
|
||||
from audio_capture import AudioCapture
|
||||
from speech_translation import SpeechTranslator
|
||||
from llm_response import ResponseGenerator
|
||||
|
||||
# 신호 클래스 정의
|
||||
class Communicator(QObject):
|
||||
update_question = pyqtSignal(str, str) # 영어, 한국어
|
||||
update_answer = pyqtSignal(str)
|
||||
|
||||
class InterviewAssistantApp(QMainWindow):
|
||||
def __init__(self, project_id):
|
||||
super().__init__()
|
||||
|
||||
# 신호 객체 생성
|
||||
self.communicator = Communicator()
|
||||
|
||||
# 시스템 컴포넌트 초기화
|
||||
self.audio_capture = AudioCapture(sample_rate=16000)
|
||||
self.speech_translator = SpeechTranslator(self.audio_capture)
|
||||
self.response_generator = ResponseGenerator(project_id)
|
||||
|
||||
# UI 설정
|
||||
self.setWindowTitle("실시간 영어 인터뷰 보조")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
self.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint) # 항상 위에 표시
|
||||
|
||||
# UI 레이아웃 구성
|
||||
self.setup_ui()
|
||||
|
||||
# 신호 연결
|
||||
self.communicator.update_question.connect(self.on_question_received)
|
||||
self.communicator.update_answer.connect(self.on_answer_received)
|
||||
|
||||
# 시스템 시작
|
||||
self.start_system()
|
||||
|
||||
def setup_ui(self):
|
||||
central_widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# 안내 라벨
|
||||
self.status_label = QLabel("면접관의 질문을 기다리는 중...")
|
||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
# 영어 질문 영역
|
||||
self.english_label = QLabel("질문 (영어):")
|
||||
layout.addWidget(self.english_label)
|
||||
|
||||
self.english_question = QTextEdit()
|
||||
self.english_question.setReadOnly(True)
|
||||
self.english_question.setMaximumHeight(80)
|
||||
layout.addWidget(self.english_question)
|
||||
|
||||
# 한국어 번역 영역
|
||||
self.korean_label = QLabel("질문 (한국어):")
|
||||
layout.addWidget(self.korean_label)
|
||||
|
||||
self.korean_translation = QTextEdit()
|
||||
self.korean_translation.setReadOnly(True)
|
||||
self.korean_translation.setMaximumHeight(80)
|
||||
layout.addWidget(self.korean_translation)
|
||||
|
||||
# 응답 영역
|
||||
self.answer_label = QLabel("추천 답변:")
|
||||
layout.addWidget(self.answer_label)
|
||||
|
||||
self.answer_text = QTextEdit()
|
||||
self.answer_text.setReadOnly(True)
|
||||
layout.addWidget(self.answer_text)
|
||||
|
||||
# 컨트롤 버튼
|
||||
self.control_button = QPushButton("일시정지")
|
||||
self.control_button.clicked.connect(self.toggle_system)
|
||||
layout.addWidget(self.control_button)
|
||||
|
||||
central_widget.setLayout(layout)
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
def start_system(self):
|
||||
"""시스템 구성요소 시작"""
|
||||
self.system_active = True
|
||||
self.control_button.setText("일시정지")
|
||||
self.status_label.setText("면접관의 질문을 기다리는 중...")
|
||||
|
||||
# 오디오 캡처 시작
|
||||
self.audio_capture.start_capture()
|
||||
|
||||
# 음성 인식 시작
|
||||
self.speech_translator.start_listening(self.on_speech_translated)
|
||||
|
||||
def stop_system(self):
|
||||
"""시스템 구성요소 중지"""
|
||||
self.system_active = False
|
||||
self.control_button.setText("시작")
|
||||
self.status_label.setText("시스템 일시정지됨")
|
||||
|
||||
# 음성 인식 중지
|
||||
self.speech_translator.stop_listening()
|
||||
|
||||
# 오디오 캡처 중지
|
||||
self.audio_capture.stop_capture()
|
||||
|
||||
def toggle_system(self):
|
||||
"""시스템 시작/중지 전환"""
|
||||
if self.system_active:
|
||||
self.stop_system()
|
||||
else:
|
||||
self.start_system()
|
||||
|
||||
def on_speech_translated(self, english_text, korean_text):
|
||||
"""음성 인식 및 번역 결과 처리"""
|
||||
# UI 스레드에 신호 전송
|
||||
self.communicator.update_question.emit(english_text, korean_text)
|
||||
|
||||
# 상태 업데이트
|
||||
self.status_label.setText("답변 생성 중...")
|
||||
|
||||
# LLM 응답 생성 (별도 스레드에서)
|
||||
threading.Thread(
|
||||
target=self.generate_llm_response,
|
||||
args=(english_text, korean_text),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
def generate_llm_response(self, english_text, korean_text):
|
||||
"""LLM 응답 생성 (별도 스레드에서 실행)"""
|
||||
try:
|
||||
response = self.response_generator.generate_response(
|
||||
english_text, korean_text,
|
||||
callback=lambda r: self.communicator.update_answer.emit(r)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"응답 생성 오류: {e}")
|
||||
self.communicator.update_answer.emit(
|
||||
"답변 생성 중 오류가 발생했습니다. 자신의 경험을 바탕으로 답변해보세요."
|
||||
)
|
||||
|
||||
def on_question_received(self, english_text, korean_text):
|
||||
"""질문 텍스트 UI 업데이트"""
|
||||
self.english_question.setText(english_text)
|
||||
self.korean_translation.setText(korean_text)
|
||||
self.answer_text.setText("답변 생성 중...")
|
||||
|
||||
def on_answer_received(self, answer_text):
|
||||
"""응답 텍스트 UI 업데이트"""
|
||||
self.answer_text.setText(answer_text)
|
||||
self.status_label.setText("면접관의 다음 질문을 기다리는 중...")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""앱 종료 시 정리"""
|
||||
self.stop_system()
|
||||
event.accept()
|
||||
|
||||
# 메인 실행 코드
|
||||
if __name__ == "__main__":
|
||||
# Google Cloud 프로젝트 ID 설정
|
||||
PROJECT_ID = "real-time-interview-assistant" # 여기에 프로젝트 ID 입력
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
window = InterviewAssistantApp(PROJECT_ID)
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
128
speech_translation.py
Normal file
128
speech_translation.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# speech_translation.py
|
||||
import os
|
||||
from google.cloud import speech_v1p1beta1 as speech
|
||||
from google.cloud import translate_v2 as translate
|
||||
import threading
|
||||
import time
|
||||
|
||||
class SpeechTranslator:
|
||||
def __init__(self, audio_capture, language_code="en-US", target_lang="ko"):
|
||||
self.audio_capture = audio_capture
|
||||
self.language_code = language_code
|
||||
self.target_lang = target_lang
|
||||
self.speech_client = speech.SpeechClient()
|
||||
self.translate_client = translate.Client()
|
||||
|
||||
self.is_listening = False
|
||||
self.thread = None
|
||||
self.callback = None
|
||||
self.last_transcript = ""
|
||||
self.silence_threshold = 2.0 # 2초 침묵 = 발화 종료로 간주
|
||||
self.last_audio_time = time.time()
|
||||
|
||||
def start_listening(self, callback):
|
||||
"""STT 및 번역 시작, callback(english_text, korean_text)은 결과 반환 함수"""
|
||||
self.callback = callback
|
||||
self.is_listening = True
|
||||
self.thread = threading.Thread(target=self._listen_loop)
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def stop_listening(self):
|
||||
self.is_listening = False
|
||||
if self.thread:
|
||||
self.thread.join(timeout=2.0)
|
||||
|
||||
def _listen_loop(self):
|
||||
"""실시간 STT 스트리밍 처리"""
|
||||
streaming_config = speech.StreamingRecognitionConfig(
|
||||
config=speech.RecognitionConfig(
|
||||
encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
|
||||
sample_rate_hertz=16000,
|
||||
language_code=self.language_code,
|
||||
enable_automatic_punctuation=True,
|
||||
model="latest_short",
|
||||
use_enhanced=True,
|
||||
),
|
||||
interim_results=True,
|
||||
single_utterance=False,
|
||||
)
|
||||
|
||||
print("음성 인식 대기 중...")
|
||||
|
||||
while self.is_listening:
|
||||
# 스트리밍 인식 상태 초기화
|
||||
responses = None
|
||||
streaming_recognize = None
|
||||
audio_generator = self._audio_generator()
|
||||
|
||||
# 스트리밍 요청 생성
|
||||
requests = (
|
||||
speech.StreamingRecognizeRequest(audio_content=content)
|
||||
for content in audio_generator
|
||||
)
|
||||
|
||||
try:
|
||||
responses = self.speech_client.streaming_recognize(
|
||||
config=streaming_config,
|
||||
requests=requests
|
||||
)
|
||||
|
||||
# 응답 처리
|
||||
for response in responses:
|
||||
if not self.is_listening:
|
||||
break
|
||||
|
||||
if not response.results:
|
||||
continue
|
||||
|
||||
result = response.results[0]
|
||||
|
||||
# 음성 감지 시 타임스탬프 업데이트
|
||||
self.last_audio_time = time.time()
|
||||
|
||||
if result.is_final:
|
||||
transcript = result.alternatives[0].transcript
|
||||
if transcript.strip():
|
||||
# 번역 수행
|
||||
try:
|
||||
translation = self.translate_client.translate(
|
||||
transcript,
|
||||
target_language=self.target_lang
|
||||
)
|
||||
translated_text = translation['translatedText']
|
||||
|
||||
# 콜백으로 결과 전달
|
||||
if self.callback:
|
||||
self.callback(transcript, translated_text)
|
||||
|
||||
self.last_transcript = transcript
|
||||
print(f"인식: {transcript}")
|
||||
print(f"번역: {translated_text}")
|
||||
except Exception as e:
|
||||
print(f"번역 오류: {e}")
|
||||
except Exception as e:
|
||||
print(f"STT 오류: {e}")
|
||||
time.sleep(1) # 오류 발생 시 잠시 대기 후 재시도
|
||||
|
||||
def _audio_generator(self):
|
||||
"""오디오 데이터 스트리밍 생성기"""
|
||||
silence_counter = 0
|
||||
|
||||
while self.is_listening:
|
||||
data = self.audio_capture.get_audio_data(block=True, timeout=0.1)
|
||||
|
||||
if data:
|
||||
yield data
|
||||
silence_counter = 0
|
||||
else:
|
||||
silence_counter += 1
|
||||
# 약 2초간 소리가 없으면 발화 종료로 처리
|
||||
if silence_counter > 20: # 0.1초 * 20 = 약 2초
|
||||
print("침묵 감지: 발화 종료")
|
||||
break
|
||||
|
||||
# 시간 기반 침묵 감지 (보조 방법)
|
||||
if time.time() - self.last_audio_time > self.silence_threshold:
|
||||
print("장시간 침묵: 발화 종료")
|
||||
break
|
||||
Reference in New Issue
Block a user