Compare commits

..

3 Commits

Author SHA1 Message Date
wehub-resource-sync fc584f266d docs: make Chinese README the default 2026-07-13 10:43:58 +00:00
wehub-resource-sync ad6bbd3331 docs: preserve upstream English README 2026-07-13 10:43:32 +00:00
wehub-resource-sync 2dd9ea9aee chore: import upstream snapshot with attribution 2026-07-13 12:45:58 +08:00
90 changed files with 2504 additions and 345 deletions
+1
View File
@@ -10,6 +10,7 @@ add_library(vosk
src/recognizer.cc
src/spk_model.cc
src/vosk_api.cc
src/postprocessor.cc
)
find_package(kaldi REQUIRED)
+27
View File
@@ -0,0 +1,27 @@
# Vosk Speech Recognition Toolkit
Vosk is an offline open source speech recognition toolkit. It enables
speech recognition for 20+ languages and dialects - English, Indian
English, German, French, Spanish, Portuguese, Chinese, Russian, Turkish,
Vietnamese, Italian, Dutch, Catalan, Arabic, Greek, Farsi, Filipino,
Ukrainian, Kazakh, Swedish, Japanese, Esperanto, Hindi, Czech, Polish.
More to come.
Vosk models are small (50 Mb) but provide continuous large vocabulary
transcription, zero-latency response with streaming API, reconfigurable
vocabulary and speaker identification.
Speech recognition bindings implemented for various programming languages
like Python, Java, Node.JS, C#, C++, Rust, Go and others.
Vosk supplies speech recognition for chatbots, smart home appliances,
virtual assistants. It can also create subtitles for movies,
transcription for lectures and interviews.
Vosk scales from small devices like Raspberry Pi or Android smartphone to
big clusters.
# Documentation
For installation instructions, examples and documentation visit [Vosk
Website](https://alphacephei.com/vosk).
+13 -19
View File
@@ -1,27 +1,21 @@
<!-- WEHUB_ZH_README -->
> [!NOTE]
> 本文档由 WeHub 基于上游 README 翻译整理,属于社区翻译,非官方中文文档。
> [English](./README.en.md) · [原始项目](https://github.com/alphacep/vosk-api) · [上游 README](https://github.com/alphacep/vosk-api/blob/HEAD/README.md)
> 原作者、版权与许可证归属以原始项目及本仓库 LICENSE 文件为准。
# Vosk Speech Recognition Toolkit
Vosk is an offline open source speech recognition toolkit. It enables
speech recognition for 20+ languages and dialects - English, Indian
English, German, French, Spanish, Portuguese, Chinese, Russian, Turkish,
Vietnamese, Italian, Dutch, Catalan, Arabic, Greek, Farsi, Filipino,
Ukrainian, Kazakh, Swedish, Japanese, Esperanto, Hindi, Czech, Polish.
More to come.
Vosk 是一款离线开源语音识别工具包。它支持 20 多种语言及方言的语音识别——英语、印度英语、德语、法语、西班牙语、葡萄牙语、中文、俄语、土耳其语、越南语、意大利语、荷兰语、加泰罗尼亚语、阿拉伯语、希腊语、波斯语、菲律宾语、乌克兰语、哈萨克语、瑞典语、日语、世界语、印地语、捷克语、波兰语。更多语言即将推出。
Vosk models are small (50 Mb) but provide continuous large vocabulary
transcription, zero-latency response with streaming API, reconfigurable
vocabulary and speaker identification.
Vosk 模型体积小(50 Mb),但可提供连续大词汇量转写、通过流式 API(streaming API)实现零延迟响应、可重新配置的词汇表以及说话人识别。
Speech recognition bindings implemented for various programming languages
like Python, Java, Node.JS, C#, C++, Rust, Go and others.
已为 Python、Java、Node.JS、C#、C++、Rust、Go 等多种编程语言实现语音识别绑定。
Vosk supplies speech recognition for chatbots, smart home appliances,
virtual assistants. It can also create subtitles for movies,
transcription for lectures and interviews.
Vosk 为聊天机器人、智能家居设备、虚拟助手提供语音识别,还可为电影生成字幕,为讲座和访谈提供转写。
Vosk scales from small devices like Raspberry Pi or Android smartphone to
big clusters.
Vosk 的部署规模可从小型设备(如 Raspberry Pi Android 智能手机)扩展到大型集群。
# Documentation
# 文档
For installation instructions, examples and documentation visit [Vosk
Website](https://alphacephei.com/vosk).
有关安装说明、示例和文档,请访问 [Vosk 网站](https://alphacephei.com/vosk).
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`alphacep/vosk-api`
- 原始仓库:https://github.com/alphacep/vosk-api
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+4 -4
View File
@@ -4,13 +4,13 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.0'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.24.0'
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.34.0'
}
}
allprojects {
version = '0.3.50'
version = '0.3.75'
}
subprojects {
@@ -24,7 +24,7 @@ subprojects {
}
mavenPublishing {
publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.S01, false)
publishToMavenCentral()
signAllPublications()
}
+11 -6
View File
@@ -29,7 +29,7 @@ set -x
OS_NAME=`echo $(uname -s) | tr '[:upper:]' '[:lower:]'`
ANDROID_TOOLCHAIN_PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/${OS_NAME}-x86_64
WORKDIR_BASE=`pwd`/build
PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/${OS_NAME}-x86_64/bin:$PATH
PATH=$ANDROID_TOOLCHAIN_PATH/bin:$PATH
OPENFST_VERSION=1.8.0
for arch in armeabi-v7a arm64-v8a x86_64 x86; do
@@ -45,6 +45,7 @@ case $arch in
CC=armv7a-linux-androideabi21-clang
CXX=armv7a-linux-androideabi21-clang++
ARCHFLAGS="-mfloat-abi=softfp -mfpu=neon"
PAGESIZE_LDFLAGS=""
;;
arm64-v8a)
BLAS_ARCH=ARMV8
@@ -54,6 +55,8 @@ case $arch in
CC=aarch64-linux-android21-clang
CXX=aarch64-linux-android21-clang++
ARCHFLAGS=""
# Ensure compatibility with 16KiB page size devices
PAGESIZE_LDFLAGS="-Wl,-z,common-page-size=4096 -Wl,-z,max-page-size=16384"
;;
x86_64)
BLAS_ARCH=ATOM
@@ -63,6 +66,7 @@ case $arch in
CC=x86_64-linux-android21-clang
CXX=x86_64-linux-android21-clang++
ARCHFLAGS=""
PAGESIZE_LDFLAGS=""
;;
x86)
BLAS_ARCH=ATOM
@@ -72,6 +76,7 @@ case $arch in
CC=i686-linux-android21-clang
CXX=i686-linux-android21-clang++
ARCHFLAGS=""
PAGESIZE_LDFLAGS=""
;;
esac
@@ -79,16 +84,16 @@ mkdir -p $WORKDIR/local/lib
# openblas first
cd $WORKDIR
git clone -b v0.3.13 --single-branch https://github.com/xianyi/OpenBLAS
make -C OpenBLAS TARGET=$BLAS_ARCH ONLY_CBLAS=1 AR=$AR CC=$CC HOSTCC=gcc ARM_SOFTFP_ABI=1 USE_THREAD=0 NUM_THREADS=1 -j4
git clone -b v0.3.20 --single-branch https://github.com/xianyi/OpenBLAS
make -C OpenBLAS TARGET=$BLAS_ARCH ONLY_CBLAS=1 AR=$AR CC=$CC HOSTCC=gcc ARM_SOFTFP_ABI=1 USE_THREAD=0 NUM_THREADS=1 -j 8
make -C OpenBLAS install PREFIX=$WORKDIR/local
# CLAPACK
cd $WORKDIR
git clone -b v3.2.1 --single-branch https://github.com/alphacep/clapack
mkdir -p clapack/BUILD && cd clapack/BUILD
cmake -DCMAKE_C_FLAGS=$ARCHFLAGS -DCMAKE_C_COMPILER_TARGET=$HOST \
-DCMAKE_C_COMPILER=$CC -DCMAKE_SYSTEM_NAME=Generic -DCMAKE_AR=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/${OS_NAME}-x86_64/bin/$AR \
cmake -DCMAKE_C_FLAGS="$ARCHFLAGS" -DCMAKE_C_COMPILER_TARGET=$HOST \
-DCMAKE_C_COMPILER=$CC -DCMAKE_SYSTEM_NAME=Generic -DCMAKE_AR=$ANDROID_TOOLCHAIN_PATH/bin/$AR \
-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \
-DCMAKE_CROSSCOMPILING=True ..
make -j 8 -C F2CLIBS/libf2c
@@ -129,7 +134,7 @@ make -j 8 -C ${WORKDIR_BASE}/../../../src \
OPENFST_ROOT=${WORKDIR}/local \
OPENBLAS_ROOT=${WORKDIR}/local \
CXX=$CXX \
EXTRA_LDFLAGS="-llog -static-libstdc++ -Wl,-soname,libvosk.so"
EXTRA_LDFLAGS="-llog -static-libstdc++ -Wl,-soname,libvosk.so ${PAGESIZE_LDFLAGS}"
cp $WORKDIR/vosk/libvosk.so $WORKDIR/../../src/main/jniLibs/$arch/libvosk.so
done
+4 -4
View File
@@ -4,14 +4,14 @@ def pomDescription = "Vosk speech recognition library for Android"
android {
namespace 'org.vosk'
compileSdkVersion 33
compileSdkVersion 36
defaultConfig {
minSdkVersion 21
targetSdkVersion 33
targetSdkVersion 36
versionCode 10
versionName = version
archivesBaseName = archiveName
ndkVersion = "25.2.9519653"
ndkVersion = "28.2.13676358"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -25,7 +25,7 @@ task buildVosk(type: Exec) {
}
dependencies {
api 'net.java.dev.jna:jna:5.13.0@aar'
api 'net.java.dev.jna:jna:5.18.1@aar'
}
//preBuild.dependsOn buildVosk
@@ -56,8 +56,18 @@ public class LibVosk {
public static native void vosk_recognizer_reset(Pointer recognizer);
public static native void vosk_recognizer_set_endpointer_mode(Pointer recognizer, int mode);
public static native void vosk_recognizer_set_endpointer_delays(Pointer recognizer, float t_start_max, float t_end, float t_max);
public static native void vosk_recognizer_free(Pointer recognizer);
public static native Pointer vosk_text_processor_new(String verbalizer, String tagger);
public static native void vosk_text_processor_free(Pointer processor);
public static native String vosk_text_processor_itn(Pointer processor, String input);
/**
* Set log level for Kaldi messages.
*
@@ -236,6 +236,34 @@ public class Recognizer extends PointerType implements AutoCloseable {
LibVosk.vosk_recognizer_reset(this.getPointer());
}
/**
* Endpointer delay mode
*/
public class EndpointerMode {
public static final int DEFAULT = 0;
public static final int SHORT = 1;
public static final int LONG = 2;
public static final int VERY_LONG = 3;
}
/**
* Configures endpointer mode for recognizer
*/
public void setEndpointerMode(int mode) {
LibVosk.vosk_recognizer_set_endpointer_mode(this.getPointer(), mode);
}
/**
* Set endpointer delays
*
* @param t_start_max timeout for stopping recognition in case of initial silence (usually around 5.0)
* @param t_end timeout for stopping recognition in milliseconds after we recognized something (usually around 0.5 - 1.0)
* @param t_max timeout for forcing utterance end in milliseconds (usually around 20-30)
**/
public void setEndpointerDelays(float t_start_max, float t_end, float t_max) {
LibVosk.vosk_recognizer_set_endpointer_delays(this.getPointer(), t_start_max, t_end, t_max);
}
/**
* Releases recognizer object.
* Underlying model is also unreferenced and if needed, released.
@@ -0,0 +1,21 @@
package org.vosk;
import com.sun.jna.PointerType;
public class TextProcessor extends PointerType implements AutoCloseable {
public TextProcessor() {
}
public TextProcessor(String verbalizer, String tagger) {
super(LibVosk.vosk_text_processor_new(verbalizer, tagger));
}
@Override
public void close() {
LibVosk.vosk_text_processor_free(this.getPointer());
}
public String itn(String input) {
return LibVosk.vosk_text_processor_itn(this.getPointer(), input);
}
}
@@ -66,6 +66,51 @@ public class SpeechService {
}
}
/**
* Creates speech service with a caller-supplied {@link AudioRecord}.
* <p>
* Use this when you need to control the audio input device - for example,
* to pin recording to the built-in microphone when an external USB device
* without a microphone is present:
* <pre>
* AudioRecord recorder = new AudioRecord.Builder()
* .setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION)
* .setAudioFormat(format)
* .build();
* if (Build.VERSION.SDK_INT >= 28) {
* AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
* for (AudioDeviceInfo d : am.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
* if (d.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC) {
* recorder.setPreferredDevice(d);
* break;
* }
* }
* }
* SpeechService service = new SpeechService(recognizer, 16000f, recorder);
* </pre>
* <p>
* The caller retains ownership of {@code recorder}: if this constructor
* throws, the recorder is <em>not</em> released. Call
* {@link AudioRecord#release()} yourself in that case.
*
* @param recognizer the Vosk recognizer
* @param sampleRate sample rate in Hz; must match {@code recorder}'s configuration
* @param recorder a fully-initialised {@link AudioRecord}
* @throws IOException if {@code recorder} is in STATE_UNINITIALIZED
*/
public SpeechService(Recognizer recognizer, float sampleRate, AudioRecord recorder)
throws IOException {
this.recognizer = recognizer;
this.sampleRate = (int) sampleRate;
this.recorder = recorder;
bufferSize = Math.round(this.sampleRate * BUFFER_SIZE_SECONDS);
if (recorder.getState() == AudioRecord.STATE_UNINITIALIZED) {
throw new IOException(
"Failed to initialize recorder. Microphone might be already in use.");
}
}
/**
* Starts recognition. Does nothing if recognition is active.
@@ -137,6 +182,19 @@ public class SpeechService {
return stopRecognizerThread();
}
/**
* Returns the audio session ID of the underlying {@link AudioRecord}.
* <p>
* The session ID can be used to attach audio effects such as
* {@link android.media.audiofx.NoiseSuppressor} to the recording session.
*
* @return audio session ID, or {@link AudioRecord#ERROR} if unavailable
*/
public int getAudioSessionId() {
return recorder.getAudioSessionId();
}
/**
* Shutdown the recognizer and release the recorder
*/
+2 -2
View File
@@ -4,10 +4,10 @@ def pomDescription = "Small English model for Android"
android {
namespace "org.vosk"
compileSdkVersion 33
compileSdkVersion 36
defaultConfig {
minSdkVersion 21
targetSdkVersion 33
targetSdkVersion 36
versionCode 10
versionName = version
archivesBaseName = archiveName
@@ -1,7 +0,0 @@
US English model for mobile Vosk applications
Copyright 2020 Alpha Cephei Inc
Accuracy: 10.38 (tedlium test) 9.85 (librispeech test-clean)
Speed: 0.11xRT (desktop)
Latency: 0.15s (right context)
+3
View File
@@ -28,6 +28,9 @@ public class VoskDemo
{
// Demo float array
VoskRecognizer rec = new VoskRecognizer(model, 16000.0f);
rec.SetEndpointerMode(EndpointerMode.LONG);
using(Stream source = File.OpenRead("test.wav")) {
byte[] buffer = new byte[4096];
int bytesRead;
+2 -2
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>VoskDemo</RootNamespace>
</PropertyGroup>
@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Vosk" Version="0.3.50" />
<PackageReference Include="Vosk" Version="0.3.75" />
</ItemGroup>
</Project>
+17
View File
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>Vosk</PackageId>
<Version>0.3.75</Version>
<authors>Alpha Cephei Inc</authors>
<owners>Alpha Cephei Inc</owners>
</PropertyGroup>
<Target Name="CopyFiles" AfterTargets="Build">
<Copy SourceFiles="bin/Release/net8.0/Vosk.dll" DestinationFolder="lib/net8.0" />
</Target>
</Project>
+3 -3
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Vosk</id>
<version>0.3.50</version>
<version>0.3.75</version>
<authors>Alpha Cephei Inc</authors>
<owners>Alpha Cephei Inc</owners>
<license type="expression">Apache-2.0</license>
@@ -23,10 +23,10 @@ Vosk scales from small devices like Raspberry Pi or Android smartphone to big cl
<copyright>Copyright 2020-2050 Alpha Cephei Inc</copyright>
<tags>speech recognition voice stt asr speech-to-text ai offline privacy</tags>
<dependencies>
<group targetFramework=".NETStandard2.0"/>
<group targetFramework="net8.0"/>
</dependencies>
</metadata>
<files>
<file src="**" exclude="src/*.cs;build.sh;**/.keep-me;*.nupkg" />
<file src="**" exclude="bin/**;obj/**;build.sh;src/*.cs;*.nupkg;**/.keep-me" />
</files>
</package>
+2 -2
View File
@@ -1,2 +1,2 @@
mcs -out:lib/netstandard2.0/Vosk.dll -target:library src/*.cs
nuget pack
rm -rf bin lib obj
/home/shmyrev/local/dotnet/dotnet pack Vosk.csproj -p:NuspecFile=Vosk.nuspec -o .
+7 -2
View File
@@ -65,6 +65,12 @@ class VoskPINVOKE {
[global::System.Runtime.InteropServices.DllImport("libvosk", EntryPoint="vosk_recognizer_reset")]
public static extern void VoskRecognizer_Reset(global::System.Runtime.InteropServices.HandleRef jarg1);
[global::System.Runtime.InteropServices.DllImport("libvosk", EntryPoint="vosk_recognizer_set_endpointer_mode")]
public static extern void VoskRecognizer_SetEndpointerMode(global::System.Runtime.InteropServices.HandleRef jarg1, int jarg2);
[global::System.Runtime.InteropServices.DllImport("libvosk", EntryPoint="vosk_recognizer_set_endpointer_delays")]
public static extern void VoskRecognizer_SetEndpointerDelays(global::System.Runtime.InteropServices.HandleRef jarg1, float jarg2, float jarg3, float jarg4);
[global::System.Runtime.InteropServices.DllImport("libvosk", EntryPoint="vosk_set_log_level")]
public static extern void SetLogLevel(int jarg1);
@@ -107,7 +113,6 @@ class VoskPINVOKE {
[global::System.Runtime.InteropServices.DllImport("libvosk", EntryPoint = "vosk_batch_recognizer_get_pending_chunks")]
public static extern int VoskBatchRecognizer_GetPendingChunks(global::System.Runtime.InteropServices.HandleRef jarg1);
}
}
}
+15
View File
@@ -1,5 +1,12 @@
namespace Vosk {
public enum EndpointerMode {
DEFAULT = 0,
SHORT = 1,
LONG = 2,
VERY_LONG = 3
}
public class VoskRecognizer : System.IDisposable {
private System.Runtime.InteropServices.HandleRef handle;
@@ -91,6 +98,14 @@ public class VoskRecognizer : System.IDisposable {
VoskPINVOKE.VoskRecognizer_Reset(handle);
}
public void SetEndpointerMode(EndpointerMode mode) {
VoskPINVOKE.VoskRecognizer_SetEndpointerMode(handle, (int) mode);
}
public void SetEndpointerDelays(float t_start_max, float t_end, float t_max) {
VoskPINVOKE.VoskRecognizer_SetEndpointerDelays(handle, t_start_max, t_end, t_max);
}
}
}
+99
View File
@@ -0,0 +1,99 @@
package vosk
// #cgo CPPFLAGS: -I ${SRCDIR}/../src
// #cgo !windows LDFLAGS: -L ${SRCDIR}/../src -lvosk -ldl -lpthread
// #cgo windows LDFLAGS: -L ${SRCDIR}/../src -lvosk -lpthread
// #include <stdlib.h>
// #include <vosk_api.h>
import "C"
import "unsafe"
// VoskBatchModel contains a reference to the C VoskBatchModel
type VoskBatchModel struct {
model *C.struct_VoskBatchModel
}
// NewBatchModel creates a new VoskBatchModel instance
func NewBatchModel(modelPath string) (*VoskBatchModel, error) {
cmodelPath := C.CString(modelPath)
defer C.free(unsafe.Pointer(cmodelPath))
internal := C.vosk_batch_model_new(cmodelPath)
model := &VoskBatchModel{model: internal}
return model, nil
}
func (m *VoskBatchModel) Free() {
C.vosk_batch_model_free(m.model)
}
func (m *VoskBatchModel) Wait() {
C.vosk_batch_model_wait(m.model);
}
func freeBatchModel(model *VoskBatchModel) {
C.vosk_batch_model_free(model.model)
}
// VoskBatchRecognizer contains a reference to the C VoskBatchRecognizer
type VoskBatchRecognizer struct {
rec *C.struct_VoskBatchRecognizer
}
func freeBatchRecognizer(recognizer *VoskBatchRecognizer) {
C.vosk_batch_recognizer_free(recognizer.rec)
}
func (r *VoskBatchRecognizer) Free() {
C.vosk_batch_recognizer_free(r.rec)
}
// NewBatchRecognizer creates a new VoskBatchRecognizer instance
func NewBatchRecognizer(model *VoskBatchModel, sampleRate float64) (*VoskBatchRecognizer, error) {
internal := C.vosk_batch_recognizer_new(model.model, C.float(sampleRate))
rec := &VoskBatchRecognizer{rec: internal}
return rec, nil
}
// AcceptWaveform accepts and processes a new chunk of the voice data.
func (r *VoskBatchRecognizer) AcceptWaveform(buffer []byte) {
cbuf := C.CBytes(buffer)
defer C.free(cbuf)
C.vosk_batch_recognizer_accept_waveform(r.rec, (*C.char)(cbuf), C.int(len(buffer)))
}
/** Set NLSML output
* @param nlsml - boolean value
*/
//void vosk_batch_recognizer_set_nlsml(VoskBatchRecognizer *recognizer, int nlsml);
func (r *VoskBatchRecognizer) SetNlsml(nlsml int) {
C.vosk_batch_recognizer_set_nlsml(r.rec, C.int(nlsml))
}
/** Closes the stream */
//void vosk_batch_recognizer_finish_stream(VoskBatchRecognizer *recognizer);
func (r *VoskBatchRecognizer) FinishStream() {
C.vosk_batch_recognizer_finish_stream(r.rec)
}
/** Return results */
//const char *vosk_batch_recognizer_front_result(VoskBatchRecognizer *recognizer);
func (r *VoskBatchRecognizer) FrontResult() string {
return C.GoString(C.vosk_batch_recognizer_front_result(r.rec))
}
/** Release and free first retrieved result */
//void vosk_batch_recognizer_pop(VoskBatchRecognizer *recognizer);
func (r *VoskBatchRecognizer) Pop() {
C.vosk_batch_recognizer_pop(r.rec)
}
/** Get amount of pending chunks for more intelligent waiting */
//int vosk_batch_recognizer_get_pending_chunks(VoskBatchRecognizer *recognizer);
func (r *VoskBatchRecognizer) GetPendingChunks() int {
i := C.vosk_batch_recognizer_get_pending_chunks(r.rec)
return int(i)
}
+5
View File
@@ -0,0 +1,5 @@
This example expects a `s16le` converted audio file and converts it to text in a
manner that imitates the Python example of [test_gpu_batch.py](../python/example/test_gpu_batch.py).
Note that the `libvosk.so` must be in the library path. This was successfully tested on
Ubuntu 24.04 with Go 1.18, gcc-11, NVIDIA driver 570.172.08.
+54
View File
@@ -0,0 +1,54 @@
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
vosk "github.com/alphacep/vosk-api/go"
)
func main() {
var filename string
flag.StringVar(&filename, "f", "", "file to transcribe")
flag.Parse()
vosk.GPUInit()
model, err := vosk.NewBatchModel("model")
if err != nil {
log.Fatal(err)
}
rec, err := vosk.NewBatchRecognizer(model, 16000.0)
if err != nil {
log.Fatal(err)
}
file, err := os.Open(filename)
if err != nil {
panic(err)
}
defer file.Close()
buf := make([]byte, 8000)
for {
if _, err := file.Read(buf); err != nil {
if err != io.EOF {
log.Fatal(err)
}
break
}
rec.AcceptWaveform(buf)
model.Wait()
if rec.FrontResult() != "" {
fmt.Println(rec.FrontResult())
rec.Pop()
}
}
// Is this needed? rec.FinishStream()
}
+2
View File
@@ -16,6 +16,8 @@ func main() {
flag.StringVar(&filename, "f", "", "file to transcribe")
flag.Parse()
vosk.GPUInit()
model, err := vosk.NewModel("model")
if err != nil {
log.Fatal(err)
+10
View File
@@ -125,6 +125,16 @@ func (r *VoskRecognizer) SetPartialWords(words int) {
C.vosk_recognizer_set_partial_words(r.rec, C.int(words))
}
// SetEndpointerDelays sets the recognition timeouts, where startMax
// is the timeout for stopping recognition in case of initial silence
// (usually around 5), end is the timeout for stopping recognition
// in milliseconds after we recognized something (usually around 0.5-1.0),
// and max is the timeout for forcing utterance end in milliseconds
// (usually around 20-30).
func (r *VoskRecognizer) SetEndpointerDelays(startMax, end, max float64) {
C.vosk_recognizer_set_endpointer_delays(r.rec, C.float(startMax), C.float(end), C.float(max))
}
// AcceptWaveform accepts and processes a new chunk of the voice data.
func (r *VoskRecognizer) AcceptWaveform(buffer []byte) int {
cbuf := C.CBytes(buffer)
+1 -1
View File
@@ -11,5 +11,5 @@ repositories {
}
dependencies {
implementation group: 'com.alphacephei', name: 'vosk', version: '0.3.50'
implementation group: 'com.alphacephei', name: 'vosk', version: '0.3.75'
}
+2 -2
View File
@@ -16,7 +16,7 @@ repositories {
archivesBaseName = 'vosk'
group = 'com.alphacephei'
version = '0.3.50'
version = '0.3.75'
mavenPublish {
group = 'com.alphacephei'
@@ -25,7 +25,7 @@ mavenPublish {
}
dependencies {
api group: 'net.java.dev.jna', name: 'jna', version: '5.13.0'
api group: 'net.java.dev.jna', name: 'jna', version: '5.18.1'
testImplementation 'junit:junit:4.13'
}
@@ -82,8 +82,18 @@ public class LibVosk {
public static native void vosk_recognizer_reset(Pointer recognizer);
public static native void vosk_recognizer_set_endpointer_mode(Pointer recognizer, int mode);
public static native void vosk_recognizer_set_endpointer_delays(Pointer recognizer, float t_start_max, float t_end, float t_max);
public static native void vosk_recognizer_free(Pointer recognizer);
public static native Pointer vosk_text_processor_new(String verbalizer, String tagger);
public static native void vosk_text_processor_free(Pointer processor);
public static native String vosk_text_processor_itn(Pointer processor, String input);
/**
* Set log level for Kaldi messages.
*
@@ -236,6 +236,34 @@ public class Recognizer extends PointerType implements AutoCloseable {
LibVosk.vosk_recognizer_reset(this.getPointer());
}
/**
* Endpointer delay mode
*/
public class EndpointerMode {
public static final int DEFAULT = 0;
public static final int SHORT = 1;
public static final int LONG = 2;
public static final int VERY_LONG = 3;
}
/**
* Configures endpointer mode for recognizer
*/
public void setEndpointerMode(int mode) {
LibVosk.vosk_recognizer_set_endpointer_mode(this.getPointer(), mode);
}
/**
* Set endpointer delays
*
* @param t_start_max timeout for stopping recognition in case of initial silence (usually around 5.0)
* @param t_end timeout for stopping recognition in milliseconds after we recognized something (usually around 0.5 - 1.0)
* @param t_max timeout for forcing utterance end in milliseconds (usually around 20-30)
**/
public void setEndpointerDelays(float t_start_max, float t_end, float t_max) {
LibVosk.vosk_recognizer_set_endpointer_delays(this.getPointer(), t_start_max, t_end, t_max);
}
/**
* Releases recognizer object.
* Underlying model is also unreferenced and if needed, released.
@@ -15,8 +15,10 @@ import javax.sound.sampled.UnsupportedAudioFileException;
import org.vosk.LogLevel;
import org.vosk.Recognizer;
import org.vosk.Recognizer.EndpointerMode;
import org.vosk.LibVosk;
import org.vosk.Model;
import org.vosk.TextProcessor;
public class DecoderTest {
@@ -95,9 +97,24 @@ public class DecoderTest {
Assert.assertTrue(true);
}
@Test
public void decoderEndpointerDelays() throws IOException, UnsupportedAudioFileException {
try (Model model = new Model("model");
Recognizer recognizer = new Recognizer(model, 16000)) {
recognizer.setEndpointerMode(EndpointerMode.VERY_LONG);
recognizer.setEndpointerDelays(5.0f, 3.0f, 50.0f);
}
Assert.assertTrue(true);
}
@Test(expected = IOException.class)
public void decoderTestException() throws IOException {
Model model = new Model("model_missing");
}
@Test
public void testItn() throws IOException {
TextProcessor p = new TextProcessor("model/itn/en_itn_tagger.fst", "model/itn/en_itn_verbalizer.fst");
System.out.println(p.itn("as easy as one two three"));
}
}
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
-1
View File
@@ -1 +0,0 @@
vosk-api-kotlin
-127
View File
@@ -1,127 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
-5
View File
@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright &amp;#36;today.year Alpha Cephei Inc.&#10;&#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10; http://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License." />
<option name="myName" value="Apache-2.0" />
</copyright>
</component>
-7
View File
@@ -1,7 +0,0 @@
<component name="CopyrightManager">
<settings default="Apache-2.0">
<module2copyright>
<element module="All" copyright="Apache-2.0" />
</module2copyright>
</settings>
</component>
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
-25
View File
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Embedded JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+30 -16
View File
@@ -1,5 +1,6 @@
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.config.JvmTarget
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
/*
* Copyright 2020 Alpha Cephei Inc. & Doomsdayrs
@@ -18,15 +19,15 @@ import org.jetbrains.kotlin.config.JvmTarget
*/
plugins {
kotlin("multiplatform") version "1.8.10"
kotlin("multiplatform") version "2.0.0"
id("com.android.library")
`maven-publish`
id("org.jetbrains.dokka") version "1.7.20"
kotlin("plugin.serialization") version "1.8.10"
id("org.jetbrains.dokka") version "1.9.20"
kotlin("plugin.serialization") version "2.0.0"
}
group = "com.alphacephei"
version = "0.3.50"
version = "0.3.75"
repositories {
google()
@@ -67,9 +68,11 @@ fun org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension.native(
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = JvmTarget.JVM_11.description
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
environment("MODEL", "VOSK_MODEL")
@@ -80,7 +83,7 @@ kotlin {
}
}
android {
androidTarget {
publishAllLibraryVariants()
}
@@ -102,6 +105,16 @@ kotlin {
}
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyDefaultHierarchyTemplate {
withJvm()
withAndroidTarget()
if (enableNative)
withNative()
}
publishing {
publications {
withType<MavenPublication> {
@@ -130,13 +143,13 @@ kotlin {
}
}
val jna_version = "5.13.0"
val coroutines_version = "1.6.4"
val jna_version = "5.14.0"
val coroutines_version = "1.7.3"
sourceSets {
val commonMain by getting {
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
}
}
@@ -161,7 +174,7 @@ kotlin {
api("net.java.dev.jna:jna:$jna_version@aar")
}
}
val androidTest by getting {
val androidUnitTest by getting {
dependencies {
implementation("junit:junit:4.13.2")
}
@@ -170,15 +183,16 @@ kotlin {
}
android {
compileSdk = 33
namespace = "com.alphacephei.library"
compileSdk = 34
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 24
targetSdk = 33
targetSdk = 34
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
publishing {
multipleVariants {
+1 -1
View File
@@ -23,7 +23,7 @@ pluginManagement {
resolutionStrategy {
eachPlugin {
if (requested.id.namespace == "com.android") {
useModule("com.android.tools.build:gradle:7.3.0")
useModule("com.android.tools.build:gradle:8.3.0")
}
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2024 Alpha Cephei Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.vosk
/**
* VoskEpMode
*/
enum class EndPointerMode {
ANSWER_DEFAULT,
ANSWER_SHORT,
ANSWER_LONG,
ANSWER_VERY_LONG
}
@@ -151,6 +151,7 @@ expect class Recognizer : Freeable {
* }],
* </pre>
*
* C equivalent = vosk_recognizer_set_words
* @param words - boolean value
*/
fun setOutputWordTimes(words: Boolean)
@@ -168,6 +169,23 @@ expect class Recognizer : Freeable {
*/
fun setNLSML(nlsml: Boolean)
/**
* Set endpointer scaling factor
*
* @param mode Endpointer mode
**/
fun setEndPointerMode(mode: EndPointerMode)
/**
* Set endpointer delays
*
* @param tStartMax timeout for stopping recognition in case of initial silence (usually around 5.0)
* @param tEnd timeout for stopping recognition in milliseconds after we recognized something (usually around 0.5 - 1.0)
* @param tMax timeout for forcing utterance end in milliseconds (usually around 20-30)
**/
fun setEndPointerDelays(tStartMax: Float, tEnd: Float, tMax: Float)
/**
* Accept voice data
*
@@ -0,0 +1,32 @@
/*
* Copyright 2024 Alpha Cephei Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.vosk
/**
* Inverse text normalization
*
* @since 2024/06/19
* @constructor Create text processor
*/
expect class TextProcessor constructor(tagger: Char, verbalizer: Char) : Freeable {
/** Release text processor */
override fun free()
/** Convert string */
fun itn(input: Char): Char
}
@@ -19,4 +19,4 @@ package org.vosk.exception
/**
* Internal common IO exception. On JVM this is just a type alias.
*/
expect open class IOException(message: String? = null) : Exception
expect open class IOException(message: String?) : Exception
+24 -8
View File
@@ -42,14 +42,13 @@ internal object LibVosk {
@Throws(IOException::class)
private fun unpackDll(targetDir: File, lib: String) {
val source: InputStream =
Vosk::class.java.getResourceAsStream("/win32-x86-64/$lib.dll")!!
Files.copy(
source,
File(targetDir, "$lib.dll").toPath(),
StandardCopyOption.REPLACE_EXISTING
)
Vosk::class.java.getResourceAsStream("/win32-x86-64/$lib.dll")!!.use {
Files.copy(
it,
File(targetDir, "$lib.dll").toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
}
init {
@@ -57,6 +56,7 @@ internal object LibVosk {
Platform.isAndroid() -> {
Native.register(LibVosk::class.java, "vosk")
}
Platform.isWindows() -> {
// We have to unpack dependencies
try {
@@ -79,6 +79,7 @@ internal object LibVosk {
Native.register(LibVosk::class.java, "libvosk");
}
}
else -> {
Native.register(LibVosk::class.java, "vosk");
}
@@ -194,4 +195,19 @@ internal object LibVosk {
external fun vosk_batch_recognizer_pop(recognizer: BatchRecognizer)
external fun vosk_batch_recognizer_get_pending_chunks(recognizer: BatchRecognizer): Int
external fun vosk_text_processor_new(tagger: Char, verbalizer: Char): Pointer
external fun vosk_text_processor_free(processor: TextProcessor)
external fun vosk_text_processor_itn(processor: TextProcessor, input: Char): Char
external fun vosk_recognizer_set_endpointer_mode(recognizer: Recognizer, ordinal: Int)
external fun vosk_recognizer_set_endpointer_delays(
recognizer: Recognizer,
tStartMax: Float,
tEnd: Float,
tMax: Float
)
}
@@ -327,4 +327,27 @@ actual class Recognizer : Freeable, PointerType, AutoCloseable {
free()
}
/**
* Set endpointer scaling factor
*
* @param mode Endpointer mode
**/
actual fun setEndPointerMode(mode: EndPointerMode) {
LibVosk.vosk_recognizer_set_endpointer_mode(this, mode.ordinal)
}
/**
* Set endpointer delays
*
* @param tStartMax timeout for stopping recognition in case of initial silence (usually around 5.0)
* @param tEnd timeout for stopping recognition in milliseconds after we recognized something (usually around 0.5 - 1.0)
* @param tMax timeout for forcing utterance end in milliseconds (usually around 20-30)
**/
actual fun setEndPointerDelays(
tStartMax: Float,
tEnd: Float,
tMax: Float
) {
LibVosk.vosk_recognizer_set_endpointer_delays(this, tStartMax, tEnd, tMax)
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2024 Alpha Cephei Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.vosk
import com.sun.jna.PointerType
/**
* Inverse text normalization
*
* @since 2024/06/19
*/
actual class TextProcessor :
Freeable, PointerType, AutoCloseable {
/**
* Create text processor
*/
actual constructor(tagger: Char, verbalizer: Char) :
super(LibVosk.vosk_text_processor_new(tagger, verbalizer))
/** Release text processor */
actual override fun free() {
LibVosk.vosk_text_processor_free(this)
}
/** Convert string */
actual fun itn(input: Char): Char =
LibVosk.vosk_text_processor_itn(this, input)
/**
* @see free
*/
override fun close() {
free()
}
}
@@ -22,4 +22,4 @@ linkerOpts.linux = \
linkerOpts.linux_x64 = \
-L/usr/lib64/ \
-L/usr/local/lib64/
-L/usr/local/lib64/
+19 -7
View File
@@ -69,16 +69,18 @@ const vosk_recognizer_ptr = ref.refType(vosk_recognizer);
let soname;
if (os.platform() == 'win32') {
// Update path to load dependent dlls
let currentPath = process.env.Path;
let dllDirectory = path.resolve(path.join(__dirname, "lib", "win-x86_64"));
process.env.Path = dllDirectory + path.delimiter + currentPath;
// Update path to load dependent dlls
let currentPath = process.env.Path;
let dllDirectory = path.resolve(path.join(__dirname, 'lib', 'win-x86_64'));
process.env.Path = dllDirectory + path.delimiter + currentPath;
soname = path.join(__dirname, "lib", "win-x86_64", "libvosk.dll")
soname = path.join(__dirname, 'lib', 'win-x86_64', 'libvosk.dll');
} else if (os.platform() == 'darwin') {
soname = path.join(__dirname, "lib", "osx-universal", "libvosk.dylib")
soname = path.join(__dirname, 'lib', 'osx-universal', 'libvosk.dylib');
} else if (os.platform() == 'linux' && os.arch() == 'arm64') {
soname = path.join(__dirname, 'lib', 'linux-arm64', 'libvosk.so');
} else {
soname = path.join(__dirname, "lib", "linux-x86_64", "libvosk.so")
soname = path.join(__dirname, 'lib', 'linux-x86_64', 'libvosk.so');
}
const libvosk = ffi.Library(soname, {
@@ -128,6 +130,9 @@ class Model {
* @type {unknown}
*/
this.handle = libvosk.vosk_model_new(modelPath);
if (!this.handle) {
throw new Error('Failed to create a model.');
}
}
/**
@@ -161,6 +166,9 @@ class SpeakerModel {
* @type {unknown}
*/
this.handle = libvosk.vosk_spk_model_new(modelPath);
if (!this.handle) {
throw new Error('Failed to create a speaker model.');
}
}
/**
@@ -235,6 +243,10 @@ class Recognizer {
: hasOwnProperty(param, 'grammar')
? libvosk.vosk_recognizer_new_grm(model.handle, sampleRate, JSON.stringify(param.grammar))
: libvosk.vosk_recognizer_new(model.handle, sampleRate);
if (!this.handle) {
throw new Error('Failed to create a recognizer.');
}
}
/**
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vosk",
"version": "0.3.50",
"version": "0.3.75",
"description": "Node binding for continuous offline voice recoginition with Vosk library.",
"repository": {
"type": "git",
+1 -1
View File
@@ -45,7 +45,7 @@ with open("README.md", "rb") as fh:
setuptools.setup(
name="vosk",
version="0.3.50",
version="0.3.75",
author="Alpha Cephei Inc",
author_email="contact@alphacephei.com",
description="Offline open source speech recognition API based on Kaldi and Vosk",
+1 -1
View File
@@ -28,7 +28,7 @@ def recognize(line):
def main():
p = Pool(8)
texts = p.map(recognize, open(sys.argv[1], encoding="uft-8").readlines())
texts = p.map(recognize, open(sys.argv[1], encoding="utf-8").readlines())
print ("\n".join(texts))
main()
+12
View File
@@ -0,0 +1,12 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
# rspec failure tracking
.rspec_status
.idea
+3
View File
@@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper
+53
View File
@@ -0,0 +1,53 @@
plugins:
- rubocop-rake
- rubocop-rspec
AllCops:
TargetRubyVersion: 2.5
NewCops: enable
Style/StringLiterals:
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes
Style/TrailingCommaInArguments:
EnforcedStyleForMultiline: consistent_comma
Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: consistent_comma
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: consistent_comma
Style/NumericPredicate:
EnforcedStyle: comparison
Layout/CaseIndentation:
EnforcedStyle: end
Metrics/AbcSize:
Max: 27
Metrics/CyclomaticComplexity:
Max: 10
Metrics/PerceivedComplexity:
Max: 10
Metrics/MethodLength:
Max: 20
Style/SymbolArray:
Exclude:
- 'lib/vosk/ffi.rb'
RSpec/MultipleMemoizedHelpers:
Max: 10
RSpec/ExampleLength:
Max: 10
RSpec/MultipleExpectations:
Max: 3
+18
View File
@@ -0,0 +1,18 @@
# frozen_string_literal: true
source "https://rubygems.org"
# Specify your gem's dependencies in vosk.gemspec
gemspec
gem "rake", "~> 13.0"
gem "rspec", "~> 3.0"
gem "rubocop", "~> 1.21"
gem "rubocop-rake", "~> 0.7.1"
gem "rubocop-rspec", "~> 3.9"
gem "wavefile", "~> 1.1"
gem "webmock", "~> 3.26"
+109
View File
@@ -0,0 +1,109 @@
PATH
remote: .
specs:
vosk (0.3.45)
bytesize (~> 0.1)
ffi (~> 1.6)
fileutils (~> 1.7)
httparty (~> 0.21)
progressbar (~> 1.13)
rubyzip (~> 2.4)
srt (~> 0.1.5)
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.2)
bigdecimal (4.0.1)
bytesize (0.1.2)
crack (1.0.1)
bigdecimal
rexml
csv (3.2.6)
diff-lcs (1.5.1)
ffi (1.17.0-x86_64-linux-gnu)
fileutils (1.7.3)
hashdiff (1.2.1)
httparty (0.22.0)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
json (2.9.0)
language_server-protocol (3.17.0.3)
lint_roller (1.1.0)
mini_mime (1.1.5)
multi_xml (0.6.0)
parallel (1.26.3)
parser (3.3.10.2)
ast (~> 2.4.1)
racc
prism (1.9.0)
progressbar (1.13.0)
public_suffix (7.0.2)
racc (1.8.1)
rainbow (3.1.1)
rake (13.2.1)
regexp_parser (2.9.3)
rexml (3.4.4)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.2)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.2)
rubocop (1.84.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-rake (0.7.1)
lint_roller (~> 1.1)
rubocop (>= 1.72.1)
rubocop-rspec (3.9.0)
lint_roller (~> 1.1)
rubocop (~> 1.81)
ruby-progressbar (1.13.0)
rubyzip (2.4.1)
srt (0.1.5)
unicode-display_width (3.1.2)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
wavefile (1.1.2)
webmock (3.26.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
PLATFORMS
x86_64-linux
DEPENDENCIES
rake (~> 13.0)
rspec (~> 3.0)
rubocop (~> 1.21)
rubocop-rake (~> 0.7.1)
rubocop-rspec (~> 3.9)
vosk!
wavefile (~> 1.1)
webmock (~> 3.26)
BUNDLED WITH
2.4.22
+138
View File
@@ -0,0 +1,138 @@
# Vosk Ruby
Ruby bindings for [Vosk](https://alphacephei.com/vosk/) — an offline speech recognition toolkit supporting 20+ languages.
## Installation
Add to your Gemfile:
```ruby
gem "vosk"
```
Or install directly:
```bash
gem install vosk
```
The gem ships with a precompiled `libvosk` for supported platforms. On other platforms it will attempt to load a system-installed `libvosk`.
## Usage
### Basic transcription
```ruby
require "vosk"
require "wavefile"
# Load a model by language (downloaded automatically if not cached)
model = Vosk::Model.new(lang: "en-us")
# Or by name, or by local path:
# model = Vosk::Model.new(model_name: "vosk-model-small-en-us-0.4")
# model = Vosk::Model.new(model_path: "/path/to/model")
WaveFile::Reader.new("audio.wav") do |reader|
rec = Vosk::KaldiRecognizer.new(model, reader.format.sample_rate)
reader.each_buffer(4000) do |buffer|
data = buffer.samples.pack(WaveFile::PACK_CODES.dig(:pcm, 16))
if rec.accept_waveform(data).nonzero?
puts rec.result # JSON: {"text": "..."}
else
puts rec.partial_result # JSON: {"partial": "..."}
end
end
puts rec.final_result
end
```
Audio must be mono, 16-bit PCM WAV. Use [wavefile](https://github.com/jstrait/wavefile) to read it.
### Grammar / keyword recognition
Pass a JSON array of phrases as the third argument:
```ruby
rec = Vosk::KaldiRecognizer.new(model, sample_rate, '["one two three", "[unk]"]')
```
### Recognizer options
```ruby
rec.words = true # include per-word timing in results
rec.partial_words = true # include per-word timing in partial results
rec.max_alternatives = 5 # return n-best list instead of single result
rec.nlsml = true # return NLSML instead of JSON
```
### SRT subtitle generation
`srt_result` reads raw PCM from any IO stream and returns an SRT-formatted string. Use ffmpeg to decode any audio format on the fly:
```ruby
require "vosk"
SAMPLE_RATE = 16_000
model = Vosk::Model.new(lang: "en-us")
rec = Vosk::KaldiRecognizer.new(model, SAMPLE_RATE)
rec.words = true # required for word-level timestamps
IO.popen(["ffmpeg", "-loglevel", "quiet", "-i", "audio.mp4",
"-ar", SAMPLE_RATE.to_s, "-ac", "1", "-f", "s16le", "-",]) do |stream|
puts rec.srt_result(stream)
end
```
The `words_per_line:` keyword controls how many words appear per subtitle line (default: 7):
```ruby
rec.srt_result(stream, words_per_line: 5)
```
### Speaker identification
```ruby
spk_model = Vosk::SpkModel.new("/path/to/spk-model")
rec = Vosk::KaldiRecognizer.new(model, sample_rate)
rec.spk_model = spk_model
```
### Listing available models
```ruby
puts Vosk.models # all model names
puts Vosk.languages # all supported language codes
```
### Logging
```ruby
Vosk.log_level = -1 # suppress all output
Vosk.log_level = 0 # default
```
### Transcriber CLI
The gem includes a `vosk-transcriber` executable:
```bash
vosk-transcriber audio.wav
```
## Model storage
Models are cached in `~/.cache/vosk/` by default, or in the directory set by `$VOSK_MODEL_PATH`.
## Development
```bash
bundle install
bundle exec rake spec
```
## License
Apache-2.0
+50
View File
@@ -0,0 +1,50 @@
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require "rubygems/package_task"
RSpec::Core::RakeTask.new(:spec)
require "rubocop/rake_task"
require "fileutils"
RuboCop::RakeTask.new
# TODO: modify 'build' task somehow to include pre-compiled binary
# (or add this logic into gemspec)
spec = Gem::Specification.load("vosk.gemspec")
# Tweak spec when building pre-compiled gem:
# (see example in https://github.com/oxidize-rb/actions/blob/main/cross-gem/action.yml and https://github.com/oxidize-rb/rb-sys/blob/main/gem/lib/rb_sys/extensiontask.rb)
# See also https://github.com/rake-compiler/rake-compiler/blob/master/lib/rake/extensiontask.rb and https://github.com/rake-compiler/rake-compiler-dock
# def cross_compile?
# # TODO: impl
# false
# end
# TODO: maybe it should be a separate task that extends Gem::PackageTask
# if cross_compile?
# # Can't use 'file' task bc we don't know file names exactly
# task :libs do
# # TODO
# # vosk_source = os.getenv("VOSK_SOURCE", os.path.abspath(os.path.join(os.path.dirname(__file__),
# # "..")))
# FileUtils.cp Dir[File.join(vosk_source, "src/lib*.*")], "lib/vosk"
# # TODO: check it works properly (spec is modified after `gem` task is already created)
# # Matches package_data = {'vosk': ['*.so', '*.dll', '*.dyld']},
# spec.files += Dir["lib/vosk/*.{so,dll,dyld}"]
# end
# task gem: :libs
# # TODO: figure out platform
# # system = os.environ.get('VOSK_SYSTEM', platform.system())
# # architecture = os.environ.get('VOSK_ARCHITECTURE', platform.architecture()[0])
# # machine = os.environ.get('VOSK_MACHINE', platform.machine())
# spec.platform = Gem::Platform.new(platf)
# end
# add your default gem packing task
# Run with `rake build`
Gem::PackageTask.new(spec)
task default: %i[spec rubocop]
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "vosk"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
require "irb"
IRB.start(__FILE__)
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here
+1
View File
@@ -0,0 +1 @@
../../python/example/test.wav
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "vosk"
require "wavefile"
# You can set log level to -1 to disable debug messages
Vosk.log_level = 0
WaveFile::Reader.new(ARGV[0]) do |reader|
reader_format = reader.format
unless reader_format.channels == 1 && reader_format.bits_per_sample == 16 && reader_format.sample_format == :pcm
puts("Audio file must be WAV format mono PCM.")
exit(1)
end
model = Vosk::Model.new(lang: "en-us")
# You can also init model by name or with a folder path
# model = Model(model_name: "vosk-model-en-us-0.21")
# model = Model(model_path: "models/en")
rec = Vosk::KaldiRecognizer.new(model, reader_format.sample_rate)
rec.words = true
rec.partial_words = true
reader.each_buffer(4000) do |buffer|
data = buffer.samples.pack(WaveFile::PACK_CODES.dig(:pcm, 16))
if rec.accept_waveform(data).nonzero?
puts rec.result
else
puts rec.partial_result
end
end
puts rec.final_result
end
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "vosk"
SAMPLE_RATE = 16_000
Vosk.log_level = -1
model = Vosk::Model.new(lang: "en-us")
rec = Vosk::KaldiRecognizer.new(model, SAMPLE_RATE)
rec.words = true
IO.popen(["ffmpeg", "-loglevel", "quiet", "-i", "audio.mp4",
"-ar", SAMPLE_RATE.to_s, "-ac", "1", "-f", "s16le", "-",]) do |stream|
puts rec.srt_result(stream)
end
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "vosk"
require "wavefile"
WaveFile::Reader.new(ARGV[0]) do |reader|
reader_format = reader.format
unless reader_format.channels == 1 && reader_format.bits_per_sample == 16 && reader_format.sample_format == :pcm
puts("Audio file must be WAV format mono PCM.")
exit(1)
end
model = Vosk::Model.new(lang: "en-us")
# You can also specify the possible word or phrase list as JSON list,
# the order doesn't have to be strict
rec = Vosk::KaldiRecognizer.new(
model, reader_format.sample_rate,
'["oh one two three", "four five six", "seven eight nine zero", "[unk]"]',
)
reader.each_buffer(4000) do |buffer|
data = buffer.samples.pack(WaveFile::PACK_CODES.dig(:pcm, 16))
if rec.accept_waveform(data).nonzero?
puts rec.result
rec.grammar = '["one zero one two three oh", "four five six", "seven eight nine zero", "[unk]"]'
else
puts rec.partial_result
end
end
puts rec.final_result
end
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "vosk"
# TODO
+343 -3
View File
@@ -1,5 +1,345 @@
class Vosk
def self.hi
puts "Hello world!"
# frozen_string_literal: true
require_relative "vosk/version"
require "httparty"
require_relative "vosk/progressbar"
require_relative "vosk/ffi"
require "zip"
require "fileutils"
require "json"
require "srt"
# Vosk speech recognition system
module Vosk
class Error < StandardError; end
# Remote location of the models and local folders
MODEL_PRE_URL = "https://alphacephei.com/vosk/models/"
MODEL_LIST_URL = "#{MODEL_PRE_URL}model-list.json"
# TODO: Test on Windows
MODEL_DIRS = [
ENV.fetch("VOSK_MODEL_PATH", nil), "/usr/share/vosk",
File.join(Dir.home, "AppData/Local/vosk"), File.join(Dir.home, ".cache/vosk"),
].compact.freeze
# Different from Python: no need to print inside the method, simply use +puts Vosk.models+
def self.models
response = HTTParty.get(MODEL_LIST_URL, timeout: 10)
response.map { |model| model["name"] }
end
# Different from Python: no need to print inside the method, simply use +puts Vosk.languages+
def self.languages
response = HTTParty.get(MODEL_LIST_URL, timeout: 10)
response.map { |model| model["lang"] }.uniq
end
# Model stores all the data required for recognition
# it contains static data and can be shared across processing
# threads.
class Model
attr_reader :handle
def initialize(model_path: nil, model_name: nil, lang: nil)
model_path ||= get_model_path(model_name, lang)
@handle = C.vosk_model_new(model_path)
end
def vosk_model_find_word(word)
C.vosk_model_find_word(@handle, word)
end
private
def get_model_path(model_name, lang)
if model_name
get_model_by_name(model_name)
else
get_model_by_lang(lang)
end
end
def get_model_by_name(model_name)
MODEL_DIRS.each do |directory|
next unless Dir.exist?(directory)
entry = Dir.entries(directory).find { |f| f == model_name }
return File.join(directory, entry) if entry
end
response = HTTParty.get(MODEL_LIST_URL, timeout: 10)
result_model = response.map { |m| m["name"] }.find { |n| n == model_name }
unless result_model
# It's not common for Ruby gems to exit the whole process, but I decided to match Python behavior
puts "model name #{model_name} does not exist"
exit(1)
end
# It always selects the last dir for downloads, ignoring env and windows-specific dir
dest = File.join(MODEL_DIRS.last, result_model)
download_model(dest)
dest
end
def get_model_by_lang(lang)
MODEL_DIRS.each do |directory|
next unless Dir.exist?(directory)
entry = Dir.entries(directory).find { |f| f.match?(/\Avosk-model(-small)?-#{Regexp.escape(lang)}/) }
return File.join(directory, entry) if entry
end
response = HTTParty.get(MODEL_LIST_URL, timeout: 10)
result_model = response.find do |m|
m["lang"] == lang && m["type"] == "small" && m["obsolete"] == "false"
end&.dig("name")
unless result_model
# It's not common for Ruby gems to exit the whole process, but I decided to match Python behavior
puts "lang #{lang} does not exist"
exit(1)
end
# It always selects the last dir for downloads, ignoring env and windows-specific dir
dest = File.join(MODEL_DIRS.last, result_model)
download_model(dest)
dest
end
# Python param "model_name" is, in fact, a full path
# rubocop:disable Metrics/MethodLength
def download_model(model_path)
dir = File.dirname(model_path)
# Python version won't try to create the directory if a file with the same name exists
FileUtils.makedirs(dir)
model_name = File.basename(model_path)
zip_path = "#{model_path}.zip"
url = "#{MODEL_PRE_URL}#{model_name}.zip"
progressbar = ProgressBar.create(
# Why add MODEL_PRE_URL and then split it away?
title: "#{model_name}.zip",
total: nil,
progress_mark: "",
format: "%t: %j%%|%B| %s/%z [%d<%o, %r/s]",
rate_scale: ->(rate) { ByteSize.new(rate.to_i).to_s },
)
begin
download_file(url, zip_path) do |bsize, tsize|
progressbar.total = tsize if tsize && tsize >= progressbar.progress
progressbar.progress += bsize
end
progressbar.finish
ensure
progressbar&.stop
end
Zip::File.open(zip_path) do |zip_file|
zip_file.each do |entry|
entry_path = File.join(dir, entry.name)
FileUtils.makedirs(File.dirname(entry_path))
entry.extract(entry_path) { true }
end
end
File.unlink(zip_path)
end
# rubocop:enable Metrics/MethodLength
def download_file(url, dest, &callback)
File.open(dest, File::CREAT | File::WRONLY | File::TRUNC | File::BINARY) do |file|
response = HTTParty.get(url, stream_body: true) do |fragment|
next unless fragment.http_response.is_a?(Net::HTTPSuccess)
file.write(fragment)
callback&.call(fragment.bytesize, fragment.http_response["Content-Length"]&.to_i)
end
raise HTTParty::ResponseError.new(response), "Code #{response.code}" unless response.success?
end
end
end
# Speaker model is the same as model but contains the data
# for speaker identification.
class SpkModel
attr_reader :handle
def initialize(model_path)
@handle = C.vosk_spk_model_new(model_path)
end
end
# Endpointer scaling factor
class EndpointerMode
C::VoskEndpointerMode.symbol_map.each do |name, value|
const_set(name.upcase, value)
end
end
# Recognizer object is the main object which processes data.
# Each recognizer usually runs in own thread and takes audio as input.
# Once audio is processed recognizer returns JSON object as a string
# which represent decoded information - words, confidences, times, n-best lists,
# speaker information and so on
class KaldiRecognizer
# Python version accepts *args, so in case of a wrong number of arguments it'll raise TypeError,
# while Ruby raises ArgumentError
def initialize(model, sample_rate, grammar_or_spk_model = nil)
@handle = case grammar_or_spk_model
when nil
C.vosk_recognizer_new(model.handle, sample_rate.to_f)
when SpkModel
C.vosk_recognizer_new_spk(model.handle, sample_rate.to_f, grammar_or_spk_model.handle)
when String
C.vosk_recognizer_new_grm(model.handle, sample_rate.to_f, grammar_or_spk_model)
else
raise TypeError, "Unknown arguments"
end
end
def max_alternatives=(max_alternatives)
C.vosk_recognizer_set_max_alternatives(@handle, max_alternatives)
end
def words=(enable_words)
C.vosk_recognizer_set_words(@handle, enable_words ? 1 : 0)
end
def partial_words=(enable_partial_words)
C.vosk_recognizer_set_partial_words(@handle, enable_partial_words ? 1 : 0)
end
def nlsml=(enable_nlsml)
C.vosk_recognizer_set_nlsml(@handle, enable_nlsml ? 1 : 0)
end
def endpointer_mode=(mode)
C.vosk_recognizer_set_endpointer_mode(@handle, mode.to_i)
end
def set_endpointer_delays(t_start_max, t_end, t_max)
C.vosk_recognizer_set_endpointer_delays(@handle, t_start_max.to_f, t_end.to_f, t_max.to_f)
end
def spk_model=(spk_model)
C.vosk_recognizer_set_spk_model(@handle, spk_model.handle)
end
def grammar=(grammar)
C.vosk_recognizer_set_grm(@handle, grammar)
end
def accept_waveform(data)
res = C.vosk_recognizer_accept_waveform(@handle, data, data.bytesize)
raise Error, "Failed to process waveform" if res < 0
res
end
def result
C.vosk_recognizer_result(@handle)
end
def partial_result
C.vosk_recognizer_partial_result(@handle)
end
def final_result
C.vosk_recognizer_final_result(@handle)
end
def reset
C.vosk_recognizer_reset(@handle)
end
def srt_result(stream, words_per_line: 7)
results = []
while (data = stream.read(4000))
results.push(result) if accept_waveform(data).nonzero?
end
results.push(final_result)
create_srt(results, words_per_line)
end
private
def create_srt(results, words_per_line)
srt = SRT::File.new
results.each do |res|
jres = JSON.parse(res)
next unless jres.key?("result")
jres["result"].each_slice(words_per_line) do |line|
sub = SRT::Line.new
sub.sequence = srt.lines.length + 1
sub.start_time = line.first["start"]
sub.end_time = line.last["end"]
sub.text = line.map { |w| w["word"] }.join(" ")
srt.lines.push(sub)
end
end
srt.to_s
end
end
# Batch model object
class BatchModel
attr_reader :handle
# Python version accepts additional ignored args for some reason
def initialize(model_path)
@handle = C.vosk_batch_model_new(model_path)
end
def wait
C.vosk_batch_model_wait(@handle)
end
end
# Batch recognizer object
class BatchRecognizer
# Python version accepts *args, but I don't just use regular arguments
def initialize(batch_model, sample_rate)
@handle = C.vosk_batch_recognizer_new(batch_model.handle, sample_rate.to_f)
end
def accept_waveform(data)
C.vosk_batch_recognizer_accept_waveform(@handle, data, data.bytesize)
end
def result
res = C.vosk_batch_recognizer_front_result(@handle)
C.vosk_batch_recognizer_pop(@handle)
res
end
def finish_stream
C.vosk_batch_recognizer_finish_stream(@handle)
end
def pending_chunks
C.vosk_batch_recognizer_get_pending_chunks(@handle)
end
end
# Inverse text normalization
class Processor
# Python version accepts *args, but I don't just use regular arguments
def initialize(lang, type)
@handle = C.vosk_text_processor_new(lang, type)
end
def process(text)
C.vosk_text_processor_itn(@handle, text)
end
end
def self.log_level=(level)
C.vosk_set_log_level(level)
end
def self.gpu_init
C.vosk_gpu_init
end
def self.gpu_thread_init
C.vosk_gpu_thread_init
end
end
+180
View File
@@ -0,0 +1,180 @@
# frozen_string_literal: true
require "ffi"
module Vosk
module C # :nodoc: all
extend FFI::Library
# FIXME: Load same way as in Python, test on Windows
# This second option, 'vosk', allows system-wide installed library to be loaded.
# I see you search /usr/share/vosk, so I guess it's supported somehow.
# It'll allow the gem to be used on systems not supported in pre-compiled releases.
# (in fact, we only need the first in a pre-compiled release and the second otherwise,
# but not worth the effort to put more configuration in the build stage - not possible without hacks)
# But when we load a lib not shipped with the gem itself, we (might) need to ensure it's a compatible version
# Note: options in the array are alternatives, only the first found is loaded
# Note: probably needs RubyInstaller::Runtime.add_dll_directory on Windows - if FFI::Platform.windows?
ffi_lib [File.join(__dir__, FFI.map_library_name("vosk")), "vosk"]
class VoskModel < FFI::AutoPointer
def self.from_native(ptr, _ctx)
raise Error, "Failed to create a model" if ptr.null?
super
end
def self.release(ptr)
C.vosk_model_free(ptr)
end
end
class VoskSpkModel < FFI::AutoPointer
def self.from_native(ptr, _ctx)
raise Error, "Failed to create a speaker model" if ptr.null?
super
end
def self.release(ptr)
C.vosk_spk_model_free(ptr)
end
end
VoskEndpointerMode = enum(
:VoskEndpointerMode,
[
:default, 0,
:short, 1,
:long, 2,
:very_long, 3,
],
)
class VoskRecognizer < FFI::AutoPointer
def self.from_native(ptr, _ctx)
raise Error, "Failed to create a recognizer" if ptr.null?
super
end
def self.release(ptr)
C.vosk_recognizer_free(ptr)
end
end
class VoskBatchModel < FFI::AutoPointer
def self.from_native(ptr, _ctx)
raise Error, "Failed to create a model" if ptr.null?
super
end
def self.release(ptr)
C.vosk_batch_model_free(ptr)
end
end
class VoskBatchRecognizer < FFI::AutoPointer
def self.from_native(ptr, _ctx)
raise Error, "Failed to create a recognizer" if ptr.null?
super
end
def self.release(ptr)
C.vosk_batch_recognizer_free(ptr)
end
end
class VoskTextProcessor < FFI::AutoPointer
def self.from_native(ptr, _ctx)
raise Error, "Failed to create processor" if ptr.null?
super
end
def self.release(ptr)
C.vosk_text_processor_free(ptr)
end
end
attach_function :vosk_model_new, [:string], VoskModel
attach_function :vosk_model_free, [VoskModel], :void
attach_function :vosk_model_find_word, [VoskModel, :string], :int
attach_function :vosk_spk_model_new, [:string], VoskSpkModel
attach_function :vosk_spk_model_free, [VoskSpkModel], :void
attach_function :vosk_recognizer_new, [VoskModel, :float], VoskRecognizer
attach_function :vosk_recognizer_new_spk, [VoskModel, :float, VoskSpkModel], VoskRecognizer
attach_function :vosk_recognizer_new_grm, [VoskModel, :float, :string], VoskRecognizer
attach_function :vosk_recognizer_set_spk_model, [VoskRecognizer, VoskSpkModel], :void
attach_function :vosk_recognizer_set_grm, [VoskRecognizer, :string], :void
attach_function :vosk_recognizer_set_max_alternatives, [VoskRecognizer, :int], :void
attach_function :vosk_recognizer_set_words, [VoskRecognizer, :int], :void
attach_function :vosk_recognizer_set_partial_words, [VoskRecognizer, :int], :void
attach_function :vosk_recognizer_set_nlsml, [VoskRecognizer, :int], :void
# TODO: remove this, it was needed only because I used libvosk from Python package
if Gem::Version.new(VERSION) >= Gem::Version.new("0.3.46")
attach_function :vosk_recognizer_set_endpointer_mode, [VoskRecognizer, VoskEndpointerMode], :void
attach_function :vosk_recognizer_set_endpointer_delays, [VoskRecognizer, :float, :float, :float], :void
end
attach_function :vosk_recognizer_accept_waveform, [VoskRecognizer, :buffer_in, :int], :int
# vosk_recognizer_accept_waveform_s; vosk_recognizer_accept_waveform_f - skipped
attach_function :vosk_recognizer_result, [VoskRecognizer], :string
attach_function :vosk_recognizer_partial_result, [VoskRecognizer], :string
attach_function :vosk_recognizer_final_result, [VoskRecognizer], :string
attach_function :vosk_recognizer_reset, [VoskRecognizer], :void
attach_function :vosk_recognizer_free, [VoskRecognizer], :void
attach_function :vosk_set_log_level, [:int], :void
attach_function :vosk_gpu_init, [], :void
attach_function :vosk_gpu_thread_init, [], :void
attach_function :vosk_batch_model_new, [:string], VoskBatchModel
attach_function :vosk_batch_model_free, [VoskBatchModel], :void
attach_function :vosk_batch_model_wait, [VoskBatchModel], :void
attach_function :vosk_batch_recognizer_new, [VoskBatchModel, :float], VoskBatchRecognizer
attach_function :vosk_batch_recognizer_free, [VoskBatchRecognizer], :void
attach_function :vosk_batch_recognizer_accept_waveform, [VoskBatchRecognizer, :buffer_in, :int], :void
# vosk_batch_recognizer_set_nlsml - skipped
attach_function :vosk_batch_recognizer_finish_stream, [VoskBatchRecognizer], :void
attach_function :vosk_batch_recognizer_front_result, [VoskBatchRecognizer], :string
attach_function :vosk_batch_recognizer_pop, [VoskBatchRecognizer], :void
attach_function :vosk_batch_recognizer_get_pending_chunks, [VoskBatchRecognizer], :int
# https://github.com/ffi/ffi/issues/467
class OwnedString
extend FFI::DataConverter
native_type :strptr
def self.to_native(_value, _context)
raise TypeError, "owned_string can't be used for input"
end
def self.from_native((str, ptr), _context)
C.free(ptr)
str
end
end
typedef OwnedString, :owned_string
attach_function :free, [:pointer], :void
# TODO: remove this, it was needed only because I used libvosk from Python package
if Gem::Version.new(VERSION) >= Gem::Version.new("0.3.48")
attach_function :vosk_text_processor_new, [:string, :string], VoskTextProcessor
attach_function :vosk_text_processor_free, [VoskTextProcessor], :void
# NOTE: you have a memory leak here in your python version, needs to be
# ptr = _c.vosk_text_processor_itn(self._handle, text.encode('utf-8'))
# str = _ffi.string(ptr).decode('utf-8')
# _ffi.gc(ptr, _c.free) # or call libc free directly
attach_function :vosk_text_processor_itn, [VoskTextProcessor, :string], :owned_string
end
end
private_constant :C
end
+58
View File
@@ -0,0 +1,58 @@
# frozen_string_literal: true
require "progressbar"
require "bytesize"
# Extends progressbar with tqdm-like output:
# %s / %z → human-readable byte sizes: current size / total siZe
# %d / %o → compact time: elapsed Duration / Outstanding remaining
# %r → rate_scale lambda may return a ByteSize string
# Override rate_of_change so rate_scale can return a string.
# The default format '%i' coerces the result to integer, breaking string values.
ProgressBar::Components::Rate.prepend(Module.new do
def rate_of_change(format_string = "%s")
return "0" if elapsed_seconds <= 0
format_string % scaled_rate
end
end)
class ProgressBar # :nodoc: all
# Add ByteSize-formatted progress methods to the Progress component.
class Progress
def progress_with_precision
ByteSize.new(progress).to_s
end
def total_with_unknown_indicator_with_precision
total ? ByteSize.new(total).to_s : total_with_unknown_indicator
end
end
module Components
# Add label-free time methods to the Time component.
class Time
def elapsed_no_label
val = elapsed
val.start_with?("00:") ? val[3..-1] : val
end
def estimated_no_label
val = estimated_with_friendly_oob.split(": ", 2).last
val.start_with?("00:") ? val[3..-1] : val
end
end
end
end
# Extend MOLECULES with new non-overlapping keys — originals are untouched.
# remove_const avoids the "already initialized constant" warning.
molecules = ProgressBar::Format::Molecule::MOLECULES
ProgressBar::Format::Molecule.send(:remove_const, :MOLECULES)
ProgressBar::Format::Molecule::MOLECULES = molecules.merge(
s: %i[progressable progress_with_precision],
z: %i[progressable total_with_unknown_indicator_with_precision],
d: %i[time_component elapsed_no_label],
o: %i[time_component estimated_no_label],
).freeze
+5
View File
@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Vosk
VERSION = "0.3.45"
end
+4
View File
@@ -0,0 +1,4 @@
module Vosk
VERSION: String
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
end
+18
View File
@@ -0,0 +1,18 @@
# frozen_string_literal: true
require "vosk"
require "webmock/rspec"
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
Vosk.log_level = -1
+173
View File
@@ -0,0 +1,173 @@
# frozen_string_literal: true
RSpec.describe Vosk::Model do
let(:tmpdir) { Dir.mktmpdir }
let(:en_model_path) { File.join(Dir.home, ".cache/vosk/vosk-model-small-en-us-0.4") }
let(:model_name) { "vosk-model-small-en-us-0.4" }
let(:model_list) do
[{ "name" => model_name, "lang" => "en-us", "type" => "small", "obsolete" => "false" }]
end
# Allocate an instance without calling initialize — no C library needed.
let(:stub_model) { described_class.allocate }
after { FileUtils.rm_rf(tmpdir) }
it "raises Vosk::Error on a bad path" do
expect do
described_class.new(model_path: "/nonexistent/path")
end.to raise_error(Vosk::Error, "Failed to create a model")
end
context "when loaded from a path" do
subject(:model) { described_class.new(model_path: en_model_path) }
it "constructs successfully" do
expect(model).to be_a(described_class)
end
it "finds a known word" do
expect(model.vosk_model_find_word("one")).to be >= 0
end
it "returns -1 for an unknown word" do
expect(model.vosk_model_find_word("xyzzy")).to eq(-1)
end
end
describe "#get_model_by_name" do
context "when the model directory already exists in MODEL_DIRS" do
before do
FileUtils.makedirs(File.join(tmpdir, model_name))
stub_const("Vosk::MODEL_DIRS", [tmpdir])
end
it "returns its path without hitting the network" do
expect(stub_model.send(:get_model_by_name, model_name)).to eq(File.join(tmpdir, model_name))
end
end
context "when the model is not in any MODEL_DIR" do
before do
stub_const("Vosk::MODEL_DIRS", [tmpdir])
stub_request(:get, Vosk::MODEL_LIST_URL)
.to_return(status: 200, body: model_list.to_json, headers: { "Content-Type" => "application/json" })
allow(stub_model).to receive(:download_model)
end
it "downloads the model to the last MODEL_DIR" do
stub_model.send(:get_model_by_name, model_name)
expect(stub_model).to have_received(:download_model).with(File.join(tmpdir, model_name))
end
it "returns the expected path after downloading" do
expect(stub_model.send(:get_model_by_name, model_name)).to eq(File.join(tmpdir, model_name))
end
it "exits when the model name is not in the remote list" do
expect { stub_model.send(:get_model_by_name, "vosk-model-unknown") }.to raise_error(SystemExit)
end
end
end
describe "#get_model_by_lang" do
context "when a matching model directory already exists" do
before do
FileUtils.makedirs(File.join(tmpdir, model_name))
stub_const("Vosk::MODEL_DIRS", [tmpdir])
end
it "returns its path without hitting the network" do
expect(stub_model.send(:get_model_by_lang, "en-us")).to eq(File.join(tmpdir, model_name))
end
it "also matches vosk-model-<lang> (without -small-)" do
other = "vosk-model-en-us-0.22"
FileUtils.makedirs(File.join(tmpdir, other))
stub_const("Vosk::MODEL_DIRS", [tmpdir])
expect(stub_model.send(:get_model_by_lang, "en-us")).not_to be_nil
end
end
context "when no local model exists for the language" do
before do
stub_const("Vosk::MODEL_DIRS", [tmpdir])
stub_request(:get, Vosk::MODEL_LIST_URL)
.to_return(status: 200, body: model_list.to_json, headers: { "Content-Type" => "application/json" })
allow(stub_model).to receive(:download_model)
end
it "downloads a small model for the language" do
stub_model.send(:get_model_by_lang, "en-us")
expect(stub_model).to have_received(:download_model).with(File.join(tmpdir, model_name))
end
it "exits when no model is available for the language" do
expect { stub_model.send(:get_model_by_lang, "xx-unknown") }.to raise_error(SystemExit)
end
end
end
describe "#download_model" do
let(:model_path) { File.join(tmpdir, model_name) }
let(:zip_content) do
Zip::OutputStream.write_buffer do |zip|
zip.put_next_entry("#{model_name}/conf/model.conf")
zip.write("# model config\n" * 1_000)
zip.put_next_entry("#{model_name}/README")
zip.write("test model\n")
end.string
end
let(:progress_bar_content) { StringIO.new }
let(:progress_bar_stream) do
dbl = double
allow(dbl).to receive(:tty?).with(no_args).and_return(true)
allow(dbl).to receive(:print) do |*args|
progress_bar_content.print(*args)
end
allow(dbl).to receive(:flush).with(no_args).and_return(dbl)
allow(dbl).to receive(:winsize).with(no_args).and_return([40, 180])
dbl
end
before do
stub_const("ProgressBar::Output::DEFAULT_OUTPUT_STREAM", progress_bar_stream)
stub_request(:get, "#{Vosk::MODEL_PRE_URL}#{model_name}.zip")
.to_return(status: 200, body: zip_content, headers: { "Content-Length" => zip_content.bytesize.to_s })
end
it "extracts the archive into the parent directory" do
stub_model.send(:download_model, model_path)
expect(File.exist?(File.join(model_path, "conf/model.conf"))).to be(true)
end
it "deletes the zip file after extraction" do
stub_model.send(:download_model, model_path)
expect(File.exist?("#{model_path}.zip")).to be(false)
end
it "creates the parent directory when it does not yet exist" do
nested_path = File.join(tmpdir, "new_subdir", model_name)
stub_model.send(:download_model, nested_path)
expect(Dir.exist?(File.dirname(nested_path))).to be(true)
end
it "uses the correct download URL" do
stub_model.send(:download_model, model_path)
expect(WebMock).to have_requested(:get, "#{Vosk::MODEL_PRE_URL}#{model_name}.zip")
end
it "displays a progress bar during download" do
stub_model.send(:download_model, model_path)
# TODO: test callback when multiple fragments are received
expect(progress_bar_content.string).to eq(
(" " * 180).concat(
"\r" \
"vosk-model-small-en-us-0.4.zip: 0%|=---=---=---=---=---=---=---=---=---=---=---=---=---=---=---=---=--" \
"-=---=---=---=---=---=---=---=---=---=---=| 0 bytes/?? [00:00<??:??:??, 0/s]\r" \
"vosk-model-small-en-us-0.4.zip: 100%|███████████████████████████████████████████████████████████████████" \
"████████████████████████████████████| 402 bytes/402 bytes [00:00<00:00, 0/s]\n",
),
)
end
end
end
+182
View File
@@ -0,0 +1,182 @@
# frozen_string_literal: true
require "json"
require "srt"
require "wavefile"
RSpec.describe Vosk do
let(:en_model_path) { File.join(Dir.home, ".cache/vosk/vosk-model-small-en-us-0.4") }
let(:test_wav_path) { File.expand_path("../example/test.wav", __dir__) }
let(:wave_reader) { WaveFile::Reader.new(test_wav_path) }
let(:wave_chunks) do
chunks = []
wave_reader.each_buffer(4000) { |buffer| chunks.push(buffer.samples.pack(WaveFile::PACK_CODES.dig(:pcm, 16))) }
chunks
end
after { wave_reader.close }
it "has a version number" do
expect(Vosk::VERSION).not_to be_nil
end
describe "EndpointerMode" do
it "defines the correct integer constants" do
expect([
Vosk::EndpointerMode::DEFAULT,
Vosk::EndpointerMode::SHORT,
Vosk::EndpointerMode::LONG,
Vosk::EndpointerMode::VERY_LONG,
]).to eq([0, 1, 2, 3])
end
end
describe Vosk::SpkModel do
it "raises Vosk::Error on a bad path" do
expect do
described_class.new("/nonexistent/path")
end.to raise_error(Vosk::Error, "Failed to create a speaker model")
end
end
describe Vosk::KaldiRecognizer do
let(:model) { Vosk::Model.new(model_path: en_model_path) }
let(:wave_sample_rate) { wave_reader.format.sample_rate }
describe "initialization" do
it "accepts (model, sample_rate)" do
expect(described_class.new(model, wave_sample_rate)).to be_a(described_class)
end
it "accepts (model, sample_rate, grammar_string)" do
rec = described_class.new(model, wave_sample_rate, '["one two three", "[unk]"]')
expect(rec).to be_a(described_class)
end
it "raises TypeError for an unknown third argument type" do
expect { described_class.new(model, wave_sample_rate, 42) }.to raise_error(TypeError)
end
end
context "when processing audio" do
subject(:rec) { described_class.new(model, wave_sample_rate) }
let(:wave_stream) do
stream = wave_chunks.each_with_object(StringIO.new) { |chunk, stream| stream.write(chunk) }
stream.rewind
stream
end
let(:expected_srt) do
<<~SRT
1
00:00:00,870 --> 00:00:02,610
what zero zero zero one
2
00:00:03,930 --> 00:00:04,950
no no to uno
3
00:00:06,240 --> 00:00:08,010
cyril one eight zero three
SRT
end
it "accept_waveform returns 0 or 1" do
results = wave_chunks.map { |chunk| rec.accept_waveform(chunk) }.uniq
expect(results).to contain_exactly(0, 1)
end
it "result returns valid JSON" do
rec.accept_waveform(wave_chunks.first)
expect { JSON.parse(rec.result) }.not_to raise_error
end
it "partial_result returns valid JSON" do
rec.accept_waveform(wave_chunks.first)
expect { JSON.parse(rec.partial_result) }.not_to raise_error
end
it "final_result returns valid JSON" do
expect { JSON.parse(rec.final_result) }.not_to raise_error
end
it "transcribes the test file to non-empty text" do
wave_chunks.each { |chunk| rec.accept_waveform(chunk) }
text = JSON.parse(rec.final_result)["text"]
expect(text).not_to be_empty
end
it "reset clears the partial result" do
rec.accept_waveform(wave_chunks.first)
rec.reset
expect(JSON.parse(rec.partial_result)["partial"]).to be_nil.or(eq(""))
end
it "set_words does not raise" do
expect { rec.words = true }.not_to raise_error
end
it "set_words includes per-word timing in results" do
rec.words = true
wave_chunks.each { |chunk| rec.accept_waveform(chunk) }
result = JSON.parse(rec.final_result)
expect(result).to have_key("text")
expect(result["result"]).to be_an(Array)
end
it "set_partial_words does not raise" do
expect { rec.partial_words = true }.not_to raise_error
end
it "set_max_alternatives produces an alternatives array" do
rec.max_alternatives = 5
wave_chunks.each { |chunk| rec.accept_waveform(chunk) }
result = JSON.parse(rec.final_result)
expect(result).to have_key("alternatives")
expect(result["alternatives"]).to be_an(Array)
end
it "set_endpointer_mode accepts EndpointerMode constants",
skip: Gem::Version.new(Vosk::VERSION) < Gem::Version.new("0.3.46") && "requires libvosk >= 0.3.46" do
expect { rec.endpointer_mode = :short }.not_to raise_error
end
it "set_endpointer_delays accepts float values",
skip: Gem::Version.new(Vosk::VERSION) < Gem::Version.new("0.3.46") && "requires libvosk >= 0.3.46" do
expect { rec.set_endpointer_delays(0.5, 1.0, 30.0) }.not_to raise_error
end
it "set_grammar changes the active grammar" do
expect { rec.grammar = '["one two three", "[unk]"]' }.not_to raise_error
end
it "srt_result produces a valid SRT" do
rec.words = true
expect(rec.srt_result(wave_stream)).to eq(expected_srt)
end
end
context "with a grammar recognizer" do
subject(:rec) do
described_class.new(
model, wave_sample_rate,
'["one two three four five six seven eight nine zero", "[unk]"]',
)
end
it "produces a result constrained to the grammar vocabulary" do
wave_chunks.each { |chunk| rec.accept_waveform(chunk) }
expect(JSON.parse(rec.final_result)).to have_key("text")
end
end
end
describe Vosk::Processor,
skip: Gem::Version.new(Vosk::VERSION) < Gem::Version.new("0.3.48") && "requires libvosk >= 0.3.48" do
it "raises Vosk::Error on a bad lang/type" do
expect { described_class.new("xx_invalid", "itn") }.to raise_error(Vosk::Error, "Failed to create processor")
end
end
end
+40 -11
View File
@@ -1,12 +1,41 @@
Gem::Specification.new do |s|
s.name = "vosk"
s.version = "0.3.50"
s.summary = "Offline speech recognition API"
s.description = "Vosk is an offline open source speech recognition toolkit. It enables speech recognition for 20+ languages and dialects - English, Indian English, German, French, Spanish, Portuguese, Chinese, Russian, Turkish, Vietnamese, Italian, Dutch, Catalan, Arabic, Greek, Farsi, Filipino, Ukrainian, Kazakh, Swedish, Japanese, Esperanto, Hindi, Czech, Polish. More to come."
s.authors = ["Alpha Cephei Inc"]
s.email = "contact@alphacphei.com"
s.files = ["lib/vosk.rb"]
s.homepage =
"https://rubygems.org/gems/vosk"
s.license = "Apache 2.0"
# frozen_string_literal: true
require_relative "lib/vosk/version"
Gem::Specification.new do |spec|
spec.name = "vosk"
spec.version = Vosk::VERSION
spec.authors = ["Alpha Cephei Inc", "Vladimir Ulianitsky"]
spec.email = ["contact@alphacphei.com", "uvlad7@gmail.com"]
spec.summary = "Offline speech recognition API"
spec.description =
"Vosk is an offline open source speech recognition toolkit. " \
"It enables speech recognition for 20+ languages and dialects - " \
"English, Indian English, German, French, Spanish, Portuguese, Chinese, Russian, Turkish, Vietnamese, " \
"Italian, Dutch, Catalan, Arabic, Greek, Farsi, Filipino, Ukrainian, Kazakh, Swedish, Japanese, " \
"Esperanto, Hindi, Czech, Polish. More to come."
spec.homepage = "https://alphacephei.com/vosk"
spec.license = "Apache-2.0"
spec.required_ruby_version = ">= 2.5"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/alphacep/vosk-api"
spec.files = [
*Dir["lib/**/*.rb"], *Dir["exe/**/*"], *Dir["sig/**/*"],
]
spec.bindir = "exe"
spec.executables = ["vosk-transcriber"]
spec.require_paths = ["lib"]
spec.add_dependency "bytesize", "~> 0.1"
spec.add_dependency "ffi", "~> 1.6"
spec.add_dependency "fileutils", "~> 1.7"
spec.add_dependency "httparty", "~> 0.21"
spec.add_dependency "progressbar", "~> 1.13"
spec.add_dependency "rubyzip", "~> 2.4"
spec.add_dependency "srt", "~> 0.1.5"
# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html
spec.metadata["rubygems_mfa_required"] = "true"
end
+17 -17
View File
@@ -46,9 +46,6 @@ void LanguageModelEstimator::AddCounts(const std::vector<int32> &sentence) {
void LanguageModelEstimator::IncrementCount(const std::vector<int32> &history,
int32 next_phone) {
int32 lm_state_index = FindOrCreateLmStateIndexForHistory(history);
if (lm_states_[lm_state_index].tot_count == 0) {
num_active_lm_states_++;
}
lm_states_[lm_state_index].AddCount(next_phone, 1);
}
@@ -106,6 +103,7 @@ int32 LanguageModelEstimator::FindOrCreateLmStateIndexForHistory(
int32 backoff_lm_state = FindOrCreateLmStateIndexForHistory(backoff_hist);
lm_states_[ans].backoff_lmstate_index = backoff_lm_state;
}
num_active_lm_states_++;
return ans;
}
@@ -156,12 +154,13 @@ int32 LanguageModelEstimator::FindInitialFstState() const {
void LanguageModelEstimator::OutputToFst(
int32 num_states,
fst::StdVectorFst *fst) const {
fst::StdVectorFst *out_fst) const {
KALDI_ASSERT(num_states == num_active_lm_states_);
fst->DeleteStates();
fst::StdVectorFst fst;
for (int32 i = 0; i < num_states; i++)
fst->AddState();
fst->SetStart(FindInitialFstState());
fst.AddState();
fst.SetStart(FindInitialFstState());
int64 tot_count = 0;
double tot_logprob = 0.0;
@@ -184,28 +183,29 @@ void LanguageModelEstimator::OutputToFst(
tot_count += count;
tot_logprob += logprob * count;
if (phone == 0) { // Go to final state
fst->SetFinal(lm_state.fst_state, fst::TropicalWeight(-logprob));
fst.SetFinal(lm_state.fst_state, fst::TropicalWeight(-logprob));
} else { // It becomes a transition.
std::vector<int32> next_history(lm_state.history);
next_history.push_back(phone);
int32 dest_lm_state = FindNonzeroLmStateIndexForHistory(next_history),
dest_fst_state = lm_states_[dest_lm_state].fst_state;
KALDI_ASSERT(dest_fst_state != -1);
fst->AddArc(lm_state.fst_state,
fst.AddArc(lm_state.fst_state,
fst::StdArc(phone, phone, fst::TropicalWeight(-logprob),
dest_fst_state));
}
}
if (lm_state.backoff_lmstate_index >= 0) {
fst->AddArc(lm_state.fst_state, fst::StdArc(0, 0, fst::TropicalWeight(-log(1 - opts_.discount)), lm_states_[lm_state.backoff_lmstate_index].fst_state));
fst.AddArc(lm_state.fst_state, fst::StdArc(0, 0, fst::TropicalWeight(-log(1 - opts_.discount)), lm_states_[lm_state.backoff_lmstate_index].fst_state));
}
}
fst::Connect(fst);
// Make sure that Connect does not delete any states.
int32 num_states_connected = fst->NumStates();
KALDI_ASSERT(num_states_connected == num_states);
fst::DeterminizeOptions<fst::StdArc> opts;
fst::Determinize(fst, out_fst, opts);
fst::Connect(out_fst);
// arc-sort. ilabel or olabel doesn't matter, it's an acceptor.
fst::ArcSort(fst, fst::ILabelCompare<fst::StdArc>());
KALDI_LOG << "Created language model with " << num_states
<< " states and " << fst::NumArcs(*fst) << " arcs.";
fst::ArcSort(out_fst, fst::ILabelCompare<fst::StdArc>());
KALDI_LOG << "Created language model with " << out_fst->NumStates()
<< " states and " << fst::NumArcs(*out_fst) << " arcs.";
KALDI_LOG << "Originally language model with " << fst.NumStates()
<< " states and " << fst::NumArcs(fst) << " arcs.";
}
+16 -3
View File
@@ -18,6 +18,8 @@
#include "lat/sausages.h"
#include "language_model.h"
#include <unordered_set>
using namespace fst;
using namespace kaldi::nnet3;
@@ -355,6 +357,7 @@ void Recognizer::UpdateGrammarFst(char const *grammar)
}
estimator.AddCounts(sentence);
}
delete g_fst_;
g_fst_ = new StdVectorFst();
estimator.Estimate(g_fst_);
@@ -461,13 +464,12 @@ bool Recognizer::GetSpkVector(Vector<BaseFloat> &out_xvector, int *num_spk_frame
int num_frames = spk_feature_->NumFramesReady() - frame_offset_ * 3;
Matrix<BaseFloat> mfcc(num_frames, spk_feature_->Dim());
// Not very efficient, would be nice to have faster search
std::unordered_set<int32> nonsilence_set(nonsilence_frames.begin(), nonsilence_frames.end());
int num_nonsilence_frames = 0;
Vector<BaseFloat> feat(spk_feature_->Dim());
for (int i = 0; i < num_frames; ++i) {
if (std::find(nonsilence_frames.begin(),
nonsilence_frames.end(), i / 3) == nonsilence_frames.end()) {
if (nonsilence_set.find(i / 3) == nonsilence_set.end()) {
continue;
}
@@ -698,6 +700,17 @@ const char *Recognizer::NbestResult(CompactLattice &clat)
obj["alternatives"].append(entry);
}
if (spk_model_) {
Vector<BaseFloat> xvector;
int num_spk_frames;
if (GetSpkVector(xvector, &num_spk_frames)) {
for (int i = 0; i < xvector.Dim(); i++) {
obj["spk"].append(xvector(i));
}
obj["spk_frames"] = num_spk_frames;
}
}
return StoreReturn(obj.dump());
}
+119 -2
View File
@@ -1,3 +1,120 @@
A proper simple setup to train a Vosk model
# Vosk API Training
More documentation later
This directory contains scripts and tools for training speech recognition models using the Kaldi toolkit.
## Table of Contents
1. [Overview](#overview)
2. [Directory Structure](#directory-structure)
3. [Installation](#installation)
4. [Training Process](#training-process)
- [Data Preparation](#data-preparation)
- [Dictionary Preparation](#dictionary-preparation)
- [MFCC Feature Extraction](#mfcc-feature-extraction)
- [Acoustic Model Training](#acoustic-model-training)
- [TDNN Chain Model Training](#tdnn-chain-model-training)
- [Decoding](#decoding)
5. [Results](#results)
6. [Contributing](#contributing)
## Overview
This repository provides tools for training custom speech recognition models using Kaldi. It supports acoustic model training, language model creation, and decoding pipelines.
## Directory Structure
```plaintext
.
├── cmd.sh # Command configuration for training and decoding
├── conf/
│ ├── mfcc.conf # Configuration for MFCC feature extraction
│ └── online_cmvn.conf # Online Cepstral Mean Variance Normalization (currently empty)
├── local/
│ ├── chain/
│ │ ├── run_ivector_common.sh # Script for i-vector extraction during chain model training
│ │ └── run_tdnn.sh # Script for training a TDNN model
│ ├── data_prep.sh # Data preparation script for creating Kaldi data directories
│ ├── download_and_untar.sh # Script for downloading and extracting datasets
│ ├── download_lm.sh # Downloads language models
│ ├── prepare_dict.sh # Prepares the pronunciation dictionary
│ └── score.sh # Scoring script for evaluation
├── path.sh # Script for setting Kaldi paths
├── RESULTS # Script for printing the best WER results
├── RESULTS.txt # Contains WER results from decoding
├── run.sh # Main script for the entire training pipeline
├── steps -> ../../wsj/s5/steps/ # Link to Kaldis WSJ steps for acoustic model training
└── utils -> ../../wsj/s5/utils/ # Link to Kaldis utility scripts
```
### Key Files:
- **cmd.sh**: Defines commands for running training and decoding tasks.
- **path.sh**: Sets up paths for Kaldi binaries and scripts.
- **run.sh**: Main entry point for the training pipeline, running tasks in stages.
- **RESULTS**: Displays Word Error Rate (WER) for the trained models.
## Installation
### Prerequisites
- [Kaldi](https://github.com/kaldi-asr/kaldi): Kaldi toolkit must be installed and configured.
- Required tools: `ffmpeg`, `sox`, `sctk` for data preparation and scoring.
### Steps
1. Clone the Vosk API repository.
2. Install Kaldi and ensure the `KALDI_ROOT` is correctly set in `path.sh`.
3. Set environment variables using `cmd.sh` and `path.sh`.
## Training Process
### Data Preparation
Run the data preparation stage in `run.sh`:
```bash
bash run.sh --stage 0 --stop_stage 0
```
This stage downloads and prepares the LibriSpeech dataset.
### Dictionary Preparation
Prepare the pronunciation dictionary with:
```bash
bash run.sh --stage 1 --stop_stage 1
```
This step generates the necessary files for Kaldi's `prepare_lang.sh` script.
### MFCC Feature Extraction
Run the MFCC extraction process:
```bash
bash run.sh --stage 2 --stop_stage 2
```
This step extracts Mel-frequency cepstral coefficients (MFCC) features and computes Cepstral Mean Variance Normalization (CMVN).
### Acoustic Model Training
Train monophone, LDA+MLLT, and SAT models:
```bash
bash run.sh --stage 3 --stop_stage 3
```
This stage trains GMM-based models and aligns the data for TDNN training.
### TDNN Chain Model Training
Train a Time-Delay Neural Network (TDNN) chain model:
```bash
bash run.sh --stage 4 --stop_stage 4
```
The chain model uses i-vectors for speaker adaptation.
### Decoding
After training, decode the test data:
```bash
bash run.sh --stage 5 --stop_stage 5
```
This step decodes using the trained model and evaluates the Word Error Rate (WER).
## Results
WER can be evaluated by running:
```bash
bash RESULTS
```
Example of `RESULTS.txt`:
```plaintext
%WER 14.10 [ 2839 / 20138, 214 ins, 487 del, 2138 sub ] exp/chain/tdnn/decode_test/wer_11_0.0
%WER 12.67 [ 2552 / 20138, 215 ins, 406 del, 1931 sub ] exp/chain/tdnn/decode_test_rescore/wer_11_0.0
```
+1
View File
@@ -44,6 +44,7 @@ RUN cd /opt \
&& cd /opt/kaldi/src \
&& sed -i "s:TARGET_ARCH=\"\`uname -m\`\":TARGET_ARCH=$(echo $CROSS_TRIPLE|cut -d - -f 1):g" configure \
&& sed -i "s: -O1 : -O3 :g" makefiles/linux_openblas_arm.mk \
&& sed -i "s: -O1 : -O3 :g" makefiles/linux_openblas_arm.mk \
&& ./configure --mathlib=OPENBLAS_CLAPACK --shared --use-cuda=no \
&& make -j 10 online2 rnnlm \
&& find /opt/kaldi -name "*.o" -exec rm {} \;
+50
View File
@@ -0,0 +1,50 @@
ARG DOCKCROSS_IMAGE=alphacep/dockcross-linux-armv7
FROM ${DOCKCROSS_IMAGE}
LABEL description="A docker image for building portable Python linux binary wheels and Kaldi on other architectures"
LABEL maintainer="contact@alphacephei.com"
RUN apt-get update && \
apt-get install -y --no-install-recommends \
wget \
libffi-dev \
libpcre3-dev \
zlib1g-dev \
automake \
autoconf \
libtool \
cmake \
python3 \
python3-pip \
python3-wheel \
python3-setuptools \
python3-cffi \
&& rm -rf /var/lib/apt/lists/*
ARG OPENBLAS_ARGS=
RUN cd /opt \
&& git clone -b vosk --single-branch https://github.com/alphacep/kaldi \
&& cd kaldi/tools \
&& git clone -b v0.3.20 --single-branch https://github.com/xianyi/OpenBLAS \
&& git clone -b v3.2.1 --single-branch https://github.com/alphacep/clapack \
&& echo ${OPENBLAS_ARGS} \
&& make -C OpenBLAS ONLY_CBLAS=1 ${OPENBLAS_ARGS} HOSTCC=gcc USE_LOCKING=1 USE_THREAD=0 all \
&& make -C OpenBLAS ${OPENBLAS_ARGS} HOSTCC=gcc USE_LOCKING=1 USE_THREAD=0 PREFIX=$(pwd)/OpenBLAS/install install \
&& mkdir -p clapack/BUILD && cd clapack/BUILD && cmake .. \
&& make -j 10 -C F2CLIBS \
&& make -j 10 -C BLAS \
&& make -j 10 -C SRC \
&& find . -name "*.a" | xargs cp -t ../../OpenBLAS/install/lib \
&& cd /opt/kaldi/tools \
&& git clone --single-branch https://github.com/alphacep/openfst openfst \
&& cd openfst \
&& autoreconf -i \
&& CFLAGS="-g -O3" ./configure --prefix=/opt/kaldi/tools/openfst --enable-static --enable-shared --enable-far --enable-ngram-fsts --enable-lookahead-fsts --with-pic --disable-bin --host=${CROSS_TRIPLE} --build=x86-linux-gnu \
&& make -j 10 && make install \
&& cd /opt/kaldi/src \
&& sed -i "s:TARGET_ARCH=\"\`uname -m\`\":TARGET_ARCH=$(echo $CROSS_TRIPLE|cut -d - -f 1):g" configure \
&& sed -i "s: -O1 : -O3 :g" makefiles/linux_openblas_arm.mk \
&& sed -i "s:-DHAVE_EXECINFO_H=1::g" makefiles/linux_openblas_arm.mk \
&& ./configure --mathlib=OPENBLAS_CLAPACK --shared --use-cuda=no \
&& make -j 10 online2 rnnlm \
&& find /opt/kaldi -name "*.o" -exec rm {} \;
+70
View File
@@ -0,0 +1,70 @@
FROM ubuntu:20.04
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install -y --no-install-recommends \
ca-certificates \
g++ \
bzip2 \
unzip \
make \
wget \
git \
python3 \
python3-pip \
python3-wheel \
python3-setuptools \
python3-cffi \
zlib1g-dev \
patch \
cmake \
xz-utils \
automake \
autoconf \
libtool \
pkg-config \
sudo \
&& rm -rf /var/lib/apt/lists/*
RUN cd /opt && \
wget https://github.com/mstorsjo/llvm-mingw/releases/download/20240820/llvm-mingw-20240820-msvcrt-ubuntu-20.04-x86_64.tar.xz \
&& tar xf llvm-mingw-20240820-msvcrt-ubuntu-20.04-x86_64.tar.xz \
&& mv llvm-mingw-20240820-msvcrt-ubuntu-20.04-x86_64 llvm-mingw
ENV PATH="$PATH:/opt/llvm-mingw/bin"
RUN mkdir /opt/kaldi \
&& git clone https://github.com/alphacep/openfst \
&& cd openfst \
&& autoreconf -i \
&& CXX=aarch64-w64-mingw32-g++ CXXFLAGS="-O3 -ftree-vectorize -DFST_NO_DYNAMIC_LINKING" \
./configure --prefix=/opt/kaldi/local \
--enable-shared --enable-static --with-pic --disable-bin \
--enable-lookahead-fsts --enable-ngram-fsts --host=aarch64-w64-mingw32 \
&& make -j $(nproc) \
&& make install
RUN cd /opt/kaldi \
&& git clone -b v0.3.20 --single-branch https://github.com/xianyi/OpenBLAS \
&& cd OpenBLAS \
&& make HOSTCC=gcc CC=aarch64-w64-mingw32-gcc ONLY_CBLAS=1 USE_LOCKING=1 USE_THREAD=0 USE_OPENMP=0 DYNAMIC_ARCH=0 TARGET=ARMV8 ARCH=arm64 BINARY=64 -j $(nproc) \
&& make PREFIX=/opt/kaldi/local install
RUN cd /opt/kaldi \
&& git clone -b v3.2.1 --single-branch https://github.com/alphacep/clapack \
&& mkdir clapack/BUILD \
&& cd clapack/BUILD \
&& cmake -DCMAKE_C_COMPILER_TARGET=aarch64-w64-mingw32 -DCMAKE_C_COMPILER=aarch64-w64-mingw32-gcc -DCMAKE_SYSTEM_NAME=Windows -DCMAKE_CROSSCOMPILING=True .. \
&& make -C F2CLIBS/libf2c \
&& make -C BLAS \
&& make -C SRC \
&& find . -name *.a -exec cp {} /opt/kaldi/local/lib \;
RUN cd /opt/kaldi \
&& git clone -b vosk-android --single-branch https://github.com/alphacep/kaldi \
&& cd kaldi/src \
&& CXX=aarch64-w64-mingw32-g++ CXXFLAGS="-O3 -ftree-vectorize -DFST_NO_DYNAMIC_LINKING" ./configure --shared --mingw=yes --use-cuda=no \
--mathlib=OPENBLAS_CLAPACK \
--host=aarch64-w64-mingw32 --openblas-clapack-root=/opt/kaldi/local \
--fst-root=/opt/kaldi/local --fst-version=1.8.0 \
&& make depend -j \
&& make LLVM_BUILD=1 -j $(nproc) online2 rnnlm
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -e
set -x
docker build --build-arg="DOCKCROSS_IMAGE=dockcross/linux-armv7l-musl" --build-arg="OPENBLAS_ARGS=TARGET=ARMV7" --file Dockerfile.dockcross-musl --tag alphacep/kaldi-dockcross-armv7-musl:latest .
docker run --rm -v /home/shmyrev/travis/vosk-api/:/io alphacep/kaldi-dockcross-armv7-musl /io/travis/build-wheels-dockcross.sh
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -e -x
docker build --file Dockerfile.winaarch64 --tag alphacep/kaldi-winaarch64:latest .
docker run --rm -v `realpath ..`:/io alphacep/kaldi-winaarch64 /io/travis/build-wheels-winaarch64.sh
+8 -3
View File
@@ -14,6 +14,11 @@ case $CROSS_TRIPLE in
export VOSK_MACHINE=armv7l
export VOSK_ARCHITECTURE=32bit
;;
*armv7l-linux-musleabihf*)
export VOSK_MACHINE=armv7l
export VOSK_ARCHITECTURE=32bit
export VOSK_VARIANT="-musl"
;;
*i686-*)
export VOSK_MACHINE=x86
export VOSK_ARCHITECTURE=32bit
@@ -29,9 +34,9 @@ case $CROSS_TRIPLE in
esac
# Copy library to output folder
mkdir -p /io/wheelhouse/vosk-linux-$VOSK_MACHINE
cp /opt/vosk-api/src/*.so /opt/vosk-api/src/vosk_api.h /io/wheelhouse/vosk-linux-$VOSK_MACHINE
mkdir -p /io/wheelhouse/vosk-linux-${VOSK_MACHINE}${VOSK_VARIANT}
cp /opt/vosk-api/src/*.so /opt/vosk-api/src/vosk_api.h /io/wheelhouse/vosk-linux-$VOSK_MACHINE${VOSK_VARIANT}
# Build wheel
python3 -m pip install requests tqdm srt websockets wheel
python3 -m pip install requests tqdm srt websockets wheel --break-system-packages
python3 -m pip wheel /opt/vosk-api/python --no-deps -w /io/wheelhouse
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
set -e -x
# Build libvosk
cd /opt
git clone https://github.com/alphacep/vosk-api
cd vosk-api/src
EXTRA_LDFLAGS=-Wl,--out-implib,libvosk.lib CXX=aarch64-w64-mingw32-g++ EXT=dll KALDI_ROOT=/opt/kaldi/kaldi OPENFST_ROOT=/opt/kaldi/local OPENBLAS_ROOT=/opt/kaldi/local make -j $(nproc)
# Copy dlls to output folder
mkdir -p /io/wheelhouse/vosk-winaarch64
cp /opt/vosk-api/src/*.{dll,lib} /opt/vosk-api/src/vosk_api.h /io/wheelhouse/vosk-winaarch64
# Build wheel and put to the output folder
export VOSK_SOURCE=/opt/vosk-api
export VOSK_SYSTEM=Windows
export VOSK_ARCHITECTURE=64bit
python3 -m pip -v wheel /opt/vosk-api/python --no-deps -w /io/wheelhouse
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vosk-js",
"version": "0.3.50",
"version": "0.3.75",
"description": "Node binding for continuous voice recoginition through vosk-api.",
"repository": {
"type": "git",