• [案例共创] 【案例共创】基于仓颉、DeepSeek与RGF的AI桌宠开发实践
    前言不久前在仓颉的交流群中看到了关于仓颉案例共创的活动消息,于是想到可以用仓颉结合自身所长开发一个AI桌面宠物供大家娱乐学习。正好笔者封装的仓颉桌面渲染库 RGF_CJ 和控件库 RGF_UI 能满足这方面的需求,那么说干就干!成品效果开发前的准备在正式进行应用的开发之前,需要先准备好相关的依赖。领取华为云DeepSeek免费资源华为云为开发者提供了单模型200万Tokens的免费额度,包含DeepSeek-R1&V3满血版,登录华为云ModelArts Studio(MaaS)控制台即可领取,此处可以选择DeepSeek-R1满血版来搭建AI聊天桌宠。提示:点击此链接领取:https://www.huaweicloud.com/product/modelarts/studio.html进入网站签署服务声明领取免费额度并查看调用说明通过说明中的链接去创建API点击按钮创建API填写API相关信息获取API并保存提示:请保存好此处的API链接,后续代码开发中会用到。获取RGF依赖文件在后续应用的开发中会依赖到RGF_UI库,此库依赖到一些文件,可以从RGF_UI的库中获取。这些依赖文件后续会链接入应用中去。提示:点击此链接下载:https://gitcode.com/raozj/RGF_UI/tree/master/libs进入RGF_UI库的Libs目录,下载依赖文件保存依赖文件,留待后续使用安装仓颉STDX及相关依赖因为是基于仓颉开发通信相关的应用,因此需要依赖到STDX中的stdx.net.http、stdx.encoding.json、stdx.net.tls等包,而这些包自Cangjie v0.60.5版本开始从std标准库中移出,因此需要自行安装这些包以及这些包所依赖的OpenSSL。提示:点击此链接下载:https://gitcode.com/Cangjie/Cangjie-STDX注’ 由于笔者发文时Cangjie v0.60.5版本尚且处于内测阶段,因此若开发者无内测资格则无法下载到此版本的相关文件。关于 STDX 和 OpenSSL 的安装请参考上述链接中官方的文档,此处不做赘述。序列帧合图资源制作作为一个桌面宠物,自然需要拥有对应的动画,对此可以使用DragonBones、Spine或者Adobe Effect等软件制作。制作动画笔者在此使用的是DragonBones,关于DragonBones的安装和使用不在本文的探讨范畴之内,读者可自行选择喜欢的动画制作软件,设计并制作完成动画后输出序列帧以用于应用的开发。设计动画制作出序列帧制作合图资源如果每帧的图片都单独存储,对于应用的初始化加载和渲染都是一个较大的负担,因此建议将一类的序列帧动画按照相等宽高、间距合并为一张图片。由于手动合图比较麻烦,因此笔者写了一个Python脚本,快速的将目录中的序列帧图片按照指定的宽度分布数量合并为一张图片。注’ 由于此部分不属于文章核心部分,因此直接贴出代码以供参考,读者可通过其它工具或代码实现自己个性化的合图方案。import os import cv2 import numpy as np def load_images_from_folder(folder): """ 加载指定文件夹下的所有 .png 图像 """ image_files = [f for f in os.listdir(folder) if f.lower().endswith('.png')] image_files.sort() # 按文件名排序 images = [] for filename in image_files: file_path = os.path.join(folder, filename) # 读取图像(支持中文路径) with open(file_path, 'rb') as f: img_data = np.frombuffer(f.read(), dtype=np.uint8) img = cv2.imdecode(img_data, cv2.IMREAD_UNCHANGED) images.append(img) print(f"已加载: {filename}") return images def stitch_images(images, cols, bg_color=(0, 0, 0, 0)): """ 将图像按指定列数拼接成一张大图 :param images: 图像列表 :param cols: 每行显示的图像数量 :param bg_color: 背景颜色(支持 RGBA) :return: 拼接后的图像 """ if not images: raise ValueError("没有图像可供拼接") h, w = images[0].shape[:2] rows = (len(images) + cols - 1) // cols # 创建空白背景图 channels = images[0].shape[2] if len(images[0].shape) == 3 else 1 if channels == 1: bg_color = (bg_color[0], ) # 灰度图 elif channels == 3: bg_color = bg_color[:3] # RGB elif channels == 4: bg_color = bg_color[:4] # RGBA canvas = np.full((h * rows, w * cols, channels), bg_color, dtype=np.uint8) for idx, img in enumerate(images): row = idx // cols col = idx % cols x = col * w y = row * h canvas[y:y+h, x:x+w] = img return canvas def main(): folder = input("请输入包含PNG图像的文件夹路径:").strip() if not os.path.isdir(folder): print("输入的不是有效文件夹路径!") return try: cols = int(input("请输入每行显示的图像数量:")) except ValueError: print("请输入有效的数字!") return output_path = input("请输入输出图像的完整路径(如 output.png):").strip() images = load_images_from_folder(folder) if not images: print("未找到任何PNG图像!") return # 检查所有图像是否尺寸一致 first_h, first_w = images[0].shape[:2] for img in images[1:]: h, w = img.shape[:2] if h != first_h or w != first_w: print("警告:图像尺寸不一致,可能影响拼接效果。") # 拼接图像 print("开始拼接图像...") stitched_image = stitch_images(images, cols) # 保存结果 cv2.imwrite(output_path, stitched_image) print(f"拼接完成,已保存为: {output_path}") if __name__ == '__main__': main() 开发AI桌宠基本规划首先对项目目录和文件进行基本的规划,后续的开发就基于规划进行。目录规划Yez/ -------------------------------------------# 项目主目录 ├─libs/ ---------------------------------------# 依赖库 ├─res/ ----------------------------------------# 资源 │ └─SequenceFrame/ ---------------------------# 序列帧资源 │ └─skip_white/ ---------------------------# 椰椰的白色皮肤资源 ├─src/ ----------------------------------------# 源代码目录 └─tool/ ---------------------------------------# 工具目录文件规划Yez/ ├┈cjpm.lock ├┈cjpm.toml ├─libs/ │ ├┈libimm32.a -------------------------------# Windows 输入法库 │ ├┈resources.rc.o ---------------------------# 资源配置清单 内部声明应用依赖的最低系统版本 │ └┈libRgf.dll -------------------------------# RGF渲染库依赖的动态库 ├─res/ │ ├┈config.json ------------------------------# 椰椰的配置文件 │ └─SequenceFrame/ │ └─skip_white/ │ ├┈blink.json -------------------------# 眨眼合图描述文件 │ ├┈blink.png --------------------------# 眨眼动画合图 │ ├┈jump.json --------------------------# 跳跃合图描述文件 │ ├┈jump.png ---------------------------# 跳跃动画合图 │ ├┈speak.json -------------------------# 说话合图描述文件 │ ├┈speak.png --------------------------# 说话动画合图 │ ├┈wait.json --------------------------# 待机合图描述文件 │ └┈wait.png ---------------------------# 待机动画合图 ├─src/ │ ├┈chat.cj ----------------------------------# 大模型网络通信代码 │ ├┈config.cj --------------------------------# 配置管理代码 │ ├┈main.cj ----------------------------------# 程序主入口代码 │ ├┈res.cj -----------------------------------# 资源管理代码 │ ├┈win_anm.cj -------------------------------# 动画绘制窗口代码 │ ├┈win_input.cj -----------------------------# 输入框窗口代码 │ └┈win_text.cj ------------------------------# 鼠标文本渲染代码 └─tool/ └┈picture_puzzle.py ------------------------# python 合图脚本代码链接文件与库配置在开发之前,需要现为当前项目链接依赖文件和添加依赖库。链接文件在之前的小节中,下载了RGF_UI的依赖文件压缩包,在下载的压缩包内应该包含三个文件,即libRgf.dll、resources.rc.o和libimm32.a。这三个文件中,libRgf.dll是RGF_CJ库的依赖文件,必须随应用打包发布;resources.rc.o文件是Windows资源描述文件,读者也可以自行编写resources.rc文件并生成此文件(需设置最低支持Windows 8,即内核NT 6.2);libimm32.a文件是自绘编辑框控件依赖的文件,主要提供输入法相关的操作能力。在仓颉包管理配置toml文件中,需要加入链接上述文件的代码:[target] [target.x86_64-w64-mingw32] link-option = "-L ./libs -l:libRgf.dll -l user32 -l imm32" 库配置完成链接后,还需要为应用声明依赖RGF_CJ和RGF_UI包,并且为RGF_CJ包配置宏。提示:点击此链接查看依赖库RGF_CJ:https://gitcode.com/Cangjie-SIG/RGF_CJ提示:点击此链接查看依赖库RGF_UI:https://gitcode.com/raozj/RGF_UI[dependencies] [dependencies.rgf] git = "https://gitcode.com/Cangjie-SIG/RGF_CJ.git" [dependencies.rgfui] git = "https://gitcode.com/raozj/RGF_UI.git" [profile] [profile.customized-option] rgfCfg = "--cfg \"RGF_LANG=zh-cn\"" 上述代码中配置的“RGF_LANG=zh-cn”表示:当RGF_CJ出现异常时,捕获输出的异常提示内容为中文。配置加载模块笔者希望桌宠能够具备记忆配置的能力,因此需要为桌宠写一个配置的加载、保存模块,这个模块会记住桌宠所使用的皮肤、最后所在的桌面位置等信息,以确保桌宠重启后依然符合之前的情况。编写一个配置管理相关的恶汉单例类ConfigMgr,单例具备两个方法,一个用于从文件中加载配置,一个用于写出配置到文件中去。(其实也可以直接编写静态方法或直接编写函数而不使用类,看大家自己的选择吧。)代码如下:// Yez/src/config.cj // 配置管理类 class ConfigMgr { private static let _instance:ConfigMgr = ConfigMgr() public static prop i:ConfigMgr{ get(){ _instance } } private init() {} ... /** * 加载配置文件 * @return 是否成功 */ public func readConfig():Bool{ ... } /** 写出配置文件 */ public func writeConfig():Unit{ ... } ... } 编写一个ConfigInfo类,这个类用于存放实际的配置项,并且提供转换为Json的能力,代码如下:// Yez/src/config.cj class ConfigInfo <: Serializable<ConfigInfo> { // 椰椰右下角窗口坐标系的X坐标 var _x:Int32 = Int32.Min // 椰椰右下角窗口坐标系的Y坐标 var _y:Int32 = Int32.Min // 椰椰使用的皮肤 var _skip = "skip_white" // 椰椰画面缩放比例 var _zoom:Float32 = 0.125 // 椰椰的字体大小 var _fontSize:Float32 = 16.1 public func serialize(): DataModel { return DataModelStruct().add(field<Int32>("x", _x)) .add(field<Int32>("y", _y)) .add(field<String>("skip", _skip)) .add(field<Float32>("zoom", _zoom)) .add(field<Float32>("fontSize", _fontSize)) } public static func deserialize(dm: DataModel): ConfigInfo { var dms = match (dm) { case data: DataModelStruct => data case _ => throw Exception("this data is not DataModelStruct") } var result = ConfigInfo() result._x = Int32.deserialize(dms.get("x")) result._y = Int32.deserialize(dms.get("y")) result._skip = String.deserialize(dms.get("skip")) result._zoom = Float32.deserialize(dms.get("zoom")) result._fontSize = Float32.deserialize(dms.get("fontSize")) return result } } 将ConfigInfo以成员变量的形式加入到ConfigMgr中,并且由ConfigMgr对外提供访问属性,代码如下:// Yez/src/config.cj // 配置管理类 class ConfigMgr { // 配置信息 private var configImpl:ConfigInfo = ConfigInfo() ... public mut prop x:Int32{ get(){ return configImpl._x } set(val){ configImpl._x = val } } public mut prop y:Int32{ get(){ return configImpl._y } set(val){ configImpl._y = val } } public mut prop skip:String{ get(){ return configImpl._skip } set(val){ configImpl._skip = val } } public prop zoom:Float32{ get(){ return configImpl._zoom } } public prop fontSize:Float32{ get(){ return configImpl._fontSize } } } 接下来只要完善readConfig和writeConfig的读取和写出逻辑即可,代码如下:// Yez/src/config.cj // 配置文件路径 const CONFIG_FILE_PATH = "./res/config.json" ... public func readConfig():Bool{ // 判断目录是否存在 if(exists(CONFIG_FILE_PATH)){ let seqFramePath = canonicalize(CONFIG_FILE_PATH) let bytes = File.readFrom(seqFramePath) var jv = JsonValue.fromStr(StringReader(ByteBuffer(bytes)).readToEnd()) var dm = DataModel.fromJson(jv) configImpl = ConfigInfo.deserialize(dm) } // 目前始终返回 true // 当配置不存在时,则使用默认配置 return true } public func writeConfig():Unit{ if(!exists(CONFIG_FILE_PATH)){ File.create(CONFIG_FILE_PATH) } let seqFramePath = canonicalize(CONFIG_FILE_PATH) var Anm = configImpl.serialize() var dm = Anm.toJson() File.writeTo(seqFramePath,dm.toString().toArray()) } 最后,在入口函数中启用配置加载与写出即可。// Yez/src/main.cj main(): Int64 { // 加载配置文件数据 if(!ConfigMgr.i.readConfig()){ return 0 } ... // 保存配置到文件中去 ConfigMgr.i.writeConfig() return 0 } 资源加载模块除了配置之外,应用资源也是一个重要的部分,这部分的完整性会影响到应用是否正常渲染。首先采用和Config部分类似的结构准备恶汉单例类ResMgr和动画信息类AnmInfo,代码如下:// Yez/src/res.cj // 资源管理类 class ResMgr { private static let _instance:ResMgr = ResMgr() public static prop i:ResMgr{ get(){ _instance } } private init() {} // 动画 private static const ANM_BLINK ="blink" private static const ANM_JUMP ="jump" private static const ANM_SPEAK ="speak" private static const ANM_WAIT ="wait" private let _skipImg:HashMap<String, Array<(AnmInfo,MemBitmap)>> = HashMap<String, Array<(AnmInfo,MemBitmap)>>() public func loadRes():Bool{ ... } public func unloadRes(){ ... } public prop skip:HashMap<String, Array<(AnmInfo,MemBitmap)>>{ get(){ return _skipImg } } } class AnmInfo <: Serializable<AnmInfo> { // 单帧图片宽度 var width:Int32 = 0 // 单帧图片高度 var height:Int32 = 0 // 合图图片每行图片数量 var hCount:Int32 = 0 // 总帧数 var frames:Int32 = 0 ... } 然后就是实现在loadRes方法中加载合图及合图相关配置文件,代码逻辑还就是:从"./res/SequenceFrame"目录开始确保目录存在,然后遍历SequenceFrame目录下的所有皮肤目录,并且将皮肤目录内的合图资源及对应的配置文件载入到成员中去。如果目录下的资源文件不完整,则不会加载对应的内容到成员中。// Yez/src/res.cj /** * 代码较为复杂且不是核心代码,读者了解功能即可。 * 若需要查看相关代码,可下载项目查看。 */ public func loadRes():Bool{ ... } public func unloadRes(){ //释放所有图像资源(设备无关资源) for(skipPath in _skipImg){ skipPath[1][0][1].release() skipPath[1][1][1].release() skipPath[1][2][1].release() skipPath[1][3][1].release() } } 最后,在入口函数中启用资源加载与释放即可。// Yez/src/main.cj main(): Int64 { ... // 加载依赖资源 if(ResMgr.i.loadRes()){ ... } // 释放一切可能被加载的资源 ResMgr.i.unloadRes() ... return 0 } 大模型模块接下来便是与AI通信的核心部分了,此部分需要完成与大模型的互通。我们需要准备一个LLM类,这个类负责基于API、URL等信息与大模型建立链接,并且解析信息。// Yez/src/chat.cj class LLM { let client: Client let history = StringBuilder() public LLM( // 大模型URL链接 let url!: String, // 大模型 API Key let key!: String, // 大模型类型标识 let model!: String, // 上下文 let context!: Bool = false ) { var config = TlsClientConfig() config.verifyMode = TrustAll client = ClientBuilder() .tlsConfig(config) // AI 服务响应有时候比较慢,这里设置为无限等待 .readTimeout(Duration.Max) .build() } /** * 编码函数,将角色与内容编码为字符串 * return 字符串 */ func encode(role: Role, content: String) { ... } /** * 发送函数,将对话数据发送到服务器 * return HttpResponse */ func send(input: String, stream!: Bool = false) { ... } /** * 分析函数,解析服务器返回的数据并且提取其中有效的文本内容 * return 有效内容 */ func parse(text: String, stream!: Bool = false) { ... } /** * 流式对话 * @param input 输入的文本 * @param task 流式返回内容的回调函数 * @param taskEnd 返回内容结束的回调函数 */ public func chat(input: String, task!: (String) -> Unit = {o => print(o)},taskEnd!:()->Unit = {=>}) { ... } /** * 非流式对话 */ public func chat(input!: String) { ... } /** * 历史记忆预置 */ public func preset(context: Array<(Role, String)>) { ... } /** * 历史记忆清除 */ public func reset() { ... } } 根据华为云提供的调用参考,完善 send 方法代码如下:// Yez/src/chat.cj func send(input: String, stream!: Bool = false) { let message = encode(I, input) let content = '{"model":"${model}","messages":[${history}${message}],"stream":${stream}}' if (context) { history.append(message) } let request = HttpRequestBuilder() .url(url) .header('Authorization', 'Bearer ${key}') .header('Content-Type', 'application/json') .header('Accept', if (stream) { 'text/event-stream' } else { 'application/json' }) .body(content) .post() .build() client.send(request) } 根据华为云返回的数据格式,完善 parse 方法代码如下:// Yez/src/chat.cj func parse(text: String, stream!: Bool = false) { let json = JsonValue.fromStr(text).asObject() let choices = json.getFields()['choices'].asArray() if(choices.size() == 0){ return "" } // 流式和非流式情况下,这个字段名称不同 let key = if (stream) { 'delta' } else { 'message' } let message = choices[0].asObject().getFields()[key].asObject() if(message.containsKey("content")){ let content = message.getFields()['content'].asString().getValue() // 移除开头的两个 \n if(content == "\n\n"){ return "" } return content }else{ return "" } } 最后实现核心的流式对话部分,代码如下:// Yez/src/chat.cj public func chat(input: String, task!: (String) -> Unit = {o => print(o)},taskEnd!:()->Unit = {=>}) { // 根据传入的字符串构建上发服务器的 HttpResponse let response = send(input, stream: true) // 准备一个字符串缓冲区 let output = StringBuilder() // 准备一个字节缓冲区 let buffer = Array<Byte>(1024 * 8, {i=>0}) // 获取返回的内容 var length = response.body.read(buffer) while (length != 0) { // 将字节缓冲区内的内容按照UTF-8编码解析为字符串 let text = String.fromUtf8(buffer[..length]) const INDEX = 6 // 按行分割 for (line in text.split('\n', removeEmpty: true)) { // 确保获取的是完整的:data: {...} if (line.size > INDEX && line[INDEX] == b'{' && line[line.size - 1] == b'}') { let json = line[INDEX..line.size] // 分析出有效信息 let slice = parse(json, stream: true) if (context) { output.append(slice) } // 若有效信息不为空,则通过回调函数返回 if(slice != ""){ task(slice) } } } length = response.body.read(buffer) } if (context) { history.append(',${encode(AI, output.toString())},') } // 所有处理结束后,执行结束回调 taskEnd() } LLM类是一个较为通用的类,但是使用较为繁琐,因此对于外部调用而言,只需要一个简化的实例即可。因此,可以再编写一个独立的懒汉单例LLMImpl类,并且仅提供一个对外实现流式对话的chat方法,代码如下:// Yez/src/chat.cj class LLMImpl { private static var _i: Option<LLMImpl> = None public static prop i:LLMImpl { get(){ if(_i.isNone()){ _i = LLMImpl() } return _i.getOrThrow() } } private let robot:LLM // 初始化一个对话模型 private init(){ robot = LLM( // 华为云大模型调用URL url: 'https://maas-cn-southwest-2.modelarts-maas.com/v1/infers/8a062fd4-7367-4ab4-a936-5eeb8fb821c4/v1/chat/completions', // 如果示例自带的密钥失效,请自行注册,https://www.huaweicloud.com/product/modelarts/studio.html key: '/* !!! 这里填写读者自己的 API Key !!! */', // 要使用的 大模型 model: 'DeepSeek-R1', context: true ); // 设定基本信息 robot.preset([(System, #"我会自称“椰椰”并亲昵的在三句话之内回复所有问题。对于限定长度内无法阐述清晰的问题,我会表现出呆傻可爱的效果并说出“宕机”。如果对我说“拍照”或“截图”,我就会说“咔嚓~”。如果问我是谁开发的,我会说出“尧佥&椰子”。"#)]) } /** * 流式对话 * @param content 输入的文本 * @param task 流式返回内容的回调函数 * @param end 返回内容结束的回调函数 */ public func chat(content:String, task!: (String) -> Unit = {o => print(o)},end!:()->Unit = {=>}){ robot.chat(content,task:task,taskEnd:end) } } 注’ 完整的代码请参考提供的代码文件桌宠渲染接下来,需要实现可视化的窗口渲染,赋予“椰椰”一副“身体”。动画播放窗口为了实现AI桌宠的渲染,首先需要有一个窗口。对此,笔者选择基于RGF_CJ中的WinBase类,实现一个自己的窗口类WinMain。// Yez/src/win_anm.cj // 使用 RGF_CJ 的宏实现代码简化 @LifeCycle // 自动管理 设备相关资源 的释放 @HookProc // 自动挂载窗口类的消息事件方法 class WinMain <: WinBase{ ... // 窗口类初始化时,传入 ResMgr 载入的资源(设备无关资源) init(anm:Array<(AnmInfo, MemBitmap)>){ _anmRes = anm } /** * 加载设备无关资源 -> 设备相关资源 * @note 本部分将资源ResMgr类中的图片资源,载入到窗口类中 */ private func loadAnm(anm:Array<(AnmInfo, MemBitmap)>){ surface .createBitmapFromMemory(_bmps[0],anm[0][1]) .createBitmapFromMemory(_bmps[1],anm[1][1]) .createBitmapFromMemory(_bmps[2],anm[2][1]) .createBitmapFromMemory(_bmps[3],anm[3][1]) } /** * 创建设备资源事件,窗口渲染表面创建完成后,开始载入资源 */ public func createDeviceResources():Bool{ // 这里调用 loadAnm 载入图片资源 loadAnm(_anmRes) return true; } /** * 窗口创建完成事件,当窗口创建完成时,开始渲染动画 */ public func created(hwnd:RgfHwnd,x:Int32, y:Int32, nWidth:Int32, nHeight:Int32):Unit{ ... } /** * 绘制窗口事件,此处根据当前的动画状态渲染窗口 */ public override func onPaint(wRect:Rect):Unit{ surface.clear(0.0,0.0,0.0,0.0) match(_nowState){ case Wait => {=> // 等待状态的动画渲染比较特殊,因为需要实现眼睛跟随鼠标移动 if(_bmps[3].isValid() && _bmps[0].isValid()){ // 这一部分计算当前渲染帧的静态部分,即:身体, // 并且将这部分内容渲染到窗口 .... // 然后下面计算出鼠标对眼睛造成的偏移量 // 并且将眼睛渲染到身体上 if(_anmStep < _anmRes[0][0].frames){ .... }else{ .... } } }() case Jump => {=> // 这里渲染椰椰的跳跃动画 if(_bmps[1].isValid()){ .... } }() case Speak => {=> // 这里渲染椰椰正在说话的动画 if(_bmps[2].isValid()){ .... } }() } } } 在入口函数中,启用UIMain并且创建窗口,代码如下:// Yez/src/win_anm.cj main(): Int64 { ... if(ResMgr.i.loadRes()){ // 开始创建UI UIMain{=> // 确保皮肤配置有效 if(!ResMgr.i.skip.contains(ConfigMgr.i.skip)){ ConfigMgr.i.skip = ResMgr.i.skip.keys().toArray()[0] } // 创建窗口类 let anmWin:WinMain = WinMain(ResMgr.i.skip.get(ConfigMgr.i.skip).getOrThrow()) // 先随便在桌面 (0,0) 的位置创建一个 10像素×10像素 的窗口 // 窗口类内部会在显示前根据配置中的信息自动布局到正确位置和尺寸 anmWin.createWin(uiGetContext(), "Yez", 0, 0, 10, 10, 0, WinStyle.e2n(WinStyle.Popup), // 使用的是 TopMost 分层窗口(支持透明背景) dwExStyle:WinStyleEx.Default | WinStyleEx.TopMost, // 渲染使用的是 Direct2D 计数 layered:true,substrate:Substrate.Direct2D ) } } ... } 完成上述代码后,窗口已经可以正确的渲染动画了注’ 完整的代码请参考提供的代码文件编辑框窗口对于编辑框的开发是比较繁琐的,因此选择直接继承RGF_UI的UIInput类,然后将此控件修改为窗口,首先需要创建一个编辑框窗口类InputWin,代码如下:// Yez/src/win_input.cj public open class InputWin <: UIInput{ // 为编辑框设置独立的UI样式,而不是继承全局的样式 let css:UITheme = UITheme() init(){ super(theme:css) // 从配置中获取字体大小 css.fontSizeM = ConfigMgr.i.fontSize // 更新样式配置 css.update() } /** * 获取输入框窗口预期出现的位置 * @return 编辑框窗口坐标 * @note 本部分计算编辑框在当前椰椰所在桌面中心时的位置 */ private func getTargetLocation():Point{ // 方案二、桌面中心 let monitor = rsGetMonitorFromPoint(rsGetCursorPos()) let pos = Point( ((monitor.right - monitor.left) - INPUT_WIN_WIDTH) / 2, ((monitor.bottom - monitor.top) - INPUT_WIN_HEIGHT) / 2 ) return pos } /** * 创建窗口 */ public func createWin(hwndParent:RgfHwnd):RgfHwnd{ // 获取出现位置坐标 let pos = getTargetLocation() // 创建一个置顶层的无边框编辑框窗口 let retv = super.createWin( uiGetContext(),"",pos.x,pos.y,INPUT_WIN_WIDTH,INPUT_WIN_HEIGHT,hwndParent, WinStyle.PopupWindow | WinStyle.ClipChildren, dwExStyle:WinStyleEx.Default | WinStyleEx.TopMost, substrate:Substrate.Direct2D,layered:false,bgMod:BackGroundMod.CoverNotDraw,drawInterval:0 ) // 设置默认的内容为:“想对椰椰说什么:” content = INPUT_PROMPT // 创建好就显示窗口 show(true) return retv } } 完成编辑框窗口的创建后,还需要能响应相关的输入事件。对此,笔者希望:a) 当编辑框被按下或输入内容时,如果内容为默认内容,则清空提示内容;b) 当编辑框失去焦点时,窗口主动销毁自身;c) 当编辑框按下回车键时,将编辑框内容上发给大模型,并开始处理返回消息。实现自动清空内容很简单,代码如下:// Yez/src/win_input.cj /** 鼠标左键按下时,判断内容并清空 */ public override open func onLButtonDown(e:EvMouse):Unit{ if(content == INPUT_PROMPT){ content = "" } super.onLButtonDown(e) } /** 键盘按下某键时,判断内容并清空 */ public override open func onKeyDown(e:EvKey):Unit{ if(content == INPUT_PROMPT){ content = "" } super.onKeyDown(e) } 得益于RGF_CJ库的安全性,窗口销毁会自动释放资源,避免了繁琐的资源释放,和释放时机的管理。实现窗口失去焦点就是否窗口的代码如下:// Yez/src/win_input.cj /** 窗口失去焦点时,销毁窗口并释放窗口资源 */ public override open func onKillFocus(e:EvKillFocus):Unit{ destroyWin() } 希望实现按下回车键触发对话,则需要在对应的事件中调用LLMImpl类的流式对话方法,代码如下:// Yez/src/win_input.cj /** 字符输入事件 */ public override open func onChar(e:EvChar):Unit{ // 判断输入的是否为回车键 if(e.char == 0x0D/*VK_RETURN*/){ // 是回车键就销毁本窗口 destroyWin() // 并且调用 开始回答函数 start() // 然后调用 LLMImpl 类的流式对话方法 // 并且传入 流的回调函数 以及 回答结束的函数 LLMImpl.i.chat(content,task:task,end:end) }else{ super.onChar(e) } } // 回复流 public var task:(String) -> Unit = {str=>} // 开始回答 public var start:() -> Unit = {=>} // 开始回答 public var end:() -> Unit = {=>} 编辑框控件的效果如下图所示:注’ 完整的代码请参考提供的代码文件反馈窗口完成上述内容后,输入与处理已经基本完备,接下来需要实现文本的显示,这一部分涉及到内容绘制,同样还是基于WinBase类进行窗口开发。创建一个反馈文本的窗口类 WinText,并且准备基本的资源代码如下:// Yez/src/win_text.cj @Abbr // 文本色彩值转Color对象宏 @LifeCycle @HookProc open class WinText <: WinBase{ // 内容纯色画刷 private let _brhCnt:SolidColorBrush // 背景纯色画刷 private let _brhbg:SolidColorBrush // 文本渲染核心 private let _textCore:TextCore // 文本格式 private let _textformat:TextFormat // 文本布局 private let _textLayout:TextLayout // 文本内容 private var _text:String = "" /** 创建窗口 */ public func createWin( x:Int32, y:Int32, nWidth:Int32, nHeight:Int32, hWndParent:RgfHwnd, substrate!:Substrate = Substrate.Direct2D ):RgfHwnd{ ... } /** 文本对象管理方法,根据配置和窗口尺寸生成文本渲染依赖的对象 */ private func createFontObj(){ let rect:Rect = surface.getRect() surface.createTextCore(_textCore) .createTextFormat(_textformat,ConfigMgr.i.fontSize) .createTextLayout(_textLayout,_text,_textformat,Float32(rect.right),Float32(rect.bottom)) } /** 创建设备/渲染相关资源事件 */ public override open func createDeviceResources():Bool{ // 创建 纯黑内容画刷 和 白色半透明背景画刷 surface.createSolidColorBrush(_brhCnt,"#000",1.0) .createSolidColorBrush(_brhbg,"#FFFA",1.0) // 创建文本相关对象 createFontObj() return true; } } 完成渲染事件,确保内容正确渲染// Yez/src/win_text.cj // 绘制窗口 public override open func onPaint(wRect:Rect):Unit{ surface.clear(0.0,0.0,0.0,0.0) // 透明模式 // 渲染背景圆角矩形 surface.fillRoundedRectangle(surface.getRect(),4.0,4.0,_brhbg) // 渲染大模型返回的文本内容 surface.drawTexts(_textCore,_brhCnt,_textLayout,originX:0.0,originY:0.0) } 完成显示文本方法。当设置文本时,若窗口类已经存在,就更新内部文本;若窗口不存在,就根据当前内容创建窗口并显示。// Yez/src/win_text.cj public func showText(str:String,p:RgfHwnd):Unit{ // 获取光标位置 let pos = rsGetCursorPos() // 获取光标所在屏幕矩形范围 let monitor = rsGetMonitorFromPoint(pos) // 如果窗口未创建,就创建窗口 if(!isCreated()){ createWin(pos.x,pos.y,100,100,p) } if(_textLayout.isValid()){ if(str != _text){ // 更新文本内容 _text = str; // 创建文本布局信息 surface.createTextLayout(_textLayout,_text,_textformat,Float32(monitor.right - monitor.left) / 2.0,Float32(monitor.bottom - monitor.top) / 2.0) } // 获取文本自动布局后的测量数据 let m = _textLayout.getMetrics() let w = Int32(m.getOrThrow().width) let h = Int32(m.getOrThrow().height) 按照测量数据移动窗口 if(pos.x > (monitor.right - monitor.left) / 2){ move(pos.x - w,pos.y - h,w,h) }else{ move(pos.x,pos.y - h,w,h) } } // 窗口显示 show(true) } 文本控件的效果如下图所示:注’ 完整的代码请参考提供的代码文件联结各个窗口逻辑完成上述各个控件后,需要实现各个组件之间的联合。实现流式信息与动画的联动// Yez/src/win_anm.cj /** 文本变更事件 */ func onTextChange(str:String):Unit{ if(!_firstText){ _firstText = true // 当文本输出开始,切换“跳跃”动画状态到“说话”动画状态 play(AnmState.Speak) } _text += str _textBox.showText(_text,hWnd) } /** 大模型开始输出文本 */ func onTextStart():Unit{ killTimer(0) _text = "" // 给大模型上发请求后,播放“跳跃”动画 play(AnmState.Jump) _firstText = false } /** 大模型结束输出文本 */ func onTextEnd():Unit{ // 大模型输出结束后,延迟5s,以让用户完整阅读完内容 const cj_bug_val:Option<CFunc<(HWND, UInt32, UInt64, UInt32) -> Unit>> = None setTimer(0,5000,cj_bug_val) } /** 时钟事件 */ func onTimer(e:EvTimer):Unit{ killTimer(0) // 用户阅读完内容后,销毁文本框窗口 _textBox.destroyWin() // 播放“等待”动画 play(AnmState.Wait) } 双击“椰椰”时打开输入框窗口// Yez/src/win_anm.cj // 客户区双击鼠标左键 func onLButtonDblClk(e:EvMouse):Unit{ // 在“等待”状态下双击“椰椰” match(_nowState){ case AnmState.Wait => {=> // 销毁文本框(虽然已无可能还显示,不过保险起见) _textBox.destroyWin() if(!_inputBox.isCreated()){ // 创建文本输入框窗口 _inputBox.createWin(hWnd) }else{ // 如果窗口存在就移动输入框窗口 // 这部分适用于希望输入框跟随鼠标位置的情况 // 对于方案二的桌面居中而言,没有必要 _inputBox.move() } () }() case _ => () } } 结语至此,仓颉AI桌宠“椰椰”就基本开发完成了,其实还有很多规划的内容可以增加,比如当和椰椰说“咔嚓”或者“拍照”时激活截图功能。这些内容就留给读者继续探索啦!本文相关链接本文项目代码库:https://gitcode.com/raozj/YezRGF_CJ库:https://gitcode.com/Cangjie-SIG/RGF_CJRGF_UI库:https://gitcode.com/raozj/RGF_UIRGF案例库:https://gitcode.com/raozj/RGF_CJ_Example华为云ModelArts:https://www.huaweicloud.com/product/modelarts/studio.html
  • [问题求助] 用小熊派h3863官方代码实现sle_uart1对4通信为什么第二个服务端就连不上客户端
    我就只最后一个改为0x04
  • [交流反馈] 由于驱动程序 EdrDriver,BypassIO 不受支持
    Hisec Endpoint 的驱动会导致 Windows 11 的 DirectStorage API 无法正常工作,Xbox Game Bar 的设置-游戏功能界面会提示“由于驱动程序 EdrDriver,BypassIO 不受支持”,影响游戏性能,而卸载 Hisec Endpoint 可以解决此问题(Xbox Game Bar 显示“C:\ 支持 DirectStorage”,表明 DirectStorage API 恢复正常)。望华为相关项目组后续改善该问题。
  • [技术干货] 怎么解决刷卡或扫码后,点击软件输入框,win10屏幕键盘不会自动弹出的问题
    在Windows 10环境中,WPF应用有时可能会遇到屏幕键盘无法自动弹出的问题,尤其是在添加了外部输入设备(如扫码头或刷卡器)后。这个问题的发生可能是由于焦点管理或系统输入法状态被这些外部设备打乱引起的。下面是一些可能的解决方案:1. 强制显示屏幕键盘可以通过代码强制调用屏幕键盘。在WPF应用中,当需要显示屏幕键盘时,可以使用以下代码:using System.Diagnostics; private void ShowTouchKeyboard() { Process.Start(@"C:\Program Files\Common Files\Microsoft Shared\Ink\TabTip.exe"); }你可以在点击输入框时调用这个方法,以确保屏幕键盘弹出。例如,可以订阅输入框的GotFocus事件:private void TextBox_GotFocus(object sender, RoutedEventArgs e) { ShowTouchKeyboard(); }2. 检查设备影响如果问题是由扫描头或刷卡器引起的,可以尝试以下步骤:设备驱动更新:确保所有外部设备的驱动程序都是最新的。设备配置检查:检查外部设备的配置,看看是否提供了选项来避免干扰系统输入法。3. 使用 Windows 触摸服务确保与触摸和输入相关的服务在Windows 10上正确运行,例如“Touch Keyboard and Handwriting Panel Service”。4. 设置 WPF 应用为触摸优先您可以将WPF应用设置为触摸优先,确保输入焦点在触摸交互时始终正确处理。可以在App.xaml.cs中设置:protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 设置应用为触摸优先 if (Tablet.TabletDevices.Count > 0) { Tablet.TabletDevices.First().CaptureMode = CaptureMode.None; } }5. 重新设置输入焦点当发现屏幕键盘不会自动弹出时,可以尝试在点击输入框时强制设置输入焦点。可以在输入框的GotFocus事件中加入以下代码:private void TextBox_GotFocus(object sender, RoutedEventArgs e) { var textBox = sender as TextBox; if (textBox != null && !textBox.IsKeyboardFocused) { textBox.Focus(); ShowTouchKeyboard(); } }综合示例综合以上方法,你可以这样调整你的WPF应用:using System; using System.Diagnostics; using System.Linq; using System.Windows; namespace YourNamespace { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.Loaded += MainWindow_Loaded; } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { if (Tablet.TabletDevices.Count > 0) { Tablet.TabletDevices.First().CaptureMode = CaptureMode.None; } } private void TextBox_GotFocus(object sender, RoutedEventArgs e) { var textBox = sender as TextBox; if (textBox != null && !textBox.IsKeyboardFocused) { textBox.Focus(); ShowTouchKeyboard(); } } private void ShowTouchKeyboard() { Process.Start(@"C:\Program Files\Common Files\Microsoft Shared\Ink\TabTip.exe"); } } }结论通过上述方法,你应该能够提高WPF应用中触摸输入框的可靠性,确保屏幕键盘在需要时能够正常弹出。如果问题依然存在,可能需要更深入的调试,包括检查外部输入设备与操作系统之间的交互。
  • [技术干货] Window下SRS服务器的搭建-转载
                                                                           ---2023.7.23 准备材料 srs下载:GitHub - ossrs/srs at 3.0release 目前srs release到5.0版本。  srs官方文档:Introduction | SRS (ossrs.net)  Docker下载:Download Docker Desktop | Docker 进入docker官网选择window版本直接下载。由于srs无法直接部署在Window中,因此需要使用docker容器。  搭建步骤 2.1 安装docker Docker 并非是一个通用的容器工具,它依赖于已存在并运行的 Linux 内核环境。  Docker 实质上是在已经运行的 Linux 下制造了一个隔离的文件环境,因此它执行的效率几乎等同于所部署的 Linux 主机。  在安装docker之前先要打开window的虚拟机。  2.2.1 安装 Hyper-V Hyper-V 是微软开发的虚拟机,类似于 VMWare 或 VirtualBox,仅适用于 Windows 10。这是 Docker Desktop for Windows 所使用的虚拟机。  但是,这个虚拟机一旦启用,QEMU、VirtualBox 或 VMWare Workstation 15 及以下版本将无法使用!如果你必须在电脑上使用其他虚拟机(例如开发 Android 应用必须使用的模拟器),请不要使用 Hyper-V!  1、开启 Hyper-V  1)鼠标移动到window图标上,有机鼠标右键。点击选择“应用和功能”。   2)下拉弹出框右边的滑动条到最底下,点击“程序和功能”。  3)点击“启动或者关闭window功能”   4)勾选Hyper-V   至此Hyper-v打开完成,启动可以使用命令也可以点击鼠标右键启动。  启动命令如下:  Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All 2.2.2 安装docker 双击下载的 Docker for Windows Installer 安装文件,一路 Next,点击 Finish 完成安装。  在cmd中输入docker出现docker 命令格式则说明安装成功了。   2.2、编译安装srs服务器 1、启动window中的cmd控制台窗口。  2、启动docker并映射srs文件的命令。  docker run -it --name=srs -v /G/srs:/tmp/srs -w /tmp/srs/trunk -p 1935:1935  -p 1985:1985 -p 8080:8080 -p 8085:8085 registry.cn-hangzhou.aliyuncs.com/ossrs/srs:dev bash 这个命令我是链接的G:\srs\路径。  这里不能使用pwd和window的路径格式。例如G:\srs要修改为/G/srs。  执行完成之后,执行ls命令,出现srs的文件结构就说明映射成功了。   3、docker启动映射成功之后执行./configure。  此处可能出现问题:   bad interpreter: No such file or directory 解决方法如下:  vi configure   :set ff :set fileformat=unix :wq 4、修改问题之后继续执行 ./configure。出现如下界面说明./configure执行成功。    5、 执行make命令。出现如下界面说明make成功了。   6、执行 ./objs/srs -c conf/srs.conf命令启动srs服务。出现如下界面说明启动成功了。   至此,srs服务启动成功。  SRS测试使用方法 一、ffmpeg推拉流测试  1、使用ffmpeg命令推流  ffmpeg -re -i C:\Users\admin\Desktop\ship\pasf2.flv -vcodec copy -acodec copy -f flv -y rtmp://192.168.10.103/live/livestream 以上命令中的ip地址是服务器所在的window系统的ip地址。  使用ffmpeg命令拉流 ffplay rtmp:///live/livestream 二、WebRTC推拉流  1、使用ffmpeg命令推流  ffmpeg -re -i C:\Users\admin\Desktop\ship\pasf2.flv -vcodec copy -acodec copy -f flv -y rtmp://192.168.10.103/live/livestream 以上命令中的ip地址是服务器所在的window系统的ip地址。  2、使用WebRTC拉流  推送流成功之后,使⽤srs⾃带的rtc_player播放器进⾏播放,直接请求srs服务的8080端⼝即可。  在浏览器中输入如下网址:  http://192.168.10.103:8080/players/srs_player.html 三、VLC推拉流  四、docker命令  1、查看decker中运行的容器  docker ps -a 2、 获取容器/镜像的元数据  docker inspect [OPTIONS] NAME|ID docker inspect registry.cn-hangzhou.aliyuncs.com/ossrs/srs:dev 3、连接到正在运行中的容器  docker attach [OPTIONS] CONTAINER 问题:Error response from daemon: No such container: registry.cn-hangzhou.aliyuncs.com/ossrs/srs:dev   解决办法:出现此问题的原因是由于docker容器没有启动,需要启动docker容器。   链接命令:docker attach 39031b94cb0e 4、启动docker容器  docker start 39031b94cb0e 5、停止docker容器  docker stop 39031b94cb0e 6、直接关闭容器  docker kill 容器ID或容器名 7、重启docker容器  docker restart 容器ID或容器名 五、srs命令  1、指定配置文件,即可启动srs  ./objs/srs -c conf/srs.conf 2、启动srs  ./etc/init.d/srs start 3、查看SRS状态  ./etc/init.d/srs status 4、停止SRS  ./etc/init.d/srs stop 5、重启SRS  ./etc/init.d/srs restart 6、Reload SRS  ./etc/init.d/srs reload ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/qq_43812868/article/details/134141219 
  • [技术干货] 2024年2月人工智能问题总结合集
    二月问题总结如下:【1】在ECS windows部署Llama2 尝试使用MLC运行,但出现以下报错,求助cid:link_0【2】atlas300P3 在容器中访问rtsp流地址报错No route to host cid:link_1【3】ECS上面,我看机器学习推荐的只有N卡,想问下华为自己的显卡在ModelArts那边不是能用,为啥还没上ECS cid:link_2【4】昇腾310(Ascend 310)能不能用来搭建stable diffusecid:link_3【5】 acl init failed, errorCode = 100039cid:link_4
  • [技术干货] OpenResty windows安装教程
    1 什么是OpenRestyOpenResty是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。简单地说OpenResty 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。2 OpenResty安装·下载地址 http://openresty.org/cn/download.html选择符合自己电脑的安装包下载完成后,选择安装目录(建议非中文)进行解压双击nginx.exe运行即可测试:打开浏览器访问 localhost:80
  • [分享交流] 鸿蒙PC系统能否成为Windows的真正竞争者?
    在不久的将来,鸿蒙PC系统能否成为Windows的真正竞争者?
  • [问题求助] 建议增加 Scoop 镜像
    Scoop是一个强大的Windows软件包管理器,在Windows用户中相当受欢迎。它旨在简化和加速在Windows操作系统上的软件安装和管理。通过命令行操作,用户可以轻松地搜索、安装、更新和卸载各种应用程序,而无需手动浏览官方网站或执行繁琐的安装步骤。Scoop的重要性在于为用户提供了一种便捷的方式,使其能够快速、一致地配置其开发环境或安装常用工具,提高了Windows系统上软件管理的效率和灵活性。建议增加 Scoop 镜像。
  • [技术干货] 如何查看自己的操作系统是否支持 64 位架构
    可以按照以下步骤来检查自己的操作系统是否支持 64 位架构: 1:打开"此电脑"或"我的电脑"等文件资源管理器; 2:右键单击计算机图标或界面空白处,选择属性选项; 3:在弹出的窗口中,可以看到基本信息部分,其中有一个 “系统类型” 的栏目,如果显示 "64 位操作系统",则表示你的操作系统支持 64 位架构;如果显示 "32 位操作系统",则表示你的操作系统只支持 32 位架构。 
  • [技术干货] 无人值守执行器的连接器RDP附加参数说明指导
    1、背景23.3版本管理中心新特性增加了支持连接器参数配置,当任务使用无人值守执行器运行时,支持对无人值守执行器的连接器参数进行配置:RDP窗口全屏显示RDP桌面缩放比RDP附加参数2、参数说明RDP附件参数官方列举的见右边链接:https://github.com/awakecoding/FreeRDP-Manuals/blob/master/User/FreeRDP-User-Manual.markdown如果要导出完整的参数信息,按以下步骤操作:①进入无人值守连接器rdp目录连接器安装目录\agent\freerdp (连机器版本是3.0.0及以上版本为该目录)连接器安装目录\agent\wfreerdp (连接器版本是3.0.0以下版本为该目录)②在以上目录输入cmd打开命令行窗口,然后输入命令:wfreerdp.exe /help > help.txt 就可以把包含完整参数信息的help.txt导出到rdp目录下。注意事项:1. 关闭RDP窗口全屏显示后可以设置窗口分辨率的宽、高2. 23.3管理中心配置的RDP连接参数只有在3.2.0及以上版本的连接器上才能生效
  • [技术干货] 【Windows】六种正确清理C盘的方法,解决你的红色烦恼-转载
     前言 Windows操作系统一般是安装在磁盘驱动器的C盘中,运行时会产生许多垃圾文件,C盘空间在一定程度上会越来越小。 伴随着电脑工作的时间越久,C盘常常会提示显示其内存已不足。 C盘容量不足将会极大影响系统的运行速度,电脑会变卡、死机。 释放磁盘空间可以提高计算机的性能。那么,如何正确清理C盘呢?  清理方法 1. 利用Windows自己附带的磁盘清理工具 1.鼠标右击【C盘】选择【属性】。  2.右击【磁盘清理】。  3.勾选需要清理的文件,点击【清理系统文件】,清理完成后,点击【确定】即可。   2. 开启自动清理 开启自动清理后,Windows可以通过删除不需要的文件自动释放空间。  进入【设置】—【系统】—【存储】—将【存储感知】设置为【开】,电脑就会自动清理。   3. 通过“配置存储感知或立即运行”来清理 进入【设置】—【系统】—【存储】—【配置存储感知或立即运行】  点击【立即清理】  4. 管理C盘中的程序 进入【设置】—【系统】—【存储】—【应用和功能】  进入以后,我们把【筛选条件】设置成【window10 C:】  3. 通过卸载C盘下的一些不必要软件,可以进一步释放C盘空间。 如果遇到自己不认识的软件可以百度:xxx可以卸载吗?这样就可以避免误卸载掉一些有用的软件。  5. 系统文件夹转移 . 进入【设置】—【系统】—【存储】—【更多存储设置】—【更改新内容保存位置】—【选择别的盘符】— 点击【应用】  6. 将C盘现有内容转移到别的盘 手动转移:打开【此电脑】我们可以看到【文档】、【下载】、【桌面】等文件夹。它们目前默认的路径都是C盘。  右键【桌面】— 选择【属性】  【位置】— 【移动】— 【选择一个别的盘的路径】 最好提前在其他盘中建一个同名的文件夹,如Desktop,移进去就好。  ———————————————— 版权声明:本文为CSDN博主「想变厉害的大白菜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_44211968/article/details/120129476 
  • [区域初赛赛题问题] windows下判题器使用出错
    windows下判题器报错,这个怎么解决呀
  • [区域初赛赛题问题] windows下bat脚本问题
    windows下bat和其他demo都只输出这一行,不知道是哪的问题
  • [技术干货] 如何用Python让你的电脑说话-转载
     如何用Python让你的电脑说话 你成为亿万花花公子的第一步  如果你是像《钢铁侠》这样的电影的粉丝,你可能已经幻想过得到你自己的贾维斯。那么,在这篇文章中,我将告诉你如何开始制作你自己的电脑助手。我们将通过一个小的编程和一些聪明的python包在引擎盖下进行数据科学。  现在,制作像超级智能人工智能这样复杂的东西是很难的,对于我来说,在一篇文章中甚至很可能在一般情况下都很难做到。然而,我们可以做的是把这个问题分解成更小的部分,使问题看起来更容易。这是你在每一个项目中都应该做的事情,这样就不会一下子被其所有的复杂性所困住了。  从我对这个问题的简单思考来看,我相信我们可以把一个超级智能的人工智能助手分成四个主要部分。  文本到语音(以获得回应) 语音转文字(用于询问事情) 一些计算,以了解我们的问题并创建一个响应 将回应变成现实世界中的行动 在今天的文章中,我将专注于计算机助手的文本到语音方面,让我们的计算机与我们交谈。如果一切顺利的话,我将在今后的文章中继续发展,使我们的助手变得更加复杂和有用。  找到一个文本到语音库 现在,用python做这样的事情有一个巨大的好处,那就是我们有大量的库可以使用,以快速完成事情。毕竟,如果我们从头开始开发其中的每一个部分,我们会在这里呆很久,以至于我们永远不会完成任何事情。让我们站在巨人的肩膀上,使用python软件包。  对于文本到语音来说,有几个Python包在人群中脱颖而出。  谷歌文本转语音(gTTs),以及 pyttsx3(我不知道那到底代表什么)。 谷歌文本转语音是一个Python库,用于连接谷歌翻译的文本转语音API。它具有谷歌自己的文本到语音应用程序的性能,但需要互联网连接才能使用。  另一方面,pyttsx3是一个文本到语音的转换库,它寻找预装在你的平台上的文本到语音引擎并使用它们。因此,它可以离线工作。  以下是它在主要操作系统上使用的文本转语音引擎。  Windows上的SAPI5 2. MacOSX上的NSSpeechSynthesizer  3.其他所有平台上的espeak  看看这两个,我不希望我的助手依赖谷歌或在线连接来工作。我更愿意使用pyttsx3,让一切都在我自己的机器上运行。  设置项目 现在,在我们开始运行一切之前,让我们设置我们的项目。  我们将通过文本编辑器和终端来完成一切。如果你不知道这意味着什么,那么我向人们推荐的文本编辑器是vscode,而终端通常内置在你的文本编辑器中(如vscode),或者是你电脑上的一个叫做 "终端 "或 "cmd "的程序。  现在,我想让你打开你的终端,改变目录到你保存项目的地方,例如,用  cd ~/projects 1 接下来,我们将需要创建一个目录来存储我们的项目。这完全取决于你,但我希望我的助手叫罗伯特。因此,我将创建一个名为 "robert "的新目录,然后用以下方式进入该目录  mkdir robert cd robert 1 你可以把这个名字改成你喜欢的样子,比如说Brandy或者Kumar之类的。  接下来,我们需要让python启动并运行。为此,我们将需要安装Python 3。如果你没有安装,请参阅https://www.python.org/,了解安装说明。我们还需要创建一个Python虚拟环境。如果你想了解更多这方面的信息,请看这里,我最近的一篇文章。  假设你已经安装了Python,你可以在终端用以下方法验证  python3 --version 1 现在你应该能够在你的robert目录下创建你的python虚拟环境了。  python3 -m venv venv 1 注意,如果你安装的python的版本是python,python3.7或者python3.9或者其他的,那么就使用这个版本。  然后你应该能够用以下方法激活你的虚拟环境。  (在MacOS和Linux上)  source venv/bin/activate 或 (Windows)  venv/Scripts/activate 1 现在我们需要安装我们需要的软件包。为了做到这一点,我们将创建一个requirements.txt文件。进入你最喜欢的文本编辑器,例如vscode,或者,如果你很冒险的话,vim,打开你的 "robert "文件夹,现在创建这个文件。  对于我们的项目,到目前为止,我们只需要pyttsx3。很简单。让我们现在把它添加到我们的requirements.txt文件中,如下所示  接下来,让我们用pip来安装我们的需求  pip install -r requirements.txt 1 使用pyttsx3  现在一切都安装好了,让我们开始使用pyttsx3。为了了解该怎么做,我查看了这里的文档。  然后,你可以通过创建一个名为speech.py的文件并添加以下代码来制作一个漂亮的例子。  import pyttsx3  engine = pyttsx3.init()  voice_num = 2 text_to_say = "Hello World! I am Robert!"  voices = engine.getProperty('voices') engine.setProperty('voice', voices[voice_num].id)  engine.say(text_to_say) engine.runAndWait() 1 我们首先导入pyttsx3以加载其所有的类和变量。然后我们初始化语音引擎,设置一个我们想要的声音,然后是我们想要说的文字。最后我们使用engine.runAndWait()来说话。  然后我们可以在终端用下面的命令运行这个文件。  python speech.py 1 玩一玩这个,改变text_to_say变量。你应该能说出任何你喜欢的东西。  酷!调整声音和速度  现在我们有了一些工作,让我们给我们的助手一些调整。Pyttsx3让我们调整声音和速度。  import pyttsx3  engine = pyttsx3.init()  # change voice  # getting details of current voice voices = engine.getProperty('voices')        for i in range(len(voices)):     engine.setProperty('voice', voices[i].id)     # say something     engine.say("Pick me, pick me! My voice is number " + str(i))     engine.runAndWait()  1 在上面的例子中,你可以改变声音_num到一个不同的数字来获得一个新的声音。从我的测试来看,这似乎取决于平台(可能取决于你的平台有SAPI5,NSSpeechSynthesizer或espeak)。  我创建了这个怪物般的文件(当很多机器人的声音用奇怪的口音和你说话时,你就会明白为什么),以帮助你决定什么声音最适合你。一旦你找到了你喜欢的声音号码,把这个号码换成在voice_num变量中找到的号码。  接下来的步骤  恭喜你达到了终点。如果你有任何问题或只是想打招呼,请在下面发表。  如果你想进一步阅读,我建议你查看下面的链接,并在即将到来的文章中获得先机。  https://www.geeksforgeeks.org/python-text-to-speech-by-using-pyttsx3/  https://realpython.com/python-speech-recognition/  对于我的下一篇文章,我将专注于语音转文字,这样我们的助手就可以对我们的命令作出回应🤖。给我一个关注,以确保你不会错过它。  本文由 mdnice 多平台发布 ———————————————— 版权声明:本文为CSDN博主「qq_40523298」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_40523298/article/details/127766068