一些检查失败了
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
250 行
8.3 KiB
C++
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();
|
|
}
|