또 우리 팀에 이사님이 아래의 사진과 같이 하나의 캡쳐사진을 웍스 단체 DM에 보내며 자막 추출 AI를 만들라 해본다...

뭐 당연히 우리 팀에서는 나한테 시키면서 이번에도 해보라고 한다.
솔직히 뭔지도 모르고 아는건 없지만 저번에 해봤던 ComfyUI를 설치하고 아웃페인팅을 해봤던 것때문에 이전처럼 당황하지 않았다.
뭐 우리 듬직한 클로드와 함께 공부해가면서 구축해보면 될일이지 뭐~

뭐 일단 Qwen3-ASR이 뭔지 부터 찾아보자.
Qwne3-ASR은 알리바바에서 만든 30개 언어 + 중국 방언 22개 의 언어를 Automatic Speech Recognition (자동 음성 인식) 하는 AI 오픈소스 모델이라고 한다. 모델은 총 두개의 모델을 제공해주는데 0.6B 의 가볍고 빠른 모델과 성능이 좋은 1.7B 를 제공한다.

뭐 사실 정확한 모델의 안의 코드는 나중에 시간될때 이해해볼거고 지금은 우리 AI 서버에 이 라이브러리를 어떻게 깔고 어떻게 테스트 해볼지 생각해보자.
내가 이전에 우리 GPU AI 서버에 Docker Compose 를 사용하여 마이크로서비스 아키텍처 로 구축해놨다.
이유는 각 서비스는 독립적인 컨테이너로 실행시켜 여러 AI 를 독립적으로 사용해 볼 수 있기 때문이였다.
이전 ComfyUI 를 설치해본거와 같이 Docker 로 인스턴스 배포를 해보겠다.
일단 초기 아키텍처 를 생각해봤을때 가장 간단하게 Qwen3-ASR 는 영상을 분석하는게 아니라 음원을 분석하는거니 프로세스는 다음과 같다.
- 영상의 음원을 추출한다.(이건 그냥 인터넷에 변환기 있으니까 이걸로 뽑기)
- input 으로 WAV/MP3 파일 을 AI 서버 로컬에 저장한다.
- Qwen3-ASR 모델을 model\. 폴더에 직접 다운받는다. (테스트이니 0.6B 사용)
- 결과를 추출한다. (SRT 자막 파일 + 콘솔 출력)
huggingface-cli download Qwen/Qwen3-ASR-0.6B \
--local-dir ./subtitle-extractor-ai/app/models/Qwen3-ASR-0.6B
구조는 다음과 같다.
AI231 서버
│
├── 루트 시스템 (/root)
├── 데이터 및 모델 (/data)
└── 사용자 홈 (/home)
│
├── aiadmin
│ └── aiserver/
│ │
│ ├── ComfyUI/ #이미지 생성 AI
│ ├── docker-compose.yml #Docker 구성 파일 (Docker 컨테이너들을 정의하고 관리하는 설정 파일)
│ ├── Dockerfile.comfyui #ComfyUI Docker 빌드
│ ├── subtitle-extractor-ai/ #자막 생성 AI
│ │ ├── app/
│ │ │ ├── models/
│ │ │ │ └── Qwen3-ASR-0.6B/ #모델
│ │ │ ├── result/
│ │ │ │ └── test1.srt #결과
│ │ │ ├── test/
│ │ │ │ └── test1.wav #테스트 음원
│ │ │ ├── _init_.py
│ │ │ └── transcribe.py #실행코드
│ │ ├── Dockerfile #도커설계도
│ │ ├── .dockerignore
│ │ └── requirements.txt #패키지 목록
│ └── README.md
├── aiadmin_ [백업 파일]
├── aiserver [권한 없음]
├── ncadmin [권한 없음]
└── uaremine [권한 없음]
코드
requirements.txt
# Qwen3-ASR 공식 패키지
qwen-asr==0.0.6
# Utils
numpy>=1.26.0
python-multipart>=0.0.6
Qwen3-ASR은 매우 최신 모델이라서, Transformers 라이브러리가 이 모델 아키텍처를 인식하지 못했다. 해서 Transformers로 직접 모델을 로드하려 하지 말고, Qwen이 제공하는 공식 qwen-asr 패키지를 사용하였다.
requirements.txt 는 python에 필요한 패키지 목록을 명시할때 쓰인다.
해서 아래 명령어시 해당 도커에 해당 패키지 버전을 설치한다.
pip install -r requirements.txt
Dockerfile
# 1. 베이스 이미지
FROM pytorch/pytorch:2.2.2-cuda12.1-cudnn8-runtime
# 2. 환경 설정
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# 3. 시스템 패키지 설치
RUN apt-get update && apt-get install -y \
git \
curl \
ca-certificates \
ffmpeg \
libsm6 \
libxrender1 \
libxext6 \
libgl1 \
&& rm -rf /var/lib/apt/lists/*
# 4. 작업 폴더 설정
WORKDIR /app
# 5. 파일 복사
COPY requirements.txt .
# 6. pip 업그레이드
RUN pip install --upgrade pip
# 7. Python 패키지 설치 (requirements.txt 사용)
RUN pip install -r requirements.txt
# 8. 포트 설정
EXPOSE 8000
# 9. 실행 명령
CMD ["python", "transcribe.py"]
Dockerfile 은 실행 과정을 작성해둔다. 이미지를 만드는 법이라고 생각하면 된다.
어떤 환경을 만들것인지 (베이스 이미지 선택, 패키지 설치, 파일 복사, 실행 명령 정의 등) 어떻게 설치할지 차례로 실행을 시키는데 라이브러리 버전은 requirements.txt 을 참조한다. 여러 세팅을 맞추고 이후 transcribe.py 을 실행시킨다.
이 동작은 처음에만 실행되고 이후에는 캐시를 사용한다. 대략 처음은 4분 ~ 5분 정도 걸린다;;
이후에는 docker-compose.yml 파일이 해당 도커 컨테이너를 관리한다.
docker-compose.yml
comfyui:
# image: zeroclue/comfyui:base-torch2.8.0-cu126
.
.
.
# ════════════════════════════════════════════════════════
# 자막 추출기 (Qwen3-ASR-0.6B)
# ════════════════════════════════════════════════════════
subtitle-extractor-ai:
build:
context: ./subtitle-extractor-ai # Docker 빌드 시 기준이 될 폴더 위치
dockerfile: Dockerfile # Dockerfile의 이름 (기본값은 "Dockerfile")
container_name: subtitle-extractor-ai # 컨테이너 이름
restart: unless-stopped
volumes:
- /data/hf_cache:/root/.cache/huggingface # Hugging Face 캐시 공유
- ./subtitle-extractor-ai/app:/app # 애플리케이션 코드 마운트
deploy:
resources:
reservations:
devices:
- driver: nvidia # GPU 드라이버 지정 (nvidia)
device_ids: ['0'] # GPU 설정(현재 0,1 두개의 GPU가 있음)
capabilities: [gpu] # GPU 기능 활성화
environment:
- HF_TOKEN=${HF_TOKEN} # Hugging Face API 토큰
- HF_HOME=/root/.cache/huggingface # Hugging Face 캐시 위치 명시
- CUDA_VISIBLE_DEVICES=0 # CUDA GPU 선택 (Dockerfile과 일치해야 함)
- PYTHONUNBUFFERED=1
shm_size: "8gb" # 음성 데이터를 메모리에 임시 저장할때 쓰이는 (공유 메모리) 크기 설정
ipc: host # 컨테이너가 호스트와 메모리 공유
transcribe.py
#!/usr/bin/env python3
"""
자막 추출기 - Qwen3-ASR-0.6B (qwen-asr 공식 패키지 사용)
"""
import sys
import torch
import logging
from pathlib import Path
from qwen_asr import Qwen3ASRModel
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 디렉토리 설정
BASE_DIR = Path(__file__).resolve().parent
MODEL_PATH = BASE_DIR / "models" / "Qwen3-ASR-0.6B"
RESULT_DIR = BASE_DIR / "result"
RESULT_DIR.mkdir(exist_ok=True)
def get_file_type(file_path: Path) -> str:
"""파일 형식 판별"""
ext = file_path.suffix.lower()
audio_formats = {'.wav', '.mp3', '.m4a', '.flac', '.aac', '.ogg', '.wma'}
if ext in audio_formats:
return "audio"
else:
return "unknown"
def main():
"""메인 함수"""
if len(sys.argv) < 2:
print("❌ 사용법: python transcribe.py <파일 경로>")
print(" python transcribe.py test/audio.wav")
sys.exit(1)
audio_file = sys.argv[1]
file_path = Path(audio_file)
if not file_path.is_absolute():
file_path = BASE_DIR / file_path
logger.info(f"📂 입력 파일: {file_path.name}")
logger.info(f"📏 파일 경로: {file_path}")
if not file_path.exists():
logger.error(f"❌ 파일을 찾을 수 없습니다: {file_path}")
sys.exit(1)
logger.info(f"📏 파일 크기: {file_path.stat().st_size / 1024 / 1024:.2f}MB")
file_type = get_file_type(file_path)
logger.info(f"📋 파일 형식: {file_type}")
if file_type != "audio":
logger.error(f"❌ 지원하지 않는 파일 형식입니다")
sys.exit(1)
try:
logger.info("="*70)
logger.info("🎙️ 자막 추출기 (Qwen3-ASR-0.6B)")
logger.info("="*70)
# GPU 확인
device = "cuda" if torch.cuda.is_available() else "cpu"
logger.info(f"📱 디바이스: {device}")
if device == "cuda":
logger.info(f"🖥️ GPU: {torch.cuda.get_device_name(0)}")
logger.info(f"💾 GPU 메모리: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f}GB")
# 모델 로드
logger.info("🔄 모델 로드 중...")
model = Qwen3ASRModel.from_pretrained(
"Qwen/Qwen3-ASR-0.6B",
dtype=torch.bfloat16,
device_map=device,
max_inference_batch_size=8,
max_new_tokens=256,
)
logger.info("✅ 모델 로드 완료")
# 자막 생성
logger.info(f"🎵 파일 처리 중: {file_path.name}")
results = model.transcribe(audio=str(file_path), language=None)
text = results[0].text
language = results[0].language
logger.info(f"✅ 처리 완료")
logger.info(f"📝 감지된 언어: {language}")
logger.info(f"📝 자막 길이: {len(text)}자")
# 결과 출력
logger.info("\n" + "="*70)
logger.info("📝 추출된 자막:")
logger.info("="*70)
print(text)
logger.info("="*70)
# 결과 저장
output_filename = file_path.stem + ".srt"
output_path = RESULT_DIR / output_filename
srt_content = "1\n00:00:00,000 --> 00:00:59,999\n" + text + "\n"
with open(output_path, 'w', encoding='utf-8') as f:
f.write(srt_content)
logger.info(f"💾 저장 완료: {output_path}")
logger.info(f"\n✅ 모든 작업 완료!")
except Exception as e:
logger.error(f"\n❌ 작업 실패: {e}")
import traceback
logger.error(traceback.format_exc())
sys.exit(1)
if __name__ == "__main__":
main()
이제 사용자가 아래와 같이 명령어 입력시 실행한다.
$ docker compose run --rm subtitle-extractor-ai \
python transcribe.py test/test1.wav
main() 함수 시작
- test/test1.wav 받음
- /app/test/test1.wav 의 절대 경로에 저장
- 파일 존재 유무 체크
- 파일 형식 확인 (.wav 형식의 음성만 가능)
- GPU 확인 (NVIDIA H200 NVL)
- 모델 로드 (Qwen3-ASR-0.6B)
- 음성 처리 (여기서 텍스트로 변환)
- 결과 출력 (콘솔에 텍스트 출력)
- 결과 저장(result/test1.srt 생성)
종료
해서 대략 38초의 음성을 docker 작동시긴을 제외하고 10초 미만으로 걸렸다. (아마 H200의 힘인가보다.)
나는 유튜브의 영상 중 매미킴 김상욱 영상 쇼츠(Link)이 있어 사용했다. (이유는 김상욱을 응원하기 때문!! 김상욱 UFC 파이팅!!!)
결과
1
00:00:00,000 --> 00:00:59,999
훈련 중 갑자기 눈물을 흘리는 김상욱 선수인데. 저번에 이거 똑같이 할 때는 사우가 형으로보다 잘했어요.
최종 감량을 많이 들어가주고 팀이 예전보다는 없는 것 같아.
한 번도 통화를 할 때 배우고 다시 찍고 가야 될 것 같습니다.
컨디션이 좋지 않은 채로 훈련을 하던 김상욱이.
어디 안 좋나?
어디 안 좋아? 왜 왜 왜.
수학 갑자기 해서 한 번씩이라던 자주 그래?
아 강력 스텐스 하는 거지. 조수 없어. 하는 거죠.
이렇게 스트레스 받고 잘 안 되고 해야. 시합 때가 가지고.
너무 힘든. 내가 UFC를 그렇게 하면서. 잘 풀린 적이 한 번도 없어.
잘 풀리면은 스트레스야. 내가 도와주는 파트너가 약한 거야.
감독이 강도가 약한 거야. 내가 지금 보는
대박...!! 생각보다 잘나온다...!!! (사투리는 좀 이상하게 나온다.)
아마 언어를 한국어로 설정하지 않아서이다.
다음에는 언어를 한국어로 설정해 정확도를 높이고 해당 나온 텍스트를 시간별로 쪼개 보도록 하겠다.
이만...!!

참고
'AI에서 살아남기(to. Android Developer) > 나도 AI 개발자 되어보기?' 카테고리의 다른 글
| AI 전문가란 무엇인가... (0) | 2026.01.26 |
|---|---|
| AI 서버 구축하기 GPU 다루기 (0) | 2026.01.26 |
| [AI개발자 되어보기]1. 안드로이드 개발자의 AI 도전기 (0) | 2026.01.14 |