feat: 添加自定义唤醒词相关教程

This commit is contained in:
Del Wang
2025-05-06 23:33:11 +08:00
parent a74d92b4b6
commit 20c21ca609
6 changed files with 489 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
# Open-XiaoAI x Sherpa-ONNX
> [!CAUTION]
> 下面的教程和脚本正在开发整理中,请勿直接使用!🚨
> [!IMPORTANT]
> 本教程仅适用于 **小爱音箱 ProLX06** 和 **Xiaomi 智能音箱 ProOH2P**
小爱音箱自定义唤醒词,基于 [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx)。
## 刷机
首先按照 [open-xiaoai](https://github.com/idootop/open-xiaoai) 里的教程将小爱音箱刷机, 然后 SSH 连接到小爱音箱。
## 安装脚本
在小爱音箱上安装启动脚本,然后重启小爱音响。
```shell
# 下载到 /data/init.sh 开机时自启动
curl -L -o /data/init.sh https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-kws/init.sh
```
## 设置唤醒词
在你的电脑上克隆本项目,然后修改 `my-keywords.txt` 文件里的唤醒词。注意:
- 仅支持中文唤醒词
- 支持多个唤醒词,每行一个
运行以下命令,更新 `keywords.txt` 配置文件:
```shell
# 你可能需要先安装 uv 👉 https://github.com/astral-sh/uv
uv run keywords.py --tokens tokens.txt --output keywords.txt --text my-keywords.txt
```
将更新后的 `keywords.txt`(你的电脑)复制到 `/data/open-xiaoai/kws/keywords.txt`(小爱音箱),然后重启小爱音箱即可。
+55
View File
@@ -0,0 +1,55 @@
#! /bin/sh
set -e
MIN_SPACE_MB=32
DOWNLOAD_URL="https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-kws/kws.tar.gz"
check_disk_space() {
local space_kb=$(df -k "$1" | awk 'NR==2 {print $4}')
if [ $((space_kb / 1024)) -lt "$MIN_SPACE_MB" ]; then
echo 1
else
echo 0
fi
}
BASE_DIR="/data"
if [ $(check_disk_space "$BASE_DIR") -eq 1 ]; then
BASE_DIR="/tmp"
if [ $(check_disk_space "$BASE_DIR") -eq 1 ]; then
echo "❌ 磁盘空间不足,请先清理磁盘空间(至少需要 $MIN_SPACE_MB MB 空间)"
exit 1
fi
fi
WORK_DIR="$BASE_DIR/open-xiaoai"
if [ ! -d "$WORK_DIR" ]; then
mkdir -p "$WORK_DIR"
fi
if [ ! -f "$WORK_DIR/kws/kws" ]; then
echo "🔥 正在下载模型文件..."
curl -L -# -o "$WORK_DIR/kws.tar.gz" https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-kws/kws.tar.gz
tar -xzvf "$WORK_DIR/kws.tar.gz" -C "$WORK_DIR"
rm "$WORK_DIR/kws.tar.gz"
echo "✅ 模型文件下载完毕"
fi
echo "🔥 正在启动唤醒词识别服务..."
"$WORK_DIR/kws/kws" \
--model-type=zipformer2 \
--tokens="$WORK_DIR/kws/models/tokens.txt" \
--encoder="$WORK_DIR/kws/models/encoder.onnx" \
--decoder="$WORK_DIR/kws/models/decoder.onnx" \
--joiner="$WORK_DIR/kws/models/joiner.onnx" \
--keywords-file="/data/open-xiaoai/kws/keywords.txt" \
--provider=cpu \
--num-threads=1 \
--chunk-size=1024 \
noop
+163
View File
@@ -0,0 +1,163 @@
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "pypinyin",
# ]
# [tool.uv]
# exclude-newer = "2025-05-06T00:00:00Z"
# ///
import argparse
from pathlib import Path
from typing import List, Union
from pypinyin import pinyin
from pypinyin.contrib.tone_convert import to_initials, to_finals_tone
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--text",
type=str,
required=False,
default="my-keywords.txt",
help="""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
""",
)
parser.add_argument(
"--tokens",
type=str,
required=False,
default="tokens.txt",
help="The path to tokens.txt.",
)
parser.add_argument(
"--output",
type=str,
required=False,
default="keywords.txt",
help="Path where the encoded tokens will be written to.",
)
return parser.parse_args()
def text2token(
texts: List[str],
tokens: str,
output_ids: bool = False,
):
"""将文本转换为 token 列表"""
assert Path(tokens).is_file(), f"File not exists, {tokens}"
tokens_table = {}
with open(tokens, "r", encoding="utf-8") as f:
for line in f:
toks = line.strip().split()
assert len(toks) == 2, len(toks)
assert toks[0] not in tokens_table, f"Duplicate token: {toks} "
tokens_table[toks[0]] = int(toks[1])
texts_list: List[List[str]] = []
for txt in texts:
res = []
py = [x[0] for x in pinyin(txt)]
for x in py:
initial = to_initials(x, strict=False)
final = to_finals_tone(x, strict=False)
if initial == "" and final == "":
res.append(x)
else:
if initial != "":
res.append(initial)
if final != "":
res.append(final)
texts_list.append(res)
result: List[List[Union[int, str]]] = []
for text in texts_list:
text_list = []
contain_oov = False
for txt in text:
if txt in tokens_table:
text_list.append(tokens_table[txt] if output_ids else txt)
else:
print(
f"Can't find token {txt} in token table, check your "
f"tokens.txt see if {txt} in it. skipping text : {text}."
)
contain_oov = True
break
if contain_oov:
continue
else:
result.append(text_list)
return result
def main() -> None:
args = get_args()
texts = []
# extra information like boosting score (start with :), triggering threshold (start with #)
# original keyword (start with @)
extra_info = []
with open(args.text, "r", encoding="utf8") as f:
for line in f:
extra = []
text = []
toks = line.strip().split()
if len(toks) == 1:
text.append(toks[0])
extra.append('@'+toks[0])
else:
for tok in toks:
if tok[0] == ":" or tok[0] == "#" or tok[0] == "@":
extra.append(tok)
else:
text.append(tok)
texts.append(" ".join(text))
extra_info.append(extra)
encoded_texts = text2token(
texts,
tokens=args.tokens,
)
with open(args.output, "w", encoding="utf8") as f:
for i, txt in enumerate(encoded_texts):
txt += extra_info[i]
f.write(" ".join(txt) + "\n")
print(f"✅ 唤醒词已保存到 {args.output}")
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
n ǐ h ǎo x iǎo zh ì @你好小智
d òu b āo d òu b āo @豆包豆包
n ǐ h ǎo sh ǎ n iū @你好傻妞
+3
View File
@@ -0,0 +1,3 @@
你好小智
豆包豆包
你好傻妞
+227
View File
@@ -0,0 +1,227 @@
<blk> 0
<sos/eos> 1
<unk> 2
A 3
B 4
S 5
I 6
f 7
ù 8
ǔ 9
zh 10
y 11
ī 12
sh 13
ēng 14
uè 15
p 16
iàn 17
K 18
L 19
P 20
R 21
T 22
M 23
j 24
W 25
q 26
s 27
ān 28
b 29
ā 30
l 31
íng 32
iǔ 33
ch 34
ǐ 35
ì 36
èr 37
w 38
r 39
án 40
én 41
ó 42
g 43
uǎn 44
c 45
āng 46
ǎn 47
x 48
iān 49
ōng 50
àng 51
òng 52
d 53
ài 54
iù 55
ìng 56
ū 57
m 58
àn 59
ǎ 60
í 61
z 62
k 63
ǒu 64
ūn 65
uó 66
èng 67
òu 68
ú 69
éng 70
à 71
iào 72
āo 73
ào 74
h 75
uáng 76
t 77
iáo 78
è 79
iǎn 80
er 81
n 82
èi 83
ǐng 84
uā 85
ēi 86
ǎo 87
iú 88
uàng 89
uí 90
ǜ 91
āi 92
óu 93
ià 94
ǎng 95
īn 96
uà 97
uǐ 98
ái 99
ò 100
ē 101
óng 102
uàn 103
é 104
ìn 105
ùn 106
uò 107
ún 108
uì 109
uān 110
ǐn 111
iāo 112
ōu 113
uán 114
iǎo 115
á 116
ér 117
èn 118
C 119
D 120
E 121
O 122
N 123
Z 124
ěi 125
ián 126
ēn 127
iā 128
ǚ 129
áng 130
iě 131
ǒng 132
éi 133
iāng 134
J 135
V 136
ín 137
ié 138
īng 139
ō 140
ěr 141
F 142
ué 143
iǎng 144
uō 145
G 146
ǎi 147
ěn 148
H 149
en 150
X 151
i 152
ǒ 153
uǒ 154
iè 155
iū 156
U 157
iàng 158
uāng 159
uá 160
Q 161
ě 162
e 163
iǎ 164
iáng 165
Y 166
○ 167
iē 168
uī 169
ěng 170
uài 171
áo 172
a 173
ou 174
ǔn 175
uái 176
uāi 177
ióng 178
ei 179
ń 180
iá 181
iōng 182
üè 183
uē 184
uǎng 185
uǎ 186
o 187
uǎi 188
uě 189
ǘ 190
uo 191
iǒng 192
ang 193
ia 194
u 195
üě 196
#0 197
#1 198
#2 199
#3 200
#4 201
#5 202
#6 203
#7 204
#8 205
#9 206
#10 207
#11 208
#12 209
#13 210
#14 211
#15 212
#16 213
#17 214
#18 215
#19 216
#20 217
#21 218
#22 219
#23 220
#24 221
#25 222
#26 223
#27 224
#28 225
#29 226