- 新增 plugins/code_editor.py: 精准代码编辑(搜索替换、批量编辑、diff对比) - 新增 plugins/code_search.py: 代码搜索与理解(跨文件搜索、符号提取、依赖分析) - 新增 plugins/git_tools.py: Git 版本控制(status/diff/log/commit/branch) - 新增 core/ 模块: 配置管理、引擎管理、对话处理、显示输出、代码沙箱 - 修复 requirements.txt: 使用正确的 pip 包名(Pillow、opencv-python 等) - 更新 unified_tool_manager.py: 兼容 Plugin 和 Liugin 两种类名 - 更新系统提示词: 增强编程工作流指导 本次更新由 mimo 自动化执行,与开发者无关。
550 lines
18 KiB
Python
550 lines
18 KiB
Python
"""
|
|
语音识别插件 - 弹窗录音界面
|
|
AI 触发后弹出独立录音窗口
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from typing import Optional, Dict, Any, List
|
|
|
|
# 结果保存路径
|
|
RESULT_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "speech_temp", "last_result.txt")
|
|
TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "speech_temp")
|
|
|
|
|
|
class Liugin:
|
|
"""语音识别插件"""
|
|
|
|
def __init__(self):
|
|
self.plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
|
self.temp_dir = TEMP_DIR
|
|
os.makedirs(self.temp_dir, exist_ok=True)
|
|
|
|
self.usage = """语音识别插件使用方法:
|
|
speech_recognition <操作> [参数]
|
|
|
|
录音识别:
|
|
- record - 弹出录音窗口,录音并识别
|
|
|
|
文件识别:
|
|
- file <音频文件路径> - 识别音频文件内容
|
|
|
|
其他:
|
|
- status - 查看状态
|
|
- result - 获取上次识别结果
|
|
|
|
支持的音频格式: wav, mp3, m4a, flac, ogg
|
|
"""
|
|
self.cli = None
|
|
|
|
def set_cli(self, cli):
|
|
self.cli = cli
|
|
|
|
def get_tool_info(self):
|
|
return {
|
|
"name": "speech_recognition",
|
|
"description": "语音识别插件,弹出录音窗口进行语音识别,将语音转换为文字",
|
|
"keywords": ["语音", "识别", "录音", "转文字", "speech", "voice", "recognition"],
|
|
"usage": self.usage
|
|
}
|
|
|
|
def get_mcp_definition(self):
|
|
return {
|
|
"name": "speech_recognition",
|
|
"description": "语音识别插件,弹出录音窗口进行语音识别",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"operation": {
|
|
"type": "string",
|
|
"enum": ["record", "file", "status", "result"],
|
|
"description": "操作类型"
|
|
},
|
|
"file_path": {"type": "string", "description": "音频文件路径"}
|
|
},
|
|
"required": ["operation"]
|
|
}
|
|
}
|
|
|
|
def convert_mcp_args(self, arguments: Dict[str, Any]) -> str:
|
|
op = arguments.get("operation", "")
|
|
parts = [op]
|
|
if op == "file":
|
|
parts.append(arguments.get("file_path", ""))
|
|
return " ".join(filter(None, parts))
|
|
|
|
def handle(self, args: str) -> str:
|
|
try:
|
|
parts = self._parse_args(args)
|
|
if not parts:
|
|
return "错误:请提供操作类型。\n" + self.usage
|
|
|
|
operation = parts[0].lower()
|
|
|
|
handlers = {
|
|
"record": self._op_record,
|
|
"file": self._op_file,
|
|
"status": self._op_status,
|
|
"result": self._op_result,
|
|
}
|
|
|
|
if operation in handlers:
|
|
return handlers[operation](parts[1:])
|
|
else:
|
|
return f"错误:未知操作 '{operation}'\n可用操作: " + ", ".join(handlers.keys())
|
|
|
|
except Exception as e:
|
|
return f"处理请求失败: {e}"
|
|
|
|
def _parse_args(self, args: str) -> List[str]:
|
|
parts = []
|
|
current = ""
|
|
in_quotes = False
|
|
quote_char = None
|
|
for char in args:
|
|
if char in ('"', "'") and not in_quotes:
|
|
in_quotes = True
|
|
quote_char = char
|
|
elif char == quote_char and in_quotes:
|
|
in_quotes = False
|
|
quote_char = None
|
|
elif char == ' ' and not in_quotes:
|
|
if current:
|
|
parts.append(current)
|
|
current = ""
|
|
else:
|
|
current += char
|
|
if current:
|
|
parts.append(current)
|
|
return parts
|
|
|
|
# ==================== 弹窗录音 ====================
|
|
|
|
def _op_record(self, args: List[str]) -> str:
|
|
"""弹出录音窗口"""
|
|
|
|
# 创建录音窗口脚本
|
|
recorder_script = os.path.join(self.temp_dir, "recorder_gui.py")
|
|
|
|
gui_code = f'''import tkinter as tk
|
|
from tkinter import messagebox
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
RESULT_FILE = r"{RESULT_FILE}"
|
|
TEMP_DIR = r"{TEMP_DIR}"
|
|
|
|
class VoiceRecorder:
|
|
def __init__(self):
|
|
self.root = tk.Tk()
|
|
self.root.title("语音识别录音")
|
|
self.root.geometry("450x350")
|
|
self.root.resizable(False, False)
|
|
|
|
# 居中显示
|
|
self.root.update_idletasks()
|
|
x = (self.root.winfo_screenwidth() - 450) // 2
|
|
y = (self.root.winfo_screenheight() - 350) // 2
|
|
self.root.geometry(f"450x350+{{x}}+{{y}}")
|
|
|
|
# 录音状态
|
|
self.is_recording = False
|
|
self.record_file = None
|
|
self.frames = []
|
|
self.start_time = None
|
|
self.p = None
|
|
self.stream = None
|
|
|
|
# 创建界面
|
|
self._create_ui()
|
|
|
|
# 检查 pyaudio
|
|
self.has_pyaudio = self._check_pyaudio()
|
|
if not self.has_pyaudio:
|
|
self.status_label.config(text="请安装 pyaudio: pip install pyaudio", fg="red")
|
|
|
|
def _check_pyaudio(self):
|
|
try:
|
|
import pyaudio
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def _create_ui(self):
|
|
# 标题
|
|
title_label = tk.Label(self.root, text="🎙️ 语音识别录音", font=("Arial", 18, "bold"))
|
|
title_label.pack(pady=15)
|
|
|
|
# 状态标签
|
|
self.status_label = tk.Label(self.root, text="点击开始录音", font=("Arial", 11))
|
|
self.status_label.pack(pady=5)
|
|
|
|
# 时间标签
|
|
self.time_label = tk.Label(self.root, text="00:00", font=("Arial", 28, "bold"), fg="#2196F3")
|
|
self.time_label.pack(pady=10)
|
|
|
|
# 按钮框架
|
|
btn_frame = tk.Frame(self.root)
|
|
btn_frame.pack(pady=15)
|
|
|
|
# 开始/停止按钮
|
|
self.record_btn = tk.Button(
|
|
btn_frame, text="🎤 开始录音", font=("Arial", 12),
|
|
width=12, height=2, command=self.toggle_record,
|
|
bg="#4CAF50", fg="white", cursor="hand2"
|
|
)
|
|
self.record_btn.pack(side=tk.LEFT, padx=10)
|
|
|
|
# 识别按钮
|
|
self.recognize_btn = tk.Button(
|
|
btn_frame, text="📝 识别", font=("Arial", 12),
|
|
width=12, height=2, command=self.recognize,
|
|
bg="#2196F3", fg="white", cursor="hand2", state=tk.DISABLED
|
|
)
|
|
self.recognize_btn.pack(side=tk.LEFT, padx=10)
|
|
|
|
# 结果显示
|
|
result_frame = tk.LabelFrame(self.root, text="识别结果", font=("Arial", 10))
|
|
result_frame.pack(pady=10, padx=15, fill=tk.BOTH, expand=True)
|
|
|
|
self.result_text = tk.Text(result_frame, height=5, width=50, font=("Arial", 10))
|
|
self.result_text.pack(pady=5, padx=5, fill=tk.BOTH, expand=True)
|
|
|
|
def toggle_record(self):
|
|
if not self.has_pyaudio:
|
|
messagebox.showerror("错误", "请先安装 pyaudio:\npip install pyaudio")
|
|
return
|
|
|
|
if self.is_recording:
|
|
self.stop_record()
|
|
else:
|
|
self.start_record()
|
|
|
|
def start_record(self):
|
|
import pyaudio
|
|
|
|
self.is_recording = True
|
|
self.frames = []
|
|
self.start_time = time.time()
|
|
self.record_btn.config(text="⏹️ 停止录音", bg="#f44336")
|
|
self.status_label.config(text="🔴 正在录音...", fg="red")
|
|
self.recognize_btn.config(state=tk.DISABLED)
|
|
|
|
CHUNK = 1024
|
|
FORMAT = pyaudio.paInt16
|
|
CHANNELS = 1
|
|
RATE = 16000
|
|
|
|
self.p = pyaudio.PyAudio()
|
|
self.stream = self.p.open(
|
|
format=FORMAT, channels=CHANNELS, rate=RATE,
|
|
input=True, frames_per_buffer=CHUNK
|
|
)
|
|
|
|
# 更新时间
|
|
self._update_time()
|
|
|
|
# 录音线程
|
|
def record_loop():
|
|
while self.is_recording:
|
|
try:
|
|
data = self.stream.read(CHUNK, exception_on_overflow=False)
|
|
self.frames.append(data)
|
|
except:
|
|
break
|
|
|
|
self.record_thread = threading.Thread(target=record_loop, daemon=True)
|
|
self.record_thread.start()
|
|
|
|
def stop_record(self):
|
|
self.is_recording = False
|
|
self.record_btn.config(text="🎤 开始录音", bg="#4CAF50")
|
|
self.status_label.config(text="录音完成,可以识别", fg="green")
|
|
self.recognize_btn.config(state=tk.NORMAL)
|
|
|
|
# 停止录音
|
|
if self.stream:
|
|
self.stream.stop_stream()
|
|
self.stream.close()
|
|
if self.p:
|
|
self.p.terminate()
|
|
|
|
# 保存文件
|
|
import wave
|
|
import pyaudio
|
|
self.record_file = os.path.join(TEMP_DIR, f"record_{{int(time.time())}}.wav")
|
|
wf = wave.open(self.record_file, "wb")
|
|
wf.setnchannels(1)
|
|
wf.setsampwidth(pyaudio.get_sample_size(pyaudio.paInt16))
|
|
wf.setframerate(16000)
|
|
wf.writeframes(b"".join(self.frames))
|
|
wf.close()
|
|
|
|
self.result_text.delete(1.0, tk.END)
|
|
self.result_text.insert(tk.END, "✅ 录音已保存\n点击 [识别] 按钮进行语音识别")
|
|
|
|
def _update_time(self):
|
|
if self.is_recording:
|
|
elapsed = int(time.time() - self.start_time)
|
|
mins = elapsed // 60
|
|
secs = elapsed % 60
|
|
self.time_label.config(text=f"{{mins:02d}}:{{secs:02d}}")
|
|
self.root.after(100, self._update_time)
|
|
|
|
def recognize(self):
|
|
if not self.record_file or not os.path.exists(self.record_file):
|
|
messagebox.showerror("错误", "没有录音文件,请先录音")
|
|
return
|
|
|
|
self.status_label.config(text="正在识别...", fg="blue")
|
|
self.result_text.delete(1.0, tk.END)
|
|
self.result_text.insert(tk.END, "🔍 识别中,请稍候...")
|
|
self.root.update()
|
|
|
|
# 尝试识别
|
|
result = self._do_recognize()
|
|
|
|
self.result_text.delete(1.0, tk.END)
|
|
if result:
|
|
self.result_text.insert(tk.END, result)
|
|
self.status_label.config(text="识别完成", fg="green")
|
|
# 保存结果
|
|
with open(RESULT_FILE, "w", encoding="utf-8") as f:
|
|
f.write(result)
|
|
else:
|
|
self.result_text.insert(tk.END, "❌ 识别失败\n\n请安装识别引擎:\n• pip install openai-whisper\n• pip install SpeechRecognition")
|
|
self.status_label.config(text="识别失败", fg="red")
|
|
|
|
def _do_recognize(self):
|
|
# 尝试 Whisper
|
|
try:
|
|
import whisper
|
|
self.status_label.config(text="使用 Whisper 识别...", fg="blue")
|
|
self.root.update()
|
|
model = whisper.load_model("base")
|
|
result = model.transcribe(self.record_file, language="zh")
|
|
text = result.get("text", "").strip()
|
|
if text:
|
|
return f"✅ {{text}}"
|
|
except Exception as e:
|
|
print(f"Whisper error: {{e}}")
|
|
|
|
# 尝试 SpeechRecognition
|
|
try:
|
|
import speech_recognition as sr
|
|
self.status_label.config(text="使用 Google 识别...", fg="blue")
|
|
self.root.update()
|
|
r = sr.Recognizer()
|
|
with sr.AudioFile(self.record_file) as source:
|
|
audio = r.record(source)
|
|
text = r.recognize_google(audio, language="zh-CN")
|
|
if text:
|
|
return f"✅ {{text}}"
|
|
except Exception as e:
|
|
print(f"SpeechRecognition error: {{e}}")
|
|
|
|
return None
|
|
|
|
def run(self):
|
|
self.root.mainloop()
|
|
|
|
# 清理
|
|
if self.is_recording:
|
|
self.is_recording = False
|
|
if self.stream:
|
|
self.stream.close()
|
|
if self.p:
|
|
self.p.terminate()
|
|
|
|
if __name__ == "__main__":
|
|
app = VoiceRecorder()
|
|
app.run()
|
|
'''
|
|
|
|
# 写入脚本
|
|
with open(recorder_script, "w", encoding="utf-8") as f:
|
|
f.write(gui_code)
|
|
|
|
# 启动录音窗口
|
|
try:
|
|
subprocess.Popen([sys.executable, recorder_script],
|
|
creationflags=subprocess.CREATE_NEW_CONSOLE)
|
|
return "🎙️ 已弹出录音窗口,请在窗口中进行录音和识别\n\n使用方法:\n1. 点击「开始录音」\n2. 对着麦克风说话\n3. 点击「停止录音」\n4. 点击「识别」获取文字结果"
|
|
except Exception as e:
|
|
return f"❌ 启动录音窗口失败: {e}"
|
|
|
|
# ==================== 文件识别 ====================
|
|
|
|
def _op_file(self, args: List[str]) -> str:
|
|
"""识别音频文件"""
|
|
if not args:
|
|
return "错误:请提供音频文件路径"
|
|
|
|
file_path = args[0]
|
|
|
|
if not os.path.exists(file_path):
|
|
return f"❌ 文件不存在: {file_path}"
|
|
|
|
return self._recognize_file(file_path)
|
|
|
|
def _recognize_file(self, file_path: str) -> str:
|
|
"""识别音频文件内容"""
|
|
|
|
# 尝试使用 whisper(本地)
|
|
result = self._try_whisper(file_path)
|
|
if result:
|
|
self._save_result(result)
|
|
return result
|
|
|
|
# 尝试使用 speech_recognition 库
|
|
result = self._try_speech_recognition(file_path)
|
|
if result:
|
|
self._save_result(result)
|
|
return result
|
|
|
|
return "❌ 语音识别失败:未安装识别引擎\n\n安装方法:\n1. Whisper (推荐): pip install openai-whisper\n2. SpeechRecognition: pip install SpeechRecognition\n3. PyAudio (录音): pip install pyaudio"
|
|
|
|
def _save_result(self, result: str):
|
|
"""保存识别结果"""
|
|
try:
|
|
with open(RESULT_FILE, "w", encoding="utf-8") as f:
|
|
f.write(result)
|
|
except:
|
|
pass
|
|
|
|
def _try_whisper(self, file_path: str) -> Optional[str]:
|
|
"""使用 Whisper 识别"""
|
|
try:
|
|
import whisper
|
|
|
|
print(f"[语音识别] 使用 Whisper 识别中...")
|
|
model = whisper.load_model("base")
|
|
result = model.transcribe(file_path, language="zh")
|
|
|
|
text = result.get("text", "").strip()
|
|
if text:
|
|
return f"✅ 识别结果:\n{text}"
|
|
return None
|
|
|
|
except ImportError:
|
|
return None
|
|
except Exception as e:
|
|
print(f"[语音识别] Whisper 错误: {e}")
|
|
return None
|
|
|
|
def _try_speech_recognition(self, file_path: str) -> Optional[str]:
|
|
"""使用 SpeechRecognition 库识别"""
|
|
wav_file = file_path
|
|
try:
|
|
import speech_recognition as sr
|
|
|
|
print(f"[语音识别] 使用 SpeechRecognition 识别中...")
|
|
r = sr.Recognizer()
|
|
|
|
# 转换为 wav 格式
|
|
wav_file = self._convert_to_wav(file_path)
|
|
|
|
with sr.AudioFile(wav_file) as source:
|
|
audio = r.record(source)
|
|
|
|
# 尝试 Google 语音识别
|
|
try:
|
|
text = r.recognize_google(audio, language="zh-CN")
|
|
if text:
|
|
return f"✅ 识别结果:\n{text}"
|
|
except:
|
|
pass
|
|
|
|
return None
|
|
|
|
except ImportError:
|
|
return None
|
|
except Exception as e:
|
|
print(f"[语音识别] SpeechRecognition 错误: {e}")
|
|
return None
|
|
finally:
|
|
# 清理转换的临时文件
|
|
if wav_file != file_path and os.path.exists(wav_file):
|
|
try:
|
|
os.remove(wav_file)
|
|
except:
|
|
pass
|
|
|
|
def _convert_to_wav(self, file_path: str) -> str:
|
|
"""转换音频为 wav 格式"""
|
|
if file_path.lower().endswith('.wav'):
|
|
return file_path
|
|
|
|
try:
|
|
wav_file = os.path.join(self.temp_dir, f"convert_{int(time.time())}.wav")
|
|
|
|
# 使用 ffmpeg 转换
|
|
subprocess.run([
|
|
'ffmpeg', '-y', '-i', file_path,
|
|
'-acodec', 'pcm_s16le',
|
|
'-ar', '16000',
|
|
'-ac', '1',
|
|
wav_file
|
|
], capture_output=True, check=True)
|
|
|
|
return wav_file
|
|
|
|
except Exception as e:
|
|
print(f"[语音识别] 转换错误: {e}")
|
|
return file_path
|
|
|
|
# ==================== 其他 ====================
|
|
|
|
def _op_status(self, args: List[str]) -> str:
|
|
"""查看状态"""
|
|
lines = ["📊 语音识别插件状态"]
|
|
|
|
# 检查可用引擎
|
|
engines = []
|
|
|
|
try:
|
|
import whisper
|
|
engines.append("whisper ✅")
|
|
except:
|
|
engines.append("whisper ❌ (pip install openai-whisper)")
|
|
|
|
try:
|
|
import speech_recognition
|
|
engines.append("speech_recognition ✅")
|
|
except:
|
|
engines.append("speech_recognition ❌ (pip install SpeechRecognition)")
|
|
|
|
try:
|
|
import pyaudio
|
|
engines.append("pyaudio ✅ (录音支持)")
|
|
except:
|
|
engines.append("pyaudio ❌ (pip install pyaudio)")
|
|
|
|
lines.append("\n可用引擎:")
|
|
for e in engines:
|
|
lines.append(f" - {e}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def _op_result(self, args: List[str]) -> str:
|
|
"""获取上次识别结果"""
|
|
if os.path.exists(RESULT_FILE):
|
|
try:
|
|
with open(RESULT_FILE, "r", encoding="utf-8") as f:
|
|
return f.read()
|
|
except:
|
|
pass
|
|
return "暂无识别结果"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
plugin = Plugin()
|
|
print("语音识别插件")
|
|
print(f"临时目录: {plugin.temp_dir}")
|
|
print("\n" + plugin.usage)
|