중국어 음성 합성: 텍스트 정규화 및 엔지니어링
이 스킬은 매일 말하는 AI 데스크펫(하단 '출처 및 유지보수' 참조)에서 비롯되었습니다: Claude 스트리밍 응답을 문장 단위로 로컬 TTS에 전달하여 읽습니다. 아래 규칙은 모두 테스트되었으며, 숫자는 측정된 값입니다.
한 줄 원칙: 읽기 방식은 우리 쪽에서 변환하고, 엔진은 발음만 담당합니다. 엔진에 넘기면 엔진마다 읽는 방식이 달라집니다. 기성 TN 라이브러리로 교체해도 몇몇 부분이 달라집니다. 기성 라이브러리(wetext, PaddleSpeech)는 비교 및 규칙 보완용으로 사용합니다.
흐름
모델의 스트리밍 텍스트
→ ① 필터링: 장식, 이모지, 목록 기호 제거; 코드 블록, 표, 링크는 한 문장으로 변환(스트리밍 시 분할된 것을 인식할 수 있어야 함)
→ ② 읽기 방식 변환: 숫자, 시간, 단위, 기호, 발음 대체표(스트리밍 시 경계 유지)
→ ③ 문장 분할: 첫 번째 단락은 첫 번째 쉼표까지만 자르고, 이후 40자를 넘으면 쉼표에서 자름
→ ④ 합성: 현재 읽고 있는 위치보다 약 20초 앞서서만 합성; 중단 시 실제로 추론을 멈춤
순서를 바꾸면 안 됩니다: 먼저 읽기 방식 변환, 그다음 문장 분할. 먼저 자르고 정리하면 실제로 세 가지 오류가 발생합니다:
| 원문 | 먼저 자른 결과 | 문제점 |
|---|---|---|
오후 14:30 회의 | 오후 14: / 30 회의 | 콜론이 쉼표로 처리되어 시간이 분할됨 |
여기 봐: https://…/pull/7 하면 됨 | https: / //…/pull/7 하면 됨 | 링크가 반으로 잘려 주소가 한 글자씩 읽힘 |
대략 3~5일 | 대략 35일 | ~가 장식으로 제거되어 '삼십오일'로 읽힘 |
반각 :는 뒤에 공백이 있을 때만 쉼표로 처리합니다.
스트리밍 시 경계 유지
전체 문장을 올바르게 변환해도 스트리밍에서는 다를 수 있습니다. 한 글자씩 스트리밍할 때 '일한 지 26년', '80년대'가 '이육년', '팔영대'로 읽히는 것은 숫자 뒤의 글자가 아직 도착하지 않았기 때문입니다. -10°C 부분은 영어로 그대로 읽힙니다.
- 숫자 뒤에 2글자를 더 기다렸다가 내보냅니다. 단위는 공백이 있어도 인식합니다(
달렸다 5먼저 보류,km를 기다림). - 다음 단락으로 넘어갈 때 이전 단락 끝의 몇 글자를 포함시킵니다('한 지 26 년'을 판단하려면 앞의 '한 지'를 봐야 함).
- 최대 200자를 보류하며, 코드 블록, 표는 최대 4000자까지 보류하고, 상한에 도달하면 내보냅니다.
- 각 테스트는 '한 글자씩 입력'과 '전체 단락 입력' 각각 실행하여 결과가 같아야 합니다.
규칙 빠른 참조 (자세한 내용은 references/rules.md 참조)
| 분류 | 예시 | 읽기 방식 |
|---|---|---|
| 연도 | 2026년, 2026회, 26년에 보자 | 각 자리: 이영이육년, 이영이육회, 이육년에 보자 |
| 기간 | 일한 지 26년, 80년대, 26년 전 | 수량대로: 이십육 년, 팔십 년대, 이십육 년 전 |
| 번호 | 전화번호, 인증코드, 번호 27149, 방 1201 | 각 자리, 1은 '요', 하이픈과 공백에서 잠시 멈춤 |
| 다섯 자리 수 | 95588 / 가격 12999 | 수량사 없음, 0으로 끝나지 않음, 앞에 돈 관련 표현이 아님 → 번호; 그 외 → 수량대로 |
| 인터넷 용어 | 666, 2333, 520 단독 / 666개 좋아요, 888 씀 | 각 자리 / 수량대로 (고정된 목록만 인식) |
| 단위 앞의 2 | 2개, 2L, 2℃ | 두 개, 두 리터; 단 온도는 제외: 이 섭씨도 |
| 단위 | 60km/h, 3kWh, 5분, 10mg | 매시간 육십 킬로미터, 삼 킬로와트시, 오 분, 십 밀리그램 |
| 변환하지 않는 단위 | 4G 네트워크, 5A 관광지, 16GB | 원본 그대로 엔진에 전달 |
| 범위, 퍼센트 | 3~5%, 3-5km, -1.5~2 | 백분의 삼에서 오, 삼에서 오 킬로미터, 음 일점 오에서 이 |
| 시간 | 14:30, 13:00, 9:05 | 십사시 삼십분, 십삼시, 구시 오분 (24시간제) |
| 날짜, 분수 | 10/26 제출 / 관객의 7/12 | 시월 이십육일 / 십이분의 칠 (앞에 '의, 약' 등이 있을 때만 분수) |
| 기호 | #15, 2x, x3, 100w | 십오호, 두 배, 곱하기 삼, 백만 |
대조 예제로 확인
references/golden-cases.json에는 115개의 전체 단락 예제와 24개의 스트리밍 예제가 있으며, 모두 원 프로젝트에서 검증된 읽기 방식입니다. scripts/check-tn.mjs는 이 예제로 구현을 테스트합니다:
node scripts/check-tn.mjs ./my-tn.mjs # normalize(text) 및 (선택 사항) createStream()을 내보내는 모듈
node scripts/check-tn.mjs ./my-tn.mjs --only 번호 # 특정 그룹만 테스트
- 비교 시 공백과 일반적인 구두점은 무시하고 읽기 방식만 비교합니다.
note가 있는 예제는 '참고'로, 실패로 간주하지 않고 보고만 합니다. - 새 TN을 작성할 때 이를 합격 기준으로 사용하고, 규칙을 변경할 때는 전체를 실행하여 한 곳을 고치고 다른 곳이 망가지지 않도록 합니다.
- 논란이 있는 읽기 방식(예: '이 컴퓨터 12999'가 번호로 처리되는 경우)은 원 프로젝트에서 수용했습니다. 제품에서 다른 읽기 방식을 원하면 예제를 변경하고 예제 옆에 이유를 적습니다.
잘못 읽은 경우 확인 방법
네 가지 분류로 나눕니다: 변환 전에 읽기 방식으로 작성되지 않음; 다의문자는 모델이 잘못 추측; 참조 녹음에 문제 있음; 무작위(다시 한 번 시도).
현재 음성과 다른 두 음성으로 같은 문장을 각각 읽어봅니다: 모든 음성에서 같은 글자를 잘못 읽으면 발음 대체표를 추가하고, 한 음성만 이상하면 음성을 바꿉니다. 매번 원문과 변환된 읽기 방식을 로그(jsonl)에 함께 기록하여 나중에 확인할 수 있도록 합니다.
엔지니어링 (자세한 내용은 references/engines.md 참조)
- 첫 문장 지연: 첫 번째 단락은 첫 번째 쉼표까지만 자릅니다. Matcha에서 첫 글자가 도착한 후 0.65초 후에 첫 번째 오디오가 출력됩니다.
- 가장 효과적인 속도 향상은 캐싱: 합성되지 않은 문장의 경우 CosyVoice 첫 블록이 2.6~3초, 캐시 적중 시 6밀리초. 사용자가 말을 시작하면 미리 합성하여 채워넣습니다('음', '생각 좀 해볼게').
- 취소는 실제로 추론을 멈춰야 함: vLLM 연결이 끊어지면
abort_request를 호출하지 않으면 다음 문장을 4.9초 기다려야 함(수정 후 0.8~1.7초). - 영어는 자동으로 건너뛸 수 있음: 일부 엔진은
GitHub,PR,%를 읽지 않습니다. 의존성 업그레이드 전에 영어를 다시 확인하세요. - 백업 음원: 주 음원이 준비되지 않았거나 처음에 오류가 발생하면 백업으로 전환; 읽는 도중 오류가 발생하면 다시 읽지 않습니다. 서비스의 HTTP가 올라왔다고 모델이 로드된 것은 아닙니다. 503을 만나면 재시도해야 합니다.
출처 및 유지보수
- 출처: Cirra (AI 데스크펫, RedCarpG/cirra-deskpet) 및 해당 음성 서비스 cirra-voice, 2026-10 ~ 2026-11.
- 코드:
sidecar/src/tts/speech/(numbers.ts,units.ts,links.ts,filter.ts,overrides.ts,speech.test.ts),sidecar/src/tts/splitter.ts,sidecar/src/tts/pronunciations.ts. - 문서:
docs/speech.md,docs/speech-tn.md(119문장 삼자 대조),docs/tts.md,docs/voices.md,docs/voice-chat.md,docs/voice-chat-2.md,docs/voice-install.md,docs/perf-checkup.md. - 경험: 개인 저장소의
lessons/chinese-tts.md.
- 코드:
- 원천: RedCarpG/redcarpg-skills의
skills/chinese-tts/.~/.claude/skills/에 있는 것은 설치된 사본이므로 직접 수정하지 마십시오.references/golden-cases.json은 2026-11-10에 Cirra의speech.test.ts에서 스크립트로 추출한 것으로, 양측이 자동으로 동기화되지는 않습니다. - 수정 방법:
- redcarpg-skills에서 수정하고 PR을 제출합니다.
manifest.json버전을 올리고node scripts/validate.mjs통과 후 태그를 발행합니다(tagchinese-tts-v<버전>). 그런 다음 로컬 사본을 다시 설치합니다. 먼저lessons/chinese-tts.md에 추가할 새 항목이 있는지 확인합니다. - Cirra의 TN 규칙이 변경되면 예제를 다시 추출합니다(추출 방법:
speech.test.ts에서readNumbers그룹과 '스트리밍' 그룹의[원문, 읽기 방식]을 읽습니다). 그리고 Cirra가 빌드한sidecar/dist/tts/를 사용하여 어댑터 모듈을 만들어check-tn.mjs를 실행합니다. 모두 통과해야 합니다. - 엔진 수치(지연 시간, 실시간 비율, GPU 메모리)는 2026-10에 8GB VRAM, CPU에 VNNI가 없는 컴퓨터에서 측정된 것입니다. 하드웨어가 변경되면 다시 측정하십시오.
- redcarpg-skills에서 수정하고 PR을 제출합니다.
- 업데이트 기록:
- 2026-11-12 · 1.0.0, redcarpg-skills로 이동하여 공개 배포. 로컬 절대 경로 수정, 원천 프로젝트는 링크로 작성. 내용은 변경되지 않음.
- 2026-11-10 · Cirra · 초판: 흐름 및 순서, 스트리밍 경계, 규칙 빠른 참조 및 세부 규칙, 잘못 읽은 경우 디버깅, 엔진 엔지니어링; 139개 대조 예제 및
check-tn.mjs(Cirra의 구현으로 실행하여 모두 통과 확인);evals/에 두 개의 테스트 프롬프트.