진짜 목소리 vs AI 목소리 주파수 차이 비교! Streamlit 대시보드 제작

Streamlit 오디오 대시보드를 구축하며
AI 복제 목소리와 실제 사람 목소리의 주파수 스펙트럼 차이(고주파 대역 절단 현상 등)를 직관적으로 비교해 보기 위해 Streamlit과 Librosa 라이브러리를 활용한 주파수 분석 도구를 만들었습니다.
처음에는 단순히 오디오 파일을 업로드받아 그래프를 그려주는 토이 프로젝트로 시작했지만, 웹 대시보드 환경에서 오디오 데이터와 시각화 객체를 다룰 때는 몇 가지 실무적인 자원 관리 요소들을 함께 고려해야 더욱 안정적으로 작동한다는 점을 알게 되었습니다.
이번 글에서는 Streamlit 환경에서 음성 데이터를 처리할 때 서버 자원을 깔끔하게 관리하고 예외 상황을 방지하기 위해 적용한 코드와 소소한 팁을 공유해 봅니다.
핵심 코드 전체상
가독성을 위해 핵심 흐름만 요약했으며, 전체 예외 처리 및 상세 코드는 깃허브를 참고해 주세요.
Python
import os
import tempfile
import streamlit as st
import matplotlib.pyplot as plt
import librosa
import librosa.display
import numpy as np
def analyze_voice_spectrum(human_audio_file, ai_audio_file):
# 1. 파일 객체 커서 초기화 (재실행 오류 방지)
human_audio_file.seek(0)
ai_audio_file.seek(0)
# 2. 물리적 임시 파일 생성으로 디코딩 무한 대기 방지
human_ext = os.path.splitext(human_audio_file.name)[1] or ".mp3"
ai_ext = os.path.splitext(ai_audio_file.name)[1] or ".mp3"
with tempfile.NamedTemporaryFile(delete=False, suffix=human_ext) as tmp_human:
tmp_human.write(human_audio_file.getvalue())
human_temp_path = tmp_human.name
with tempfile.NamedTemporaryFile(delete=False, suffix=ai_ext) as tmp_ai:
tmp_ai.write(ai_audio_file.getvalue())
ai_temp_path = tmp_ai.name
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
try:
# 3. 오디오 데이터 로드 및 STFT 주파수 변환
y_human, sr_human = librosa.load(human_temp_path, sr=None)
y_ai, sr_ai = librosa.load(ai_temp_path, sr=None)
stft_human = np.abs(librosa.stft(y_human))
stft_ai = np.abs(librosa.stft(y_ai))
db_human = librosa.amplitude_to_db(stft_human, ref=np.max)
db_ai = librosa.amplitude_to_db(stft_ai, ref=np.max)
# 4. 히트맵 시각화
img1 = librosa.display.specshow(db_human, sr=sr_human, x_axis='time', y_axis='hz', ax=axes[0])
axes[0].set_title('Real Human Voice', fontsize=12)
fig.colorbar(img1, ax=axes[0], format='%+2.0f dB')
img2 = librosa.display.specshow(db_ai, sr=sr_ai, x_axis='time', y_axis='hz', ax=axes[1])
axes[1].set_title('AI Voice', fontsize=12)
fig.colorbar(img2, ax=axes[1], format='%+2.0f dB')
plt.tight_layout()
st.pyplot(fig)
except Exception as e:
st.error(f"주파수 분석 처리 중 오류 발생: {e}")
finally:
# 5. 메모리 자원 해제 및 임시 파일 삭제
plt.close(fig)
if os.path.exists(human_temp_path):
os.remove(human_temp_path)
if os.path.exists(ai_temp_path):
os.remove(ai_temp_path)
코드에 쓰인 주요 라이브러리 함수
librosa.stft(): 단시간 푸리에 변환(Short-Time Fourier Transform) 함수로, 오디오 신호를 시간 흐름에 따른 주파수 성분 데이터로 변환합니다.
librosa.amplitude_to_db(): 진폭 스케일을 사람이 인지하기 쉬운 데시벨(dB) 스케일로 변환합니다.
librosa.display.specshow(): 변환된 주파수 성분을 시간축과 주파수축(Hz)에 맞춰 히트맵 형태의 스펙트로그램으로 출력합니다.
st.pyplot(): Matplotlib을 통해 그려진 그래프 객체를 웹 화면에 출력해 주는 역할을 합니다.
실무에서 바로 쓰는 자원 관리 노하우
재실행 안정성을 위한 파일 포인터 초기화
Streamlit에서 업로드된 파일 객체는 데이터를 한 번 읽고 나면 커서가 맨 끝에 머물게 됩니다. 사용자가 웹 화면에서 동작을 재요청할 때 0바이트 읽기 에러가 나는 것을 방지하기 위해, 파일 로딩 직전 seek(0)을 호출하여 포인터를 초기화하는 것이 좋습니다.
디코딩 무한 대기 방지를 위한 물리 임시 파일 활용
MP3나 M4A 같은 압축 음성을 Librosa로 읽어올 때 가상 메모리 버퍼를 직접 전달하면 백그라운드 프로세스가 파일 경로를 인식하지 못해 무한 대기가 발생하는 경우가 있습니다. tempfile 모듈로 하드디스크에 임시 파일 경로를 지정해 읽어오는 방식이 훨씬 안전합니다.
서버 메모리 누수 방지를 위한 자원 강제 해제
사용자 요청이 누적될 때 Matplotlib 객체나 임시 파일이 메모리에 남아있으면 서버 자원이 오염될 수 있습니다. try-finally 구문 내에서 plt.close(fig)와 os.remove를 강제 호출해 처리가 끝난 직후 메모리와 파일 자원을 깨끗하게 비워줍니다.
🔗 관련 링크 모음
🎬 유튜브 분석 시연 영상: https://youtu.be/cThE0wH4EAY/
🚀 웹 앱 직접 실행해보기: https://voicefrequencyanalyzer-67zfryfptjxwdjofkymjyw.streamlit.app/
📺 워드프레스: https://gohard.pe.kr/
💻 GitHub 소스 코드: https://github.com/gohard-lab
📺 잡학다식 개발자 유튜브 채널: https://www.youtube.com/@PolymathDev_KR
마무리 소통 및 영상 안내
Streamlit 대시보드를 구축해 보시면서 오디오 처리나 메모리 관리 관련하여 더 좋은 경험이나 아이디어가 있다면 댓글로 편하게 나누어 주시면 감사하겠습니다!