Porcupine 实时语音唤醒引擎 - Windows 部署指南
1. 产品介绍
什么是 Porcupine?
Porcupine 是由 Picovoice 开发的轻量级、跨平台实时语音唤醒引擎,专门用于检测设备上预定义的唤醒词(如 “Hey Porcupine”)。它可以在本地离线运行,无需网络连接,保护用户隐私。
核心特性
-
离线运行: 完全在设备端处理,无需云端连接
-
多平台支持: Windows、Linux、macOS、Android、iOS、Raspberry Pi
-
低延迟: 实时唤醒词检测,响应迅速
-
多语言支持: 英语、德语、法语、西班牙语、中文等
-
自定义唤醒词: 支持训练和部署自定义唤醒词
-
资源高效: 内存占用小(约 2MB),CPU 使用率低
应用场景
-
智能音箱和语音助手
-
车载语音系统
-
IoT 设备语音控制
-
桌面应用语音交互
-
无障碍辅助工具
2. Windows 系统部署指南
系统要求
-
操作系统: Windows 10 或更高版本(64位)
-
处理器: x86_64 兼容 CPU
-
内存: 最少 4GB RAM
-
存储空间: 最少 100MB 可用空间
-
音频设备: 支持麦克风输入
2.1 安装步骤
Python 环境安装
pip3 install pvporcupinedemo
获取 Demo
-
访问下载页:打开浏览器,访问 Picovoice GitHub Releases 页面。
-
找到最新版本:滚动页面,找到最新的稳定版(如
v4.0)

安装并获取 AccessKey
用于激活 SDK(免费)。
-
在电脑上访问Picovoice Console (控制台)。
-
个人使用注册(支持goole、github、linkedin)。
-
登录后,在左侧菜单点击 AccessKey,系统会自动生成一个形如
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx的密钥。复制它。


2.2 命令行使用
Picovoice 官方提供了不同环境的Demo:

在windows上可以使用python进行测试,可以熟读README.md文件,里面有简易demo的测试方式,或者直接porcupine_demo_mic.py --help查看

唤醒词选择
-
内置唤醒词:
“alexa”, “americano”, “blueberry”, “bumblebee”, “computer”,
“grapefruit”, “grasshopper”, “hey google”, “hey siri”, “jarvis”, “ok
google”, “picovoice”, “porcupine”, “terminator” -
自定义唤醒词: 可在 Console 中免费训练(非商业用途),月免费额度为1次
Porcupine Python 示例脚本参数详解
| 参数名称 | 必填/选填 | 说明与示例 |
|---|---|---|
--access_key |
必填 | 核心密钥。这是你从 Picovoice 官网控制台获取的授权码,用于验证身份。 |
--keywords |
选填 | 内置唤醒词。如果你不想用默认的 “porcupine”,可以在这里指定其他内置词,如 alexa, siri, ok google等。 |
--keyword_paths |
选填 | 自定义唤醒词。如果你训练了自己的唤醒词模型(.ppn 文件),需要在这里指定文件的绝对路径。 |
--library_path |
选填 | 库文件路径。通常不需要手动设置,脚本会自动寻找。 |
--model_path |
选填 | 模型参数路径。通常不需要手动设置。 |
--device |
选填 | 运行设备。默认自动选择最佳设备(CPU/GPU)。 |
--sensitivities |
选填 | 灵敏度。数值范围 0-1。0.5 是默认值。数值越高,越容易误触发(False Alarm);数值越低,越难唤醒。 |
--audio_device_index |
选填 | 麦克风索引。如果你有多个麦克风,可以通过这个参数指定使用哪一个。 |
--output_path |
选填 | 录音保存路径。用于调试,将录音保存为文件。 |
--show_audio_devices |
选填 | 列出设备。运行此命令会打印出系统中所有可用的音频输入设备列表,方便你确认麦克风索引。 |
--show_inference_devices |
选填 | 列出推理设备。打印可用于运行推理的设备(CPU/GPU)。 |
最简单的运行命令如下(请替换你的 Access Key):
porcupine_demo_mic --access_key "你的密钥字符串" --keywords computer
运行后,对着麦克风说 “computer”(或者你设置的其他唤醒词),如果配置正确,控制台会打印出检测结果。

3. gui界面实现
Porcupine 语音唤醒 GUI 系统 是一个基于 PyQt5 开发的图形化界面应用程序,用于管理和使用 Picovoice Porcupine 语音唤醒引擎。该系统提供了完整的语音唤醒功能,包括设备管理、关键词配置、参数调整和实时监控。
3.1 核心组件
MainWindow (主窗口)
├── PorcupineWorker (后台工作线程)
│ ├── Porcupine 引擎实例
│ ├── PvRecorder 录音器
│ └── 音频处理模块
├── UI 控制面板
└── 状态监控面板
3.2 系统初始化
点击"🚀 初始化 Porcupine" → 验证访问密钥 → 加载关键词模型 → 初始化完成
-
初始化成功后,"开始监听"按钮启用
-
状态指示器保持绿色
-
日志窗口显示初始化成功信息
开始监听
点击"▶️ 开始监听" → 启动音频捕获 → 实时检测关键词 → 输出检测结果
3.3 代码结构说明
porcupine_gui_fixed.py
├── PorcupineWorker (工作线程类)
│ ├── 音频设备管理
│ ├── Porcupine 初始化
│ ├── 关键词检测循环
│ └── 资源清理
├── MainWindow (主窗口类)
│ ├── UI 布局设计
│ ├── 信号槽连接
│ ├── 事件处理
│ └── 日志管理
└── main() (程序入口)
3.4 完整实现代码
# porcupine_gui_fixed.py
import sys
import os
import wave
import struct
from datetime import datetime
from pathlib import Path
import threading
import pvporcupine
from pvrecorder import PvRecorder
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QComboBox, QTextEdit, QListWidget,
QListWidgetItem, QGroupBox, QSpinBox, QDoubleSpinBox, QCheckBox,
QFileDialog, QMessageBox, QStatusBar, QSplitter, QTabWidget
)
from PyQt5.QtCore import Qt, pyqtSignal, QThread, QObject, pyqtSlot
from PyQt5.QtGui import QFont, QIcon, QPalette, QColor
class PorcupineWorker(QThread):
"""工作线程,处理音频监听和关键词检测"""
# 信号定义
keyword_detected = pyqtSignal(str) # 检测到关键词
error_occurred = pyqtSignal(str) # 错误信息
status_updated = pyqtSignal(str) # 状态更新
devices_loaded = pyqtSignal(list, list) # 设备列表加载完成
initialized = pyqtSignal(bool) # 初始化完成信号
def __init__(self):
super().__init__()
self.porcupine = None
self.recorder = None
self.is_running = False
self.keywords = []
self.sensitivities = []
self.audio_device_index = -1
self.device_type = "best"
self.access_key = ""
self.output_path = None
self.wav_file = None
self._init_data = {} # 初始化参数存储
def initialize(self, **kwargs):
"""设置初始化参数"""
self._init_data = kwargs
self.status_updated.emit("初始化参数已设置")
def run_load_devices(self):
"""加载设备(在主线程运行)"""
try:
# 加载推理设备
inference_devices = pvporcupine.available_devices()
# 加载音频设备
audio_devices = []
for i, device in enumerate(PvRecorder.get_available_devices()):
audio_devices.append(f"Device {i}: {device}")
self.devices_loaded.emit(inference_devices, audio_devices)
self.status_updated.emit("设备加载完成")
except Exception as e:
self.error_occurred.emit(f"加载设备失败: {str(e)}")
def run(self):
"""线程主函数"""
if not self._init_data:
self.error_occurred.emit("未设置初始化参数")
return
try:
self.status_updated.emit("正在初始化 Porcupine...")
access_key = self._init_data.get('access_key')
keywords = self._init_data.get('keywords')
keyword_paths = self._init_data.get('keyword_paths')
sensitivities = self._init_data.get('sensitivities', [0.5])
device = self._init_data.get('device', 'best')
model_path = self._init_data.get('model_path')
library_path = self._init_data.get('library_path')
if keyword_paths is None and keywords is None:
self.error_occurred.emit("必须设置关键词或关键词路径")
self.initialized.emit(False)
return
if keyword_paths is None:
keyword_paths = [pvporcupine.KEYWORD_PATHS[x] for x in keywords]
self.keywords = keywords.copy()
else:
# 从路径中提取关键词名称
self.keywords = []
for x in keyword_paths:
keyword_phrase_part = os.path.basename(x).replace('.ppn', '').split('_')
if len(keyword_phrase_part) > 6:
self.keywords.append(' '.join(keyword_phrase_part[0:-6]))
else:
self.keywords.append(keyword_phrase_part[0])
self.sensitivities = sensitivities.copy()
self.device_type = device
self.access_key = access_key
# 创建 Porcupine 实例
self.porcupine = pvporcupine.create(
access_key=access_key,
library_path=library_path,
model_path=model_path,
device=device,
keyword_paths=keyword_paths,
sensitivities=sensitivities
)
self.status_updated.emit(f"Porcupine 初始化成功 (v{self.porcupine.version})")
self.initialized.emit(True)
except pvporcupine.PorcupineInvalidArgumentError as e:
self.error_occurred.emit(f"参数错误: {str(e)}")
self.initialized.emit(False)
except pvporcupine.PorcupineActivationError as e:
self.error_occurred.emit("激活密钥错误")
self.initialized.emit(False)
except pvporcupine.PorcupineActivationLimitError as e:
self.error_occurred.emit(f"密钥 '{access_key}' 已达到设备限制")
self.initialized.emit(False)
except pvporcupine.PorcupineActivationRefusedError as e:
self.error_occurred.emit(f"密钥 '{access_key}' 被拒绝")
self.initialized.emit(False)
except pvporcupine.PorcupineActivationThrottledError as e:
self.error_occurred.emit(f"密钥 '{access_key}' 已被限流")
self.initialized.emit(False)
except pvporcupine.PorcupineError as e:
self.error_occurred.emit(f"初始化失败: {str(e)}")
self.initialized.emit(False)
except Exception as e:
self.error_occurred.emit(f"未知错误: {str(e)}")
self.initialized.emit(False)
def start_listening(self, audio_device_index=-1, output_path=None):
"""开始监听"""
if not self.porcupine:
self.error_occurred.emit("请先初始化 Porcupine")
return False
try:
self.status_updated.emit("正在启动音频录制...")
self.audio_device_index = audio_device_index
self.output_path = output_path
# 初始化录音器
self.recorder = PvRecorder(
frame_length=self.porcupine.frame_length,
device_index=audio_device_index
)
self.recorder.start()
# 初始化 WAV 文件
if output_path:
self.wav_file = wave.open(output_path, "w")
self.wav_file.setnchannels(1)
self.wav_file.setsampwidth(2)
self.wav_file.setframerate(16000)
self.is_running = True
self.status_updated.emit("正在监听... 请说出关键词")
# 监听循环
while self.is_running:
pcm = self.recorder.read()
# 保存音频数据
if self.wav_file is not None:
self.wav_file.writeframes(struct.pack("h" * len(pcm), *pcm))
# 检测关键词
result = self.porcupine.process(pcm)
if result >= 0:
keyword = self.keywords[result]
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
self.keyword_detected.emit(f"[{timestamp}] 检测到: {keyword}")
except KeyboardInterrupt:
self.status_updated.emit("用户中断")
except Exception as e:
self.error_occurred.emit(f"监听错误: {str(e)}")
def stop_listening(self):
"""停止监听"""
self.is_running = False
if self.recorder:
self.recorder.stop()
self.recorder.delete()
self.recorder = None
if self.wav_file:
self.wav_file.close()
self.wav_file = None
self.status_updated.emit("已停止监听")
def cleanup(self):
"""清理资源"""
self.stop_listening()
if self.porcupine:
self.porcupine.delete()
self.porcupine = None
self.quit()
class MainWindow(QMainWindow):
"""主窗口"""
def __init__(self):
super().__init__()
self.worker = None
self.init_ui()
def init_ui(self):
"""初始化界面"""
self.setWindowTitle("Porcupine 关键词唤醒系统 v1.0")
self.setGeometry(100, 100, 900, 600)
# 设置样式
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
}
QGroupBox {
font-weight: bold;
border: 2px solid #cccccc;
border-radius: 8px;
margin-top: 10px;
padding-top: 10px;
background-color: white;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 8px 0 8px;
}
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
min-width: 80px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:disabled {
background-color: #cccccc;
color: #666666;
}
QPushButton#startBtn {
background-color: #2196F3;
}
QPushButton#startBtn:hover {
background-color: #1976D2;
}
QPushButton#stopBtn {
background-color: #f44336;
}
QPushButton#stopBtn:hover {
background-color: #d32f2f;
}
QTextEdit, QListWidget {
background-color: white;
border: 1px solid #cccccc;
border-radius: 4px;
font-family: Consolas, 'Courier New', monospace;
}
QLineEdit {
padding: 6px;
border: 1px solid #cccccc;
border-radius: 4px;
}
QStatusBar {
background-color: #e0e0e0;
color: #333333;
}
""")
# 创建中心部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
# 顶部控制栏
top_bar = QHBoxLayout()
# 访问密钥
key_layout = QHBoxLayout()
key_layout.addWidget(QLabel("访问密钥:"))
self.access_key_edit = QLineEdit()
self.access_key_edit.setPlaceholderText("请输入 Picovoice 访问密钥")
self.access_key_edit.setEchoMode(QLineEdit.Password)
self.access_key_edit.setFixedWidth(300)
key_layout.addWidget(self.access_key_edit)
top_bar.addLayout(key_layout)
# 设备选择
top_bar.addWidget(QLabel("音频设备:"))
self.audio_device_combo = QComboBox()
self.audio_device_combo.addItem("默认设备")
self.audio_device_combo.setFixedWidth(200)
top_bar.addWidget(self.audio_device_combo)
# 加载设备按钮
self.load_devices_btn = QPushButton("🔧 加载设备")
top_bar.addWidget(self.load_devices_btn)
main_layout.addLayout(top_bar)
# 创建分割器
splitter = QSplitter(Qt.Horizontal)
main_layout.addWidget(splitter, 1)
# 左侧配置面板
config_panel = QWidget()
config_layout = QVBoxLayout(config_panel)
# 关键词配置组
keyword_group = QGroupBox("关键词配置")
keyword_layout = QVBoxLayout()
# 内置关键词
builtin_layout = QHBoxLayout()
builtin_layout.addWidget(QLabel("内置关键词:"))
self.keyword_combo = QComboBox()
self.keyword_combo.addItems(sorted(pvporcupine.KEYWORDS))
builtin_layout.addWidget(self.keyword_combo, 1)
self.add_keyword_btn = QPushButton("➕ 添加")
builtin_layout.addWidget(self.add_keyword_btn)
self.remove_keyword_btn = QPushButton("➖ 移除")
builtin_layout.addWidget(self.remove_keyword_btn)
keyword_layout.addLayout(builtin_layout)
# 关键词列表
self.keyword_list = QListWidget()
keyword_layout.addWidget(self.keyword_list, 1)
# 自定义关键词
custom_layout = QHBoxLayout()
self.custom_keyword_edit = QLineEdit()
self.custom_keyword_edit.setPlaceholderText("自定义关键词文件路径 (.ppn)")
custom_layout.addWidget(QLabel("自定义:"))
custom_layout.addWidget(self.custom_keyword_edit, 1)
self.browse_btn = QPushButton("📁 浏览")
custom_layout.addWidget(self.browse_btn)
keyword_layout.addLayout(custom_layout)
keyword_group.setLayout(keyword_layout)
config_layout.addWidget(keyword_group, 1)
# 参数设置组
param_group = QGroupBox("参数设置")
param_layout = QVBoxLayout()
# 灵敏度
sensitivity_layout = QHBoxLayout()
sensitivity_layout.addWidget(QLabel("灵敏度 (0.0-1.0):"))
self.sensitivity_spin = QDoubleSpinBox()
self.sensitivity_spin.setRange(0.0, 1.0)
self.sensitivity_spin.setSingleStep(0.1)
self.sensitivity_spin.setValue(0.5)
self.sensitivity_spin.setFixedWidth(100)
sensitivity_layout.addWidget(self.sensitivity_spin)
sensitivity_layout.addStretch()
param_layout.addLayout(sensitivity_layout)
# 推理设备
device_layout = QHBoxLayout()
device_layout.addWidget(QLabel("推理设备:"))
self.device_combo = QComboBox()
self.device_combo.addItems(["best", "cpu:1", "cpu:2", "cpu:4", "gpu:0"])
self.device_combo.setFixedWidth(120)
device_layout.addWidget(self.device_combo)
device_layout.addStretch()
param_layout.addLayout(device_layout)
param_group.setLayout(param_layout)
config_layout.addWidget(param_group)
# 初始化按钮
self.init_btn = QPushButton("🚀 初始化 Porcupine")
self.init_btn.setEnabled(False)
config_layout.addWidget(self.init_btn)
splitter.addWidget(config_panel)
# 右侧监控面板
monitor_panel = QWidget()
monitor_layout = QVBoxLayout(monitor_panel)
# 日志显示
log_group = QGroupBox("检测日志")
log_layout = QVBoxLayout()
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
log_layout.addWidget(self.log_text)
log_group.setLayout(log_layout)
monitor_layout.addWidget(log_group, 1)
# 控制按钮
button_layout = QHBoxLayout()
self.start_btn = QPushButton("▶️ 开始监听")
self.start_btn.setObjectName("startBtn")
self.start_btn.setEnabled(False)
self.stop_btn = QPushButton("⏹️ 停止监听")
self.stop_btn.setObjectName("stopBtn")
self.stop_btn.setEnabled(False)
self.clear_log_btn = QPushButton("🗑️ 清空日志")
self.save_log_btn = QPushButton("💾 保存日志")
button_layout.addWidget(self.start_btn)
button_layout.addWidget(self.stop_btn)
button_layout.addStretch()
button_layout.addWidget(self.clear_log_btn)
button_layout.addWidget(self.save_log_btn)
monitor_layout.addLayout(button_layout)
splitter.addWidget(monitor_panel)
# 设置分割器比例
splitter.setSizes([300, 600])
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_label = QLabel("就绪")
self.status_bar.addWidget(self.status_label, 1)
# 状态指示器
self.status_indicator = QLabel("●")
self.status_indicator.setStyleSheet("color: gray; font-weight: bold;")
self.status_bar.addWidget(self.status_indicator)
# 连接信号
self.load_devices_btn.clicked.connect(self.load_devices)
self.init_btn.clicked.connect(self.initialize_porcupine)
self.start_btn.clicked.connect(self.start_listening)
self.stop_btn.clicked.connect(self.stop_listening)
self.clear_log_btn.clicked.connect(self.log_text.clear)
self.save_log_btn.clicked.connect(self.save_log)
self.add_keyword_btn.clicked.connect(self.add_keyword)
self.remove_keyword_btn.clicked.connect(self.remove_keyword)
self.browse_btn.clicked.connect(self.browse_custom_keyword)
def add_keyword(self):
"""添加关键词"""
keyword = self.keyword_combo.currentText()
if keyword and not any(self.keyword_list.item(i).text() == keyword for i in range(self.keyword_list.count())):
self.keyword_list.addItem(keyword)
self.status_label.setText(f"已添加关键词: {keyword}")
def remove_keyword(self):
"""移除选中的关键词"""
selected = self.keyword_list.selectedItems()
for item in selected:
self.keyword_list.takeItem(self.keyword_list.row(item))
self.status_label.setText(f"已移除关键词: {item.text()}")
def browse_custom_keyword(self):
"""浏览自定义关键词文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择关键词模型文件", "", "Porcupine 模型文件 (*.ppn);;所有文件 (*.*)"
)
if file_path:
self.custom_keyword_edit.setText(file_path)
self.status_label.setText(f"已选择模型文件: {os.path.basename(file_path)}")
def load_devices(self):
"""加载设备"""
try:
self.status_label.setText("正在加载设备...")
self.status_indicator.setStyleSheet("color: orange; font-weight: bold;")
# 创建worker(如果不存在)
if not self.worker:
self.worker = PorcupineWorker()
self.worker.devices_loaded.connect(self.on_devices_loaded)
self.worker.error_occurred.connect(self.on_error)
self.worker.status_updated.connect(self.on_status_updated)
self.worker.initialized.connect(self.on_initialized)
self.worker.keyword_detected.connect(self.on_keyword_detected)
# 在线程中加载设备
self.worker.run_load_devices()
except Exception as e:
self.on_error(f"加载设备失败: {str(e)}")
def on_devices_loaded(self, inference_devices, audio_devices):
"""设备加载完成"""
# 更新音频设备列表
self.audio_device_combo.clear()
self.audio_device_combo.addItem("默认设备", -1)
for i, device in enumerate(audio_devices):
self.audio_device_combo.addItem(device, i)
self.status_label.setText(f"加载完成: {len(audio_devices)} 个音频设备")
self.status_indicator.setStyleSheet("color: green; font-weight: bold;")
self.init_btn.setEnabled(True)
self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] 设备加载完成")
def initialize_porcupine(self):
"""初始化 Porcupine"""
access_key = self.access_key_edit.text().strip()
if not access_key:
QMessageBox.warning(self, "警告", "请输入访问密钥")
return
# 获取关键词
keywords = []
for i in range(self.keyword_list.count()):
keywords.append(self.keyword_list.item(i).text())
# 获取自定义关键词路径
custom_path = self.custom_keyword_edit.text().strip()
keyword_paths = [custom_path] if custom_path and os.path.exists(custom_path) else None
if not keywords and not keyword_paths:
QMessageBox.warning(self, "警告", "请添加至少一个关键词")
return
# 灵敏度设置
sensitivity = self.sensitivity_spin.value()
sensitivities = [sensitivity] * (len(keywords) if keywords else 1)
# 设备选择
device = self.device_combo.currentText()
# 创建worker线程
if not self.worker:
self.worker = PorcupineWorker()
self.worker.devices_loaded.connect(self.on_devices_loaded)
self.worker.error_occurred.connect(self.on_error)
self.worker.status_updated.connect(self.on_status_updated)
self.worker.initialized.connect(self.on_initialized)
self.worker.keyword_detected.connect(self.on_keyword_detected)
# 设置初始化参数
self.worker.initialize(
access_key=access_key,
keywords=keywords if not keyword_paths else None,
keyword_paths=keyword_paths,
sensitivities=sensitivities,
device=device
)
# 启动初始化线程
self.worker.start()
self.init_btn.setEnabled(False)
self.status_label.setText("正在初始化 Porcupine...")
self.status_indicator.setStyleSheet("color: orange; font-weight: bold;")
def on_initialized(self, success):
"""初始化完成"""
if success:
self.start_btn.setEnabled(True)
self.status_label.setText("Porcupine 初始化成功")
self.status_indicator.setStyleSheet("color: green; font-weight: bold;")
self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] Porcupine 初始化成功")
else:
self.init_btn.setEnabled(True)
self.status_indicator.setStyleSheet("color: red; font-weight: bold;")
def start_listening(self):
"""开始监听"""
audio_device_index = self.audio_device_combo.currentData()
# 在单独的线程中开始监听
def listen_thread():
self.worker.start_listening(audio_device_index)
# 启动监听线程
threading.Thread(target=listen_thread, daemon=True).start()
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.status_label.setText("正在监听... 请说出关键词")
self.status_indicator.setStyleSheet("color: blue; font-weight: bold;")
self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] 开始监听...")
def stop_listening(self):
"""停止监听"""
if self.worker:
self.worker.stop_listening()
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.status_label.setText("监听已停止")
self.status_indicator.setStyleSheet("color: green; font-weight: bold;")
self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] 停止监听")
def on_keyword_detected(self, message):
"""检测到关键词"""
self.log_text.append(message)
# 滚动到底部
cursor = self.log_text.textCursor()
cursor.movePosition(cursor.End)
self.log_text.setTextCursor(cursor)
def on_error(self, error_msg):
"""处理错误"""
QMessageBox.critical(self, "错误", error_msg)
self.status_label.setText(f"错误: {error_msg}")
self.status_indicator.setStyleSheet("color: red; font-weight: bold;")
self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] 错误: {error_msg}")
def on_status_updated(self, status_msg):
"""更新状态"""
self.status_label.setText(status_msg)
self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] {status_msg}")
def save_log(self):
"""保存日志"""
file_path, _ = QFileDialog.getSaveFileName(
self, "保存日志", f"porcupine_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
"文本文件 (*.txt);;所有文件 (*.*)"
)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.log_text.toPlainText())
self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] 日志已保存: {file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存日志失败: {str(e)}")
def closeEvent(self, event):
"""关闭窗口事件"""
if self.worker and self.worker.isRunning():
self.worker.stop_listening()
self.worker.quit()
self.worker.wait()
event.accept()
def main():
app = QApplication(sys.argv)
app.setStyle('Fusion')
# 设置应用字体
font = QFont("Microsoft YaHei", 9)
app.setFont(font)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
运行结果如下图

更多推荐


所有评论(0)