文件
mynode/packages/whisper-voice-typing/cpp/utils/WhisperSession.cpp
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

250 行
8.3 KiB
C++

#include "WhisperSession.hpp"
#include <utility>
#include <sstream>
#include <algorithm>
#include "whisper.h"
#include "findLongestSilence.hpp"
#include "androidUtil.h"
WhisperSession::WhisperSession(const std::string& modelPath, std::string lang, std::string prompt, bool shortAudioContext)
: lang_ {std::move(lang)}, prompt_ {std::move(prompt)}, shortAudioContext_ {shortAudioContext} {
whisper_context_params contextParams = whisper_context_default_params();
// Lifetime(pModelPath): Whisper.cpp creates a copy of pModelPath and stores it in a std::string.
// whisper_init_from_file_with_params doesn't seem to otherwise save pModelPath. As such, it's
// safe to pass a pointer to a std::string's representation:
const char *pModelPath = modelPath.c_str();
pContext_ = whisper_init_from_file_with_params(pModelPath, contextParams);
if (pContext_ == nullptr) {
throw std::runtime_error("Unable to initialize the Whisper context.");
}
}
WhisperSession::~WhisperSession() {
if (pContext_ != nullptr) {
whisper_free(pContext_);
}
}
whisper_full_params
WhisperSession::buildWhisperParams_() {
whisper_full_params params = whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
// WHISPER_SAMPLING_BEAM_SEARCH is an alternative to greedy:
// params.beam_search = { .beam_size = 2 };
params.print_realtime = false;
// Disable timestamps: They make creating custom Whisper models more difficult:
params.print_timestamps = false;
params.no_timestamps = true;
params.print_progress = false;
params.translate = false;
params.offset_ms = 0;
params.single_segment = true;
// Avoid non-speech tokens (e.g. "(crackle)"). For now, this is disabled because it seems to
// cause increased hallucinations (e.g. repeated "Thank you"s).
// params.suppress_nst = true;
params.temperature = 0; // Initial randomness
// There's also a temperature_inc variable, which is used when decoding fails (Whisper increases
// the temperature by temperature_inc and retries).
// Following the whisper streaming example in setting prompt_tokens to nullptr
// when using VAD (Voice Activity Detection)
params.initial_prompt = prompt_.c_str();
params.prompt_tokens = nullptr;
params.prompt_n_tokens = 0;
params.audio_ctx = 0;
// Lifetime: lifetime(params) < lifetime(lang_) = lifetime(this).
params.language = lang_.c_str();
return params;
}
std::string
WhisperSession::transcribe_(const std::vector<float>& audio, size_t transcribeCount) {
// Whisper won't transcribe anything shorter than 1s.
int minTranscribeLength = WHISPER_SAMPLE_RATE; // 1s
if (transcribeCount < minTranscribeLength) {
return "";
}
float seconds = static_cast<float>(transcribeCount) / WHISPER_SAMPLE_RATE;
if (seconds > 30.0f) {
LOGW("Warning: Audio is longer than 30 seconds. Not all audio will be transcribed");
}
whisper_full_params params = buildWhisperParams_();
// If supported by the model, allow shortening the transcription. This can significantly
// improve performance, but requires a fine-tuned model.
// See https://github.com/futo-org/whisper-acft
if (this->shortAudioContext_) {
// audio_ctx: 1500 every 30 seconds (50 units in one second).
// See https://github.com/futo-org/whisper-acft/issues/6
float padding = 64.0f;
params.audio_ctx = static_cast<int>(seconds * (1500.0f / 30.0f) + padding);
if (params.audio_ctx > 1500) {
params.audio_ctx = 1500;
}
}
whisper_reset_timings(pContext_);
transcribeCount = std::min(audio.size(), transcribeCount);
if (whisper_full(pContext_, params, audio.data(), transcribeCount) != 0) {
throw std::runtime_error("Failed to run Whisper (non-zero exit status).");
} else {
whisper_print_timings(pContext_);
}
// Tokens to be used as a prompt for the next run of Whisper
unsigned int segmentCount = whisper_full_n_segments(pContext_);
// Build the results
std::stringstream results;
for (int i = 0; i < segmentCount; i++) {
results << " " << whisper_full_get_segment_text(pContext_, i);
}
std::string result = results.str();
LOGD("Transcribed: %s (audio len %.2f)", result.c_str(), audio.size() / (float) WHISPER_SAMPLE_RATE);
return result;
}
std::string
WhisperSession::splitAndTranscribeBefore_(int transcribeUpTo, int trimTo) {
std::string result = transcribe_(audioBuffer_, transcribeUpTo);
// Trim
LOGI("Trim to %.2f s, transcribe to %.2f s", (float) trimTo / WHISPER_SAMPLE_RATE, (float) transcribeUpTo / WHISPER_SAMPLE_RATE);
audioBuffer_ = std::vector(audioBuffer_.begin() + trimTo, audioBuffer_.end());
return result;
}
bool WhisperSession::isBufferSilent_() {
int toleranceSamples = WHISPER_SAMPLE_RATE / 5; // 0.2s
auto silence = findLongestSilence(
audioBuffer_,
LongestSilenceOptions {
.sampleRate = WHISPER_SAMPLE_RATE,
.minSilenceLengthSeconds = 0.0f,
.maximumSilenceStartSamples = toleranceSamples, // 0.5s
.returnFirstMatch = true
}
);
return silence.end >= audioBuffer_.size() - toleranceSamples;
}
std::string
WhisperSession::transcribeNextChunk() {
std::stringstream result;
// Handles a silence detected between (splitStart, splitEnd).
auto splitAndProcess = [&] (int splitStart, int splitEnd) {
int tolerance = WHISPER_SAMPLE_RATE / 8; // 0.125s
bool isCompletelySilent = splitStart < tolerance && splitEnd > audioBuffer_.size() - tolerance;
LOGD("WhisperSession: Found silence range from %.2f -> %.2f", splitStart / (float) WHISPER_SAMPLE_RATE, splitEnd / (float) WHISPER_SAMPLE_RATE);
if (isCompletelySilent) {
audioBuffer_.clear();
return false;
} else if (splitEnd > tolerance) { // Anything to transcribe?
// Include some of the silence between the start and the end. Excluding it
// seems to make Whisper more likely to omit trailing punctuation.
int maximumSilentSamples = WHISPER_SAMPLE_RATE;
int silentSamplesToAdd = std::min(maximumSilentSamples, (splitEnd - splitStart) / 2);
splitStart += silentSamplesToAdd;
result << splitAndTranscribeBefore_(splitStart, splitEnd) << "\n\n";
return true;
}
return false;
};
int maximumSamples = WHISPER_SAMPLE_RATE * 25;
// Handle paragraph breaks indicated by long pauses
while (audioBuffer_.size() > WHISPER_SAMPLE_RATE * 3) {
LOGD("WhisperSession: Checking for a longer pauses.");
// Allow brief pauses to create new paragraphs:
float minSilenceSeconds = 1.5f;
auto splitPoint = findLongestSilence(
audioBuffer_,
LongestSilenceOptions {
.sampleRate = WHISPER_SAMPLE_RATE,
.minSilenceLengthSeconds = minSilenceSeconds,
.maximumSilenceStartSamples = maximumSamples,
.returnFirstMatch = true
}
);
if (!splitPoint.isValid) {
break;
}
if (!splitAndProcess(splitPoint.start, splitPoint.end)) {
break;
}
}
// If there are no long pauses, force a paragraph break somewhere
if (audioBuffer_.size() >= maximumSamples) {
LOGD("WhisperSession: Allowing shorter pauses to break.");
float minSilenceSeconds = 0.3f;
auto silenceRange = findLongestSilence(
audioBuffer_,
LongestSilenceOptions {
.sampleRate = WHISPER_SAMPLE_RATE,
.minSilenceLengthSeconds = minSilenceSeconds,
.maximumSilenceStartSamples = maximumSamples,
.returnFirstMatch = false
}
);
// In this case, the audio is long enough that it needs to be split somewhere. If there's
// no suitable pause available, default to splitting in the middle.
int halfBufferSize = audioBuffer_.size() / 2;
int splitStart = silenceRange.isValid ? silenceRange.start : halfBufferSize;
int splitEnd = silenceRange.isValid ? silenceRange.end : halfBufferSize;
splitAndProcess(splitStart, splitEnd);
}
return result.str();
}
void WhisperSession::addAudio(float* data, size_t count) {
// See https://en.cppreference.com/w/cpp/algorithm/copy_n.html,
// and the suggestions at https://stackoverflow.com/q/34552783.
audioBuffer_.reserve(count);
std::copy_n(data, count, std::back_inserter(audioBuffer_));
}
std::string WhisperSession::transcribeAll() {
if (isBufferSilent_()) {
return "";
}
std::stringstream result;
std::string transcribed;
auto update_transcribed = [&] {
transcribed = transcribeNextChunk();
return !transcribed.empty();
};
while (update_transcribed()) {
result << transcribed << "\n\n";
}
// Transcribe content considered by transcribeNextChunk as partial:
if (!isBufferSilent_()) {
result << transcribe_(audioBuffer_, audioBuffer_.size());
}
audioBuffer_.clear();
return result.str();
}