feat: 添加自定义唤醒词相关教程
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# Open-XiaoAI x Sherpa-ONNX
|
||||
|
||||
> [!CAUTION]
|
||||
> 下面的教程和脚本正在开发整理中,请勿直接使用!🚨
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 本教程仅适用于 **小爱音箱 Pro(LX06)** 和 **Xiaomi 智能音箱 Pro(OH2P)**
|
||||
|
||||
小爱音箱自定义唤醒词,基于 [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`(小爱音箱),然后重启小爱音箱即可。
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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ū @你好傻妞
|
||||
@@ -0,0 +1,3 @@
|
||||
你好小智
|
||||
豆包豆包
|
||||
你好傻妞
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user