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