fix: 修复 CTC 重复字符识别和 CapsLock 复位时机

- CTC 解码新增空白帧检测:空白后出现相同 token 且置信度>0.5 时
  保留为重复字符(如"好好好"),无空白时仍按 CTC 规则去重
- 长按松开后先模拟 CapsLock 恢复原始状态,再开始识别
  确保识别结果注入时 CapsLock 状态已正确

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Alvin Young 2026-06-11 14:15:26 +08:00
parent 79085b862b
commit cf44a23c57
2 changed files with 49 additions and 17 deletions

View File

@ -300,20 +300,50 @@ void SenseVoiceEngine::setDebugSaveAudio(bool enable) {
LOG_INFO(kTag, QString("调试录音保存: %1").arg(enable ? "开启" : "关闭")); LOG_INFO(kTag, QString("调试录音保存: %1").arg(enable ? "开启" : "关闭"));
} }
/** CTC 贪婪解码:去重 + 去除空白 */ /** CTC 贪婪解码:去重 + 去除空白
static std::vector<int> ctcGreedyDecode(const std::vector<int>& tokens, int blankToken) { *
* SenseVoice 使 CTC token CTC
* "好好好""是是是" token
*
*
* token
*/
static std::vector<int> ctcGreedyDecode(const std::vector<int>& tokens,
const std::vector<float>& confidences,
int blankToken)
{
std::vector<int> result; std::vector<int> result;
std::vector<float> resultConf;
int prev = -1; int prev = -1;
bool prevWasBlank = false;
for (size_t i = 0; i < tokens.size(); i++) {
int token = tokens[i];
float conf = confidences.empty() ? 1.0f : confidences[i];
for (int token : tokens) {
if (token == blankToken) { if (token == blankToken) {
prev = -1; // 重置去重状态 prev = -1;
prevWasBlank = true;
continue; continue;
} }
if (token != prev) { if (token != prev) {
// 不同 token直接加入
result.push_back(token); result.push_back(token);
resultConf.push_back(conf);
} else if (prevWasBlank) {
// 相同 token 但前面有空白:可能是重复字符("好 好 好"
// 置信度足够高时保留
if (conf > 0.5f) {
result.push_back(token);
resultConf.push_back(conf);
}
// 否则认为是 CTC 重复,跳过
} }
// else: 相同 token 前面没有空白,认为是 CTC 重复,跳过
prev = token; prev = token;
prevWasBlank = false;
} }
return result; return result;
@ -482,8 +512,9 @@ RecognitionResult SenseVoiceEngine::infer(const std::vector<float>& samples,
int seqLen = static_cast<int>(shape[1]); int seqLen = static_cast<int>(shape[1]);
int vocabSize = static_cast<int>(shape[2]); int vocabSize = static_cast<int>(shape[2]);
// 6. CTC 贪婪解码 // 6. CTC 贪婪解码:收集所有帧的 token 和置信度
std::vector<int> rawTokens; std::vector<int> frameTokens;
std::vector<float> frameConfs;
float totalConf = 0.0f; float totalConf = 0.0f;
int confCount = 0; int confCount = 0;
@ -492,19 +523,20 @@ RecognitionResult SenseVoiceEngine::infer(const std::vector<float>& samples,
int bestAbsIdx = argmax(logitsData, offset, offset + vocabSize); int bestAbsIdx = argmax(logitsData, offset, offset + vocabSize);
int bestToken = bestAbsIdx - offset; // 绝对索引 → token ID int bestToken = bestAbsIdx - offset; // 绝对索引 → token ID
if (bestToken != SenseVoiceTokenizer::kTokenBlank) { float maxLogit = logitsData[bestAbsIdx];
rawTokens.push_back(bestToken); // 计算 softmax 置信度
float conf = 1.0f / (1.0f + std::exp(-maxLogit));
frameTokens.push_back(bestToken);
frameConfs.push_back(conf);
// 计算置信度 if (bestToken != SenseVoiceTokenizer::kTokenBlank) {
float maxLogit = logitsData[bestAbsIdx]; totalConf += conf;
// 近似置信度: 使用 softmax 的最大值位置
totalConf += maxLogit;
confCount++; confCount++;
} }
} }
// CTC 去重 // CTC 去重 + 重复字符检测
std::vector<int> decodedTokens = ctcGreedyDecode(rawTokens, SenseVoiceTokenizer::kTokenBlank); std::vector<int> decodedTokens = ctcGreedyDecode(frameTokens, frameConfs, SenseVoiceTokenizer::kTokenBlank);
// 计算平均置信度 (softmax) // 计算平均置信度 (softmax)
if (confCount > 0) { if (confCount > 0) {

View File

@ -150,10 +150,10 @@ void VoiceInputService::onHotkeyDeactivated() {
simulateCapsLock(); simulateCapsLock();
emit statusChanged("短按:切换 CapsLock"); emit statusChanged("短按:切换 CapsLock");
} else { } else {
// 长按 → 停止录音并转写 // 长按 → 先恢复 CapsLock 状态,再开始识别
stopRecordingAndTranscribe(); // 这样识别结果注入时 CapsLock 已恢复原始状态
// 恢复 CapsLock 原始状态(系统已处理了一次 CapsLock 切换)
simulateCapsLock(); simulateCapsLock();
stopRecordingAndTranscribe();
} }
longPressDetected_ = false; longPressDetected_ = false;