This commit is contained in:
Dita Aji Pratama 2026-09-18 17:02:51 +07:00
parent a7152733b2
commit 7d67b4af6c
6 changed files with 164 additions and 1 deletions

View File

@ -1,6 +1,6 @@
# STT Runner # STT Runner
Speech-to-Text transcription using sherpa-onnx + Qwen3-ASR. Speech-to-Text transcription using sherpa-onnx + Qwen3-ASR, plus Text-to-Speech with ZipVoice (zero-shot voice cloning).
## Installation ## Installation
@ -11,16 +11,27 @@ python3 -m venv .venv
## Usage ## Usage
### Speech-to-Text
```bash ```bash
python stt_runner.py [--language=Indonesian] audio1.wav audio2.wav ... python stt_runner.py [--language=Indonesian] audio1.wav audio2.wav ...
``` ```
### Text-to-Speech
```bash
python tts_runner.py [--output=out.wav] [--ref-audio=ref.wav] [--ref-text="..."] "text to speak"
```
Output defaults to `output.wav`. The reference audio/text (voice to clone) is set in `config/tts.py` and can be overridden per-run with `--ref-audio` / `--ref-text` (the text must match the audio exactly).
## Configuration ## Configuration
Model paths and inference parameters are hardcoded in `config/`: Model paths and inference parameters are hardcoded in `config/`:
- `config/model.py` — model paths (conv_frontend, encoder, decoder, tokenizer under `models/`) - `config/model.py` — model paths (conv_frontend, encoder, decoder, tokenizer under `models/`)
- `config/asr.py` — inference params: `LANGUAGE`, `HOTWORDS`, `NUM_THREADS`, `PROVIDER`, `SAMPLE_RATE`, `FEATURE_DIM`, `MAX_TOTAL_LEN`, `MAX_NEW_TOKENS` - `config/asr.py` — inference params: `LANGUAGE`, `HOTWORDS`, `NUM_THREADS`, `PROVIDER`, `SAMPLE_RATE`, `FEATURE_DIM`, `MAX_TOTAL_LEN`, `MAX_NEW_TOKENS`
- `config/tts.py` — TTS model paths, `REFERENCE_AUDIO`, `REFERENCE_TEXT`, `OUTPUT_FILE`, `NUM_THREADS`, `PROVIDER`, `NUM_STEPS`
`LANGUAGE` defaults to `""` (all languages / auto-detect). Passing `--language` on the CLI overrides it. `LANGUAGE` defaults to `""` (all languages / auto-detect). Passing `--language` on the CLI overrides it.
@ -36,3 +47,13 @@ for f in vocab.json merges.txt tokenizer_config.json preprocessor_config.json co
wget -O "models/tokenizer/$f" "$BASE/tokenizer/$f" wget -O "models/tokenizer/$f" "$BASE/tokenizer/$f"
done done
``` ```
## Download Model (ZipVoice TTS)
```bash
mkdir -p models/zipvoice
wget -qO- https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2 \
| tar xjf - -C models/zipvoice --strip-components=1
wget -O models/zipvoice/vocos_24khz.onnx \
https://github.com/k2-fsa/sherpa-onnx/releases/download/vocoder-models/vocos_24khz.onnx
```

23
config/tts.py Normal file
View File

@ -0,0 +1,23 @@
from config.model import MODEL_DIR
ZIPVOICE = MODEL_DIR / "zipvoice"
TOKENS = ZIPVOICE / "tokens.txt"
ENCODER = ZIPVOICE / "encoder.int8.onnx"
DECODER = ZIPVOICE / "decoder.int8.onnx"
DATA_DIR = ZIPVOICE / "espeak-ng-data"
LEXICON = ZIPVOICE / "lexicon.txt"
VOCODER = ZIPVOICE / "vocos_24khz.onnx"
# Reference voice for zero-shot voice cloning.
# REFERENCE_TEXT must match what is spoken in REFERENCE_AUDIO exactly,
# otherwise the cloned voice quality will noticeably degrade.
REFERENCE_AUDIO = ZIPVOICE / "test_wavs" / "leijun-1.wav"
REFERENCE_TEXT = "那还是三十六年前, 一九八七年. 我呢考上了武汉大学的计算机系."
NUM_THREADS = 2
PROVIDER = "cpu"
NUM_STEPS = 4 # Generation quality/speed tradeoff (higher = better, slower)
# Default output filename.
OUTPUT_FILE = "output.wav"

7
core/tts_room.py Normal file
View File

@ -0,0 +1,7 @@
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--output", type=str, default=None, help="Output WAV file, overrides config OUTPUT_FILE")
parser.add_argument("--ref-audio", type=str, default=None, help="Reference audio for voice cloning, overrides config REFERENCE_AUDIO")
parser.add_argument("--ref-text", type=str, default=None, help="Reference text matching --ref-audio, overrides config REFERENCE_TEXT")
parser.add_argument("text", type=str, help="Text to synthesize into speech")

75
tts_runner.py Normal file
View File

@ -0,0 +1,75 @@
import sys
import time
from pathlib import Path
import sherpa_onnx, soundfile as sf
from core import tts_room as ap
from config.tts import (
TOKENS, ENCODER, DECODER, DATA_DIR, LEXICON, VOCODER,
REFERENCE_AUDIO, REFERENCE_TEXT, NUM_THREADS, PROVIDER, NUM_STEPS, OUTPUT_FILE,
)
def tts_run(args):
ref_audio = Path(args.ref_audio) if args.ref_audio is not None else Path(REFERENCE_AUDIO)
ref_text = args.ref_text if args.ref_text is not None else REFERENCE_TEXT
if not ref_audio.is_file():
print(f"Reference audio not found: {ref_audio}", file=sys.stderr)
return
print("Loading model...")
tts_config = sherpa_onnx.OfflineTtsConfig(
model=sherpa_onnx.OfflineTtsModelConfig(
zipvoice=sherpa_onnx.OfflineTtsZipvoiceModelConfig(
tokens = str(TOKENS),
encoder = str(ENCODER),
decoder = str(DECODER),
data_dir = str(DATA_DIR),
lexicon = str(LEXICON),
vocoder = str(VOCODER),
),
debug = False,
num_threads = NUM_THREADS,
provider = PROVIDER,
)
)
if not tts_config.validate():
raise ValueError("Invalid TTS config. Please read the previous error messages.")
tts = sherpa_onnx.OfflineTts(tts_config)
print("Model ready!")
reference_audio, sr = sf.read(str(ref_audio), dtype="float32")
if reference_audio.ndim > 1:
reference_audio = reference_audio[:, 0]
gen_config = sherpa_onnx.GenerationConfig()
gen_config.reference_audio = reference_audio
gen_config.reference_sample_rate = sr
gen_config.reference_text = ref_text
gen_config.num_steps = NUM_STEPS
output_file = args.output if args.output is not None else OUTPUT_FILE
print("Generating...")
start = time.time()
audio = tts.generate(args.text, gen_config)
elapsed = time.time() - start
if len(audio.samples) == 0:
print("Error in generating audio. Please read the previous error messages.", file=sys.stderr)
return
sf.write(output_file, audio.samples, samplerate=audio.sample_rate, subtype="PCM_16")
duration = len(audio.samples) / audio.sample_rate
print()
print(f"Output : {output_file}")
print(f"Text : {args.text}")
print(f"Duration : {duration:.3f} s")
print(f"Elapsed : {elapsed:.3f} s")
print(f"RTF : {elapsed:.3f} / {duration:.3f} = {elapsed / duration:.3f}")
print()
if __name__ == "__main__":
tts_run( ap.parser.parse_args() )

19
usage-tts.sh Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PYTHON="${PYTHON:-$BASE_DIR/.venv/bin/python}"
if [ ! -x "$PYTHON" ]; then
echo "Venv not found. Create it with: python3 -m venv .venv && .venv/bin/pip install -r requirements.txt" >&2
exit 1
fi
if [ $# -lt 1 ]; then
echo "Usage: $0 <text> [--output <file.wav>] [--ref-audio <ref.wav>] [--ref-text <text>]" >&2
echo " Synthesize speech with ZipVoice zero-shot TTS (sherpa-onnx)." >&2
echo " Reference audio/text defaults are set in config/tts.py." >&2
exit 1
fi
"$PYTHON" "$BASE_DIR/tts_runner.py" "$@"

18
usage.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PYTHON="${PYTHON:-$BASE_DIR/.venv/bin/python}"
if [ ! -x "$PYTHON" ]; then
echo "Venv not found. Create it with: python3 -m venv .venv && .venv/bin/pip install -r requirements.txt" >&2
exit 1
fi
if [ $# -lt 1 ]; then
echo "Usage: $0 <audio_file...>" >&2
echo " Transcribe audio with Qwen3-ASR 1.7B (sherpa-onnx)." >&2
exit 1
fi
"$PYTHON" "$BASE_DIR/stt_runner.py" "$@"