Upload files to "/"

This commit is contained in:
2026-04-11 12:45:03 +09:00
parent 63753c8a2c
commit 7a4be51965
5 changed files with 479 additions and 0 deletions

View 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
View 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
View 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
View 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
View 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