프로젝트 개요
최근 AI 기술이 발전하면서 음성 인식과 자연어 처리를 활용한 다양한 서비스들이 등장하고 있습니다. 하지만 대부분의 서비스들이 외부 API에 의존하고 있어 비용 부담과 데이터 보안 우려가 있었습니다.
이번에 개발한 ‘인터뷰 분석기’는 완전히 로컬 환경에서 동작하는 AI 서비스로, 인터뷰 녹음 파일을 업로드하면 음성을 텍스트로 변환하고, 변환된 내용을 바탕으로 자유롭게 질의응답할 수 있는 시스템입니다.
주요 특징
- 🔒 완전 오프라인: 모든 데이터가 로컬에서만 처리
- 🎯 올인원 솔루션: STT, 임베딩, LLM까지 통합
- 💰 제로 비용: 외부 API 호출 없음
- 🚀 하드웨어 최적화: Windows/Mac 환경별 자동 최적화
기술 스택 선정

핵심 기술 구성
Frontend: Streamlit (빠른 프로토타이핑)
Backend: FastAPI (비동기 처리)
STT: faster-whisper / mlx-whisper (로컬 실행)
Vector DB: ChromaDB (임베딩 저장)
LLM: Ollama (로컬 추론)
Embedding: HuggingFace Transformers
왜 이 기술들을 선택했나?
1. STT 엔진 선택
- OpenAI Whisper API: 정확하지만 비용 발생
- faster-whisper: GPU 가속, 로컬 실행, 무료
- mlx-whisper: Mac M1/M2/M3 최적화
2. LLM 선택
- ChatGPT API: 성능 좋지만 토큰당 과금
- Ollama: 로컬 실행, 다양한 모델 지원, 무료
3. 벡터 DB 선택
- Pinecone: 클라우드 기반, 유료
- ChromaDB: 로컬 저장, 가벼움, 무료
개발 과정

1단계: 기본 아키텍처 설계
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#0a4a4a', 'primaryTextColor': '#aaffee', 'primaryBorderColor': '#00e5cc', 'lineColor': '#00e5cc', 'secondaryColor': '#062e2e', 'tertiaryColor': '#041e1e', 'clusterBkg': '#062e2e', 'clusterBorder': '#00e5cc', 'titleColor': '#aaffee', 'edgeLabelBackground': '#062e2e', 'fontFamily': 'monospace'}}}%%
flowchart TD
subgraph Client["🖥️ 클라이언트"]
A["Streamlit UI<br/>(녹음/업로드/채팅 인터페이스)"]
end
subgraph Server["⚙️ 서버"]
B["FastAPI<br/>(요청 수신 및 비동기 처리)"]
C["STT<br/>(faster-whisper/CUDA<br/>음성 → 텍스트 변환)"]
G["HuggingFace Embeddings<br/>(텍스트 → 벡터 변환)"]
E[("ChromaDB<br/>(벡터 저장/검색)")]
D["RAG Chain<br/>(문맥 검색 + LLM 답변 생성)"]
end
subgraph AI["🤖 AI 모델"]
H["Ollama EXAONE 3.5<br/>(한국어 특화 LLM 추론)"]
end
A -- "① 오디오 업로드" --> B
B -- "② STT 변환" --> C
C -- "③ 텍스트" --> G
G -- "④ 벡터화 후 저장" --> E
A -- "⑤ 질문" --> B
B -- "⑥ RAG 실행" --> D
D -- "⑦ 벡터 검색" --> E
E -- "⑧ 관련 문맥 반환" --> D
D -- "⑨ 문맥 + 질문 전달" --> H
H -- "⑩ 답변 생성" --> D
D -- "⑪ 답변 반환" --> B
B -- "⑫ 답변 반환" --> A
style A fill:#0e6e4d,stroke:#34d399,color:#d1fae5
style B fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
style C fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
style D fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
style E fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
style G fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
style H fill:#2d1b4e,stroke:#c084fc,color:#f0d6ff
style Client fill:#062e1e,stroke:#34d399
style Server fill:#041e2e,stroke:#38bdf8
style AI fill:#110a1f,stroke:#c084fc처음에는 단순한 구조로 시작했지만, 성능과 사용성을 고려하여 점진적으로 개선했습니다.
2단계: 하드웨어 최적화 로직 구현
가장 까다로웠던 부분은 다양한 하드웨어 환경에서 최적의 성능을 내는 것이었습니다.
def get_optimal_hardware_config():
"""시스템 환경을 자동 감지하여 최적 설정 반환"""
os_name = platform.system()
machine_arch = platform.machine()
cpu_cores = max(1, (os.cpu_count() or 4) - 1)
if os_name == "Darwin" and machine_arch == "arm64":
# Mac M1/M2/M3: Metal GPU 활용
return {
"engine": "mlx",
"model_size": "medium"
}
elif os_name == "Windows":
# Windows: NVIDIA CUDA 활용
return {
"engine": "faster-whisper",
"model_size": "medium",
"device": "cuda",
"compute_type": "float16"
}
else:
# 기타 환경: CPU 최적화
return {
"engine": "faster-whisper",
"model_size": "medium",
"device": "auto",
"compute_type": "int8"
}3단계: CUDA 환경 설정 문제 해결
Windows 환경에서 가장 큰 난관은 CUDA 라이브러리 인식 문제였습니다.
# 핵심 해결책: faster_whisper 임포트 전에 CUDA 경로 강제 등록
if platform.system() == "Windows":
cuda_bin_path = r"D:\Program Files\NVIDIA\CUDA\v11.2\bin"
if os.path.exists(cuda_bin_path):
os.add_dll_directory(cuda_bin_path)
os.environ["PATH"] = cuda_bin_path + os.pathsep + os.environ.get("PATH", "")
from faster_whisper import WhisperModel # 이제 정상 작동4단계: 비동기 처리 및 성능 최적화
STT 변환은 시간이 오래 걸리는 작업이므로 비동기 처리가 필수였습니다.
@app.post("/api/upload-audio")
async def upload_audio(file: UploadFile = File(...)):
# 파일 저장을 비동기로 처리
loop = asyncio.get_event_loop()
await loop.run_in_executor(thread_pool, lambda: open(file_path, "wb").write(content))
# STT 변환을 별도 스레드에서 실행
transcribed_text = await loop.run_in_executor(
thread_pool, lambda: _run_stt(file_path, file.filename, start_time)
)
# 벡터 DB 저장도 비동기 처리
await loop.run_in_executor(thread_pool, lambda: save_to_vector_db(safe_text, file.filename))5단계: RAG 시스템 구현
단순한 STT를 넘어서 질의응답이 가능한 RAG(Retrieval-Augmented Generation) 시스템을 구현했습니다.
# 텍스트를 청크 단위로 분할하여 벡터 DB에 저장
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 청크 크기
chunk_overlap=50 # 겹침 정도
)
# 한국어 특화 임베딩 모델 사용
embeddings = HuggingFaceEmbeddings(
model_name="jhgan/ko-sroberta-multitask"
)
# ChromaDB에 벡터 저장
vector_db = Chroma.from_texts(
texts=chunks,
embedding=embeddings,
metadatas=metadatas,
persist_directory="chroma_db"
)6단계: 개인정보 보호 기능
인터뷰 데이터의 민감성을 고려하여 자동 마스킹 기능을 추가했습니다.
def mask_pii(text: str) -> str:
"""개인정보 자동 마스킹"""
# 주민등록번호 마스킹
text = re.sub(r'(\d{6})[-]\d{7}', r'\1-*******', text)
# 전화번호 마스킹
text = re.sub(r'(010)[-]\d{4}[-]\d{4}', r'\1-****-****', text)
return text개발 중 마주한 도전과 해결책
도전 1: Mac M1 환경에서의 성능 이슈
문제: Mac M1에서 faster-whisper 성능이 현저히 떨어짐 해결: mlx-whisper로 분기 처리하여 Metal GPU 활용
도전 2: 메모리 사용량 최적화
문제: 큰 오디오 파일 처리 시 메모리 부족 해결: 스트리밍 처리 및 임시 파일 자동 삭제
도전 3: 인터뷰 대상자 구분
문제: 여러 명이 참여한 인터뷰에서 화자 구분 어려움 해결: LLM을 활용한 이름 추출 및 메타데이터 관리
def extract_interviewee_name(text: str) -> str:
"""LLM으로 인터뷰 대상자 이름 추출, 실패 시 순번 반환"""
global _interviewee_counter
_interviewee_counter += 1
try:
llm = Ollama(model="exaone3.5:7.8b", temperature=0)
answer = llm.invoke(
f"""아래 인터뷰 텍스트에서 인터뷰 대상자(피면접자)의 이름만 추출하세요.
이름이 명확히 언급된 경우에만 이름을 반환하고, 불확실하면 반드시 'UNKNOWN'만 반환하세요.
다른 말은 절대 하지 마세요.
텍스트:\n{text[:1000]}"""
)
name = answer.strip().replace("'", "").replace('"', "")
if name and name != "UNKNOWN" and len(name) <= 10:
return name
except Exception:
pass
return f"interviewee_{_interviewee_counter}"사용자 경험 개선
실시간 진행률 표시
# STT 진행률을 실시간으로 표시
for segment in segments:
progress_percent = (segment.end / total_duration) * 100
elapsed_time = time.time() - start_time
remaining_time = (total_duration - segment.end) * (elapsed_time / segment.end)
print(f"진행률: {progress_percent:5.1f}% | "
f"경과: {elapsed_time:4.1f}초 | "
f"남은 시간: {remaining_time:4.1f}초")직관적인 웹 인터페이스
Streamlit을 활용하여 3개 탭으로 구성:
- 녹음 탭: 브라우저에서 직접 녹음
- 파일 업로드 탭: mp3, wav, m4a, webm, ogg, flac 파일 드래그 앤 드롭
- AI 채팅 탭: 분석 결과 질의응답
배포 및 운영

원클릭 실행 스크립트
@echo off
call .\venv\Scripts\activate
start "FastAPI Server" cmd /k "uvicorn main:app --reload"
start "ngrok" cmd /k "ngrok http 8000"
start "Streamlit Client" cmd /k "streamlit run client/app.py"
start "Cloudflared" cmd /k "cloudflared tunnel --url http://127.0.0.1:8501"외부 접속 지원
- ngrok: 서버 터널링
- cloudflared: 클라이언트 터널링
향후 개선 계획
1. 화자 분리 (Speaker Diarization)
현재는 이름 추출로만 구분하지만, 실제 음성 패턴을 분석하여 화자를 자동 분리하는 기능 추가 예정
2. 다국어 지원
현재 한국어 중심이지만, 영어, 일본어 등 다국어 STT 및 질의응답 지원
3. 실시간 스트리밍
파일 업로드가 아닌 실시간 음성 스트리밍 분석 기능
4. 고급 분석 기능
- 감정 분석
- 키워드 추출
- 요약 생성
- 인사이트 도출
마무리
이 프로젝트를 통해 “외부 API 없이도 충분히 실용적인 AI 서비스를 만들 수 있다”는 것을 증명할 수 있었습니다.
특히 인상 깊었던 점들:
- 로컬 AI의 가능성: 생각보다 성능이 뛰어나고 실용적
- 비용 효율성: 장시간 인터뷰도 추가 비용 없이 처리 가능
- 보안성: 민감한 인터뷰 데이터가 외부로 전송되지 않음
- 커스터마이징: 필요에 따라 모델이나 설정을 자유롭게 변경 가능
앞으로도 로컬 AI 기술이 더욱 발전하여, 개인이나 소규모 팀도 쉽게 AI 서비스를 구축할 수 있는 시대가 올 것이라 기대합니다.
이 글이 도움이 되셨다면 ❤️ 좋아요와 공유 부탁드립니다!
