이미지 다음은 일본어 음성을 판별해 보기
이전 글에서는 Cloudflare의 Clef를 사용하여 이미지 속에 고양이가 있는지 여부를 판별했습니다.
이번에는 음성 입력도 가능한 Clef-Omni를 사용해 봅니다.
다루는 소재는 예약 관련 부재중 전화입니다.
만약 '취소해 주세요'라는 내용이라면, 무엇을 해달라고 하는지는 비교적 이해하기 쉽습니다.
하지만 '취소하고 싶었는데, 역시 그대로 부탁드립니다'와 같이 말하는 경우에는 끝까지 듣고 판단해야 합니다.
이 녹음 파일을 모델에 전달하여 '취소', '예약 유지', '예약 변경', '확인 필요' 네 가지 중 하나를 선택하게 합니다.
일본어 합성 음성 4개로는 모두 예상한 선택지가 나왔습니다.
반면, 직접 녹음한 '늦을 것 같은데 괜찮은지'라는 문의에서는 '확인 필요'로 처리하고 싶었던 부분을 '예약 변경'으로 판정했습니다.
이 글에서는 Colab에서 구동한 코드와 결과를 순서대로 소개합니다.
글 중간의 Python 코드는 위쪽의 Colab 코드 셀에 붙여넣고 실행할 수 있습니다.
음성 생성 코드도 함께 제공하므로, 별도의 녹음을 준비하지 않아도 시도해 볼 수 있습니다.
Clef-Omni에 무엇을 입력하고 무엇이 반환되는가
Clef-Omni는 상황과 질문 각각에 대해 선택 가능한 후보들을 받아 그 후보들을 점수화하는 모델입니다.
여기서처럼 정해진 후보들로부터 판단을 반환하는 모델을 **Decision Model (판단 모델)**이라고 부릅니다.
공식 모델 카드에 따르면, Clef-Omni는 Qwen3-Omni-30B-A3B-Instruct를 기반으로 추가 학습된 모델이며, 텍스트, JSON, 이미지, 음성, 비디오 입력을 처리할 수 있습니다.
전에 시도했던 27B의 Clef와는 다른 모델입니다.
이번에 사용할 식별자는 Cloudflare/clef-omni 입니다.
이번 입력과 출력을 구체적으로 쓰면 다음과 같습니다.
| 항목 | 이번 내용 |
|---|
상황 state | 일본어 예약 관련 부재중 전화임 |
음성 audio | 녹음 파일 |
질문 instructions | 화자가 최종적으로 희망하는 대응은 무엇인가 |
선택지 criteria | 취소, 유지, 변경, 확인 필요 |
출력 choice | 선택된 선택지의 식별자 |
출력 probabilities | 4개 후보 각각의 확률 |
예를 들어 '취소하지 않고 그대로 부탁드립니다'라는 녹음이라면, 기대하는 출력은 keep 입니다.
Python에서는 response["answers"]["intent"]["choice"] 에서 이 식별자를 추출할 수 있습니다.
문자열 답변에서 '그대로'라는 단어를 찾는 처리는 불필요합니다.
일반적인 음성 대응 LLM(대규모 언어 모델)에서도 같은 질문은 할 수 있으며, 출력 형식 지정도 가능합니다.
Clef-Omni는 허용된 후보들의 점수를 매기는 전용 헤드(head)를 사용합니다.
공식 구현에서는 백본(backbone)의 최종 표현을 이 헤드에 전달하고, 질문마다 후보의 스코어를 softmax로 확률에 변환합니다.
긴 설명문을 생성한 후 분류 결과를 추출하는 구성이 아닙니다.
별도의 음성 인식 모델을 통한 전사(transcription)는 판정 프로그램에 통합하지 않습니다.
음성 파일을 공식적인 전처리 과정을 거쳐 읽어 Clef-Omni에 직접 입력합니다.
샘플 대본과 예상 답변은 실행 후 결과 확인용으로만 사용합니다.
예약 시스템에는 연결하지 않고, 대응 후보를 표시하는 단계까지만 시도합니다.
이번에 구동된 환경
실행 완료된 Notebook의 설정은 G4 GPU와 고메모리였습니다.
실행 로그에서 확인할 수 있는 환경은 다음과 같습니다.
| 항목 | 이번 설정 또는 기록 |
|---|
| 실행 장소 | Google Colab |
| ... | |
| 모델과 공식 코드는 다음 커밋에 고정했습니다. | |
0db1cd2607d76a7bdb2a382f659e7b313079f84b
공식 자료에는 BF16 백본만으로 약 64GB의 GPU 메모리가 필요하다고 나와 있습니다.
공식 측 검증 환경은 Torch 2.11, Transformers 5.10.2, H200이며, 이번 Colab 환경과는 다릅니다.
30B-A3B라는 표기에서 A3B를 3B 모델과 비슷한 메모리로 구동한다는 의미로 해석할 수는 없습니다.
이 글의 코드에서는 80GB급 이상의 GPU를 대상으로 하며, 총 용량이 75GiB 이상, 로드 전 여유 공간이 68GiB 이상임을 확인합니다.
이 수치는 교재 실행 조건으로 설정한 것입니다.
최소 필수 용량이나 피크 사용량을 측정하여 얻은 결과가 아니며, 80GB 모든 GPU에서 동작을 확인했다는 의미도 아닙니다.
무료 버전에서 자주 사용되는 T4를 대상으로 한 구성이 아닙니다.
하이 메모리(High Memory)는 시스템 RAM 설정이며, GPU 메모리를 늘리는 설정이 아닙니다.
Colab의 GPU 할당은 변동하므로, 계약명뿐만 아니라 실행 시 표시되는 GPU를 확인해 주십시오.
모델 카드(Model Card)의 라이선스 표기는 Apache-2.0입니다.
모델을 Colab에 다운로드하여 실행하기 때문에 Cloudflare 추론 API나 해당 API 키는 사용하지 않습니다.
모델 제공자의 추론 API 이용료와 Colab의 컴퓨팅 자원 비용은 별개입니다.
1. 라이브러리 준비
Colab에서 새 노트북을 열고, 런타임 유형(Runtime Type)에서 GPU를 선택합니다.
이번과 동일한 환경을 선택할 수 있다면, G4와 하이 메모리를 선택해 주십시오.
먼저 라이브러리를 설치합니다.
마지막의 edge-tts와 scipy는 기사 내에서 음성을 준비하기 위해 사용됩니다.
PyTorch는 Colab에 설치된 것을 사용합니다.
%pip install -q "transformers==5.19.0" "accelerate>=1.0,<2" "huggingface_hub>=1.0,<2" "av>=14" soundfile librosa pillow pandas edge-tts scipy
설치 후 세션 재시작을 요청받는 경우, 재시작한 후에 진행합니다.
다음 셀에서는 GPU 용량을 확인하고, 실행 환경을 JSON 파일로 기록합니다.
/content/clef_omni_demo는 이 실험의 음성 및 로그를 저장할 폴더입니다.
from pathlib import Path
import sys, json, time, shutil, hashlib, zipfile, platform, importlib.util
import importlib.metadata as metadata
...
이번에 표시된 값은 다음과 같습니다.
GPU: NVIDIA RTX PRO 6000 Blackwell Server Edition
GPU 메모리: 총 용량 95.0 GiB / 여유 94.4 GiB
디스크 여유: 193.0 GiB
용량 확인 단계에서 중단된 경우, 모델을 다운로드하기 전에 GPU를 다시 선택합니다.
체크(Check)를 삭제해도 필요한 메모리가 줄어드는 것은 아닙니다.
디스크 여유 90GiB라는 값 역시 준비상의 참고치일 뿐, 실제 측정된 최소값은 아닙니다.
2. 일본어 음성 4개 준비
이번에는 다음 4가지 대본을 사용하여 일본어 합성 음성을 만들었습니다.
| 파일 | 대본에서 변경한 점 | 예상 의도 |
|---|
| 01.wav | 취소를 명확히 요청함 | cancel |
| ... | | |
다음 코드에서는 edge-tts를 사용하여 일본어용 ja-JP-NanamiNeural을 사용합니다. | | |
rate="-5%"로 약간 느리게 읽고, MP3를 16kHz 모노럴 WAV로 변환합니다.
특정 인물의 목소리를 복제한 것이 아닙니다.
edge-tts는 온라인 음성 합성 서비스를 호출하므로, 음성 생성 시에는 인터넷 연결이 필요합니다. 이는 샘플 생성을 위한 통신일 뿐이며, Clef-Omni의 추론을 외부 API에 요청하는 것은 아닙니다.
import edge_tts
import soundfile as sf
from scipy.signal import resample_poly
...
02번 음성에서는 전반부와 후반부에서 의향이 바뀝니다.
'내일 예약을 취소하고 싶어서 전화했습니다. …… 아, 죄송합니다. 일정을 확인해 보니 갈 수 있을 것 같습니다. 취소하지 않고 그대로 부탁드립니다.'
모델에 전달하는 것은 이 내용을 읽어 녹음한 것입니다.
transcript와 expected는 샘플 관리를 위한 데이터이며, 후술할 요청에는 포함되지 않습니다.
생성된 음성 ZIP 사용 시
Clef_Omni_Audio_Samples.zip을 가지고 있는 경우, 위의 생성 셀 대신 다음 셀을 실행합니다.
기사만으로 진행하는 경우에는 이 셀을 건너뛰십시오.
ZIP 파일 안에는 samples.json과 audio/01.wav가 있습니다.
from audio/04.wav
입니다.
USE_AUDIO_ZIP = False # 생성된 ZIP을 사용할 경우에만 True
if USE_AUDIO_ZIP:
from google.colab import files
...
3. 모델을 처음 한 번만 로드하기
MODEL_ID로 모델을 지정하고, REVISION으로 이번에 사용한 버전을 지정합니다.
모델 업데이트에 따라 코드와 가중치(weights)의 조합이 바뀌지 않도록 동일한 커밋에서 가져옵니다.
joint_schema_model.py는 모델 제공처가 공개한 Python 코드입니다.
이를 불러와 공식 load_release_model을 호출합니다.
불러오는 코드는 실행 전에 고정 버전 소스에서 확인할 수 있습니다.
if 'model' not in globals()를 통해, 동일한 런타임(runtime)에 모델이 이미 로드되어 있다면 재사용합니다.
이 Notebook에서는 모델을 전환하지 않는 것을 전제로 합니다.
다른 모델을 같은 변수 이름으로 사용했다면, 새로운 런타임을 시작해 주십시오.
from huggingface_hub import snapshot_download
MODEL_ID = 'Cloudflare/clef-omni'
REVISION = '0db1cd2607d76a7bdb2a382f659e7b313079f84b'
...
이번의 출력 결과입니다.
다운로드 및 로드: 230.8초
모델 준비 완료.
230.8초에는 다운로드와 모델 로드가 포함됩니다.
순수한 로딩 시간만은 아닙니다.
importlib을 사용하는 부분은, 가져온 파일을 Python 모듈로 읽어들이는 처리입니다.
모델의 구조를 직접 정의하는 것이 아닙니다.
load_release_model이 백본(backbone)과 판단용 헤드(head)를 로드하고, processor가 음성 등의 전처리(pre-processing)를 담당합니다.
여기서 모델을 GPU 위에 준비한 후에는, 음성마다 다시 로드하지 않습니다.
HTTP 서버의 시작이나 127.0.0.1로의 접근도 없습니다.
Colab 세션이 사라지면, 다시 준비가 필요합니다.
4. 질문과 네 가지 선택지를 작성하기
다음 함수가 모델에 대한 입력을 만드는 부분입니다.
다른 용도로 응용할 때는, 먼저 state, instructions, criteria를 변경해야 합니다.
LABELS = {'cancel': 'キャンセル', 'keep': '予約を維持', 'change': '予約を変更', 'clarify': '確認が必要'}
def make_request(audio_path):
return {
...
intent는 저희가 붙인 질문의 이름입니다.
`type=
표시용 일본어는 LABELS로 대응시키고 있습니다.
공식 코드에서는 음성 파일을 16kHz 모노럴(mono)로 읽어 들인 후, processor가 생성한 음성 특징량을 모델에 전달합니다.
torch.inference_mode()는 학습용 계산 기록을 만들지 않고 추론하기 위한 설정입니다.
측정 전후에 torch.cuda.synchronize()를 넣은 것은 GPU 처리가 끝날 때까지 기다려서 시간을 측정하기 위함입니다.
측정 대상에는 systemone 내부의 음성 읽기, 전처리, 추론이 포함됩니다.
그 전에 수행하는 길이 확인용 읽기, 음성 재생, 화면 표시, 모델 다운로드 및 초기 로딩은 포함되지 않습니다.
결과는 건별로 results.json에 저장합니다.
이 함수를 정의하는 셀을 재실행하면 records가 비게 되므로, 남기고 싶은 로그가 있는 경우 미리 다운로드해 주십시오.
6. 4개의 음성을 순차적으로 판정하기
음성이 재생될 수 있도록 표시하고, 그 직후에 판정을 출력합니다.
모델의 출력을 받은 후, 예상 답변과 비교합니다.
import pandas as pd
summary = []
for sample in samples:
...
대본도 나란히 확인하려면 다음 셀을 사용합니다.
for sample in samples:
print(sample['file'], '예상:', LABELS[sample['expected']])
print(sample['transcript'], '
')
합성 음성 4개의 결과
이번 4개에서는 모두 예상한 후보가 선택되었습니다.
실행된 Notebook에 표시된 값을 요약합니다.
| 음성 | 예상 | 실제 판정 | 선택된 후보의 확률 | 판정 시간 |
|---|
| 01 | 취소 | 취소 | 89.8% | 2.83초 |
| ... | | | | |
01: '취소'를 명확하게 요청하기
죄송합니다. 내일 예약을 취소해 주세요. 일정이 바뀌어서 방문할 수 없게 되었습니다.
여기서는
파일은 Colab에 업로드되어 해당 런타임에서 처리됩니다.
from google.colab import files
TRY_MY_AUDIO = False
if TRY_MY_AUDIO:
...
이번에는 제가 녹음한 파일인 test00.m4a로도 시도해 보았습니다.
음성 길이는 약 3.93초입니다.
발화의 요지는 “예약에 대해, 10분 정도 지연될 것 같은데 괜찮을까요?”라는 문의 내용입니다.
여기에는 내용의 요약을 기재했으며, 엄밀한 전사(verbatim transcript)는 아닙니다. 이 음성 결과는 다음과 같았습니다.
판정: 예약 변경
취소: 6.0%
예약 유지: 8.2%
...
제 의도는 예약 변경을 확정하여 요청하는 것이 아니었습니다. 지연되어도 상대방의 대응 가능 여부를 묻고 있는 것입니다.
이번 4가지 선택지 중에서는 ‘확인이 필요함’으로 처리하고 싶은 경우입니다.
모델은 ‘10분 지연된다’는 내용을 시간 변경과 연결했을 수 있습니다.
하지만 이 결과만으로는 음성 이해와 선택지 해석 중 어느 부분에서 오류가 발생했는지 특정할 수 없습니다.
반면, ‘예약 유지’라고 답하는 것이 충분한지에 대해서도 의문이 듭니다. 지연되어도 예약을 유지할 수 있는지는 받는 측의 조건에 달려있기 때문입니다. 이번 입력에서는 그 조건을 제시하지 않았으므로, 우선 확인한다는 선택지를 마련한 의미가 있습니다.
합성 음성으로 4가지 경우 모두 예상과 일치하더라도, 저의 짧은 녹음 파일에서 이러한 경계 문제가 발생했습니다.
‘음성을 불러와 결과가 반환되는 것’과 ‘업무상 기대하는 분류가 나오는 것’은 분리하여 확인해야 합니다.
선택지 설명을 변경한다면
재실험을 한다면, 다음과 같이 기준을 구체화하는 방법이 있습니다.
이는 이번 결과를 도출한 설정이 아니며, 앞으로 시도할 수정안입니다. 개선되었다는 결과는 아직 없습니다.
| 후보 | 수정안 |
|---|
| 예약 변경 | 날짜나 시간 등을 변경해 달라고 명확하게 요청하는 경우. 공백 시간이 미확정이라도 변경 의향이 있으면 포함함 |
| 확인 필요 | 아직 의견을 결정하지 못했거나, 요금만 묻고 있거나, 지연되어도 대응 가능한지 묻는 등 답변이나 추가 확인이 필요한 경우 |
지연 예시만 맞도록 수정하면, 03의 ‘금요일로 변경하고 싶다’라는 명확한 요청까지도 확인 처리될 가능성이 있습니다. 따라서 수정 후에는 원래의 4가지 사례를 포함하여 재확인이 필요합니다.
이번에는 설정 변경 후의 결과와 섞지 않고, 위에 제시된 설정에서의 출력을 기록하겠습니다.
로딩(Loading) 이후 판정은 짧았다
이번에 초기 모델 준비에는 230.8초가 걸렸습니다. 한편, 음성 판정은 첫 번째가 2.83초였고, 이어진 4회는 0.16~0.17초였습니다.
처음과 이후의 시간이 다른 이유는 이 로그만으로는 구분할 수 없습니다. GPU의 초기 처리 등이 영향을 줄 가능성은 있지만, 각 처리 내역은 측정하지 않았습니다.
반복적으로 측정한 평균값이나 부하 테스트 결과도 아닙니다.
그럼에도 불구하고, 모델을 매번 로드하지 않는 구성으로 유지함으로써 녹음을 바꾸면서 시도할 수 있었습니다.
이 예시에서는 자유로운 문장이나 읽어주는 음성을 생성하지 않고, 후보의 확률값을 받아 표시하고 있습니다. 다만, 짧은 시간 안에 판정되었다는 것과 작은 GPU에서 구동된다는 것은 별개의 문제입니다.
이번에 사용한 모델을 유지하려면 큰 GPU 메모리가 필요했습니다.
결과와 실행 환경을 저장한다
마지막으로, 결과, 질문, 대본, 모델 버전, 라이브러리 버전을 ZIP 파일로 묶습니다. 나중에 기사나 실험 메모를 작성할 때 어떤 환경에서 작동했는지 확인할 수 있습니다.
from google.colab import files
(ROOT / 'timing.json').write_text(json.dumps({'download_and_load_seconds': LOAD_SECONDS}, indent=2))
(ROOT / 'pip_freeze.txt').write_text(__import__('subprocess').check_output([sys.executable, '-m', 'pip', 'freeze'], text=True))
...
results.json에는 입력한 질문과 모델의 출력이, summary.csv에는 4가지 사례의 비교 결과가 들어갑니다.
environment.json과 pip_freeze.txt는 실행 환경 기록입니다.
model_revision.txt는 모델과 공식 코드의 고정 버전을 보여줍니다.
사용자가 직접 업로드한 음성 파일은 이 결과 ZIP에 포함되어 있지 않습니다.
다만, 결과에는 음성 파일 이름이 기록됩니다.
Notebook 출력물에는 재생용 음성이 임베드될 수 있으므로, 실행된 Notebook을 배포할 때는 남아있는 녹음도 확인해 주십시오.
실행 시 중지되는 경우
| 상황 | 확인 사항 |
|---|
| GPU 용량 확인에서 중지됨 | High Memory가 아니라, GPU 자체의 이름과 용량을 확인하십시오 |
| ... | /content의 디스크 여유 공간을 확인하십시오 |
| 음성 생성에 실패함 | 음성 서비스 연결을 확인하십시오. 이미 생성된 ZIP 또는 본인의 녹음을 사용하십시오 |
| 30초 제한으로 중지됨 | 처음에는 짧은 녹음을 사용하십시오. 제한이 풀린 장시간 음성은 이 기사의 검증 범위를 벗어납니다 |
| 판정 결과는 나오나 예상과 다름 | 대본이나 발화 내용과 후보 설명을 대조해 보십시오. 실행 오류와는 구별합니다 |
로드 시, 이번에도 rope_parameters에 관한 경고가 표시되었습니다.
이후 모델 준비와 총 5건의 판정은 완료되었습니다.
경고만으로 실행 실패라고 간주하지는 않지만, 다른 환경에서도 무시해도 좋다고 보장하는 것은 아닙니다.
음성에 대하여
4개의 대본은 이 교재용으로 작성된 것입니다.
일본어용 ja-JP-NanamiNeural로 합성된 음성이며, 실제 고객의 녹음이 아닙니다. 게재할 때도 합성 음성에 의한 실험으로 취급합니다.
본인의 목소리로 시도한 녹음은 합성 음성 4개와 분리하여 결과를 보여주었습니다.
edge-tts의 소프트웨어 라이선스와 음성 서비스 및 생성 음성의 이용 조건은 별개입니다. 모델의 Apache-2.0 라이선스를 생성 음성의 라이선스로 취급하지 않습니다.
시도해 본 결과
일본어 음성을 제공하고, 준비된 후보 중에서 답을 선택하게 하는 과정까지 Colab 상에서 실행할 수 있었습니다.
합성 음성의 경우, 도중에 취소를 철회하는 예나 취소할지 미결정인 예도 예상한 후보가 되었습니다.
반면, 본인의 녹음의 경우에는 지각 가능 여부에 대한 질문을 예약 변경으로 판단했습니다.
실제 문의를 다룰 때는 모델뿐만 아니라, 우리가 정의하는 후보의 경계 또한 문제가 됩니다. 용도를 바꿀 때도 음성, 질문, 선택지라는 구성은 같습니다.
우선 짧은 녹음을 사용하여, 명확한 요청뿐만 아니라 말실수나 질문만 하는 발화까지 포함하여 기대하는 판정 결과와 비교해 보는 것이 좋을 것 같습니다.
참고 자료
토론 (Discussion)