详情

首页手游攻略 动口不动手——海光DCU K100_AI单卡部署 Qwen3-ASR-1.7B + Qwen3.5-9B:语音驱动Everything,实现文件智能高速检索

动口不动手——海光DCU K100_AI单卡部署 Qwen3-ASR-1.7B + Qwen3.5-9B:语音驱动Everything,实现文件智能高速检索

佚名 2026-08-28 09:29:02

动口不动手——海光DCU K100_AI单卡部署 Qwen3-ASR-1.7B + Qwen3.5-9B:语音驱动Everything,实现文件智能高速检索需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。

一、引言

你是否曾经在堆积如山的本地文件中苦苦寻找一个文档,却因为记不清文件名而束手无策?你是否幻想过对着电脑说一句“搜索文件修改时间为2026年1月至5月的PDF文件”,文件就自动出现在眼前?

本文介绍了一套基于海光DCU K100_AI国产算力平台打造的语音驱动的智能文件搜索系统。系统后端部署了Qwen3-ASR-1.7B语音识别模型和Qwen3.5-9B大语言模型,前端则是一个运行在Windows上的Python GUI程序,集成了Everything搜索引擎的SDK。

用户只需对着麦克风说出自然语言指令(如“搜索文件修改时间为2026年1月至5月的PDF文件”),系统便会自动完成语音识别→语义理解→Everything搜索→结果展示的完整链路。整个过程无需键盘输入,真正实现了“动口不动手” 。

海光DCU K100_AI单卡拥有64GB显存,FP16算力达196 TFLOPS,配合海光DTK 26.04工具包和vLLM推理框架,能够稳定流畅地同时承载ASR和大模型的推理服务。下文会完整介绍系统架构、部署方案、核心代码以及踩坑经验,希望能为国产算力平台上的AI应用开发提供一份实用的参考。

二、系统方案设计

2.1 总体架构

系统采用客户端-服务端分离架构:

服务端(海光DCU服务器,Ubuntu 22.04,DTK 26.04,K100_AI):部署Qwen3-ASR-1.7B和Qwen3.5-9B两个模型,分别提供语音识别和语义理解能力。

客户端(Windows PC):运行Python GUI程序,负责麦克风音频采集、调用服务端API、执行Everything本地搜索并展示结果。

2.2 核心工作流程

第一步:语音采集与VAD静音检测

客户端通过PyAudio库从麦克风采集16kHz单声道PCM音频流。使用WebRTC VAD(Voice Activity Detection)库实时检测语音活动,将连续语音帧缓存为音频段,当检测到连续静音帧数超过阈值(默认30帧)时,判定一个完整的语音指令结束。

第二步:ASR语音识别

语音段被封装为WAV格式,通过HTTP POST请求发送至Qwen3-ASR-1.7B服务端的/v1/audio/transcriptions接口。模型返回转录文本后,客户端会过滤掉预设的噪声词表(如“嗯”、“啊”、“uh”等),避免误触发搜索。

第三步:大模型语义理解与工具调用

将ASR输出的文本作为用户查询,送入Qwen3.5-9B大模型。模型被预先注入了search_files工具定义,包含query(Everything搜索语法)、max_results、sort_by、sort_order等参数。大模型自动判断用户意图,生成符合Everything高级语法的搜索语句——例如“找一下2026年创建的关于深度学习的PDF文件”会被转换为

ext:pdf datecreated:2026/1/1-2026/5/31 深度学习

第四步:Everything高速本地搜索

客户端通过ctypes直接调用Everything64.dll的API,执行大模型生成的搜索语句。Everything利用NTFS的USN日志机制实现毫秒级文件检索,结果包含文件名、路径、大小、修改时间、创建时间等完整元数据。程序是在windows操作系统上运行,须安装Everything 64位版本软件,并下载SDK包:Everything-SDK.zip,下载链接为:https://www.voidtools.com/Everything-SDK.zip,解压后提取dll目录里面的Everything64.dll。

第五步:结果展示与交互

搜索结果以表格形式呈现在GUI中,支持按列排序、双击打开文件、右键复制路径、打开文件所在位置以及“打开方式”对话框等丰富操作。

2.3 技术选型亮点

Everything SDK:直接调用DLL而非HTTP服务,响应速度更快、无额外端口占用。

vLLM推理框架:支持PagedAttention和Continuous Batching,显著提升显存利用率和推理吞吐。

WebRTC VAD:轻量级实时语音活动检测,无需额外模型,适合客户端本地运行。

三、系统工作流程图

流程图详解

1. 用户语音输入 → 麦克风采集

用户对着麦克风说出自然语言指令(例如:“找一下去年项目验收的PDF”)。客户端通过 PyAudio 库以 16kHz 采样率、16-bit 位深、单声道格式实时读取音频流。

2. VAD 静音检测(语音活动检测)

每 30ms 一帧的音频数据送入 WebRTC VAD 模块(敏感度设为 mode=1)。系统会:

持续累积被判定为“语音”的帧;

一旦检测到连续 30帧(约 0.9 秒)的静音,即认为用户说完了一整句话,触发语音段结束事件。

3. 封装为 WAV 并发送 ASR 服务

将累积的语音帧合成为一个完整的音频段,封装成标准 WAV 格式(16000Hz / 单声道 / 16-bit PCM),通过 HTTP POST 请求发送给服务端的 Qwen3-ASR-1.7B(监听 8084 端口)。

4. ASR 转录与文本后处理

服务端返回的原始文本可能带有 <|zh|><asr_text> 等语言标记前缀,客户端会使用正则表达式 re.sub(r'^[^<]*<asr_text>s*', '', raw_text) 将其去除。随后,文本会与预定义的噪声词表(包括“嗯”、“啊”、“uh”、“um”等)进行匹配,若命中则直接丢弃,避免无意义的搜索请求。

5. 大模型语义理解(Qwen3.5-9B)

若文本有效,客户端将其作为用户消息,连同预定义的 search_files 工具定义(包含 querymax_resultssort_bysort_order 参数)一起发送至 Qwen3.5-9B 服务(监听 8085 端口)。该模型启用了 --enable-auto-tool-choice,能够自动判断需要调用工具,并生成符合 Everything 高级语法的查询字符串。

6. 解析工具调用参数

客户端接收到模型的 tool_call 响应后,解析 JSON 参数,提取出最终的 querymax_results(默认 1000)、排序字段和排序方向。

7. Everything 本地高速搜索

客户端通过 ctypes 直接调用 Everything64.dll 的 API:

Everything_SetSearchW(query) 设置查询条件;

Everything_SetMax(max_results) 限制结果数;

Everything_QueryW(True) 执行查询;

循环 Everything_GetNumResults() 获取每条结果的文件名、路径、大小、修改时间和创建时间。

整个过程利用 Everything 的 NTFS USN 日志机制,实现毫秒级检索。

8. 结果展示与用户交互

搜索结果以表格形式呈现在 Tkinter Treeview 中,包含五列:文件名、路径、大小、修改时间、创建时间。用户可进行以下操作:

双击任意行:使用系统默认程序打开文件;

右键菜单:复制完整路径、打开文件所在位置(explorer /select)、弹出“打开方式”对话框(通过 PowerShell 调用 Shell.Application)。

颜色块表示阶段含义
金色用户输入语音指令起点
蓝色客户端采集与 VAD本地音频处理和静音检测
绿色ASR 识别语音到文本的转换(服务端)
橙色大模型理解语义解析和工具调用生成(服务端)
紫色Everything 搜索本地高速文件检索
红色结果操作用户与搜索结果的交互

四、海光DCU环境部署

4.1 硬件环境

项目配置
CPU海光 x86架构处理器
GPU海光DCU K100_AI × 8(单卡64GB显存)
算力单卡FP16 196 TFLOPS
显存带宽896 GB/s
操作系统Ubuntu 22.04
DCU工具包DTK 26.04

4.2 Qwen3-ASR-1.7B 部署

使用的Docker镜像:

harbor.sourcefind.cn:5443/dcu/admin/base/custom:vllm0.15.1-ubuntu22.04-dtk26.04-0130-py3.10-20260220

⚠️ 关键注意:Qwen3-ASR-1.7B部署时,transformers版本必须小于5,推荐使用 4.57.6。否则会导致模型加载失败或推理异常。

启动脚本:

HIP_VISIBLE_DEVICES=6 vllm serve '/home/models/Qwen3-ASR-1.7B' --trust-remote-code --gpu-memory-utilization 0.3 --limit-mm-per-prompt '{"audio": 1}' --port 8084 --served-model-name Qwen3-ASR-1.7B

参数说明:

HIP_VISIBLE_DEVICES=6:指定使用第6号DCU显卡

--gpu-memory-utilization 0.3:显存利用率限制为30%,为其他服务预留空间

--limit-mm-per-prompt '{"audio": 1}':每个请求最多处理1个音频文件

--port 8084:服务监听端口

Qwen3-ASR-1.7B是Qwen团队开源的自动语音识别模型,支持30种语言和22种中文方言的识别,在Librispeech等公开基准测试中达到SOTA水平。

4.3 Qwen3.5-9B 部署

使用的Docker镜像:

42.228.13.241:5000/jenkins/model_test_env/vllm:0.21.0-ubuntu22.04-dtk26.04-py3.10-20260624-2240

启动脚本 :

#!/bin/bashexport VLLM_SPEC_DECODE_EAGER=1export VLLM_MLA_DISABLE=0export VLLM_USE_FLASH_MLA=1export VLLM_RPC_TIMEOUT=1800000export HIP_VISIBLE_DEVICES=6export ALLREDUCE_STREAM_WITH_COMPUTE=1# 海光CPU绑定核,通过 hy-smi --showtopo 参考numa节点export VLLM_NUMA_BIND=1export VLLM_RANK0_NUMA=0export VLLM_RANK1_NUMA=0export VLLM_RANK2_NUMA=0export VLLM_RANK3_NUMA=0export VLLM_RANK4_NUMA=0export VLLM_RANK5_NUMA=0export VLLM_RANK6_NUMA=0export VLLM_RANK7_NUMA=0export NCCL_MAX_NCHANNELS=16export NCCL_MIN_NCHANNELS=16vllm serve "/home/models/Qwen3.5-9B" --gpu-memory-utilization 0.4 --port 8085 --max-model-len 32768 --max-num-seqs 32 --served-model-name Qwen3.5-9B --tensor-parallel-size 1 --enable-auto-tool-choice --tool-call-parser qwen3_xml --trust-remote-code --enable-prefix-caching --enable-chunked-prefill --api-key PassWord@123456

关键参数说明:

--enable-auto-tool-choice:启用自动工具选择功能,让模型能够自主决定是否调用工具

--tool-call-parser qwen3_xml:指定Qwen3系列的XML格式工具调用解析器

--enable-prefix-caching:启用前缀缓存,提升重复查询的推理效率

--enable-chunked-prefill:启用分块预填充,优化长文本处理

--api-key PassWord@123456:API访问密钥,客户端需保持一致

NUMA绑定说明:海光CPU为多NUMA节点架构,通过VLLM_NUMA_BIND=1VLLM_RANK*_NUMA环境变量将vLLM进程绑定到特定NUMA节点,可显著降低内存访问延迟,提升推理性能

五、部署踩坑与解决

5.1 transformers版本不兼容

问题:部署Qwen3-ASR-1.7B时,使用最新版transformers(≥5.0)导致模型加载失败,报错涉及Qwen3ASRModel类定义不兼容。

解决:将transformers版本降级至 4.57.6

pip install transformers==4.57.6

5.2 音频格式不匹配

问题:客户端发送的WAV音频采样率、声道数、位深与服务端预期不一致,导致转录返回空结果或乱码。

解决:客户端须严格遵循服务端要求:

采样率:16000 Hz声道数:单声道(Mono)采样位深:16-bit PCM音频格式:WAV封装

5.3 ASR返回文本包含多余前缀

问题:Qwen3-ASR的转录结果有时会包含 <|zh|><asr_text> 之类的语言标记前缀,导致后续大模型解析异常。

解决:在客户端增加正则过滤:

import reclean_text = re.sub(r'^[^<]*<asr_text>s*', '', raw_text).strip()

5.4 vLLM在海光DCU上的性能调优

问题:vLLM在海光DCU上运行时的默认参数可能导致显存利用率不足或推理延迟偏高。

解决:

合理设置 --gpu-memory-utilization(ASR用0.3,大模型用0.4),为两个服务在同一张卡上共存预留空间

启用 --enable-prefix-caching 和 --enable-chunked-prefill 提升推理效率

通过 VLLM_NUMA_BIND=1 绑定NUMA节点,减少跨节点内存访问延迟

5.5 Everything64.dll加载失败

问题:Python通过ctypes加载Everything64.dll时报错“找不到指定的模块”。

解决:

确保Everything软件已在Windows上安装并运行

将Everything64.dll放置在脚本所在目录或系统PATH中

使用绝对路径加载:ctypes.WinDLL(r"D:audio_searchEverything64.dll")

六、程序代码及运行截图

6.1 程序完整代码

import asyncioimport jsonimport osimport reimport threadingimport tracebackimport subprocessimport tkinter as tkfrom tkinter import ttk, scrolledtext, messageboxfrom openai import OpenAIimport ctypesfrom ctypes import wintypesimport datetime# ==================== 语音识别所需导入 ====================import pyaudioimport webrtcvadimport requestsimport ioimport waveimport collections# ==================== 配置加载 ====================DEFAULT_CONFIG = {"everything_dll": r"D:audio_searchEverything64.dll","llm_base_url": "http://192.168.222.65:8085/v1","llm_model": "Qwen3.5-9B","llm_api_key": "PassWord@123456","max_results_limit": 1000,"enable_thinking": False,# ASR 语音识别配置(vLLM /v1/audio/transcriptions 接口)"asr_api_url": "http://192.168.222.65:8084/v1/audio/transcriptions","asr_model": "Qwen3-ASR-1.7B","asr_api_key": "PassWord@123456","asr_sample_rate": 16000,"asr_frame_duration": 30,# VAD 帧长(毫秒),仅支持 10/20/30"asr_vad_mode": 1, # VAD 敏感度 0~3"asr_silence_thresh": 30 # 连续静音帧数后认为语音结束}CONFIG_FILE = "config.json"def load_config():if os.path.exists(CONFIG_FILE):try:with open(CONFIG_FILE, 'r', encoding='utf-8') as f:config = json.load(f)for key, value in DEFAULT_CONFIG.items():config.setdefault(key, value)return configexcept Exception as e:print(f"读取配置文件失败: {e},将使用默认配置")return DEFAULT_CONFIG.copy()else:try:with open(CONFIG_FILE, 'w', encoding='utf-8') as f:json.dump(DEFAULT_CONFIG, f, indent=4, ensure_ascii=False)print(f"已创建默认配置文件: {CONFIG_FILE}")except Exception as e:print(f"无法创建配置文件: {e}")return DEFAULT_CONFIG.copy()config = load_config()EVERYTHING_DLL = config["everything_dll"]LLM_BASE_URL = config["llm_base_url"]LLM_MODEL = config["llm_model"]LLM_API_KEY = config["llm_api_key"]MAX_RESULTS_LIMIT = config["max_results_limit"]# ASR 参数ASR_API_URL = config["asr_api_url"]ASR_MODEL = config["asr_model"]ASR_API_KEY = config["asr_api_key"]ASR_SAMPLE_RATE = config["asr_sample_rate"]ASR_FRAME_DURATION = config["asr_frame_duration"]ASR_VAD_MODE = config["asr_vad_mode"]ASR_SILENCE_THRESH = config["asr_silence_thresh"]# 噪声词表(可调整)NOISE_WORDS = {"¿Qué?", "No.", "ok.", "Yeah.", "hmm", "uh", "um", "ah", "oh", "mhm", "嗯。","嗯","啊。", "啊", "哦。","哦","哎。"}# ==================== 路径处理工具函数 ====================def build_full_path(dir_path, file_name):"""安全拼接路径,修复 Windows 盘符缺少反斜杠的问题(例如 'D:' -> 'D:')"""if not dir_path and not file_name:return Noneif not file_name:return dir_pathif not dir_path:return file_nameif os.name == 'nt' and re.match(r'^[A-Za-z]:$', dir_path):dir_path += ''if os.path.basename(dir_path) == file_name:return dir_pathreturn os.path.join(dir_path, file_name)# ==================== Everything SDK 直接调用封装 ====================everything_dll = ctypes.WinDLL(EVERYTHING_DLL)everything_dll.Everything_SetSearchW.argtypes = [wintypes.LPCWSTR]everything_dll.Everything_SetSearchW.restype = Noneeverything_dll.Everything_SetMax.argtypes = [wintypes.DWORD]everything_dll.Everything_SetMax.restype = Noneeverything_dll.Everything_SetSort.argtypes = [wintypes.DWORD]everything_dll.Everything_SetSort.restype = Noneeverything_dll.Everything_SetRequestFlags.argtypes = [wintypes.DWORD]everything_dll.Everything_SetRequestFlags.restype = Noneeverything_dll.Everything_QueryW.argtypes = [wintypes.BOOL]everything_dll.Everything_QueryW.restype = wintypes.BOOLeverything_dll.Everything_GetNumResults.argtypes = []everything_dll.Everything_GetNumResults.restype = wintypes.DWORDeverything_dll.Everything_GetResultFileNameW.argtypes = [wintypes.DWORD]everything_dll.Everything_GetResultFileNameW.restype = wintypes.LPCWSTReverything_dll.Everything_GetResultPathW.argtypes = [wintypes.DWORD]everything_dll.Everything_GetResultPathW.restype = wintypes.LPCWSTReverything_dll.Everything_GetResultSize.argtypes = [wintypes.DWORD, ctypes.POINTER(ctypes.c_longlong)]everything_dll.Everything_GetResultSize.restype = wintypes.BOOLeverything_dll.Everything_GetResultDateModified.argtypes = [wintypes.DWORD, ctypes.POINTER(ctypes.c_longlong)]everything_dll.Everything_GetResultDateModified.restype = wintypes.BOOLeverything_dll.Everything_GetResultDateCreated.argtypes = [wintypes.DWORD, ctypes.POINTER(ctypes.c_longlong)]everything_dll.Everything_GetResultDateCreated.restype = wintypes.BOOLEVERYTHING_SORT_NAME_ASC = 1EVERYTHING_SORT_NAME_DESC = 2EVERYTHING_SORT_PATH_ASC = 3EVERYTHING_SORT_PATH_DESC = 4EVERYTHING_SORT_SIZE_ASC = 5EVERYTHING_SORT_SIZE_DESC = 6EVERYTHING_SORT_DATE_MODIFIED_ASC = 13EVERYTHING_SORT_DATE_MODIFIED_DESC = 14EVERYTHING_SORT_DATE_CREATED_ASC = 11EVERYTHING_SORT_DATE_CREATED_DESC = 12EVERYTHING_REQUEST_FILE_NAME = 0x1EVERYTHING_REQUEST_PATH = 0x2EVERYTHING_REQUEST_SIZE = 0x10EVERYTHING_REQUEST_DATE_MODIFIED = 0x40EVERYTHING_REQUEST_DATE_CREATED = 0x80def search_files_with_everything(query, max_results=1000, sort_by=None, sort_order="ascending"):sort_map = {("name", "ascending"): EVERYTHING_SORT_NAME_ASC,("name", "descending"): EVERYTHING_SORT_NAME_DESC,("path", "ascending"): EVERYTHING_SORT_PATH_ASC,("path", "descending"): EVERYTHING_SORT_PATH_DESC,("size", "ascending"): EVERYTHING_SORT_SIZE_ASC,("size", "descending"): EVERYTHING_SORT_SIZE_DESC,("date_modified", "ascending"): EVERYTHING_SORT_DATE_MODIFIED_ASC,("date_modified", "descending"): EVERYTHING_SORT_DATE_MODIFIED_DESC,("date_created", "ascending"): EVERYTHING_SORT_DATE_CREATED_ASC,("date_created", "descending"): EVERYTHING_SORT_DATE_CREATED_DESC,}sort_key = (sort_by, sort_order) if sort_by else ("name", "ascending")sort_type = sort_map.get(sort_key, EVERYTHING_SORT_NAME_ASC)everything_dll.Everything_SetSort(sort_type)everything_dll.Everything_SetRequestFlags(EVERYTHING_REQUEST_FILE_NAME |EVERYTHING_REQUEST_PATH |EVERYTHING_REQUEST_SIZE |EVERYTHING_REQUEST_DATE_MODIFIED |EVERYTHING_REQUEST_DATE_CREATED)everything_dll.Everything_SetSearchW(query)everything_dll.Everything_SetMax(max_results)if not everything_dll.Everything_QueryW(True):return []num = everything_dll.Everything_GetNumResults()results = []for i in range(num):name = everything_dll.Everything_GetResultFileNameW(i)path = everything_dll.Everything_GetResultPathW(i)size_val = ctypes.c_longlong()everything_dll.Everything_GetResultSize(i, ctypes.byref(size_val))modified_val = ctypes.c_longlong()everything_dll.Everything_GetResultDateModified(i, ctypes.byref(modified_val))created_val = ctypes.c_longlong()everything_dll.Everything_GetResultDateCreated(i, ctypes.byref(created_val))date_modified = ""date_created = ""if modified_val.value:try:date_modified = datetime.datetime.fromtimestamp(modified_val.value / 10000000 - 11644473600).strftime("%Y-%m-%d %H:%M:%S")except:passif created_val.value:try:date_created = datetime.datetime.fromtimestamp(created_val.value / 10000000 - 11644473600).strftime("%Y-%m-%d %H:%M:%S")except:passif not date_created:try:full_path = build_full_path(path, name)if full_path and os.path.exists(full_path):ctime = os.path.getctime(full_path)date_created = datetime.datetime.fromtimestamp(ctime).strftime("%Y-%m-%d %H:%M:%S")except Exception:passresults.append({"Name": name or "","Path": path or "","Size": size_val.value,"DateModified": date_modified,"DateCreated": date_created})return results# ==================== 大模型调用与工具定义 ====================client = OpenAI(base_url=LLM_BASE_URL, api_key=LLM_API_KEY)TOOLS = [{"type": "function","function": {"name": "search_files","description": ("使用 Everything 搜索引擎查找本地文件。""query 支持 Everything 高级查询语法,例如 ext:pdf、content:关键词、datecreated:、size:>1mb 等。""sort_by 可选:name, path, size, date_modified, date_created""sort_order 可选:ascending, descending""示例:搜索创建时间为2024年文件名包含“逆向”的docx,生成的query:ext:docx datecreated:2024/1/1-2024/12/31 逆向"),"parameters": {"type": "object","properties": {"query": {"type": "string", "description": "Everything 查询字符串"},"max_results": {"type": "integer", "description": "最大结果数,默认1000,最大1000"},"sort_by": {"type": "string", "enum": ["name", "path", "size", "date_modified", "date_created"]},"sort_order": {"type": "string", "enum": ["ascending", "descending"]}},"required": ["query"]}}}]def process_user_query(user_input: str, enable_thinking: bool = True):debug_lines = []messages = [{"role": "system", "content": ("你是一个文件搜索助手。根据用户的自然语言生成 Everything 搜索参数。""query 中可直接使用 Everything 高级语法。max_results 不填则默认1000。")},{"role": "user", "content": user_input}]try:extra_body = {"chat_template_kwargs": {"enable_thinking": enable_thinking}}response = client.chat.completions.create(model=LLM_MODEL,messages=messages,tools=TOOLS,tool_choice="auto",extra_body=extra_body)except Exception as e:debug_lines.append(f"大模型调用失败: {e}")return None, "n".join(debug_lines), f"调用大模型失败: {e}"choice = response.choices[0]debug_lines.append(f"模型原始响应: {choice.message}")if not choice.message.tool_calls:direct_reply = choice.message.content or "(无文本回复)"debug_lines.append(f"大模型直接回复: {direct_reply}")return None, "n".join(debug_lines), "大模型未生成搜索指令,请更明确地描述。"tool_call = choice.message.tool_calls[0]if tool_call.function.name != "search_files":debug_lines.append(f"未知工具: {tool_call.function.name}")return None, "n".join(debug_lines), f"大模型调用了未知工具: {tool_call.function.name}"try:arguments = json.loads(tool_call.function.arguments)except json.JSONDecodeError as e:debug_lines.append(f"参数 JSON 解析失败: {e}")return None, "n".join(debug_lines), f"参数解析失败: {e}"query = arguments.get("query")max_results = min(arguments.get("max_results", 1000), MAX_RESULTS_LIMIT)sort_by = arguments.get("sort_by")sort_order = arguments.get("sort_order", "ascending")debug_lines.append(f"生成的搜索语句: {query}")debug_lines.append(f"最大结果数: {max_results}")debug_lines.append(f"排序: {sort_by} ({sort_order})")debug_lines.append("正在执行本地搜索...")try:results = search_files_with_everything(query, max_results, sort_by, sort_order)error = Noneexcept Exception:results = []error = traceback.format_exc()if error:debug_lines.append(f"搜索异常: {error}")return None, "n".join(debug_lines), f"搜索执行错误: {error}"debug_lines.append(f"返回结果数: {len(results)}")if results:first_keys = list(results[0].keys())debug_lines.append(f"第一条记录的键: {first_keys}")debug_lines.append("前3条结果预览:")for item in results[:3]:debug_lines.append(str(item))return results, "n".join(debug_lines), None# ==================== GUI 界面(使用 HTTP ASR 接口) ====================class SearchGUI:def __init__(self, root):self.root = rootroot.title("本地大模型文件搜索 + 语音识别")root.geometry("1100x850")# ---------- 文件搜索原有状态 ----------self.sort_column = Noneself.sort_reverse = Falseself.size_map = {}self.file_paths = {}self.right_click_iid = Noneself.thinking_var = tk.BooleanVar(value=config.get("enable_thinking", False))# ---------- 语音识别状态 ----------self.asr_active = False# 识别是否运行中self.asr_thread = Noneself.asr_stop_event = threading.Event()self.asr_session = requests.Session()if ASR_API_KEY:self.asr_session.headers['Authorization'] = f'Bearer {ASR_API_KEY}'# 语音搜索结果防抖定时器 IDself._asr_search_after_id = None# ---------- 文件搜索输入区域 ----------frame_input = ttk.Frame(root, padding="5")frame_input.pack(fill=tk.X, padx=10, pady=5)ttk.Label(frame_input, text="输入查询(自然语言):").pack(side=tk.LEFT)self.query_var = tk.StringVar()self.entry_query = ttk.Entry(frame_input, textvariable=self.query_var, width=60)self.entry_query.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)self.entry_query.bind("<Return>", lambda e: self.start_search())self.btn_search = ttk.Button(frame_input, text="搜索", command=self.start_search)self.btn_search.pack(side=tk.LEFT, padx=5)self.chk_thinking = ttk.Checkbutton(frame_input,text="启用思考模式",variable=self.thinking_var)self.chk_thinking.pack(side=tk.LEFT, padx=5)self.btn_clear = ttk.Button(frame_input, text="清空结果", command=self.clear_results)self.btn_clear.pack(side=tk.LEFT, padx=5)# ---------- 语音识别区域 ----------asr_frame = ttk.LabelFrame(root, text="实时语音识别 (vLLM ASR)", padding="5")asr_frame.pack(fill=tk.X, padx=10, pady=5)asr_btn_frame = ttk.Frame(asr_frame)asr_btn_frame.pack(fill=tk.X, pady=2)self.asr_stream_btn = ttk.Button(asr_btn_frame, text="开始识别", command=self.toggle_asr_stream)self.asr_stream_btn.pack(side=tk.LEFT, padx=5)self.asr_status_label = ttk.Label(asr_btn_frame, text="空闲", foreground="red")self.asr_status_label.pack(side=tk.LEFT, padx=10)self.asr_text = scrolledtext.ScrolledText(asr_frame, height=5, wrap=tk.WORD, state=tk.DISABLED)self.asr_text.pack(fill=tk.BOTH, expand=True)# ---------- 状态标签(全局) ----------self.status_var = tk.StringVar(value="就绪")ttk.Label(root, textvariable=self.status_var, foreground="gray").pack(anchor=tk.W, padx=10)# ---------- 文件搜索结果 + 调试信息 ----------main_paned = ttk.PanedWindow(root, orient=tk.VERTICAL)main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)frame_result = ttk.Frame(main_paned)main_paned.add(frame_result, weight=3)columns = ("name", "path", "size", "date_modified", "date_created")self.tree = ttk.Treeview(frame_result, columns=columns, show="headings", selectmode="extended")self.tree.heading("name", text="文件名")self.tree.heading("path", text="路径")self.tree.heading("size", text="大小")self.tree.heading("date_modified", text="修改时间")self.tree.heading("date_created", text="创建时间")for col in columns:self.tree.heading(col, command=lambda c=col: self.sort_by_column(c))self.tree.column("name", width=200)self.tree.column("path", width=350)self.tree.column("size", width=80)self.tree.column("date_modified", width=150)self.tree.column("date_created", width=150)scroll_y = ttk.Scrollbar(frame_result, orient=tk.VERTICAL, command=self.tree.yview)scroll_x = ttk.Scrollbar(frame_result, orient=tk.HORIZONTAL, command=self.tree.xview)self.tree.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)self.tree.grid(row=0, column=0, sticky="nsew")scroll_y.grid(row=0, column=1, sticky="ns")scroll_x.grid(row=1, column=0, sticky="ew")frame_result.grid_rowconfigure(0, weight=1)frame_result.grid_columnconfigure(0, weight=1)self.tree.bind("<Double-1>", self.on_double_click)self.tree.bind("<Button-3>", self.on_right_click)self.context_menu = tk.Menu(self.tree, tearoff=0)self.context_menu.add_command(label="复制完整路径和文件名", command=self.copy_full_path)self.context_menu.add_command(label="打开文件所在位置", command=self.open_file_location)self.context_menu.add_command(label="打开方式...", command=self.open_with_dialog)frame_debug = ttk.Frame(main_paned)main_paned.add(frame_debug, weight=1)ttk.Label(frame_debug, text="调试信息:").pack(anchor=tk.W)self.debug_text = scrolledtext.ScrolledText(frame_debug, height=8, state='normal')self.debug_text.pack(fill=tk.BOTH, expand=True)self.lbl_count = ttk.Label(root, text="", foreground="blue")self.lbl_count.pack(anchor=tk.W, padx=10, pady=2)self.update_column_headings()# 绑定窗口关闭事件root.protocol("WM_DELETE_WINDOW", self.on_close)# ==================== 语音识别核心方法(HTTP 转录接口) ====================def toggle_asr_stream(self):"""开始/停止实时语音识别"""if not self.asr_active:self._start_asr()else:self._stop_asr()def _start_asr(self):"""启动后台识别线程"""if self.asr_active:returnself.asr_active = Trueself.asr_stop_event.clear()self.asr_stream_btn.config(text="停止识别")self.asr_status_label.config(text="识别中", foreground="green")self.asr_thread = threading.Thread(target=self._asr_worker, daemon=True)self.asr_thread.start()def _stop_asr(self):"""停止识别线程"""if not self.asr_active:return# 取消可能正在等待的搜索if self._asr_search_after_id is not None:self.root.after_cancel(self._asr_search_after_id)self._asr_search_after_id = Noneself.asr_active = Falseself.asr_stop_event.set()self.asr_stream_btn.config(text="开始识别")self.asr_status_label.config(text="空闲", foreground="red")def _asr_worker(self):"""识别线程主函数:循环读取麦克风数据,VAD分割并发送"""p = pyaudio.PyAudio()chunk = int(ASR_SAMPLE_RATE * ASR_FRAME_DURATION / 1000)# 每帧样本数vad = webrtcvad.Vad(ASR_VAD_MODE)stream = Nonetry:stream = p.open(format=pyaudio.paInt16,channels=1,rate=ASR_SAMPLE_RATE,input=True,frames_per_buffer=chunk)# VAD 状态voiced_frames = []silence_counter = 0speech_active = Falsewhile not self.asr_stop_event.is_set():try:frame = stream.read(chunk, exception_on_overflow=False)except Exception as e:self._asr_log(f"音频读取错误: {e}")breakis_speech = vad.is_speech(frame, ASR_SAMPLE_RATE)if is_speech:voiced_frames.append(frame)silence_counter = 0if not speech_active:speech_active = Trueelse:if speech_active:voiced_frames.append(frame)# 保留尾部静音silence_counter += 1if silence_counter >= ASR_SILENCE_THRESH:# 语音段结束,发送self._send_audio_segment(voiced_frames)voiced_frames = []silence_counter = 0speech_active = False# 非激活状态的静音直接丢弃except Exception as e:self._asr_log(f"识别线程异常: {e}")finally:if stream:stream.stop_stream()stream.close()p.terminate()def _send_audio_segment(self, frame_list):if not frame_list:return# 生成 WAV 字节流wav_io = io.BytesIO()with wave.open(wav_io, 'wb') as wf:wf.setnchannels(1)wf.setsampwidth(2) # 16-bitwf.setframerate(ASR_SAMPLE_RATE)wf.writeframes(b''.join(frame_list))wav_io.seek(0)try:response = self.asr_session.post(ASR_API_URL,files={'file': ('audio.wav', wav_io, 'audio/wav')},data={'model': ASR_MODEL},timeout=10)response.raise_for_status()result = response.json()raw_text = result.get('text', '').strip()# ---- 新增:去掉 "xxx<asr_text>" 前缀 ----import reclean_text = re.sub(r'^[^<]*<asr_text>s*', '', raw_text).strip()if not clean_text:self._asr_log("[识别] <空结果或仅含标记>")returnif self._is_noise(clean_text):self._asr_log(f"🔇 已过滤噪声: {clean_text}")returnself._asr_log(f"🎤 {clean_text}")self._on_asr_result(clean_text)except requests.exceptions.RequestException as e:self._asr_log(f"[错误] 转录请求失败: {e}")def _is_noise(self, text):t = text.strip().lower().rstrip('.')return t in NOISE_WORDSdef _asr_log(self, msg):"""线程安全地在语音识别文本框中追加消息"""def append():self.asr_text.config(state=tk.NORMAL)self.asr_text.insert(tk.END, msg + "n")self.asr_text.see(tk.END)self.asr_text.config(state=tk.DISABLED)self.root.after(0, append)# ---------- 语音结果触发搜索(带防抖) ----------def _on_asr_result(self, text):if self._asr_search_after_id is not None:self.root.after_cancel(self._asr_search_after_id)self._asr_search_after_id = self.root.after(500, self._execute_search_from_asr, text)def _execute_search_from_asr(self, text):self._asr_search_after_id = Noneself.query_var.set(text)self.start_search()# ==================== 原有文件搜索方法 ====================def start_search(self):query = self.query_var.get().strip()if not query:messagebox.showwarning("输入为空", "请输入查询条件")returnthinking = self.thinking_var.get()self.btn_search.config(state=tk.DISABLED)self.status_var.set("正在与大模型交互...")self.debug_text.delete("1.0", tk.END)self.debug_text.insert(tk.END, "处理中...n")self.root.update()def task():results, debug_info, error = process_user_query(query, enable_thinking=thinking)self.root.after(0, self.on_search_complete, results, debug_info, error)threading.Thread(target=task, daemon=True).start()def on_search_complete(self, results, debug_info, error):self.btn_search.config(state=tk.NORMAL)self.debug_text.delete("1.0", tk.END)if debug_info:self.debug_text.insert(tk.END, debug_info)for row in self.tree.get_children():self.tree.delete(row)self.size_map.clear()self.file_paths.clear()self.sort_column = Noneself.sort_reverse = Falseself.update_column_headings()if error:self.status_var.set(f"错误: {error}")messagebox.showerror("搜索失败", error)returnif not results:self.status_var.set("未找到任何文件")self.lbl_count.config(text="0 个结果")returnfor item in results:name = item.get("Name", "")path = item.get("Path", "")size_raw = item.get("Size", "")date_mod = item.get("DateModified", "")date_cre = item.get("DateCreated", "")try:size_display = self.format_size(int(size_raw))except (ValueError, TypeError):size_display = str(size_raw) if size_raw else ""iid = self.tree.insert("", tk.END, values=(name, path, size_display, date_mod, date_cre))self.size_map[iid] = size_rawfull_path = build_full_path(path, name)if full_path:self.file_paths[iid] = full_pathself.status_var.set(f"搜索完成,显示 {len(results)} 条结果")self.lbl_count.config(text=f"共 {len(results)} 条结果")def on_double_click(self, event):selection = self.tree.selection()if not selection:returniid = selection[0]file_path = self.file_paths.get(iid)if not file_path:messagebox.showwarning("路径缺失", "无法获取该文件的完整路径,可能解析异常。")returnif not os.path.exists(file_path):messagebox.showerror("文件不存在", f"文件未找到:n{file_path}")returntry:self.status_var.set(f"正在打开: {os.path.basename(file_path)}")self.root.update()if os.name == 'nt':os.startfile(file_path)elif os.name == 'posix':subprocess.run(['xdg-open', file_path], check=False)else:messagebox.showwarning("平台不支持", "当前操作系统不支持直接打开文件。")except Exception as e:messagebox.showerror("打开失败", f"无法打开文件:n{file_path}nn错误: {e}")finally:self.status_var.set("就绪")def on_right_click(self, event):iid = self.tree.identify_row(event.y)if not iid:returnself.tree.selection_set(iid)self.right_click_iid = iidself.context_menu.tk_popup(event.x_root, event.y_root)def copy_full_path(self):iid = getattr(self, 'right_click_iid', None)if not iid:returnpath = self.file_paths.get(iid, "")if path:self.root.clipboard_clear()self.root.clipboard_append(path)self.status_var.set("路径已复制到剪贴板")else:messagebox.showwarning("路径缺失", "无法获取文件完整路径。")def open_file_location(self):iid = getattr(self, 'right_click_iid', None)if not iid:returnpath = self.file_paths.get(iid, "")if not path:messagebox.showwarning("路径缺失", "无法获取文件完整路径。")returnif not os.path.exists(path):messagebox.showerror("文件不存在", f"文件未找到:n{path}")returntry:if os.name == 'nt':subprocess.run(['explorer', '/select,', os.path.normpath(path)])self.status_var.set(f"已打开文件位置: {path}")else:folder = os.path.dirname(path)if os.name == 'posix':subprocess.run(['xdg-open', folder], check=False)else:messagebox.showwarning("平台不支持", "当前操作系统不支持此操作。")except Exception as e:messagebox.showerror("打开位置失败", f"操作失败:n{path}nn错误: {e}")def open_with_dialog(self):iid = getattr(self, 'right_click_iid', None)if not iid:returnpath = self.file_paths.get(iid, "")if not path:messagebox.showwarning("路径缺失", "无法获取文件完整路径。")returnif not os.path.exists(path):messagebox.showerror("文件不存在", f"文件未找到:n{path}")returnif os.name != 'nt':messagebox.showwarning("平台不支持", "“打开方式”对话框仅在 Windows 上可用。")returnfile_name = os.path.basename(path)dir_path = os.path.dirname(path)try:ps_script = (f"$shell=New-Object -ComObject Shell.Application;"f"$folder=$shell.Namespace('{dir_path}');"f"$file=$folder.ParseName('{file_name}');"f"$file.InvokeVerb('openas')")proc = subprocess.run(['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps_script],capture_output=True, text=True, timeout=10)if proc.returncode == 0:self.status_var.set(f"已打开方式对话框: {file_name}")returnelse:self.debug_text.insert(tk.END, f"n[打开方式] PowerShell 失败: {proc.stderr}n")self.debug_text.see(tk.END)except Exception as e:self.debug_text.insert(tk.END, f"n[打开方式] PowerShell 异常: {e}n")self.debug_text.see(tk.END)try:os.startfile(path, 'openas')self.status_var.set(f"已打开方式对话框: {file_name}")returnexcept Exception as e:self.debug_text.insert(tk.END, f"n[打开方式] startfile openas 失败: {e}n")self.debug_text.see(tk.END)try:os.startfile(path)self.status_var.set(f"已用默认程序打开: {file_name}")returnexcept Exception as e:self.debug_text.insert(tk.END, f"n[打开方式] startfile 直接打开也失败: {e}n")self.debug_text.see(tk.END)try:subprocess.run(['explorer', '/select,', os.path.normpath(path)])messagebox.showinfo("提示","无法自动弹出“打开方式”对话框,已打开文件所在位置。n请右键该文件手动选择“打开方式”。")except Exception:messagebox.showerror("错误", "所有方法均失败,请检查系统设置。")def _check_file_association(self, ext):try:result = subprocess.run(['cmd', '/c', 'assoc', ext],capture_output=True,text=True,timeout=5)if result.returncode == 0 and result.stdout.strip():return '=' in result.stdoutexcept Exception:passtry:import winregwith winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ext) as key:default_value = winreg.QueryValue(key, None)if default_value:return Trueexcept Exception:passreturn Falsedef sort_by_column(self, col):if self.sort_column == col:self.sort_reverse = not self.sort_reverseelse:self.sort_column = colself.sort_reverse = Falseitems = self.tree.get_children('')if col == 'size':data = []for iid in items:raw = self.size_map.get(iid, '')try:key = int(raw)except (ValueError, TypeError):key = 0data.append((key, iid))else:data = [(self.tree.set(iid, col), iid) for iid in items]data.sort(key=lambda x: x[0], reverse=self.sort_reverse)for index, (_, iid) in enumerate(data):self.tree.move(iid, '', index)self.update_column_headings()def update_column_headings(self):base_texts = {"name": "文件名","path": "路径","size": "大小","date_modified": "修改时间","date_created": "创建时间"}for col, base in base_texts.items():text = baseif col == self.sort_column:text += " ▼" if self.sort_reverse else " ▲"self.tree.heading(col, text=text)def clear_results(self):# 清空文件搜索结果树for row in self.tree.get_children():self.tree.delete(row)self.status_var.set("就绪")self.lbl_count.config(text="")self.size_map.clear()self.file_paths.clear()self.sort_column = Noneself.sort_reverse = Falseself.right_click_iid = Noneself.update_column_headings()# 清空语音识别文本框self.asr_text.config(state=tk.NORMAL)self.asr_text.delete("1.0", tk.END)self.asr_text.config(state=tk.DISABLED)@staticmethoddef format_size(size_bytes):try:size_bytes = int(size_bytes)except (ValueError, TypeError):return str(size_bytes)for unit in ['B', 'KB', 'MB', 'GB', 'TB']:if size_bytes < 1024.0:return f"{size_bytes:.1f} {unit}"size_bytes /= 1024.0return f"{size_bytes:.1f} PB"def on_close(self):# 停止语音识别线程if self.asr_active:self._stop_asr()self.root.destroy()if __name__ == "__main__":root = tk.Tk()app = SearchGUI(root)root.mainloop()

代码结构概览:

模块功能
load_config()加载/创建配置文件
search_files_with_everything()通过ctypes调用Everything64.dll执行搜索
process_user_query()调用大模型解析用户意图并生成搜索参数
SearchGUITkinter主界面,包含语音识别、搜索、结果展示
_asr_worker()后台线程:麦克风采集+VAD静音检测
_send_audio_segment()将音频段发送至ASR服务端转录

6.2 程序运行截图

七、总结与展望

总结

这篇内容以海光DCU K100_AI国产算力平台,成功搭建了一套“语音识别→语义理解→文件搜索”全链路的智能文件检索系统。系统具备以下特点:

全栈国产化:从硬件(海光DCU K100_AI)到软件栈(DTK 26.04、vLLM),再到AI模型(Qwen3-ASR、Qwen3.5-9B),实现了完整的国产化技术闭环。

交互自然:用户只需说出需求,无需记忆复杂的Everything搜索语法,大模型自动完成语义到语法的转换。

响应迅速:Everything引擎实现毫秒级本地文件检索,vLLM保障模型推理的高吞吐。

功能完备:不仅支持搜索,还提供了打开文件、复制路径、打开位置、打开方式等丰富操作。

展望

未来可以在以下方向继续优化和扩展:

多模态搜索:结合Qwen3系列的多模态能力,支持“根据图片内容搜索图片文件”或“根据音频内容搜索音频文件”。

语义向量检索:不仅依赖文件名匹配,还可通过向量数据库对文件内容进行语义检索,实现“搜内容”而非“搜文件名”。

实时流式ASR:目前采用VAD分段后发送的准实时方案,未来可接入WebSocket流式转录接口,实现真正的实时语音交互。

跨平台支持:目前客户端仅支持Windows(依赖Everything),未来可适配macOS(mdfind)和Linux(locate/plocate)。

多卡分布式推理:当前ASR和大模型共用一张DCU卡(HIP_VISIBLE_DEVICES=6),未来可将两个模型部署到不同DCU卡上,进一步提升并发能力。

相关资讯
点击查看更多
游戏推荐
推荐专题
热门阅读
推荐下载