init
This commit is contained in:
parent
568ce22ec1
commit
0553b36402
30
config/config.yaml
Normal file
30
config/config.yaml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
SoundInputStream:
|
||||||
|
device: default
|
||||||
|
channels: 1
|
||||||
|
sample_rate: 16000
|
||||||
|
dtype: int16
|
||||||
|
blocksize: 256
|
||||||
|
latency: low
|
||||||
|
|
||||||
|
|
||||||
|
WakeWordRunner:
|
||||||
|
model_paths:
|
||||||
|
- models/hi_amy.onnx
|
||||||
|
- models/hey_amy.onnx
|
||||||
|
- models/hello_amy.onnx
|
||||||
|
sample_rate: 16000
|
||||||
|
threshold: 0.3
|
||||||
|
cooldown_sec: 1.5
|
||||||
|
reset_sec: 3.0
|
||||||
|
url: http://xxx.xxx.xxx.xxx:xxxx/xx/xx/xx
|
||||||
|
|
||||||
|
ASRRunner:
|
||||||
|
asr_model: paraformer-zh-streaming
|
||||||
|
punc_model: ct-punc
|
||||||
|
disable_update: True
|
||||||
|
device: cpu
|
||||||
|
encoder_chunk_look_back: 4
|
||||||
|
decoder_chunk_look_back: 1
|
||||||
|
max_duration: 20
|
||||||
|
max_empty_txt_count: 3
|
||||||
|
url: http://xxx.xxx.xxx.xxx:xxxx/xx/xx/xx
|
||||||
BIN
models/hello_amy.onnx
Normal file
BIN
models/hello_amy.onnx
Normal file
Binary file not shown.
BIN
models/hello_amy.tflite
Normal file
BIN
models/hello_amy.tflite
Normal file
Binary file not shown.
BIN
models/hey_amy.onnx
Normal file
BIN
models/hey_amy.onnx
Normal file
Binary file not shown.
BIN
models/hey_amy.tflite
Normal file
BIN
models/hey_amy.tflite
Normal file
Binary file not shown.
BIN
models/hi_amy.onnx
Normal file
BIN
models/hi_amy.onnx
Normal file
Binary file not shown.
BIN
models/hi_amy.tflite
Normal file
BIN
models/hi_amy.tflite
Normal file
Binary file not shown.
14
run.py
Normal file
14
run.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import yaml
|
||||||
|
from src.asr_runner import ASRRunner
|
||||||
|
from src.sound_input_stream import SoundInputStream
|
||||||
|
from src.wake_word_runner import WakeWordRunner
|
||||||
|
|
||||||
|
with open(r"config/config.yaml", "r") as f:
|
||||||
|
cfg = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
|
|
||||||
|
s = SoundInputStream(cfg["SoundInputStream"])
|
||||||
|
w = WakeWordRunner(cfg['WakeWordRunner'])
|
||||||
|
a = ASRRunner(cfg['ASRRunner'])
|
||||||
|
|
||||||
|
import time
|
||||||
|
time.sleep(10)
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/asr_runner.cpython-310.pyc
Normal file
BIN
src/__pycache__/asr_runner.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/shared.cpython-310.pyc
Normal file
BIN
src/__pycache__/shared.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/sound_input_stream.cpython-310.pyc
Normal file
BIN
src/__pycache__/sound_input_stream.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/wake_word_runner.cpython-310.pyc
Normal file
BIN
src/__pycache__/wake_word_runner.cpython-310.pyc
Normal file
Binary file not shown.
122
src/asr_runner.py
Normal file
122
src/asr_runner.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import time
|
||||||
|
import json
|
||||||
|
import queue
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
import datetime as dt
|
||||||
|
import numpy as np
|
||||||
|
from funasr import AutoModel
|
||||||
|
from .shared import audio_q, stop_flag, asr_flag
|
||||||
|
|
||||||
|
|
||||||
|
class ASRRunner:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.chunk_size = [0, 10, 5] # [past_ms, cur_ms, subsample],10*60ms=600ms窗口
|
||||||
|
self.encoder_chunk_look_back = cfg['encoder_chunk_look_back']
|
||||||
|
self.decoder_chunk_look_back = cfg['decoder_chunk_look_back']
|
||||||
|
self.chunk_stride = self.chunk_size[1] * 960 # 600ms
|
||||||
|
self.chunk_stride = max(int(self.chunk_stride), 1600) # 不低于约100ms分辨率
|
||||||
|
self.max_dur = cfg['max_duration']
|
||||||
|
self.url = cfg['url']
|
||||||
|
|
||||||
|
self.model = AutoModel(
|
||||||
|
model=cfg['asr_model'],
|
||||||
|
disable_update=cfg['disable_update'],
|
||||||
|
device=cfg['device'],
|
||||||
|
)
|
||||||
|
self.punc = AutoModel(model=cfg['punc_model'], disable_update=cfg['disable_update'])
|
||||||
|
self.cache = {}
|
||||||
|
self.start_time = None
|
||||||
|
self.buf = np.empty((0,), dtype=np.int16)
|
||||||
|
|
||||||
|
self.empty_txt_count = 0
|
||||||
|
self.max_empty_txt_count = cfg['max_empty_txt_count']
|
||||||
|
self.text_buffer = ""
|
||||||
|
self.thread = threading.Thread(target=self.run)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while not stop_flag.is_set():
|
||||||
|
if not asr_flag.is_set():
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if self.start_time is None:
|
||||||
|
self.start_time = dt.datetime.now()
|
||||||
|
|
||||||
|
try:
|
||||||
|
chunk = audio_q.get(timeout=0.5) # int16 mono
|
||||||
|
except queue.Empty:
|
||||||
|
time.sleep(0.1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TODO: get audio frame from auido_q and concat them into fixed length (chunk_stride)
|
||||||
|
if chunk is None:
|
||||||
|
continue
|
||||||
|
if chunk.dtype != np.int16:
|
||||||
|
chunk = chunk.astype(np.int16, copy=False)
|
||||||
|
self.buf = np.concatenate((self.buf, chunk), dtype=np.int16)
|
||||||
|
if self.buf.shape[0] < self.chunk_stride:
|
||||||
|
continue
|
||||||
|
|
||||||
|
res = None
|
||||||
|
while self.buf.shape[0] >= self.chunk_stride:
|
||||||
|
speech_chunk = self.buf[: self.chunk_stride]
|
||||||
|
self.buf = self.buf[self.chunk_stride :]
|
||||||
|
speech_chunk = np.ascontiguousarray(
|
||||||
|
speech_chunk.astype(np.float32, copy=False)
|
||||||
|
)
|
||||||
|
res = self.model.generate(
|
||||||
|
input=speech_chunk,
|
||||||
|
cache=self.cache, # 关键:启用流式缓存
|
||||||
|
is_final=False,
|
||||||
|
chunk_size=self.chunk_size,
|
||||||
|
encoder_chunk_look_back=self.encoder_chunk_look_back,
|
||||||
|
decoder_chunk_look_back=self.decoder_chunk_look_back,
|
||||||
|
fs=16000, # 明确采样率(SoundInputStream 已以 16k 推送)
|
||||||
|
)
|
||||||
|
|
||||||
|
if res is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(res)
|
||||||
|
cur_time = dt.datetime.now()
|
||||||
|
if (cur_time - self.start_time).total_seconds() > self.max_dur:
|
||||||
|
self.switch()
|
||||||
|
|
||||||
|
for r in res:
|
||||||
|
if r['text'] == '':
|
||||||
|
self.empty_txt_count += 1
|
||||||
|
else:
|
||||||
|
self.empty_txt_count = 0
|
||||||
|
self.text_buffer += r['text']
|
||||||
|
|
||||||
|
if self.empty_txt_count > self.max_empty_txt_count:
|
||||||
|
self.switch()
|
||||||
|
continue
|
||||||
|
|
||||||
|
def do_task(self):
|
||||||
|
if len(self.text_buffer) == 0:
|
||||||
|
return
|
||||||
|
res = self.punc.generate(input=self.text_buffer)
|
||||||
|
data = ""
|
||||||
|
for r in res:
|
||||||
|
data += r['text']
|
||||||
|
print(data)
|
||||||
|
# try:
|
||||||
|
# data_json= json.dumps({'text': data})
|
||||||
|
# response = requests.post(self.url, data = data_json)
|
||||||
|
# # if response.result != "OK":
|
||||||
|
# # print("request Failed")
|
||||||
|
# except Exception as e:
|
||||||
|
# print("ASRRunner send request failed: ", e)
|
||||||
|
|
||||||
|
def switch(self):
|
||||||
|
asr_flag.clear()
|
||||||
|
self.do_task()
|
||||||
|
self.cache = {}
|
||||||
|
self.text_buffer = ""
|
||||||
|
self.buf = np.empty((0,), dtype=np.int16)
|
||||||
|
self.start_time = None
|
||||||
|
self.empty_txt_count = 0
|
||||||
|
|
||||||
13
src/shared.py
Normal file
13
src/shared.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
# from enum import Enum
|
||||||
|
|
||||||
|
# class STATE(Enum):
|
||||||
|
# WAKEWORD = 1
|
||||||
|
# ASR = 2
|
||||||
|
|
||||||
|
asr_flag = threading.Event()
|
||||||
|
stop_flag = threading.Event()
|
||||||
|
|
||||||
|
asr_flag.clear()
|
||||||
|
audio_q = queue.Queue()
|
||||||
33
src/sound_input_stream.py
Normal file
33
src/sound_input_stream.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import numpy as np
|
||||||
|
import sounddevice as sd
|
||||||
|
from .shared import audio_q
|
||||||
|
|
||||||
|
|
||||||
|
class SoundInputStream:
|
||||||
|
def __init__(self, cfg: dict):
|
||||||
|
default_in = sd.default.device[0]
|
||||||
|
if cfg["device"] == "default":
|
||||||
|
cfg["device"] = None
|
||||||
|
else:
|
||||||
|
cfg["device"] = int(cfg["device"])
|
||||||
|
# print(f"🎛️ 默认输入设备ID: {default_in} | 当前使用: {cfg["device"] if cfg["device"] is not None else cfg["device"]}")
|
||||||
|
|
||||||
|
self.stream = sd.InputStream(
|
||||||
|
device=cfg["device"],
|
||||||
|
channels=cfg['channels'],
|
||||||
|
samplerate=cfg['sample_rate'],
|
||||||
|
dtype=cfg['dtype'],
|
||||||
|
blocksize=cfg['blocksize'],
|
||||||
|
latency=cfg['latency'],
|
||||||
|
callback=self.audio_callback
|
||||||
|
)
|
||||||
|
self.stream.start()
|
||||||
|
print("SoundInputStream start")
|
||||||
|
|
||||||
|
def audio_callback(self, indata, frames, time_info, status):
|
||||||
|
if status:
|
||||||
|
# 轻量报告即可,避免阻塞回调
|
||||||
|
pass
|
||||||
|
# indata: shape (frames, channels), int16
|
||||||
|
mono = indata[:, 0].copy().astype(np.int16)
|
||||||
|
audio_q.put(mono)
|
||||||
64
src/wake_word_runner.py
Normal file
64
src/wake_word_runner.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import time
|
||||||
|
import json
|
||||||
|
import queue
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
import numpy as np
|
||||||
|
from openwakeword.model import Model
|
||||||
|
from .shared import audio_q, stop_flag, asr_flag
|
||||||
|
|
||||||
|
class WakeWordRunner:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
import openwakeword
|
||||||
|
openwakeword.utils.download_models()
|
||||||
|
self.model = Model(wakeword_models=cfg["model_paths"], inference_framework="onnx")
|
||||||
|
self.threshold = float(cfg['threshold'])
|
||||||
|
self.cooldown_sec = float(cfg['cooldown_sec'])
|
||||||
|
self.reset_sec = float(cfg['reset_sec'])
|
||||||
|
self.url = cfg['url']
|
||||||
|
|
||||||
|
self.last_trigger_time = 0.0
|
||||||
|
self.last_activity_time = time.time()
|
||||||
|
self.thread = threading.Thread(target=self.run)
|
||||||
|
self.thread.start()
|
||||||
|
print("WakeWordRunner start")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
last_reset = time.time()
|
||||||
|
frame_buffer = [] # 累积音频块
|
||||||
|
|
||||||
|
while not stop_flag.is_set():
|
||||||
|
if asr_flag.is_set():
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
chunk = audio_q.get(timeout=0.5) # int16 mono
|
||||||
|
except queue.Empty:
|
||||||
|
time.sleep(0.1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
frame_buffer.append(chunk)
|
||||||
|
if len(frame_buffer) >= 1:
|
||||||
|
data = np.concatenate(frame_buffer)
|
||||||
|
scores = self.model.predict(data)
|
||||||
|
_, score = max(scores.items(), key=lambda kv: kv[1])
|
||||||
|
now = time.time()
|
||||||
|
frame_buffer = [] # 清空缓冲区
|
||||||
|
|
||||||
|
if now - last_reset >= self.reset_sec:
|
||||||
|
self.model.reset()
|
||||||
|
last_reset = now
|
||||||
|
|
||||||
|
if (now - self.last_trigger_time) < self.cooldown_sec:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if score >= self.threshold:
|
||||||
|
print(f"WordScores: {scores}")
|
||||||
|
asr_flag.set()
|
||||||
|
# try:
|
||||||
|
# data_json = json.dump({"msg": "wakeup"})
|
||||||
|
# requests.post(self.url, data=data_json)
|
||||||
|
# self.last_trigger_time = now
|
||||||
|
# except Exception as e:
|
||||||
|
# print("WakeWordRunner send request failed: ", e)
|
||||||
Loading…
Reference in New Issue
Block a user