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

在这里插入图片描述

安装并获取 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()

运行结果如下图

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐