feat: release Find-Word v0.1

This commit is contained in:
oz
2026-10-14 06:19:24 +00:00
parent 28ee2ce498
commit 1c0069c367
258 changed files with 21509 additions and 0 deletions
+71
View File
@@ -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):,} 바이트")