package main
|
|
import (
|
"encoding/binary"
|
"flag"
|
"fmt"
|
"os"
|
"time"
|
"voicesnap/internal/engine"
|
)
|
|
func main() {
|
wavPath := flag.String("wav", "", "16kHz mono PCM16 WAV file")
|
runs := flag.Int("runs", 3, "number of recognition runs")
|
flag.Parse()
|
|
if *wavPath == "" {
|
fmt.Fprintln(os.Stderr, "missing -wav")
|
os.Exit(2)
|
}
|
if *runs < 1 {
|
*runs = 1
|
}
|
|
samples, sampleRate, err := readPCM16WAV(*wavPath)
|
if err != nil {
|
fmt.Fprintf(os.Stderr, "read wav: %v\n", err)
|
os.Exit(1)
|
}
|
if sampleRate != 16000 {
|
fmt.Fprintf(os.Stderr, "unsupported sample rate %d: provide 16kHz mono PCM16 WAV\n", sampleRate)
|
os.Exit(1)
|
}
|
|
eng, err := engine.New()
|
if err != nil {
|
fmt.Fprintf(os.Stderr, "init engine: %v\n", err)
|
os.Exit(1)
|
}
|
defer eng.Close()
|
|
audioMS := int64(len(samples)) * 1000 / 16000
|
fmt.Printf("engine=%q samples=%d audio_ms=%d runs=%d\n", eng.HardwareInfo(), len(samples), audioMS, *runs)
|
var total int64
|
for i := 1; i <= *runs; i++ {
|
start := time.Now()
|
text, err := eng.Recognize(samples)
|
elapsedMS := time.Since(start).Milliseconds()
|
if err != nil {
|
fmt.Fprintf(os.Stderr, "run=%d error=%v\n", i, err)
|
os.Exit(1)
|
}
|
total += elapsedMS
|
fmt.Printf("run=%d recognize_ms=%d rtf=%.3f text_runes=%d\n", i, elapsedMS, float64(elapsedMS)/float64(audioMS), len([]rune(text)))
|
}
|
fmt.Printf("avg_recognize_ms=%d\n", total/int64(*runs))
|
}
|
|
func readPCM16WAV(path string) ([]float32, int, error) {
|
data, err := os.ReadFile(path)
|
if err != nil {
|
return nil, 0, err
|
}
|
if len(data) < 12 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
|
return nil, 0, fmt.Errorf("not a RIFF/WAVE file")
|
}
|
|
var audioFormat, channels, bitsPerSample uint16
|
var sampleRate uint32
|
var pcm []byte
|
for offset := 12; offset+8 <= len(data); {
|
chunkID := string(data[offset : offset+4])
|
chunkSize := int(binary.LittleEndian.Uint32(data[offset+4 : offset+8]))
|
chunkStart := offset + 8
|
chunkEnd := chunkStart + chunkSize
|
if chunkEnd > len(data) {
|
return nil, 0, fmt.Errorf("truncated %s chunk", chunkID)
|
}
|
|
switch chunkID {
|
case "fmt ":
|
if chunkSize < 16 {
|
return nil, 0, fmt.Errorf("fmt chunk too small")
|
}
|
audioFormat = binary.LittleEndian.Uint16(data[chunkStart : chunkStart+2])
|
channels = binary.LittleEndian.Uint16(data[chunkStart+2 : chunkStart+4])
|
sampleRate = binary.LittleEndian.Uint32(data[chunkStart+4 : chunkStart+8])
|
bitsPerSample = binary.LittleEndian.Uint16(data[chunkStart+14 : chunkStart+16])
|
case "data":
|
pcm = data[chunkStart:chunkEnd]
|
}
|
|
offset = chunkEnd
|
if offset%2 == 1 {
|
offset++
|
}
|
}
|
|
if audioFormat != 1 || channels != 1 || bitsPerSample != 16 {
|
return nil, 0, fmt.Errorf("unsupported WAV format: audio_format=%d channels=%d bits_per_sample=%d; want PCM16 mono", audioFormat, channels, bitsPerSample)
|
}
|
if len(pcm) == 0 {
|
return nil, 0, fmt.Errorf("missing data chunk")
|
}
|
if len(pcm)%2 != 0 {
|
return nil, 0, fmt.Errorf("odd PCM data length")
|
}
|
|
samples := make([]float32, len(pcm)/2)
|
for i := range samples {
|
v := int16(binary.LittleEndian.Uint16(pcm[i*2 : i*2+2]))
|
samples[i] = float32(v) / 32768.0
|
}
|
return samples, int(sampleRate), nil
|
}
|