feat: 增强编码能力 - 新增代码编辑器、代码搜索、Git集成
- 新增 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 自动化执行,与开发者无关。
This commit is contained in:
commit
e22652fbbb
53 files changed
+20067
No files matched your search
@@ -0,0 +1,549 @@
|
||||
"""
|
||||
语音识别插件 - 弹窗录音界面
|
||||
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)
|
||||
Reference in new issue
Block a user