首页/新闻资讯/正文详情

Qt 离线语音识别:FFmpeg + sherpa-onnx(SenseVoice)视频转文字工具

发布时间:2026/9/25 20:56:57 来源:云帆数科 栏目:资讯中心
Qt 离线语音识别:FFmpeg + sherpa-onnx(SenseVoice)视频转文字工具
本文内容由 AI 辅助生成仅供学习参考。项目Qt5.14.2 MinGW64 FFmpeg sherpa-onnx-offlineSenseVoice 离线语音识别方案QProcess 调用外部 exe不编译源码项目整体架构运行依赖清单UI 控件清单MainWindow文件目录结构Release 打包目录各组件下载地址 版本注意事项5.1 FFmpeg 下载5.2 sherpa-onnx-offline.exe 下载5.3 语音模型下载 - ModelScope 国内阿里镜像备选编译 打包 测试流程6.1 组包结构6.2 测试方法调试踩坑问题汇总完整源码 mainwindow.h/mainwindow.cpp限制说明效果1. 项目整体架构视频文件 → QProcess调用ffmpeg → 提取16k 单通道pcm wav音频 → QProcess调用sherpa-onnx-offline.exeSenseVoice模型→ 输出JSON识别结果 → Qt解析JSON → 展示文本 保存txt / 生成srt字幕 → UI选择视频按钮、导出按钮、文本预览框2. 运行依赖清单Release 打包时必须一起复制到 release 目录Qt 运行库MinGW64Qt5 核心 dllQt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll平台插件platforms/qwindows.dll等windeployqt自动导出。FFmpeg 二进制文件ffmpeg.exe、ffprobe.exe作用视频解封装提取音频强制输出16000Hz单声道pcm_s16le wav命令ffmpeg -i test.mp4 -vn -acodec pcm_s16le -ar 16000 -ac 1 temp_audio.wav -ySherpa-onnx 离线识别程序sensevoicesherpa-onnx-offline.exe 配套全部 dll模型文件夹models/model.int8.onnxSenseVoice int8 模型tokens.txt命令sherpa-onnx-offline.exe --sense-voice-model./models/model.int8.onnx --tokens./models/tokens.txt --sense-voice-languageauto temp_audio.wavQt 内置模块无需额外安装库QProcess启动外部进程、读取输出、kill 终止进程QJsonDocument/QJsonObject/QJsonArray解析识别 JSON 结果QFile/QTextStream保存 txt、srt 字幕QProgressBar、QPushButton、QTextEditUI 界面3. UI 控件清单MainWindow表格控件对象名用途QPushButtonbtnStart开始任务选择视频启动 ffmpeg 音频提取QPushButtonbtnSaveTxt导出txtQPushButtonbtnSaveSrt导出srtQTextEdittextResult日志 识别文本预览QTextEdittextSubtitleView显示详细日志信息QCheckBoxchkDebugLog显示详细调试日志信息QPushButtonbtnSelectVideo打开文件选择框选择视频4. 文件目录结构Release 打包目录release/ ├─ VideoAsr.exe # Qt编译出来的主程序 ├─ ffmpeg.exe ├─ ffprobe.exe ├─ sherpa-onnx-offline.exe ├─ sherpa一堆依赖dll ├─ temp_audio.wav # 临时音频运行自动生成结束可选择删除 ├─ models/ │ ├─ model.int8.onnx │ └─ tokens.txt ├─ platforms/ # Qt平台插件windeployqt生成 └─ Qt一系列dllwindeployqt自动拷贝5. 各组件下载地址 版本注意事项5.1 FFmpeg 下载官方下载网址https://ffmpeg.org/download.html#build-windowsessential简洁版本推荐使用体积小满足音视频提取需求full完整版本包含大量编解码器体积更大5.2 sherpa-onnx-offline.exe 下载官方下载方法网址win64 位下载界面。下载依赖网络有时候可以正常下载有时候下载不了https://k2-fsa.github.io/sherpa/onnx/install/windows/generated/download/windows_x64.html选这个包下载sherpa-onnx-v1.13.8-win-x64-shared-MT-Release-no-tts.tar.bz2win-x6464 位 Windowsshared-MT-Release发布正式版带 exe 依赖 dllno-tts不带语音合成只包含语音识别体积更小⚠️重要后缀是.tar.bz2Windows 自带解压工具打不开必须安装 7-Zip 解压解压取出 sherpa-onnx-offline.exe 步骤用 7-Zip 打开这个 tar.bz2 压缩包全部解压到本地文件夹进入解压后的bin文件夹里面就能找到sherpa-onnx-offline.exe✅把 bin 文件夹里面【所有 exe 全部 dll 文件一起复制】放到你的 Qt 程序输出目录和你的 Qt 生成的 exe 放在同一文件夹少一个 dll 都会报错注意如果访问这个链接 https://github.com/k2-fsa/sherpa-onnx/releases/tag/v1.13.8 直接下载 exe 包这个 exe 包是 GUI 演示程序不是命令行工具。因此实际运行时会跳出来模型下载说明窗口不要选用。5.3 语音模型下载 - ModelScope 国内阿里镜像备选什么是语音模型为什么下载语音模型sherpa-onnx-offline.exe 只是程序外壳本身没有听懂语音的能力。语音模型就是训练好的 AI 神经网络文件负责把音频转成文字。model.int8.onnx核心 AI 模型文件int8 量化压缩CPU 速度快体积小中文识别准确率高tokens.txt字符映射字典AI 识别出来的数字编码靠这个文件翻译成中文汉字关于下载版本要求不要混用不同发布时间的模型和 sherpa-onnx exe很容易出现这类 ONNX 输入节点缺失报错下载模型的时候看页面标注的 sherpa-onnx 最低版本要求必须满足下载地址https://modelscope.cn/models/baiyang2458/sherpa-onnx-sense-voice-zh-int8/files文件sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2025-09-09.tar.bz2大小165.78MB这个压缩包里面就包含我们需要的model.int8.onnx tokens.txt支持普通话、英语、粤语、日语、韩语中文识别完全够用。6. 编译 打包 测试流程6.1 组包结构严格按照此结构放置文件参考上面第 4 章节目录结构。6.2 测试方法进入 release 文件夹打开 PowerShell 命令窗口测试 1ffmpeg验证是否运行.\ffmpeg.exe -i test.mp4 -vn -acodec pcm_s16le -ar 16000 -ac 1 temp_audio.wav -y✅成功无报错文件夹生成temp_audio.wav❌失败排查文件不在当前目录、文件名中文 / 空格需要英文双引号包裹路径ffmpeg 缺失测试 2验证 sherpa 命令行识别.\sherpa-onnx-offline.exe --sense-voice-model ./models/model.int8.onnx --tokens ./models/tokens.txt temp_audio.wav✅成功不弹出 GUI 窗口控制台直接打印识别出来的中文文本❌失败排查exe 名字写错或者放成 GUI 演示程序models 文件夹缺失、模型文件名不对缺少 sherpa 依赖 dll直接闪退wav 音频不是 16000Hz 16bit 单声道完整编译打包步骤Qt 切换 Release 模式编译生成 VideoAsr.exerelease 目录执行命令windeployqt VideoAsr.exe自动拷贝 Qt 依赖 dll手动复制ffmpeg.exe、sherpa-onnx-offline.exe、sherpa 所有 dll、models 文件夹到 release 目录分步测试重要测试 1PowerShell 手动执行 ffmpeg确认能生成 temp_audio.wav测试 2PowerShell 手动执行 sherpa 命令确认输出 JSON 识别结果测试 3界面完整跑一遍选视频 → 提取音频 → ASR 识别 → 保存 txt/srt测试 4中途点击停止按钮验证进程终止、UI 重置打包压缩整个 release 文件夹其他 Windows 电脑直接解压使用无需安装 Qt/Python7. 调试踩坑问题汇总表格问题现象原因解决方案pip 安装 sherpa-onnx-bin 找不到 bin 目录pip 安装完找不到 sherpa-onnx-offline.exesherpa-onnx-bin 是 Python 库不是独立 exe官网下载 windows x64 tar.bz2 二进制包不要 pip.\sherpa-onnx-offline.exe 无法识别powershell 提示找不到程序文件缺失 / 缺少 dll / 文件名错误完整解压 tar.bz2exe 所有 dll 拷贝到 releaseffmpeg : 无法将 ffmpeg 识别为命令命令提示找不到 ffmpeg没有写.\ffmpeg.exe当前目录无程序Qt 代码使用 exe 绝对路径命令行使用.\ffmpeg.exeFFmpeg No such file or directoryffmpeg 启动但是找不到视频文件相对路径失效工作目录错乱代码使用applicationDirPath()拼接绝对路径connect no matching member function for call to ‘connect’编译报错QProcess finished 信号歧义finished 存在重载函数使用qOverloadint(QProcess::finished)No such slot MainWindow::onAsrFinished(int)运行控制台提示不存在槽函数h 文件没有在 slots 声明槽函数头文件 private slots 添加函数声明sherpa 卡在 Started长时间无 JSON 输出模型加载成功但是不输出识别结果音频格式不对 / 音频太长 / 模型损坏powershell 单独执行 sherpa 命令排查确认 wav 为 16k 单通道 pcm相对路径不稳定Qt IDE 内运行和双击 exe 运行路径不一样程序工作目录变化全部资源使用绝对路径拼接停止按钮无法终止进程点击停止后台 ffmpeg/sherpa 继续运行仅销毁 QProcess 对象没有 killstop 槽调用m_proc-kill()增加运行状态标记ASR 无实时进度进度条只能固定模拟sherpa 命令行 exe 不输出推理进度FFmpeg 0~50%ASR 模拟 50~100%进阶使用 sherpa C SDK打包到其他电脑闪退本机正常别的电脑双击崩溃缺少 Qt dll /sherpa 配套 dllwindeployqt 导出 Qt 库完整拷贝 sherpa 全部 dll8. 完整源码mainwindow.h#ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include QProcess #include QDir #include QCoreApplication #include QProgressBar #include QTimer QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: /** * brief on_btnSelectVideo_clicked 【选择视频按钮】 * 弹出文件选择框选中视频后自动启动FFmpeg提取音频开始整个ASR任务 */ void on_btnSelectVideo_clicked(); /** * brief onFfmpegStdErr 【FFmpeg stderr输出回调】 * 读取ffmpeg输出流解析time字段实时更新当前已转换音频时长 */ void onFfmpegStdErr(); /** * brief onFfmpegFinished 【FFmpeg进程结束回调】 * 音频提取完成/失败后触发成功则启动sherpa-onnx离线识别 * param exitCode 进程退出码0代表正常成功 */ void onFfmpegFinished(int exitCode); /** * brief onAsrReadOutput 【Sherpa识别输出回调】 * 实时读取识别文本输出追加到结果缓存实时预览字幕 */ void onAsrReadOutput(); /** * brief onAsrFinished 【Sherpa识别进程结束回调】 * 语音识别全部完成汇总结果更新UI状态 * param exitCode 进程退出码 */ void onAsrFinished(int exitCode); /** * brief onAsrStdErr 【接收 sherpa 的 stderr 调试信息】 * 接收 sherpa 的 stderr 调试信息 */ void onAsrStdErr(); /** * brief on_btnStop_clicked 【停止任务按钮】 * 强制kill FFmpeg和Sherpa进程终止当前任务重置UI */ void on_btnStop_clicked(); /** * brief on_btnSaveTxt_clicked 【保存TXT按钮】 * 将全部识别文本保存为utf-8 txt文本文件 */ void on_btnSaveTxt_clicked(); /** * brief on_btnSaveSrt_clicked 【保存SRT字幕按钮】 * 简易SRT字幕保存整段识别内容作为单条字幕 */ void on_btnSaveSrt_clicked(); /** * brief on_chkDebugLog_clicked 【调试日志复选框点击事件】 * 切换是否开启详细调试日志 */ void on_chkDebugLog_clicked(); private: Ui::MainWindow *ui; QString m_videoPath; // 选中的视频完整路径 QString m_tempWavPath; // 临时wav音频文件完整路径 QProcess* m_ffmpegProc nullptr; QProcess* m_asrProc nullptr; bool m_isRunning false; // 任务运行标记true正在处理 QString m_fullResultText; // 完整识别结果文本缓存 double m_videoTotalSec 0.0; // 视频总时长(秒)用于计算FFmpeg进度百分比 bool m_enableDebugLog false;// 详细调试日志总开关 QProgressBar* m_progressBar; // 状态栏进度条 /** * brief getVideoTotalDuration 使用ffprobe读取视频总时长秒 * param videoPath 视频文件路径 * return 总秒数读取失败返回0 */ double getVideoTotalDuration(const QString videoPath); /** * brief parseFfmpegTime 解析ffmpeg time字符串 00:01:23.45 转为总秒数 * param timeStr 时间字符串 * return 浮点秒数 */ double parseFfmpegTime(const QString timeStr); /** * brief updateSubtitlePreview 更新字幕预览框 * param text 识别文本实时显示在预览控件 */ void updateSubtitlePreview(const QString text); /** * brief resetUiState 重置UI控件状态任务结束/中断后调用 */ void resetUiState(); /** * brief cleanTempAudio 删除临时wav音频文件 */ void cleanTempAudio(); /** * brief appendLog 统一日志输出函数 * param msg 日志文本 * param isDebugMsg true调试详细日志存入m_fullGlobalLogfalse基础状态日志永远输出到textResult */ void appendLog(const QString msg, bool isDebugMsg false); QListdouble m_tsList; // 存储timestamps时间戳数组 QString m_rawAsrJson; // 保存sherpa原始完整json字符串 QByteArray m_stdoutBuffer;//缓存 QString m_fullAsrDebugLog;//显示全部调试信息 QString m_fullGlobalLog; //【新增】全局完整日志后台持续保存所有操作日志 void showProgressBar(); void hideProgressBar(); }; #endif // MAINWINDOW_Hmainwindow.cpp#include mainwindow.h #include ui_mainwindow.h #include QMessageBox #include QRegularExpression #include QDateTime #include QFileDialog #include QTextStream #include QFile #include QJsonDocument #include QJsonObject #include QJsonArray #include QScrollBar /** * brief MainWindow 构造函数 * 初始化UI、初始化进程指针绑定信号槽初始化状态栏进度条 */ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui-setupUi(this); // 状态栏进度条初始化 m_progressBar new QProgressBar(this); m_progressBar-setRange(0,100); m_progressBar-setValue(0); m_progressBar-setFixedWidth(280); // 加宽一点更好看 m_progressBar-setFixedHeight(16); // 高度调小适配状态栏 m_progressBar-setTextVisible(true); // 显示百分比文字 m_progressBar-setAlignment(Qt::AlignCenter); hideProgressBar(); // 默认隐藏 m_progressBar-setStyleSheet(R( QProgressBar{ border-radius:8px; background-color:#E5E7EB; text-align:center; color:#222222; font-size:10px; } QProgressBar::chunk{ border-radius:8px; background-color:#2563EB; } )); // 添加到状态栏永久右侧控件 statusBar()-addPermanentWidget(m_progressBar); statusBar()-showMessage(就绪); ui-btnStop-setEnabled(false); ui-chkDebugLog-setChecked(false); // 默认关闭调试日志 m_enableDebugLog false; ui-textSubtitleView-setVisible(false); //【新增】默认隐藏详细日志窗口 m_fullGlobalLog.clear(); //初始化全局日志 } /** * brief ~MainWindow 析构函数 * 程序关闭时强制杀死残留的ffmpeg、sherpa进程释放资源 */ MainWindow::~MainWindow() { if(m_ffmpegProc) { m_ffmpegProc-kill(); m_ffmpegProc-waitForFinished(1000); } if(m_asrProc) { m_asrProc-kill(); m_asrProc-waitForFinished(1000); } delete ui; } void MainWindow::showProgressBar() { m_progressBar-show(); } void MainWindow::hideProgressBar() { m_progressBar-hide(); } /** * brief appendLog 统一日志输出函数 * param msg 日志文本 * param isDebugMsg true调试详细日志存入m_fullGlobalLogfalse基础状态日志永远输出到textResult */ void MainWindow::appendLog(const QString msg, bool isDebugMsg) { if(isDebugMsg) { // 调试日志追加到全局日志缓存带时间戳 QString timeStr QDateTime::currentDateTime().toString(HH:mm:ss); QString line QString([%1] %2).arg(timeStr, msg); m_fullGlobalLog line \n; // 勾选调试框实时刷新textSubtitleView并滚动到底 if(m_enableDebugLog) { ui-textSubtitleView-setPlainText(m_fullGlobalLog); QScrollBar *vbar ui-textSubtitleView-verticalScrollBar(); if(vbar) { vbar-setValue(vbar-maximum()); } } } else { // 基础业务日志直接输出到textResult不受调试开关控制 ui-textResult-append(msg); } } /** * brief on_chkDebugLog_clicked 【调试日志复选框点击事件】 * 切换是否开启详细调试日志 */ void MainWindow::on_chkDebugLog_clicked() { m_enableDebugLog ui-chkDebugLog-isChecked(); ui-textSubtitleView-setVisible(m_enableDebugLog); if(m_enableDebugLog) { appendLog(✅已开启详细调试日志, false); // 打开日志窗口加载已经收集的全部历史日志 ui-textSubtitleView-setPlainText(m_fullGlobalLog); QScrollBar *vbar ui-textSubtitleView-verticalScrollBar(); if(vbar) { vbar-setValue(vbar-maximum()); } } else { appendLog(✅已关闭详细调试日志, false); } } /** * brief on_btnSelectVideo_clicked 【选择视频按钮】 * 弹出文件选择框选中视频后自动启动FFmpeg提取音频开始整个ASR任务 */ void MainWindow::on_btnSelectVideo_clicked() { if(m_isRunning) { QMessageBox::warning(this,提示,任务正在运行请先停止); return; } QString videoFile QFileDialog::getOpenFileName(this,tr(选择视频),QDir::homePath(),tr(视频文件 (*.mp4 *.mov *.avi *.mkv))); if(videoFile.isEmpty()) return; m_videoPath videoFile; // 状态栏显示视频路径不再使用labelVideoPath statusBar()-showMessage(QString(已选中视频%1).arg(m_videoPath)); // 临时wav放在程序exe同目录避免工作目录错乱 QString appDir QCoreApplication::applicationDirPath(); m_tempWavPath QDir(appDir).filePath(temp_audio.wav); // 清空旧数据 m_fullResultText.clear(); ui-textResult-clear(); ui-textSubtitleView-clear(); m_fullGlobalLog.clear(); //【重点】新视频清空全局调试日志 m_progressBar-setValue(0); m_isRunning true; ui-btnStop-setEnabled(true); ui-btnSelectVideo-setEnabled(false); // 任务运行时禁用选择视频按钮 showProgressBar(); // 开始任务显示状态栏进度条 // 【第一步】读取视频总时长用于进度百分比计算 m_videoTotalSec getVideoTotalDuration(m_videoPath); if(m_videoTotalSec 0) { appendLog(⚠️警告读取视频总时长失败进度条只能显示原始时间无法计算百分比, false); } // 启动ffmpeg提取音频 m_ffmpegProc new QProcess(this); QStringList args; args -i m_videoPath -vn -acodec pcm_s16le -ar 16000 -ac 1 -y m_tempWavPath; // 合并通道捕获stderr解析进度 m_ffmpegProc-setProcessChannelMode(QProcess::MergedChannels); connect(m_ffmpegProc, SIGNAL(readyReadStandardError()), this, SLOT(onFfmpegStdErr())); connect(m_ffmpegProc, SIGNAL(finished(int)), this, SLOT(onFfmpegFinished(int))); m_ffmpegProc-start(QDir(appDir).filePath(ffmpeg.exe), args); appendLog(QString(选中视频%1).arg(m_videoPath), false); appendLog(开始音频提取..., false); } /** * brief onFfmpegStdErr 【FFmpeg stderr输出回调】 * 读取ffmpeg输出流解析time字段实时更新当前已转换音频时长计算进度百分比 */ void MainWindow::onFfmpegStdErr() { if(!m_ffmpegProc) return; QString data QString::fromUtf8(m_ffmpegProc-readAllStandardError()); static QRegularExpression re(time(\\d):(\\d):(\\d)\\.(\\d)); auto match re.match(data); if(match.hasMatch()) { int h match.captured(1).toInt(); int m match.captured(2).toInt(); int s match.captured(3).toInt(); double currentSec h*3600 m*60 s; // 标记为调试日志存入m_fullGlobalLog appendLog(QString(FFmpeg已转换%1 秒).arg(currentSec), true); // 有总时长计算FFmpeg阶段进度(0~50%) if(m_videoTotalSec 0) { double percent currentSec / m_videoTotalSec * 50.0; if(percent 50) percent 50; m_progressBar-setValue(static_castint(percent)); } } } /** * brief onFfmpegFinished 【FFmpeg进程结束回调】 * 音频提取完成/失败后触发成功则启动sherpa-onnx离线识别 * param exitCode 进程退出码0代表正常成功 */ void MainWindow::onFfmpegFinished(int exitCode) { m_ffmpegProc-deleteLater(); m_ffmpegProc nullptr; if(!m_isRunning) { resetUiState(); return; } if(exitCode ! 0) { appendLog(❌FFmpeg音频提取失败, false); resetUiState(); return; } appendLog(✅音频提取完成开始离线语音识别, false); m_progressBar-setValue(50); statusBar()-showMessage(正在ASR语音识别...); // 启动 sherpa-onnx-offline 识别 m_asrProc new QProcess(this); QString appDir QCoreApplication::applicationDirPath(); m_asrProc-setWorkingDirectory(appDir); QStringList args; args --sense-voice-model./models/model.int8.onnx; args --tokens./models/tokens.txt; args --sense-voice-languageauto; args m_tempWavPath; // 重点绑定stdout和stderr两个信号 connect(m_asrProc, SIGNAL(readyReadStandardOutput()), this, SLOT(onAsrReadOutput())); connect(m_asrProc, SIGNAL(readyReadStandardError()), this, SLOT(onAsrStdErr())); connect(m_asrProc, SIGNAL(finished(int)), this, SLOT(onAsrFinished(int))); // 清空缓存防止上次任务残留 m_stdoutBuffer.clear(); m_fullAsrDebugLog.clear(); m_asrProc-start(QDir(appDir).filePath(sherpa-onnx-offline.exe), args); appendLog(sherpa进程已启动等待识别输出..., false); } /** * brief onAsrReadOutput 【Sherpa识别输出回调】 * 只收集stdout原始数据存入调试日志 */ void MainWindow::onAsrReadOutput() { if(!m_asrProc) return; QByteArray data m_asrProc-readAllStandardOutput(); m_stdoutBuffer data; QString stdoutText QString::fromUtf8(data); appendLog(QString([SHERPA STDOUT] %1).arg(stdoutText), true); m_fullAsrDebugLog stdoutText; } /** * brief onAsrStdErr 【接收 sherpa 的 stderr 调试信息】 */ void MainWindow::onAsrStdErr() { if(!m_asrProc) return; QString errText QString::fromUtf8(m_asrProc-readAllStandardError()); appendLog(QString([SHERPA STDERR] %1).arg(errText), true); m_fullAsrDebugLog errText; } /** * brief onAsrFinished 【Sherpa识别进程结束回调】 * 语音识别全部完成汇总结果解析完整JSON * param exitCode 进程退出码 */ void MainWindow::onAsrFinished(int exitCode) { m_asrProc-deleteLater(); m_asrProc nullptr; m_progressBar-setValue(100); QString out QString::fromUtf8(m_stdoutBuffer).trimmed(); m_rawAsrJson out; if(exitCode 0) { appendLog(✅识别任务全部完成, false); statusBar()-showMessage(识别完成); m_progressBar-setValue(100); QJsonParseError jsonErr; QJsonDocument doc QJsonDocument::fromJson(m_stdoutBuffer, jsonErr); if(jsonErr.error QJsonParseError::NoError doc.isObject()) { QJsonObject obj doc.object(); QString pureText obj[text].toString().trimmed(); m_fullResultText pureText; QJsonArray tsArr obj[timestamps].toArray(); m_tsList.clear(); for(auto item : tsArr) { m_tsList.append(item.toDouble()); } appendLog(QString(【识别文本】%1).arg(m_fullResultText), false); appendLog(QString(Sherpa原始JSON%1).arg(out), true); } else { appendLog(⚠️JSON解析失败原始输出 out, false); appendLog(QString(JSON解析失败原始输出%1).arg(out), true); } } else { appendLog(QString(❌识别进程异常退出退出码%1).arg(exitCode), false); appendLog(QString(❌识别进程异常退出exitCode%1).arg(exitCode), true); statusBar()-showMessage(识别任务失败); m_progressBar-setValue(0); } cleanTempAudio(); resetUiState(); } /** * brief on_btnStop_clicked 【停止任务按钮】 * 强制kill FFmpeg和Sherpa进程终止当前任务重置UI */ void MainWindow::on_btnStop_clicked() { appendLog(⚠️用户点击停止按钮终止任务, true); if(m_ffmpegProc) { m_ffmpegProc-kill(); m_ffmpegProc-waitForFinished(1000); m_ffmpegProc-deleteLater(); m_ffmpegProc nullptr; } if(m_asrProc) { m_asrProc-kill(); m_asrProc-waitForFinished(1000); m_asrProc-deleteLater(); m_asrProc nullptr; } appendLog(⚠️任务已手动停止, false); statusBar()-showMessage(任务手动终止); cleanTempAudio(); resetUiState(); } /** * brief on_btnSaveTxt_clicked 【保存TXT按钮】 * 将全部识别文本保存为utf-8 txt文本文件 */ void MainWindow::on_btnSaveTxt_clicked() { if(m_fullResultText.trimmed().isEmpty()) { QMessageBox::warning(this,提示,暂无识别结果); return; } QString savePath QFileDialog::getSaveFileName(this,保存文本,QDir::homePath()/result.txt,Text (*.txt)); if(savePath.isEmpty()) return; QFile f(savePath); if(!f.open(QIODevice::WriteOnly|QIODevice::Text)) { QMessageBox::critical(this,错误,保存失败无法打开文件); return; } QTextStream ts(f); ts.setCodec(UTF-8); ts m_fullResultText; f.close(); appendLog(QString(TXT识别结果保存至%1).arg(savePath), true); QMessageBox::information(this,成功,TXT文本已保存); } /** * brief on_btnSaveSrt_clicked 【保存SRT字幕按钮】 * 简易SRT字幕保存整段识别内容作为单条字幕 */ void MainWindow::on_btnSaveSrt_clicked() { if(m_fullResultText.trimmed().isEmpty() || m_tsList.isEmpty()) { QMessageBox::warning(this,提示,暂无识别结果或时间戳数据); return; } QString savePath QFileDialog::getSaveFileName(this,保存SRT字幕,QDir::homePath()/result.srt,Subtitle (*.srt)); if(savePath.isEmpty()) return; QFile f(savePath); if(!f.open(QIODevice::WriteOnly|QIODevice::Text)) { QMessageBox::critical(this,错误,SRT保存失败无法打开文件); return; } QTextStream ts(f); ts.setCodec(UTF-8); ts 1\n; ts 00:00:00,000 -- 99:59:59,000\n; ts m_fullResultText.trimmed() \n\n; f.close(); appendLog(QString(SRT字幕保存至%1).arg(savePath), true); QMessageBox::information(this,成功,SRT字幕已保存); } /** * brief parseFfmpegTime 解析ffmpeg time字符串 00:01:23.45 转为总秒数 * param timeStr 时间字符串 * return 浮点秒数 */ double MainWindow::parseFfmpegTime(const QString timeStr) { QStringList parts timeStr.split(:); if(parts.size()3) return 0; double h parts[0].toDouble(); double m parts[1].toDouble(); double s parts[2].toDouble(); return h*3600 m*60 s; } /** * brief getVideoTotalDuration 使用ffprobe读取视频总时长秒 * param videoPath 视频文件路径 * return 总秒数读取失败返回0 */ double MainWindow::getVideoTotalDuration(const QString videoPath) { QProcess probe; QString appDir QCoreApplication::applicationDirPath(); QStringList args; args -v error -select_streams v:0 -show_entries streamduration -of defaultnoprint_wrappers1:nokey1 videoPath; probe.start(QDir(appDir).filePath(ffprobe.exe), args); if(!probe.waitForFinished(3000)) return 0; QString durStr QString::fromUtf8(probe.readAllStandardOutput()).trimmed(); return durStr.toDouble(); } /** * brief updateSubtitlePreview 更新字幕预览框 * param text 识别文本实时显示在预览控件 */ void MainWindow::updateSubtitlePreview(const QString text) { ui-textSubtitleView-setPlainText(text); } /** * brief resetUiState 重置UI控件状态任务结束/中断后调用 */ void MainWindow::resetUiState() { m_isRunning false; ui-btnStop-setEnabled(false); ui-btnSelectVideo-setEnabled(true); hideProgressBar(); //任务结束隐藏状态栏进度条 } /** * brief cleanTempAudio 删除临时wav音频文件 */ void MainWindow::cleanTempAudio() { QFile tempFile(m_tempWavPath); if(tempFile.exists()) { tempFile.remove(); appendLog(临时音频文件已自动删除, false); appendLog(临时音频文件已自动删除, true); } }9. 限制说明外部 exe 方案会生成临时 wav 音频文件分发需要附带 ffmpeg、sherpa exe、dll、模型文件夹模型文件体积较大首次加载模型存在几秒耗时。后续扩展拿到 sherpa 输出完整 JSON 后补充 JSON 解析代码提取 text 和 timestamps调用 saveTxt/saveSrt 保存字幕。10.效果

相关推荐

华为提出“2035年9000亿级AI Agent”想象:真正的AI下一站不是聊天,而是替人做事
华为提出“2035年9000亿级AI Agent”想象:真正的AI下一站不是聊天,而是替人做事

如果说2023年是聊天机器人之年,2024年是多模态之年,2025年开始进入AI Agent热潮,那么2026年越来越明显的趋势是:AI正在从“回答问题”向“执行任务”转变。 本周,华为发布相关“Intelligent World 2035”判断&#xff… · 2026/9/25 20:56:49

挖掘16自由度潜力,Allegro Hand实现多物体分批抓取
挖掘16自由度潜力,Allegro Hand实现多物体分批抓取

在机器人灵巧手科研领域,Allegro Hand是使用非常广泛的四指科研灵巧手。它拥有拇指加三根手指,共16个主动驱动自由度,手掌尺寸接近成年人手掌,常搭载于Franka协作机械臂末端,用来开展抓取、手内操作算法验证。即便硬件… · 2026/9/25 20:56:43

C++语言原理与实践(八):string类的底层实现
C++语言原理与实践(八):string类的底层实现

本篇目标:掌握string类的部分接口的实现一. string类的底层实现1.构造函数首先我们为了与tsd库里面的string 隔离一下,我们需要定义一个自己的命名空间,我的就是kong了,C 中的字符串本质上是由字符数组构成的,而 C 风格… · 2026/9/25 20:56:37

TypeDoc 中 @throws 标签详解:为 TypeScript 函数与方法标注异常
TypeDoc 中 @throws 标签详解:为 TypeScript 函数与方法标注异常

开发工具文档 【免费下载链接】typedoc Documentation generator for TypeScript projects. 项目地址: https://gitcode.com/gh_mirrors/ty/typedoc 点击查看 免费下载 throws 是 TypeDoc 支持的标准块级标签(Block Tag),用于在函… · 2026/9/26 2:04:27

Windows-universal-samples 系统媒体传输控件(SMTC)手动集成指南:JavaScript 示例源码级解析
Windows-universal-samples 系统媒体传输控件(SMTC)手动集成指南:JavaScript 示例源码级解析

示例工程 【免费下载链接】Windows-universal-samples API samples for the Universal Windows Platform. 项目地址: https://gitcode.com/gh_mirrors/wi/Windows-universal-samples 点击查看 免费下载 本指南围绕 Windows-universal-samples 仓库中 archived/Syst… · 2026/9/26 2:04:27

群晖NAS硬盘兼容性实操指南:用 Synology HDD db 脚本把第三方硬盘加进 DSM 兼容库
群晖NAS硬盘兼容性实操指南:用 Synology HDD db 脚本把第三方硬盘加进 DSM 兼容库

群晖NAS硬盘兼容性实操指南:用 Synology HDD db 脚本把第三方硬盘加进 DSM 兼容库 【免费下载链接】Synology_HDD_db Add your HDD, SSD and NVMe drives to your Synologys compatible drive database and a lot more 项目地址: https://gitcode.com/GitHub_Tren… · 2026/9/26 2:04:27

定序Probit模型实战:信用卡信用评级从建模到决策
定序Probit模型实战:信用卡信用评级从建模到决策

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 2:04:27

森林火灾检测数据集实战:362张VOC+YOLO双格式,从零跑通yolov8训练
森林火灾检测数据集实战:362张VOC+YOLO双格式,从零跑通yolov8训练

简介:本资源为森林火灾检测数据集,面向从事目标检测算法训练与验证的开发者、学生及科研人员,可用于火灾场景下的火焰识别模型训练与效果评估。数据集采用Pascal VOC与YOLO双格式标注,包含jpg图片及对应的xml、txt标注文件&#x… · 2026/9/26 2:04:07

白光干涉仪在薄膜工艺缺陷诊断中的指纹图谱分析
白光干涉仪在薄膜工艺缺陷诊断中的指纹图谱分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 2:04:01

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码