feat: release Find-Word v0.1
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""직접 작성한 한국어 문장을 로컬 포먼트 합성기로 WAV로 만든다.
|
||||
|
||||
네트워크 TTS·녹음 샘플·API 키를 사용하지 않는다. 원문은 TypeScript 데이터에서
|
||||
읽어 중복 관리를 없애고, 바이트 해시를 남겨 빌드에서 자산 누락·변조를 찾는다.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import wave
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
OUTPUT = ROOT / "public/audio"
|
||||
LOCAL = ROOT / ".cache/espeak/root/usr"
|
||||
ENV = os.environ.copy()
|
||||
ENGINE = shutil.which("espeak-ng")
|
||||
if ENGINE is None:
|
||||
# 시스템 설치 권한 없이 worktree 안에 압축 해제한 Ubuntu 패키지도 지원한다.
|
||||
ENGINE = str(LOCAL / "bin/espeak-ng")
|
||||
ENV["LD_LIBRARY_PATH"] = str(LOCAL / "lib/x86_64-linux-gnu")
|
||||
ENV["ESPEAK_DATA_PATH"] = str(LOCAL / "lib/x86_64-linux-gnu/espeak-ng-data")
|
||||
|
||||
source = subprocess.check_output([
|
||||
"node", "--experimental-strip-types", "--input-type=module", "-e",
|
||||
"import {INTRO,PATTERNS,WORDS,renderSentence} from './src/content.ts'; "
|
||||
"console.log(JSON.stringify([{file:'intro.wav',text:INTRO},"
|
||||
"...PATTERNS.flatMap(p=>WORDS.map(w=>({file:p.id+'-'+w.id+'.wav',"
|
||||
"text:renderSentence(p,w)})))]));",
|
||||
], cwd=ROOT, text=True)
|
||||
entries = json.loads(source)
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
# 버전을 상수로 쓰면 다른 PC에서 재생성할 때 출처가 거짓이 된다. 실행 중인
|
||||
# 도구에서 직접 읽되 eSpeak의 로컬 데이터 경로는 재현성 비교에서 제외한다.
|
||||
versions = {
|
||||
"espeak": subprocess.check_output([ENGINE, "--version"], env=ENV, text=True).split("Data at:")[0].strip(),
|
||||
"ffmpeg": subprocess.check_output(["ffmpeg", "-version"], text=True).splitlines()[0],
|
||||
}
|
||||
for entry in entries:
|
||||
# 명령 인자로 텍스트를 넘겨 쉘 보간을 피한다. 합성 속도·피치·리샘플링
|
||||
# 설정을 고정하고 메타데이터 없는 표준 PCM 헤더를 직접 작성한다.
|
||||
speech = subprocess.check_output([
|
||||
ENGINE, "-v", "ko", "-s", "145", "-p", "45", "-a", "100", "--stdout", entry["text"],
|
||||
], env=ENV)
|
||||
pcm = subprocess.check_output([
|
||||
"ffmpeg", "-hide_banner", "-loglevel", "error", "-i", "pipe:0",
|
||||
"-ar", "16000", "-ac", "1", "-f", "s16le", "pipe:1",
|
||||
], input=speech)
|
||||
stream = io.BytesIO()
|
||||
with wave.open(stream, "wb") as output:
|
||||
output.setnchannels(1)
|
||||
output.setsampwidth(2)
|
||||
output.setframerate(16000)
|
||||
output.writeframes(pcm)
|
||||
data = stream.getvalue()
|
||||
(OUTPUT / entry["file"]).write_bytes(data)
|
||||
entry["sha256"] = hashlib.sha256(data).hexdigest()
|
||||
entry["bytes"] = len(data)
|
||||
|
||||
manifest = {
|
||||
"generator": "eSpeak NG ko, speed 145, pitch 45, amplitude 100; FFmpeg PCM 16kHz mono s16",
|
||||
"toolVersions": versions,
|
||||
"source": "https://github.com/espeak-ng/espeak-ng",
|
||||
"method": "자체 작성 문장의 오프라인 포먼트 합성; 외부 녹음·네트워크 TTS 없음",
|
||||
"files": entries,
|
||||
}
|
||||
(OUTPUT / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")
|
||||
print(f"{len(entries)}개 WAV 생성, 합계 {sum(e['bytes'] for e in entries):,} 바이트")
|
||||
Reference in New Issue
Block a user