iOS之self.xxx与_xxx的区别

本文深入探讨Objective-C中self.xx访问方式与内存管理的关系,对比self.xx与_xx的区别,解释如何通过self.xx避免内存泄露并实现懒加载。

1.首先通过self.xxx 访问的方法的引用:包含了set和get方法。而通过下划线是获取自己的实例变量,不包含set和get的方法。

2.self.xxx

是对属性的访问;而_xxx是对局部变量的访问。所有被声明为属性的成员,再ios5之前需要使用编译指令@synthesize

来告诉编译器帮助生成属性的getter和setter方法,之后这个指令可以不用认为的指定了,默认情况下编译器会帮助我们生成。编译器在生成

getter,setter方法时是有优先级的,他首先查找当前的类中用户是否定义属性的getter,setter方法,如果有,则编译器会跳过,不会

再生成,使用用户定义的方法。也就是说你在使用self.xxx时是调用一个getter方法。会使引用计数加一,而_xxx不会使用引用技术加一的。

所有使用self.xxx是更好的选择,因为这样可以兼容懒加载,同时也避免了使用下滑线的时候忽略了self这个指针,后者容易在BLock中造成循环引用。同时,使用 _是获取不到父类的属性,因为它只是对局部变量的访问。

最后总结:self方法实际上是用了get和set方法间接调用,下划线方法是直接对变量操作。

下面详细说明, 我们经常会在官方文档里看到这样的代码:

MyClass.h

@interface MyClass : NSObject {

MyObject *myObject;

}

@property (nonatomic, retain) MyObject *myObject;

@end

MyClass.m

@synthesize myObject;

-(id)init{

if(self = [super init]){

MyObject * aMyObject = [[MyObject alloc] init];

self.myObject = aMyObject;

[aMyObject release];

}

return self;

}

有人就问, 为什么要这么复杂的赋值? 为什么要加self. ? 直接写成self.myObject = [[MyObject alloc] init];不是也没有错么? 不加self有时好像也是正常的?

现在我们来看看内存管理的内容:

先看间接赋值的:

1.加self.

MyObject * aMyObject = [[MyObject alloc] init]; //aMyObject retainCount = 1;

self.myObject = aMyObject; //myObject retainCount = 2;

[aMyObject release];//myObject retainCount = 1;

2. 不加self.

MyObject * aMyObject = [[MyObject alloc] init]; //aMyObject retainCount = 1;

myObject = aMyObject; //myObject retainCount = 1;

[aMyObject release];//对象己经被释放

再看直接赋值的:

3.加self.

self.myObject = [[MyObject alloc] init]; //myObject retainCount = 2;

4. 不加self.

myObject = [[MyObject alloc] init]; //myObject retainCount = 1;

现在是不是有点晕, 我们先来把代码改一下, 官方的一种常见写法:

MyClass.h

@interface MyClass : NSObject {

MyObject * _myObject;

}

@property (nonatomic, retain) MyObject *myObject;

@end

MyClass.m

@synthesize myObject = _myObject;

OK, 你现在再试下, 如果你用self._myObject = aMyObject; 或者 myObject = aMyObject; 你会得到一个错误, 为什么呢, 这里就是和Obj-c的存取方法有关了. 说白了很简单 , 大家都知道, @property (nonatomic, retain) MyObject *myObject; 是为一个属性设置存取方法, 只是平时我们用的方法名和属性名是一样的,现在你把它写成不同的名字, 就会很清楚了. _myObject是属性本身, myObject是存取方法名.

现在我们知道self.是访问属性的存取方法了, 那存取方法又怎么工作的? self.myObject = [[MyObject alloc] init]; 为什么会有内存泄露?

关于nonatomic我不多解释了, 它不是我要讲的重点, 而且我也没完全搞清楚, 不误导大家. 我只说assign, retain ,copy.

get方法是:

-(MyObject*)myObject{

return _myObject;

}

Set方法是:

// assign

-(void)setMyObject:(id)newValue{

_myObject = newValue;

}

// retain

-(void)setMyObject:(id)newValue{

if (_myObject != newValue) {

[_myObject release];

_myObject = [newValue retain];

}

}

// copy

-(void)setMyObject:(id)newValue{

if (_myObject != newValue) {

[_myObject release];

_myObject = [newValue copy];

}

}

其实这些方法里还有别的内容, 并不只是这些. 而且这些方法可以被重写. 比如你写一个

-(MyObject*)myObject{

return _myObject;

}

放在你的类里, 你调用self.myObject时(不要把它放在等号左边, 那会调用get方法)就会调用这个方法.

这里多说一句, @property 是为你设置存取方法, 和你的属性无关, 你可以只写一句

@property (readonly) NSString *name;

在你的类里实现

-(NSString*)name{

NSLog(@"name");

return @"MyClass";

}

同样可以用self.name调用.

现在回头说说我们开始的那四个赋值, 当不用self.的时候,  那句话只是一般的赋值, 把一个指针赋给另一个指针, 不会对分配的内存有任何影响, 所以2中不要最后[aMyObject release];这句话和4是一回事. 这里就不多说了.我们看看1和3,

当调用setMyObject:方法时, 对newValue 做了一次retain操作, 我们必须把原来的newValue释放掉, 不然就会内存泄露, 在1里, 我们有个aMyObject可以用来释放, 在3里, 我们无法释放它, 所以, 在3里, 我们会多出来一个retainCount. 内存泄露了.

说了这么多, 我只想让大家清楚, 什么是调用属性本身, 什么是调用存取方法. 怎么样才能避免内存泄露, 而且, 以上例子里是在自己类里的调用, 如果这个类被别的类调用时, 更要注意一些,

顺便说一下, 如果你想在其它类访问对象属性, 而不是通过存取方法, 你可以用myClass -> myObject来访问, 这样是直接访问对象本身, 不过你先要把myObject设成@public. 但这个是官方不提倡的。



文/GitHubPorter(简书作者)
原文链接:http://www.jianshu.com/p/d9a659a21b1a
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
# api_server.py import threading import time from flask import Flask, request, jsonify, Response from werkzeug.serving import make_server from flask_cors import CORS # --- 全局状态共享 --- current_status = { "is_listening": False, "is_tts_playing": False, "current_timeout": 8.0, "last_command_result": None, "timestamp": int(time.time()) } last_transcribed_text = "" from Progress.app import ( get_system_controller, get_task_executor, get_tts_engine, get_voice_recognizer, get_ai_assistant ) assistant = get_ai_assistant() executor = get_task_executor() tts_engine = get_tts_engine() recognizer = get_voice_recognizer() class CrossPlatformVoiceHandler: def __init__(self): self.supported_formats = { 'web': ['wav', 'mp3', 'ogg'], 'android': ['wav', 'mp3', 'aac', 'amr'], 'ios': ['wav', 'mp3', 'm4a', 'caf'], 'windows': ['wav', 'mp3', 'm4a'] } def process_voice_input(self, audio_data: dict, platform: str) -> dict: """处理来自不同平台的语音输入""" try: # 格式转换预处理 processed_audio = self._convert_to_standard_format(audio_data, platform) # 调用语音识别 if platform == "web": text_result = self._web_voice_recognition(processed_audio) elif platform == "android": text_result = self._android_voice_recognition(processed_audio) elif platform == "ios": text_result = self._ios_voice_recognition(processed_audio) else: text_result = recognizer.listen_and_recognize() return { "success": True, "text": text_result, "platform": platform, "timestamp": int(time.time()) } except Exception as e: return { "success": False, "error": f"语音处理失败: {str(e)}" } def _convert_to_standard_format(self, audio_data, platform): """将不同平台的音频数据转换为标准格式""" if platform == "web": # Web Audio API 格式处理 return self._handle_web_audio(audio_data) elif platform == "android": return self._handle_android_audio(audio_data) elif platform == "ios": return self._handle_ios_audio(audio_data) else: return audio_data class EnhancedAPIServer: def __init__(self): self.app = Flask(__name__) CORS(self.app) self.server = None self.thread = None self.running = False self.voice_handler = CrossPlatformVoiceHandler() self._add_enhanced_routes() def _update_status(self, **kwargs): """更新全局状态""" current_status.update(kwargs) current_status["timestamp"] = int(time.time()) def _add_enhanced_routes(self): """注册所有增强API路由""" # 1. 健康检查接口 @self.app.route('/api/health', methods=['GET']) def health(): return jsonify({ "status": "ok", "service": "ai_assistant", "timestamp": int(time.time()) }) # 2. 跨平台语音处理接口 @self.app.route('/api/voice/process', methods=['POST']) def process_voice(): try: data = request.get_json() if not data or 'audio_data' not in data: return jsonify({"error": "缺少音频数据"}), 400 platform = data.get('platform', 'web') audio_format = data.get('format', 'wav') session_id = data.get('session_id') # 调用跨平台语音处理器 recognition_result = self.voice_handler.process_voice_input( data['audio_data'], platform ) # 如果识别成功,继续处理指令 if recognition_result.get('success') and recognition_result.get('text'): # 调用AI助手处理文本指令 decision = assistant.process_voice_command(recognition_result['text']) execution_result = executor.execute_task_plan(decision) # 构建完整响应 response_data = { "voice_recognition": recognition_result, "ai_processing": execution_result, "timestamp": int(time.time()) } return jsonify(response_data), 200 else: return jsonify({ "success": False, "error": "语音识别失败", "details": recognition_result }), 400 except Exception as e: return jsonify({ "success": False, "error": str(e) }), 500 # 3. 流式语音输入接口 @self.app.route('/api/voice/stream', methods=['POST']) def voice_stream(): """处理流式语音输入""" try: content_type = request.content_type if content_type == 'application/json': data = request.get_json() audio_data = data.get('audio_data') platform = data.get('platform', 'unknown') # 实时处理语音流 stream_processor = VoiceStreamProcessor() result = stream_processor.process_stream(audio_data, platform) return jsonify({ "status": "processing", "session_id": data.get('session_id'), "partial_result": result.get('partial_text', '') }) except Exception as e: return jsonify({"error": str(e)}), 500 # 4. 系统能力查询接口 @self.app.route('/api/capabilities', methods=['GET']) def get_capabilities(): """返回系统支持的功能平台""" return jsonify({ "platforms": ["web", "android", "ios", "windows"], "audio_formats": { "web": ["wav", "mp3", "ogg"], "android": ["wav", "mp3", "aac", "amr"], "ios": ["wav", "mp3", "m4a", "caf"] }, "features": { "voice_recognition": True, "text_to_speech": True, "music_control": True, "file_operations": True, "system_control": True } }) # 5. 结果流式返回接口 @self.app.route('/api/result/stream', methods=['POST']) def stream_result(): """流式返回处理结果给前端""" try: data = request.get_json() session_id = data.get('session_id') callback_url = data.get('callback_url') return jsonify({ "streaming_supported": True, "protocols": ["websocket", "sse", "long_polling"] }) except Exception as e: return jsonify({"error": str(e)}), 500 # 6. 原有的命令处理接口(保持兼容) @self.app.route('/api/command', methods=['POST']) def handle_command(): try: data = request.get_json() if not data or 'text' not in data: return jsonify({ "success": False, "response_to_user": "未收到有效指令" }), 400 text = data['text'] context = data.get('context', {}) options = data.get('options', {}) should_speak = options.get('should_speak', True) return_plan = options.get('return_plan', False) decision = assistant.process_voice_command(text) result = executor.execute_task_plan(decision) ai_reply = result["message"] if not result["success"] and not ai_reply.startswith("抱歉"): ai_reply = f"抱歉,{ai_reply}" self._update_status( is_processing=True, last_command_result={"success": result["success"], "message": ai_reply, "operation": decision.get("action")} ) if should_speak: self._update_status(is_tts_playing=True) tts_engine.speak(ai_reply, async_mode=True) def _finish_tts(): time.sleep(1) self._update_status(is_tts_playing=False) threading.Thread(target=_finish_tts, daemon=True).start() response_data = { "success": result["success"], "response_to_user": ai_reply, "operation": decision.get("action"), "details": result, "should_speak": should_speak, "timestamp": int(time.time()), } if return_plan: response_data["plan"] = decision self._update_status(is_processing=False) return jsonify(response_data), 200 except Exception as e: return jsonify({ "success": False, "error": str(e), "timestamp": int(time.time()) }), 500 def start(self, host="127.0.0.1", port=5000, debug=False): """启动API服务(非阻塞)""" if self.running: print("⚠️ API服务器已在运行") return try: self.server = make_server(host, port, self.app) self.running = True def run_flask(): print(f"🌐 AI助手API已启动 → http://{host}:{port}/api") self.server.serve_forever() self.thread = threading.Thread(target=run_flask, daemon=True) self.thread.start() except Exception as e: print(f"❌ 启动API服务失败: {e}") raise def stop(self): """关闭API服务""" if not self.running: return print("🛑 正在关闭API服务...") try: self.server.shutdown() except: pass finally: self.running = False if self.thread: self.thread.join(timeout=3) if self.thread.is_alive(): print("⚠️ API服务线程未能及时退出") print("✅ API服务已关闭") # 语音流处理器 class VoiceStreamProcessor: def __init__(self): self.active_sessions = {} def process_stream(self, audio_data, platform): """处理实时语音流数据""" # 实时语音识别逻辑 partial_text = self._real_time_recognition(audio_data) return { "partial_text": partial_text, "is_final": False } def _real_time_recognition(self, audio_data): """实时语音识别实现""" # 这里可以集成WebRTC VAD等实时处理技术 return "正在识别中..." 分析一下这个代码的问题,并给出修改后的全代码 修改api工具让api工具能够调用各板块的功能完成几个接口, 1.从UI获取语音 2.将生成的语音返回给前端 需要的内容保留,不需要的删去,缺少的内容增加 注意,前端会从多种平台传入语音,window,Android,ios等等
10-28
# api_server.py import threading import time import os from flask import Flask, request, jsonify, send_file, Response from werkzeug.utils import secure_filename from werkzeug.serving import make_server from flask_cors import CORS import base64 from Progress.utils.logger_utils import logger # --- 全局状态 --- current_status = { "is_listening": False, "is_tts_playing": False, "last_command_result": None, "timestamp": int(time.time()) } # 导入真实组件 from Progress.app import ( get_ai_assistant, get_task_executor, get_tts_engine, get_voice_recognizer ) assistant = get_ai_assistant() executor = get_task_executor() tts_engine = get_tts_engine() recognizer = get_voice_recognizer() # 临时目录存储 TTS 音频 TEMP_DIR = "temp_audio" os.makedirs(TEMP_DIR, exist_ok=True) # 运行模式:'auto', 'online', 'offline' RUN_MODE = os.getenv("RUN_MODE", "auto") # 可通过环境变量控制 class APIServer: def __init__(self): self.app = Flask(__name__) CORS(self.app, supports_credentials=True) self.server = None self.thread = None self.running = False self.active_sessions = {} self._add_routes() def _update_status(self, **kwargs): current_status.update(kwargs) current_status["timestamp"] = int(time.time()) def _should_use_offline_mode(self) -> bool: """根据客户端类型或全局配置决定是否走本地全流程""" if RUN_MODE == "offline": return True if RUN_MODE == "online": return False # auto 模式下根据请求判断 client_type = request.headers.get("X-Client-Type", "") return client_type not in ["web", "mobile", "ios", "android"] def _add_routes(self): @self.app.route('/api/health', methods=['GET']) def health(): return jsonify({ "status": "ok", "service": "ai_assistant_api", "mode": "offline" if self._should_use_offline_mode() else "online", "timestamp": int(time.time()) }) @self.app.route('/api/capabilities', methods=['GET']) def capabilities(): is_offline = self._should_use_offline_mode() return jsonify({ "platforms": ["web", "android", "ios", "windows"], "features": { "voice_recognition": not is_offline, "text_to_speech": not is_offline, "command_execution": True }, "recommended_mode": "offline" if is_offline else "online" }) # === 模式一:前端传文本(线上模式)=== @self.app.route('/api/text/query', methods=['POST']) def handle_text_query(): data = request.get_json() or {} user_text = data.get("text", "").strip() session_id = data.get("session_id") if not user_text: return jsonify({"error": "缺少文本输入"}), 400 try: # AI 决策 decision = assistant.process_voice_command(user_text) execution_result = executor.execute_task_plan(decision) ai_reply = execution_result.get("message", "操作已完成。") if not execution_result.get("success") and not ai_reply.startswith("抱歉"): ai_reply = f"抱歉,{ai_reply}" # 更新状态 self._update_status(last_command_result={"text": user_text, "response": ai_reply}) # 在离线模式下,即使收到文本也主动播放语音 if self._should_use_offline_mode(): try: tts_file = os.path.join(TEMP_DIR, f"auto_{int(time.time())}.mp3") tts_engine.speak(ai_reply, output_file=tts_file) # 可选择性返回语音 URL return jsonify({ "success": True, "response_to_user": ai_reply, "tts_audio_url": f"/api/tts/audio?file={os.path.basename(tts_file)}", "details": execution_result }) except Exception as e: logger.error(f"⚠️ TTS 播放失败: {e}") return jsonify({ "success": True, "response_to_user": ai_reply, "warning": "TTS生成失败", "details": execution_result }) # 线上模式:仅返回文本 return jsonify({ "success": True, "response_to_user": ai_reply, "operation": decision.get("action"), "details": execution_result }) except Exception as e: logger.exception("❌ 文本处理出错") return jsonify({"success": False, "error": str(e)}), 500 # === 模式二:前端传语音(兼容旧接口)=== @self.app.route('/api/voice/upload', methods=['POST']) def upload_voice(): try: # 判断是否应由后端处理语音 if not self._should_use_offline_mode(): return jsonify({ "error": "当前为在线模式,请直接发送文本", "hint": "请调用 /api/text/query 发送文本" }), 400 platform = request.form.get('platform', 'unknown') session_id = request.form.get('session_id') audio_data = None if 'file' in request.files: file = request.files['file'] if file.filename == '': return jsonify({"error": "未选择文件"}), 400 filename = secure_filename(file.filename) file_path = os.path.join(TEMP_DIR, f"{session_id or 'tmp'}_{filename}") file.save(file_path) audio_data = {"path": file_path} elif 'audio_base64' in request.form: b64_str = request.form['audio_base64'].split(",")[-1] raw_data = base64.b64decode(b64_str) file_path = os.path.join(TEMP_DIR, f"{session_id or 'tmp'}.wav") with open(file_path, 'wb') as f: f.write(raw_data) audio_data = {"path": file_path} else: return jsonify({"error": "请提供 file 或 audio_base64"}), 400 self._update_status(is_listening=True) recognized_text = recognizer.listen_and_recognize(audio_file=audio_data["path"]) self._update_status(is_listening=False) if not recognized_text: return jsonify({ "success": False, "error": "语音识别失败", "response_to_user": "抱歉,我没有听清楚,请再说一遍。" }), 400 # 调用 AI 决策 decision = assistant.process_voice_command(recognized_text) execution_result = executor.execute_task_plan(decision) ai_reply = execution_result.get("message", "操作已完成。") if not execution_result.get("success") and not ai_reply.startswith("抱歉"): ai_reply = f"抱歉,{ai_reply}" # 后端 TTS 播放并返回音频链接 tts_file_path = os.path.join(TEMP_DIR, f"reply_{int(time.time())}.mp3") try: tts_engine.speak(ai_reply, output_file=tts_file_path) except Exception as e: logger.warning(f"⚠️ TTS 生成失败: {e}") self._update_status( last_command_result={"text": recognized_text, "response": ai_reply} ) return jsonify({ "success": True, "recognized_text": recognized_text, "response_to_user": ai_reply, "tts_audio_url": f"/api/tts/audio?file={os.path.basename(tts_file_path)}", "operation": decision.get("action"), "details": execution_result, "timestamp": int(time.time()) }) except Exception as e: print(f"❌ 语音处理出错: {e}") return jsonify({"success": False, "error": str(e)}), 500 # === 提供 TTS 音频下载 === @self.app.route('/api/tts/audio', methods=['GET']) def get_tts_audio(): filename = request.args.get('file') if not filename: return jsonify({"error": "缺少文件名"}), 400 file_path = os.path.join(TEMP_DIR, filename) if not os.path.exists(file_path): return jsonify({"error": "文件不存在"}), 404 return send_file(file_path, mimetype="audio/mpeg") # === 查询当前状态 === @self.app.route('/api/status', methods=['GET']) def status(): return jsonify({**current_status}) def start(self, host="127.0.0.1", port=5000, debug=False): if self.running: print("⚠️ API 服务器已在运行") return try: self.server = make_server(host, port, self.app) self.running = True def run_flask(): print(f"🌐 AI 助手 API 已启动 → http://{host}:{port} (模式: {RUN_MODE})") self.server.serve_forever() self.thread = threading.Thread(target=run_flask, daemon=True) self.thread.start() except Exception as e: print(f"❌ 启动失败: {e}") raise def stop(self): if not self.running: return print("🛑 正在关闭 API 服务...") try: self.server.shutdown() except: pass finally: self.running = False if self.thread: self.thread.join(timeout=3) print("✅ API 服务已关闭") 把简化后的api工具全代码给我
10-28
# api_server.py import threading import os from flask import Flask, request, jsonify, send_file from werkzeug.utils import secure_filename from werkzeug.serving import make_server from flask_cors import CORS import base64 import time from Progress.utils.logger_utils import logger from Progress.app import get_voice_recognizer from core.handler import handle_user_input from database.config import config recognizer = get_voice_recognizer() # 全局状态 current_status = { "is_listening": False, "is_tts_playing": False, "last_command_result": None, "timestamp": int(time.time()) } ENABLE_API_SERVER = config.get("app", "enable_api_server", default=True) API_HOST = config.get("app", "api_host", default="127.0.0.1") API_PORT = config.get("app", "api_port", default=5000) RUN_MODE = config.get("app", "run_mode", default="auto") VOICE_RECOGNIZER_TIMEOUT = config.get("stt", "timeout", default=3) TTS_ENGINE_NAME = config.get("tts", "engine", default="edge") class APIServer: def __init__(self): self.app = Flask(__name__) CORS(self.app) self.server = None self.thread = None self.running = False self._add_routes() def _update_status(self, **kwargs): current_status.update(kwargs) current_status["timestamp"] = int(time.time()) def _determine_source(self): """根据请求头判断来源""" client_type = request.headers.get("X-Client-Type", "").lower() if client_type in ["web", "browser"]: return "web" elif client_type in ["mobile", "android", "ios"]: return "mobile" elif client_type in ["raspberry", "local-device"]: return "local" # 特殊情况:本地设备通过 API 调用(如树莓派按钮) else: return "api" def _is_offline_device(self, source: str): """判断该请求是否来自离线设备(需要触发 TTS)""" return source == "local" # 只有明确标记为 local 才播音 def _add_routes(self): @self.app.route('/api/health') def health(): return jsonify({ "status": "ok", "mode": RUN_MODE, "running": True }) @self.app.route('/api/status') def status(): return jsonify(current_status) # === 文本查询接口 === @self.app.route('/api/text/query', methods=['POST']) def text_query(): data = request.get_json() or {} text = data.get("text", "").strip() if not text: return jsonify({"error": "缺少文本"}), 400 source = self._determine_source() logger.info(f"📩 [{source}] 请求: {text}") result = handle_user_input(user_text=text, source=source) # 如果是本地设备调用 API,返回 TTS 音频链接 if self._is_offline_device(source) and result.get("tts_audio_url"): return jsonify({ "success": True, "response_to_user": result["response_to_user"], "tts_audio_url": result["tts_audio_url"] }) # 其他客户端只返回文本 return jsonify({ "success": True, "response_to_user": result["response_to_user"], "details": result.get("details") }) # === 语音上传接口 === @self.app.route('/api/voice/upload', methods=['POST']) def voice_upload(): source = self._determine_source() if not self._is_offline_device(source): return jsonify({ "error": "语音上传仅支持本地设备调用" }), 400 audio_data = None session_id = request.form.get('session_id') # 方式1: 文件上传 if 'file' in request.files: file = request.files['file'] if not file.filename: return jsonify({"error": "空文件"}), 400 ext = os.path.splitext(file.filename)[1] or ".wav" filename = f"{session_id or 'tmp'}_{int(time.time())}{ext}" file_path = os.path.join(TEMP_DIR, secure_filename(filename)) file.save(file_path) audio_data = {"path": file_path} # 方式2: Base64 编码 elif 'audio_base64' in request.form: b64_str = request.form['audio_base64'].split(",")[-1] raw_data = base64.b64decode(b64_str) file_path = os.path.join(TEMP_DIR, f"{session_id or 'tmp'}.wav") with open(file_path, 'wb') as f: f.write(raw_data) audio_data = {"path": file_path} else: return jsonify({"error": "请提供 file 或 audio_base64"}), 400 # 开始识别 self._update_status(is_listening=True) try: recognized_text = recognizer.listen_and_recognize(audio_file=audio_data["path"]) finally: self._update_status(is_listening=False) if not recognized_text: return jsonify({ "success": False, "error": "语音识别失败", "response_to_user": "抱歉,我没听清,请再说一遍。" }), 400 # 复用文本处理流程 response = text_query() return response # === 下载 TTS 音频 === @self.app.route('/api/tts/audio') def tts_audio(): filename = request.args.get('file') if not filename: return jsonify({"error": "缺少文件名"}), 400 path = os.path.join(TEMP_DIR, filename) if not os.path.exists(path): return jsonify({"error": "文件不存在"}), 404 return send_file(path, mimetype="audio/mpeg") def start(self, host=None, port=None): host = host or API_HOST port = port or API_PORT if self.running: logger.warning("API 服务器已在运行") return def run(): self.server = make_server(host, port, self.app) logger.info(f"🌐 API 启动 → http://{host}:{port} (模式: {RUN_MODE})") self.running = True self.server.serve_forever() self.thread = threading.Thread(target=run, daemon=True) self.thread.start() def stop(self): if not self.running: return logger.info("🛑 正在关闭 API 服务...") try: self.server.shutdown() except: pass self.running = False if self.thread: self.thread.join(timeout=3) logger.info("✅ API 服务已关闭") 整理一下
10-29
# core/handler.py import os from typing import Dict, Any from Progress.app import get_ai_assistant, get_task_executor, get_tts_engine from Progress.utils.logger_utils import logger assistant = get_ai_assistant() executor = get_task_executor() tts_engine = get_tts_engine() def handle_user_input(user_text: str, source: str = "unknown") -> Dict[str, Any]: """ 统一处理用户输入 :param user_text: 用户说的话 :param source: 来源 ('local', 'web', 'mobile', 'api') :return: 结果字典 """ if not user_text.strip(): return { "success": False, "response_to_user": "请输入有效内容", "details": {} } try: # AI 决策 decision = assistant.process_voice_command(user_text) result = executor.execute_task_plan(decision) reply = result.get("message", "操作完成。") if not result.get("success") and not reply.startswith("抱歉"): reply = f"抱歉,{reply}" # 判断是否需要 TTS(仅本地设备) should_play_tts = source in ["local", "raspberry", "desktop"] if should_play_tts: try: audio_path = tts_engine.speak(reply) logger.info(f"🔊 已播放语音: {audio_path}") except Exception as e: logger.warning(f"TTS 播放失败: {e}") return { "success": True, "recognized_text": user_text, "response_to_user": reply, "details": result, "tts_audio_url": f"/api/tts/audio?file={os.path.basename(audio_path)}" if should_play_tts else None, "source": source } except Exception as e: logger.exception("处理用户输入时出错") return { "success": False, "response_to_user": "系统内部错误,请稍后再试。", "error": str(e) } # main.py import time import signal import threading from typing import Any from Progress.utils.logger_config import setup_logger from Progress.app import get_tts_engine, get_voice_recognizer from core.handler import handle_user_input from database.config import config logger = setup_logger("ai_assistant") _shutdown_event = threading.Event() def signal_handler(signum, frame): logger.info(f"🛑 收到信号 {signum},准备退出...") _shutdown_event.set() signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) def handle_single_interaction() -> bool: rec = get_voice_recognizer() text = rec.listen_and_recognize(timeout=3) if _shutdown_event.is_set(): return False if not text: logger.info("🔇 未检测到语音") return True logger.info(f"🗣️ 用户说: '{text}'") # 处理并自动播放 TTS(source='local' 触发播报) result = handle_user_input(user_text=text, source="local") expect_follow_up = result.get("details", {}).get("expect_follow_up", False) rec.current_timeout = 8 if expect_follow_up else 3 should_exit = result.get("details", {}).get("should_exit", False) return not should_exit def main(): ENABLE_API_SERVER = config.get("app", "enable_api_server", default=True) API_HOST = config.get("app", "api_host", default="127.0.0.1") API_PORT = config.get("app", "api_port", default=5000) logger.info("🚀 AI 助手启动中...") # 可选:启动 API 服务 if ENABLE_API_SERVER: try: from api_server import APIServer api_server = APIServer() api_server.start() logger.info(f"🌐 API 服务已启动: http://{API_HOST}:{API_PORT}") except Exception as e: logger.warning(f"⚠️ API 服务启动失败: {e}") else: logger.debug("🚫 API 服务已禁用 (ENABLE_API=false)") logger.info("👂 助手已就绪,请开始说话...") while not _shutdown_event.is_set(): try: should_continue = handle_single_interaction() if not should_continue: break except KeyboardInterrupt: break except Exception as e: logger.exception("🔁 主循环异常") time.sleep(1) # 清理资源 try: get_voice_recognizer().close() except: pass try: get_tts_engine().stop() except: pass logger.info("👋 助手已退出") if __name__ == "__main__": main() # api_server.py import threading import os import base64 import time from typing import Dict, Any from flask import Flask, request, jsonify, send_file from werkzeug.utils import secure_filename from werkzeug.serving import make_server from flask_cors import CORS from Progress.utils.logger_utils import logger from Progress.app import get_voice_recognizer from core.handler import handle_user_input from database.config import config # ============================= # 配置加载(来自全局 config) # ============================= ENABLE_API_SERVER = config.get("app", "enable_api_server", default=True) API_HOST = config.get("app", "api_host", default="127.0.0.1") API_PORT = config.get("app", "api_port", default=5000) RUN_MODE = config.get("app", "run_mode", default="auto") VOICE_RECOGNIZER_TIMEOUT = config.get("stt", "timeout", default=3) TEMP_DIR = config.get("app", "temp_dir", default="temp_audio") os.makedirs(TEMP_DIR, exist_ok=True) # 初始化组件 recognizer = get_voice_recognizer() # ============================= # 全局状态管理 # ============================= current_status = { "is_listening": False, "is_tts_playing": False, "last_command_result": None, "timestamp": int(time.time()) } class APIServer: """ RESTful API 服务器,用于支持 Web / Mobile / IoT 设备远程接入 AI 助手。 所有请求最终交由 core.handler 统一处理,根据 source 决定行为(如是否播放 TTS)。 """ def __init__(self): self.app = Flask(__name__) CORS(self.app) # 允许跨域请求 self.server = None self.thread = None self.running = False self._add_routes() logger.debug("🔧 APIServer 初始化完成") def _update_status(self, **kwargs): """更新全局运行状态""" current_status.update(kwargs) current_status["timestamp"] = int(time.time()) def _determine_source(self) -> str: """ 根据请求头判断客户端来源 返回: 'web', 'mobile', 'local', 'api' """ client_type = request.headers.get("X-Client-Type", "").lower().strip() mapping = { "web": ["web", "browser"], "mobile": ["mobile", "android", "ios"], "local": ["raspberry", "local-device", "pi", "desktop"] } for src, keywords in mapping.items(): if any(k in client_type for k in keywords): return src return "api" def _should_play_tts(self, source: str) -> bool: """判断该来源是否需要触发本地 TTS 播放""" return source == "local" # 仅本地物理设备自动播报 def _add_routes(self): """注册所有 API 路由""" self._add_health_route() self._add_status_route() self._add_text_query_route() self._add_voice_upload_route() self._add_tts_audio_route() def _add_health_route(self): @self.app.route('/api/health', methods=['GET']) def health(): return jsonify({ "status": "ok", "mode": RUN_MODE, "running": True, "timestamp": int(time.time()) }) def _add_status_route(self): @self.app.route('/api/status', methods=['GET']) def status(): return jsonify(current_status.copy()) def _add_text_query_route(self): @self.app.route('/api/text/query', methods=['POST']) def text_query(): data: Dict = request.get_json() or {} text = data.get("text", "").strip() if not text: return jsonify({"error": "缺少文本内容"}), 400 source = self._determine_source() logger.info(f"📩 [{source}] 文本请求: '{text}'") try: # 统一处理(会根据 source 决定是否播放 TTS) result = handle_user_input(user_text=text, source=source) response_data = { "success": result.get("success", False), "response_to_user": result.get("response_to_user", ""), } # 若为本地设备且生成了音频,则返回 URL if self._should_play_tts(source) and result.get("tts_audio_url"): response_data["tts_audio_url"] = result["tts_audio_url"] # 可选:附加细节(任务执行结果等) details = result.get("details") if details is not None: response_data["details"] = details return jsonify(response_data) except Exception as e: logger.exception(f"❌ 处理文本请求失败: {text}") return jsonify({ "success": False, "error": "内部服务错误", "message": str(e) }), 500 def _add_voice_upload_route(self): @self.app.route('/api/voice/upload', methods=['POST']) def voice_upload(): source = self._determine_source() if not self._should_play_tts(source): return jsonify({ "error": "语音上传功能仅限本地设备使用", "hint": "请设置 Header: X-Client-Type: local" }), 403 # 获取音频数据 audio_path = None session_id = request.form.get('session_id', f"upload_{int(time.time())}") try: if 'file' in request.files: file = request.files['file'] if not file.filename: return jsonify({"error": "上传的文件名为空"}), 400 ext = os.path.splitext(file.filename)[1] or ".wav" filename = secure_filename(f"{session_id}_{int(time.time())}{ext}") file_path = os.path.join(TEMP_DIR, filename) file.save(file_path) audio_path = file_path elif 'audio_base64' in request.form: b64_str = request.form['audio_base64'].split(",")[-1] raw_data = base64.b64decode(b64_str) file_path = os.path.join(TEMP_DIR, f"{session_id}.wav") with open(file_path, 'wb') as f: f.write(raw_data) audio_path = file_path else: return jsonify({"error": "请提供 'file' 或 'audio_base64' 字段"}), 400 # 开始语音识别 self._update_status(is_listening=True) logger.debug(f"🎤 正在识别语音文件: {audio_path}") try: recognized_text = recognizer.listen_and_recognize( audio_file=audio_path, timeout=VOICE_RECOGNIZER_TIMEOUT ) finally: self._update_status(is_listening=False) if not recognized_text: logger.warning("⚠️ 语音识别未获取到有效文本") return jsonify({ "success": False, "error": "语音识别失败", "response_to_user": "抱歉,我没听清,请再说一遍。" }), 400 logger.info(f"👂 识别结果: '{recognized_text}'") request.json = {"text": recognized_text} # 模拟 JSON 输入 return self.app.view_functions['text_query']() # 复用 text_query 逻辑 except Exception as e: logger.exception("🎙️ 语音上传处理出错") return jsonify({ "success": False, "error": "语音处理异常", "detail": str(e) }), 500 finally: # 可选:清理临时文件(也可后台定时清理) if audio_path and os.path.exists(audio_path): try: os.remove(audio_path) logger.debug(f"🗑️ 已删除临时语音文件: {audio_path}") except: pass def _add_tts_audio_route(self): @self.app.route('/api/tts/audio', methods=['GET']) def tts_audio(): filename = request.args.get('file') if not filename: return jsonify({"error": "缺少参数 'file'"}), 400 file_path = os.path.join(TEMP_DIR, filename) if not os.path.exists(file_path): logger.warning(f"📁 请求的音频文件不存在: {file_path}") return jsonify({"error": "文件不存在"}), 404 logger.debug(f"📥 下载 TTS 音频: {filename}") return send_file(file_path, mimetype="audio/mpeg") def start(self, host=None, port=None): """ 启动 API 服务(非阻塞,运行在独立线程) :param host: 绑定地址 :param port: 端口号 """ host = host or API_HOST port = port or API_PORT if self.running: logger.warning("⚠️ API 服务器已在运行,忽略重复启动") return def run(): try: self.server = make_server(host, port, self.app) logger.info(f"🌐 API 服务已启动 → http://{host}:{port} (模式: {RUN_MODE})") self.running = True self.server.serve_forever() except Exception as e: if self.running: logger.error(f"🚨 API 服务意外终止: {e}") else: logger.debug("🛑 API 服务已正常关闭") self.thread = threading.Thread(target=run, daemon=True) self.thread.start() def stop(self): """安全关闭 API 服务""" if not self.running: return logger.info("🛑 正在关闭 API 服务...") try: self.server.shutdown() except AttributeError: logger.warning("⚠️ server 对象尚未初始化,跳过 shutdown") except Exception as e: logger.error(f"❌ shutdown 出错: {e}") self.running = False if self.thread: self.thread.join(timeout=3) if self.thread.is_alive(): logger.warning("⚠️ API 线程未能及时退出") logger.info("✅ API 服务已关闭") # ================ # 全局实例(单例) # ================ _api_server_instance = None def get_api_server() -> APIServer: """获取 API 服务单例""" global _api_server_instance if _api_server_instance is None: _api_server_instance = APIServer() return _api_server_instance # 方便直接调用 __all__ = ['APIServer', 'get_api_server'] # database/config.py import json import os import sys from typing import Any, Dict, Optional from pathlib import Path from Progress.utils.logger_utils import logger # 确保 Progress 包可导入 if 'Progress' not in sys.modules: project_root = str(Path(__file__).parent.parent) if project_root not in sys.path: sys.path.insert(0, project_root) try: import Progress except ImportError as e: print(f"⚠️ 无法导入 Progress 模块,请检查路径: {project_root}, 错误: {e}") class ConfigManager: def __init__(self): from Progress.utils.resource_helper import get_internal_path, get_app_path self.BASE_CONFIG_FILE = get_internal_path("database", "base_config.json") self.CONFIG_FILE = os.path.join(get_app_path(), "config.json") self.DEFAULT_CONFIG: Optional[Dict] = None self.config = self.load_config() self._watchers = {} # 监听器字典:key_path -> callback(old, new) def watch(self, *keys, callback): """ 注册一个监听器,当指定配置项变化时触发回调 :param keys: 配置路径,如 ("tts", "voice") :param callback: 回调函数,接受两个参数 (old_value, new_value) """ key_path = ".".join(str(k) for k in keys) self._watchers[key_path] = callback logger.debug(f"👀 开始监听配置项: {key_path}") def set(self, value, *keys): """ 设置配置项,并触发变更通知。 示例: config.set("zh-CN-YunxiNeural", "tts", "voice") 注意:仅修改内存中的值,需调用 .save() 持久化。 """ if not keys: raise ValueError("必须指定至少一个键") # 获取旧值 old_value = self.get(*keys) # 安全设置新值(递归创建嵌套结构) data = self.config for k in keys[:-1]: if k not in data or not isinstance(data[k], dict): data[k] = {} data = data[k] current_key = keys[-1] # 如果值未变,跳过以避免误触发回调 if current_key in data and data[current_key] == value: return data[current_key] = value # 构造 key 路径用于查找 watcher key_path = ".".join(str(k) for k in keys) # 触发监听器 if key_path in self._watchers: try: self._watchers[key_path](old_value, value) except Exception as e: logger.error(f"❌ 执行监听回调失败 [{key_path}]: {e}") def _load_default(self) -> Dict: """加载默认配置模板""" if self.DEFAULT_CONFIG is None: if not os.path.exists(self.BASE_CONFIG_FILE): raise FileNotFoundError(f"❌ 默认配置文件不存在: {self.BASE_CONFIG_FILE}") try: with open(self.BASE_CONFIG_FILE, 'r', encoding='utf-8') as f: self.DEFAULT_CONFIG = json.load(f) except Exception as e: raise RuntimeError(f"❌ 无法读取默认配置文件: {e}") return self.DEFAULT_CONFIG.copy() def load_config(self) -> Dict: """加载用户配置,若不存在则生成""" if not os.path.exists(self.CONFIG_FILE): print(f"🔧 配置文件 {self.CONFIG_FILE} 不存在,正在基于默认模板创建...") default = self._load_default() if self.save_config(default): print(f"✅ 默认配置已生成: {self.CONFIG_FILE}") else: print(f"❌ 默认配置生成失败,请检查路径权限: {os.path.dirname(self.CONFIG_FILE)}") return default try: with open(self.CONFIG_FILE, 'r', encoding='utf-8') as f: user_config = json.load(f) default = self._load_default() merged = default.copy() self.deep_update(merged, user_config) return merged except (json.JSONDecodeError, UnicodeDecodeError) as e: print(f"⚠️ 配置文件格式错误或编码异常: {e}") return self._recover_from_corrupted() except PermissionError as e: print(f"⚠️ 无权限读取配置文件: {e}") return self._recover_from_corrupted() except Exception as e: print(f"⚠️ 未知错误导致配置加载失败: {type(e).__name__}: {e}") return self._recover_from_corrupted() def _recover_from_corrupted(self) -> Dict: """配置损坏时尝试备份并重建""" backup_file = self.CONFIG_FILE + ".backup" try: if os.path.exists(self.CONFIG_FILE): os.rename(self.CONFIG_FILE, backup_file) print(f"📁 原始损坏配置已备份为: {backup_file}") default = self._load_default() self.save_config(default) print(f"✅ 已使用默认配置重建 {self.CONFIG_FILE}") return default except Exception as e: print(f"❌ 自动恢复失败: {e},将返回内存中默认配置") return self._load_default() def deep_update(self, default: Dict, override: Dict): """递归更新嵌套字典""" for k, v in override.items(): if k in default and isinstance(default[k], dict) and isinstance(v, dict): self.deep_update(default[k], v) else: default[k] = v def save_config(self, config: Dict) -> bool: """保存当前配置到 config.json""" try: from Progress.utils.resource_helper import ensure_directory config_dir = os.path.dirname(self.CONFIG_FILE) if not ensure_directory(config_dir): print(f"❌ 无法创建配置目录: {config_dir}") return False with open(self.CONFIG_FILE, 'w', encoding='utf-8') as f: json.dump(config, f, indent=4, ensure_ascii=False) return True except PermissionError: print(f"❌ 权限不足,无法写入配置文件: {self.CONFIG_FILE}") return False except Exception as e: print(f"❌ 保存配置失败: {type(e).__name__}: {e}") return False def get(self, *keys, default=None) -> Any: """ 安全获取嵌套配置项 示例: config.get("ai_model", "api_key", default="none") """ data = self.config try: for k in keys: data = data[k] return data except (KeyError, TypeError): return default def save(self) -> bool: """ 将当前内存中的配置保存到文件 返回: 是否成功 """ return self.save_config(self.config) # 全局单例 config = ConfigManager() from functools import wraps import inspect import logging # 全局注册表 REGISTERED_FUNCTIONS = {} FUNCTION_SCHEMA = [] FUNCTION_MAP = {} # (intent, action) -> method_name logger = logging.getLogger("ai_assistant") def ai_callable( *, description: str, params: dict, intent: str = None, action: str = None, concurrent: bool = False # 新增:是否允许并发执行 ): def decorator(func): func_name = func.__name__ metadata = { "func": func, "description": description, "params": params, "intent": intent, "action": action, "signature": str(inspect.signature(func)), "concurrent": concurrent # 记录是否可并发 } REGISTERED_FUNCTIONS[func_name] = metadata FUNCTION_SCHEMA.append({ "name": func_name, "description": description, "parameters": params }) if intent and action: key = (intent, action) if key in FUNCTION_MAP: raise ValueError(f"冲突:语义 ({intent}, {action}) 已被函数 {FUNCTION_MAP[key]} 占用") FUNCTION_MAP[key] = func_name @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) wrapper._ai_metadata = metadata return wrapper return decorator # Progress/utils/resource_helper.py import os import sys from typing import Optional def get_internal_path(*relative_path_parts) -> str: """ 获取内部资源路径(如 base_config.json),适用于开发打包环境。 示例: get_internal_path("database", "base_config.json") """ if getattr(sys, 'frozen', False): base_path = sys._MEIPASS else: # __file__ → Progress/utils/resource_helper.py current_dir = os.path.dirname(os.path.abspath(__file__)) progress_root = os.path.dirname(current_dir) # Progress/ project_root = os.path.dirname(progress_root) # AI_Manager/ base_path = project_root return os.path.join(base_path, *relative_path_parts) def get_app_path() -> str: """ 获取应用运行数据保存路径(config.json、日志等) 打包后:exe 所在目录 开发时:AI_Manager/ 根目录 """ if getattr(sys, 'frozen', False): return os.path.dirname(sys.executable) else: current_dir = os.path.dirname(os.path.abspath(__file__)) progress_root = os.path.dirname(current_dir) project_root = os.path.dirname(progress_root) return project_root def resource_path(*sub_paths: str, base_key: str = "resource_path") -> str: """ 通用用户资源路径解析(基于 config 的 resource_path) 示例: resource_path("Music", "bgm.mp3") → <resource_path>/Music/bgm.mp3 :param sub_paths: 子路径组件 :param base_key: 在 config.paths 中的键名,默认 "resource_path" """ # 延迟导入,避免循环依赖 from database.config import config raw_base = config.get("paths", base_key) if not raw_base: raise ValueError(f"配置项 paths.{base_key} 未设置") if os.path.isabs(raw_base): base_path = raw_base else: base_path = os.path.join(get_app_path(), raw_base) full_path = os.path.normpath(base_path) for part in sub_paths: clean_part = str(part).strip().lstrip(r'./\ ') for p in clean_part.replace('\\', '/').split('/'): if p: full_path = os.path.join(full_path, p) return os.path.normpath(full_path) def ensure_directory(path: str) -> bool: """ 确保目录存在。若 path 是文件路径,则创建其父目录。 """ dir_path = path basename = os.path.basename(dir_path) if '.' in basename and len(basename) > 1 and not basename.startswith('.'): dir_path = os.path.dirname(path) if not dir_path or dir_path in ('.', './', '..'): return True try: os.makedirs(dir_path, exist_ok=True) return True except PermissionError: print(f"❌ 权限不足,无法创建目录: {dir_path}") return False except Exception as e: print(f"❌ 创建目录失败: {dir_path}, 错误: {type(e).__name__}: {e}") return False 检查一下,最后做个整理
10-29
我现在在使用 Electron框架构建UI实现对python主程序的api接口调用完成AI语音助手 # api_server.py import threading import os import base64 import time from typing import Dict, Any from flask import Flask, request, jsonify, send_file from werkzeug.utils import secure_filename from werkzeug.serving import make_server from flask_cors import CORS from Progress.utils.logger_utils import logger from Progress.app import get_voice_recognizer from core.handler import handle_user_input from database.config import config # ============================= # 配置加载(来自全局 config) # ============================= ENABLE_API_SERVER = config.get("app", "enable_api_server", default=True) API_HOST = config.get("app", "api_host", default="127.0.0.1") API_PORT = config.get("app", "api_port", default=5000) RUN_MODE = config.get("app", "run_mode", default="auto") VOICE_RECOGNIZER_TIMEOUT = config.get("stt", "timeout", default=3) TEMP_DIR = config.get("app", "temp_dir", default="temp_audio") os.makedirs(TEMP_DIR, exist_ok=True) # 初始化组件 recognizer = get_voice_recognizer() # ============================= # 全局状态管理 # ============================= current_status = { "is_listening": False, "is_tts_playing": False, "last_command_result": None, "timestamp": int(time.time()) } class APIServer: """ RESTful API 服务器,用于支持 Web / Mobile / IoT 设备远程接入 AI 助手。 所有请求最终交由 core.handler 统一处理,根据 source 决定行为(如是否播放 TTS)。 """ def __init__(self): self.app = Flask(__name__) CORS(self.app) # 允许跨域请求 self.server = None self.thread = None self.running = False self._add_routes() logger.debug("🔧 APIServer 初始化完成") def _update_status(self, **kwargs): """更新全局运行状态""" current_status.update(kwargs) current_status["timestamp"] = int(time.time()) def _determine_source(self) -> str: """ 根据请求头判断客户端来源 返回: 'web', 'mobile', 'local', 'api' """ client_type = request.headers.get("X-Client-Type", "").lower().strip() mapping = { "web": ["web", "browser"], "mobile": ["mobile", "android", "ios"], "local": ["raspberry", "local-device", "pi", "desktop"] } for src, keywords in mapping.items(): if any(k in client_type for k in keywords): return src return "api" def _should_play_tts(self, source: str) -> bool: """判断该来源是否需要触发本地 TTS 播放""" return source == "local" # 仅本地物理设备自动播报 def _add_routes(self): """注册所有 API 路由""" self._add_health_route() self._add_status_route() self._add_text_query_route() self._add_voice_upload_route() self._add_tts_audio_route() def _add_health_route(self): @self.app.route('/api/health', methods=['GET']) def health(): return jsonify({ "status": "ok", "mode": RUN_MODE, "running": True, "timestamp": int(time.time()) }) def _add_status_route(self): @self.app.route('/api/status', methods=['GET']) def status(): return jsonify(current_status.copy()) def _add_text_query_route(self): @self.app.route('/api/text/query', methods=['POST']) def text_query(): data: Dict = request.get_json() or {} text = data.get("text", "").strip() if not text: return jsonify({"error": "缺少文本内容"}), 400 source = self._determine_source() logger.info(f"📩 [{source}] 文本请求: '{text}'") try: # 统一处理(会根据 source 决定是否播放 TTS) result = handle_user_input(user_text=text, source=source) response_data = { "success": result.get("success", False), "response_to_user": result.get("response_to_user", ""), } # 若为本地设备且生成了音频,则返回 URL if self._should_play_tts(source) and result.get("tts_audio_url"): response_data["tts_audio_url"] = result["tts_audio_url"] # 可选:附加细节(任务执行结果等) details = result.get("details") if details is not None: response_data["details"] = details return jsonify(response_data) except Exception as e: logger.exception(f"❌ 处理文本请求失败: {text}") return jsonify({ "success": False, "error": "内部服务错误", "message": str(e) }), 500 def _add_voice_upload_route(self): @self.app.route('/api/voice/upload', methods=['POST']) def voice_upload(): source = self._determine_source() if not self._should_play_tts(source): return jsonify({ "error": "语音上传功能仅限本地设备使用", "hint": "请设置 Header: X-Client-Type: local" }), 403 # 获取音频数据 audio_path = None session_id = request.form.get('session_id', f"upload_{int(time.time())}") try: if 'file' in request.files: file = request.files['file'] if not file.filename: return jsonify({"error": "上传的文件名为空"}), 400 ext = os.path.splitext(file.filename)[1] or ".wav" filename = secure_filename(f"{session_id}_{int(time.time())}{ext}") file_path = os.path.join(TEMP_DIR, filename) file.save(file_path) audio_path = file_path elif 'audio_base64' in request.form: b64_str = request.form['audio_base64'].split(",")[-1] raw_data = base64.b64decode(b64_str) file_path = os.path.join(TEMP_DIR, f"{session_id}.wav") with open(file_path, 'wb') as f: f.write(raw_data) audio_path = file_path else: return jsonify({"error": "请提供 'file' 或 'audio_base64' 字段"}), 400 # 开始语音识别 self._update_status(is_listening=True) logger.debug(f"🎤 正在识别语音文件: {audio_path}") try: recognized_text = recognizer.listen_and_recognize( audio_file=audio_path, timeout=VOICE_RECOGNIZER_TIMEOUT ) finally: self._update_status(is_listening=False) if not recognized_text: logger.warning("⚠️ 语音识别未获取到有效文本") return jsonify({ "success": False, "error": "语音识别失败", "response_to_user": "抱歉,我没听清,请再说一遍。" }), 400 logger.info(f"👂 识别结果: '{recognized_text}'") request.json = {"text": recognized_text} # 模拟 JSON 输入 return self.app.view_functions['text_query']() # 复用 text_query 逻辑 except Exception as e: logger.exception("🎙️ 语音上传处理出错") return jsonify({ "success": False, "error": "语音处理异常", "detail": str(e) }), 500 finally: # 可选:清理临时文件(也可后台定时清理) if audio_path and os.path.exists(audio_path): try: os.remove(audio_path) logger.debug(f"🗑️ 已删除临时语音文件: {audio_path}") except: pass def _add_tts_audio_route(self): @self.app.route('/api/tts/audio', methods=['GET']) def tts_audio(): filename = request.args.get('file') if not filename: return jsonify({"error": "缺少参数 'file'"}), 400 file_path = os.path.join(TEMP_DIR, filename) if not os.path.exists(file_path): logger.warning(f"📁 请求的音频文件不存在: {file_path}") return jsonify({"error": "文件不存在"}), 404 logger.debug(f"📥 下载 TTS 音频: {filename}") return send_file(file_path, mimetype="audio/mpeg") def start(self, host=None, port=None): """ 启动 API 服务(非阻塞,运行在独立线程) :param host: 绑定地址 :param port: 端口号 """ host = host or API_HOST port = port or API_PORT if self.running: logger.warning("⚠️ API 服务器已在运行,忽略重复启动") return def run(): try: self.server = make_server(host, port, self.app) logger.info(f"🌐 API 服务已启动 → http://{host}:{port} (模式: {RUN_MODE})") self.running = True self.server.serve_forever() except Exception as e: if self.running: logger.error(f"🚨 API 服务意外终止: {e}") else: logger.debug("🛑 API 服务已正常关闭") self.thread = threading.Thread(target=run, daemon=True) self.thread.start() def stop(self): """安全关闭 API 服务""" if not self.running: return logger.info("🛑 正在关闭 API 服务...") try: self.server.shutdown() except AttributeError: logger.warning("⚠️ server 对象尚未初始化,跳过 shutdown") except Exception as e: logger.error(f"❌ shutdown 出错: {e}") self.running = False if self.thread: self.thread.join(timeout=3) if self.thread.is_alive(): logger.warning("⚠️ API 线程未能及时退出") logger.info("✅ API 服务已关闭") # ================ # 全局实例(单例) # ================ _api_server_instance = None def get_api_server() -> APIServer: """获取 API 服务单例""" global _api_server_instance if _api_server_instance is None: _api_server_instance = APIServer() return _api_server_instance # 方便直接调用 __all__ = ['get_api_server'] 这是api工具
最新发布
10-29
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值