简介

sherpa 是 Next-gen Kaldi 项目的部署框架。

使用

VAD

语音活动检测(Voice Activity Detection,简称VAD)是一种技术,用于检测音频信号中是否存在语音或其他声音活动。它在语音处理、语音识别、音频压缩等领域有广泛的应用。

VAD的主要功能

  • 语音识别系统:通过VAD,系统可以在检测到语音时启动识别过程,提高效率。
  • 音频压缩:在语音通信中,VAD可以帮助压缩算法仅对有效语音信号进行压缩,减少传输数据量。
  • 噪声抑制系统:通过检测语音活动,系统可以在静默时段增强噪声抑制效果。

在GO中使用

todo

KWS

关键词唤醒(Keyword Spotting,简称KWS)是一种技术,用于检测音频信号中特定的关键词或短语。它广泛应用于语音助手、智能家居设备、车载系统等领域,通过识别特定关键词来激活设备或执行特定命令。

主要功能

  • 关键词检测:识别音频信号中是否包含预定义的关键词或短语。
  • 唤醒设备:当检测到关键词时,激活设备或应用程序。
  • 提高用户体验:通过语音命令简化操作流程,增强用户体验。

自定义keywords

通过官方的工具sherpa-onnx-cli,可以实现自定义关键字,下面是简单的介绍 原文

# Note: You need to run pip install sherpa-onnx to get the commandline tool: sherpa-onnx-cli


sherpa-onnx-cli text2token --help
Usage: sherpa-onnx-cli text2token [OPTIONS] INPUT OUTPUT

Options:

  --text TEXT         Path to the input texts. Each line in the texts contains the original phrase, it might also contain some extra items,
                      for example, the boosting score (startting with :), the triggering threshold
                      (startting with #, only used in keyword spotting task) and the original phrase (startting with @).
                      Note: extra items will be kept in the output.

                      example input 1 (tokens_type = ppinyin):
                          小爱同学 :2.0 #0.6 @小爱同学
                          你好问问 :3.5 @你好问问
                          小艺小艺 #0.6 @小艺小艺
                      example output 1:
                          x iǎo ài tóng x ué :2.0 #0.6 @小爱同学
                          n ǐ h ǎo w èn w èn :3.5 @你好问问
                          x iǎo y ì x iǎo y ì #0.6 @小艺小艺

                      example input 2 (tokens_type = bpe):
                          HELLO WORLD :1.5 #0.4
                          HI GOOGLE :2.0 #0.8
                          HEY SIRI #0.35
                      example output 2:
                          ▁HE LL O ▁WORLD :1.5 #0.4
                          ▁HI ▁GO O G LE :2.0 #0.8
                          ▁HE Y ▁S I RI #0.35

  --tokens TEXT       The path to tokens.txt.
  --tokens-type TEXT  The type of modeling units, should be cjkchar, bpe, cjkchar+bpe, fpinyin or ppinyin.
                      fpinyin means full pinyin, each cjkchar has a pinyin(with tone). ppinyin
                      means partial pinyin, it splits pinyin into initial and final,
  --bpe-model TEXT    The path to bpe.model. Only required when tokens-type is bpe or cjkchar+bpe.
  --help              Show this message and exit.

我这里只是记录下小爱同学 :2.0 #0.6 @小爱同学 这部分。在这个例子里面:数字 是增强得分(boosting score),#数字 是触发阈值(triggering threshold)。

关于关键词识别中增强得分和触发阈值的优化技巧,我从AI那里获取了一些建议:

简单地讲是“增大 keywords_score, 减小 keywords_threshold”

增大 keywords_score:这会使系统更容易触发关键词识别

减小 keywords_threshold:降低整体触发门槛

增强得分(Boosting Score)优化技巧
  1. 优先级分配

    • 为重要指令设置更高的增强得分(如"暂停"、“结束"可设为3.0-4.0)
    • 为常用指令设置中等增强得分(如"开始”、“跳过"可设为2.0-2.5)
    • 为非关键指令设置较低增强得分(如"确认”、“是"可设为1.0-1.5)
  2. 长短句区分

    • 短词通常需要更高的增强得分,因为它们更容易被误触发(如单字词"是”、“否"可设为3.0)
    • 较长的短语可以使用相对较低的增强得分(如"跳过这个动作"可设为1.5-2.0)
  3. 音素相似性处理

    • 对于音素相似的关键词,可以为更重要的词设置更高的增强得分
    • 例如,“开始"和"结束"音素差异大,但"暂停"和"跳过"部分音素相似,可适当调整
触发阈值(Triggering Threshold)优化技巧
  1. 重要性区分

    • 紧急指令(如"结束”、“暂停”)可设置较低的触发阈值(如#0.5),使其更容易被触发
    • 非紧急指令可设置较高的阈值(如#0.7),避免误触发
  2. 环境适应

    • 在噪音大的环境中使用时,可增加阈值(如#0.7-0.8)减少误触发
    • 在安静环境中,可适当降低阈值(如#0.5-0.6)提高响应速度
  3. 常用性考虑

    • 常用指令可设置适中的阈值(如#0.6)
    • 不常用但重要的指令可设置较低的阈值(如#0.5)确保在需要时能被识别
测试和优化方法
  1. 渐进式调整

    • 从默认值开始(如增强得分:2.0,阈值#0.6)
    • 在真实使用场景中测试并记录误触发和漏触发情况
    • 基于测试结果逐步调整参数
  2. 交叉测试

    • 在不同环境下(安静/嘈杂)进行测试
    • 由不同人(不同口音、性别)测试同一组关键词
    • 综合调整达到最佳平衡
  3. 优先级矩阵: 创建类似下面的优先级矩阵来分配参数:

    关键词优先级 低噪音环境 中噪音环境 高噪音环境
    高优先级 :3.0 #0.5 :3.5 #0.6 :4.0 #0.7
    中优先级 :2.0 #0.5 :2.5 #0.6 :3.0 #0.7
    低优先级 :1.5 #0.6 :2.0 #0.7 :2.5 #0.8

根据具体应用场景,建议从中等参数开始(如:2.0 #0.6),然后根据实际表现进行微调。最终目标是在减少误触发的同时保证必要指令能被可靠识别。

ASR

自动语音识别(Automatic Speech Recognition,简称ASR)是一种技术,用于将口语转换为文本或命令。它广泛应用于语音助手、语音输入系统、语音控制设备等领域,通过识别语音内容来执行相应的操作。

主要功能

  • 语音转文本:将口语转换为对应的文本信息。
  • 语音命令识别:识别语音中的命令或指令,并执行相应的操作。
  • 提高效率:通过语音输入简化操作流程,提高用户效率。

我们可以借助portaudio来进行实时的语音识别任务。对于ubuntu系统,请使用下面的命令进行安装

apt-get install portaudio19-dev

Arch请使用源码编译安装 https://www.portaudio.com

需要编译为arm64的架构,可以参考下面的步骤:

  1. 安装相关docker及相关工具

docker

paru -S docker docker-compose docker-buildx

跨平台编译

# 1. 安装 QEMU 模拟支持
docker run --privileged --rm tonistiigi/binfmt --install all
# 2. 创建并使用 buildx 构建器
docker buildx create --use --name multiarch-builder

创建dockerfile

FROM   docker.hlmirror.com/ubuntu:noble

# 设置工作目录
WORKDIR /app

# 1. 安装基础工具和依赖
RUN apt-get update && \
    apt-get install -y  \
    wget \
    build-essential \
    gcc \
    pkg-config \
    portaudio19-dev \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# 2. 安装 Go (ARM64 版本)
ENV GO_VERSION=1.24.2
RUN wget -O go.tgz "https://go.dev/dl/go${GO_VERSION}.linux-arm64.tar.gz" && \
    tar -C /usr/local -xzf go.tgz && \
    rm go.tgz

# 3. 设置 Go 环境变量
ENV PATH="/usr/local/go/bin:${PATH}"

# 4. 复制项目代码
COPY . .

RUN go env -w GOPROXY='https://goproxy.io,https://goproxy.cn,direct'
RUN go mod tidy && go mod download
RUN CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -v -o tingAutoServer_linux_arm64 ./cmd/app/
# 6. 收集所有依赖的 .so 文件到 /output 目录
RUN mkdir -p /output && \
    cp app /output/ && \
    ldd app | awk '/=>/ {print $3}' | xargs -I '{}' cp --parents '{}' /output/ 2>/dev/null || true

# 7. 定义输出目录(用于后续从容器中拷贝文件)
VOLUME /output

脚本

#!/bin/bash

# --- 配置 ---
IMAGE_NAME="tiny-builder"       # 临时镜像名称 (可以自定义)
IMAGE_TAG="latest"            # 镜像标签
PLATFORM="linux/arm64"        # 目标平台
OUTPUT_DIR="./build_output"   # 宿主机上存放输出文件的目录名
CONTAINER_COPY_PATH="/output" # Dockerfile 中存放编译结果的路径

# --- 脚本主体 ---
rm -rf "$OUTPUT_DIR"
# 确保输出目录存在
mkdir -p "$OUTPUT_DIR"
echo "Output directory: $OUTPUT_DIR"

echo "--- Building Docker image using buildx: $IMAGE_NAME:$IMAGE_TAG for $PLATFORM ---"
# 执行 Docker Buildx 构建并加载到本地
# 如果你的 Docker 不需要 sudo,请去掉下面的 sudo
# sudo docker buildx build --platform "$PLATFORM" --load -t "$IMAGE_NAME:$IMAGE_TAG" .
docker buildx build --platform "$PLATFORM" --load -t "$IMAGE_NAME:$IMAGE_TAG" .

# 检查构建是否成功
if [ $? -ne 0 ]; then
  echo "Docker buildx build failed."
  exit 1
fi

echo "--- Creating temporary container to copy files ---"
TEMP_CONTAINER_NAME="temp_copy_$$"
# 如果你的 Docker 不需要 sudo,请去掉下面的 sudo
# sudo docker create --name "$TEMP_CONTAINER_NAME" "$IMAGE_NAME:$IMAGE_TAG"
docker create --name "$TEMP_CONTAINER_NAME" "$IMAGE_NAME:$IMAGE_TAG"
if [ $? -ne 0 ]; then
  echo "Failed to create temporary container. Was the image loaded correctly?"
  # 可选:尝试清理可能未加载的镜像
  # sudo docker rmi "$IMAGE_NAME:$IMAGE_TAG"
  docker rmi "$IMAGE_NAME:$IMAGE_TAG"
  exit 1
fi


echo "--- Copying files from container path $CONTAINER_COPY_PATH to host path $OUTPUT_DIR ---"
# 执行拷贝,确保源路径末尾有 '/.' 来拷贝目录内容
# 如果你的 Docker 不需要 sudo,请去掉下面的 sudo
# sudo docker cp "$TEMP_CONTAINER_NAME:$CONTAINER_COPY_PATH/." "$OUTPUT_DIR/"
docker cp "$TEMP_CONTAINER_NAME:$CONTAINER_COPY_PATH/." "$OUTPUT_DIR/"

# 检查拷贝是否成功
if [ $? -ne 0 ]; then
  echo "docker cp failed."
  # 清理临时容器
  # 如果你的 Docker 不需要 sudo,请去掉下面的 sudo
  # sudo docker rm "$TEMP_CONTAINER_NAME"
  docker rm "$TEMP_CONTAINER_NAME"
  # 可选:清理镜像
  # sudo docker rmi "$IMAGE_NAME:$IMAGE_TAG"
  docker rmi "$IMAGE_NAME:$IMAGE_TAG"
  exit 1
fi

echo "--- Cleaning up temporary container ---"
# 删除临时容器
# 如果你的 Docker 不需要 sudo,请去掉下面的 sudo
# sudo docker rm "$TEMP_CONTAINER_NAME"
docker rm "$TEMP_CONTAINER_NAME"

# 可选:构建完成后删除临时镜像
echo "--- Cleaning up temporary build image ---"
# 如果你的 Docker 不需要 sudo,请去掉下面的 sudo
# sudo docker rmi "$IMAGE_NAME:$IMAGE_TAG"
docker rmi "$IMAGE_NAME:$IMAGE_TAG"

echo "--- Build successful! Files copied to $OUTPUT_DIR ---"
ls -l "$OUTPUT_DIR"
exit 0

在go语言中使用

  	import "github.com/gordonklaus/portaudio" 

func (t *ASRServer) SetupEngine(ctx context.Context) {
	lastText := ""
    framesPerBuffer := 1024
	err := portaudio.Initialize()
	if err != nil {
		t.logger.Fatal().Err(err).Msg("cannot init portaudio")
	}
	defer portaudio.Terminate()

	defaultDevice, err := portaudio.DefaultInputDevice()
	if err != nil {
		t.logger.Fatal().Err(err).Msg("failed to get default input device")
	}

	portAudioParam := portaudio.StreamParameters{
		Input: portaudio.StreamDeviceParameters{
			Device:   defaultDevice,
			Channels: 1,
			Latency:  defaultDevice.DefaultLowInputLatency,
		},
		SampleRate:      16000,
		FramesPerBuffer: framesPerBuffer,
		Flags:           portaudio.ClipOff,
	}
	config := createNewOnlineRecognizerConfig()

	recognizer := sherpa.NewOnlineRecognizer(&config)
	defer sherpa.DeleteOnlineRecognizer(recognizer)

	stream := sherpa.NewOnlineStream(recognizer)
	defer sherpa.DeleteOnlineStream(stream)

	// 每次采样的时长
	samplesPerCall := int32(framesPerBuffer) 

	samples := make([]float32, samplesPerCall)
	s, err := portaudio.OpenStream(portAudioParam, samples)
	if err != nil {
		t.logger.Fatal().Err(err).Msg("failed to open stream")
	}
	defer s.Stop()
	defer s.Close()

	err = s.Start()
	if err != nil {
		t.logger.Fatal().Err(err).Msg("failed to start stream")
	}

	t.logger.Info().Msg("service is ready..")

	for {
		select {
		case <-ctx.Done():
			break
		default:
			if t.paused.Load() {
				continue
			}
			err = s.Read()
			if err != nil {
				t.logger.Error().Err(err).Msg("failed to read")
				continue
			}
			stream.AcceptWaveform(config.FeatConfig.SampleRate, samples)
			for recognizer.IsReady(stream) {
				recognizer.Decode(stream)
			}
			text := recognizer.GetResult(stream).Text
			if len(text) != 0 && lastText != text {
				lastText = strings.ToLower(text)
			}
			if recognizer.IsEndpoint(stream) {
				if len(text) != 0 {
					t.logger.Info().Str("text", lastText).Msg("recognized text")
					t.subscribers.Range(func(ch, _ any) bool {
						if channel, ok := ch.(chan string); ok {
							select {
							case channel <- text:
							case <-time.After(100 * time.Millisecond):
								t.logger.Warn().Str("text", text).Msg("failed to broadcast: channel blocked")
							}
						}
						return true
					})
				}
				recognizer.Reset(stream)
			}
		}
	}

}

实时语音识别

模型下载

curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/icefall-asr-zipformer-streaming-wenetspeech-20230615.tar.bz2

参数设置

sherpa.OnlineRecognizerConfig{
		ModelConfig: sherpa.OnlineModelConfig{
			Transducer: sherpa.OnlineTransducerModelConfig{
				Encoder: "models/icefall-asr-zipformer-streaming-wenetspeech-20230615/exp/encoder-epoch-12-avg-4-chunk-16-left-128.onnx",
				Decoder: "models/icefall-asr-zipformer-streaming-wenetspeech-20230615/exp/decoder-epoch-12-avg-4-chunk-16-left-128.onnx",
				Joiner:  "models/icefall-asr-zipformer-streaming-wenetspeech-20230615/exp/joiner-epoch-12-avg-4-chunk-16-left-128.onnx",
			},
			Tokens:     "models/icefall-asr-zipformer-streaming-wenetspeech-20230615/data/lang_char/tokens.txt",
			NumThreads: 1,
			Debug:      0,
			ModelType:  "zipformer2",
			Provider:   "cpu",
		},
		DecodingMethod:          "greedy_search",
		MaxActivePaths:          4,
		EnableEndpoint:          1,
		Rule1MinTrailingSilence: 2.4,
		Rule2MinTrailingSilence: 1.2,
		Rule3MinUtteranceLength: 20,
		FeatConfig: sherpa.FeatureConfig{
			SampleRate: 16000,
			FeatureDim: 80,
		},
	}

实时语音识别+热词

文档参考 https://k2-fsa.github.io/sherpa/onnx/hotwords/index.html

模型下载

  curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20.tar.bz2

参数设置

sherpa.OnlineRecognizerConfig{
		ModelConfig: sherpa.OnlineModelConfig{
			Transducer: sherpa.OnlineTransducerModelConfig{
				Encoder: "models/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/encoder-epoch-99-avg-1.onnx",
				Decoder: "models/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/decoder-epoch-99-avg-1.onnx",
				Joiner:  "models/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/joiner-epoch-99-avg-1.onnx",
			},
			Tokens:       "models/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/tokens.txt",
			NumThreads:   2,
			Debug:        0,
			Provider:     "cpu",
			ModelingUnit: "cjkchar",
			BpeVocab:     "models/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/bpe.vocab",
		},

		HotwordsFile:            "models/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20/hotwords.txt",
		HotwordsScore:           2.0,
		HotwordsBufSize:         1024,
		DecodingMethod:          "modified_beam_search",
		MaxActivePaths:          4,
		EnableEndpoint:          1,
		Rule1MinTrailingSilence: 2.4,
		Rule2MinTrailingSilence: 1.2,
		Rule3MinUtteranceLength: 20,
		FeatConfig: sherpa.FeatureConfig{
			SampleRate: 16000,
			FeatureDim: 80,
		},
	}

热词文件示例

关闭电视:3.5
打开PC:3.3

热词自定义方法

  1. 创建热词文件

    • 热词文件是一个纯文本文件,每行包含一个热词或热词短语
    • 可以为每个热词指定一个可选的增益值(boost值)
  2. 格式说明

    • 基本格式:热词 [boost值]

    • 如果不指定boost值,默认为1.0

    • 例如:

      咖啡 2.0
      奶茶
      今天天气真好 3.5
      
  3. 调用方式

    • 在创建识别器时通过参数传入热词文件路径
    • 在Python API中使用hotwords_file参数
    • 在C++中使用SetHotwordsFile()方法

热词分数与识别的关联

  1. boost值的作用
    • boost值(增益值)直接影响该热词在解码过程中的概率
    • 较高的boost值会增加模型对该词的偏好度
    • 取值范围通常为正数,1.0表示不做特殊处理
  2. 工作原理
    • 在解码阶段,系统会为每个可能的词序列计算分数
    • 当遇到热词时,其分数会乘以对应的boost值
    • 这使得包含热词的句子获得更高的总体分数,从而更容易被选为最终结果
  3. 权衡考虑
    • boost值设置过高可能导致过度匹配,即使发音不太接近也被识别为热词
    • boost值设置适中可以在保持准确性的同时提高热词的识别率
    • 一般建议从1.0-5.0的范围内尝试,根据实际效果调整

TTS

文本转语音(Text-to-Speech,简称TTS)是一种技术,用于将文本信息转换为合成语音。它广泛应用于语音助手、有声读物、导航系统等领域,通过合成语音来传达信息。

主要功能

  • 文本转换:将输入的文本转换为自然语音。
  • 语音合成:使用算法生成类似人类的声音。
  • 提高可访问性:通过TTS技术,视障人士可以更方便地获取信息。