cleanup: remove dead code and DRY pre_conv release via wctx_free
This commit is contained in:
+3
-2
@@ -20,8 +20,9 @@ add_custom_target(version ALL
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# Suppress MSVC fopen/sprintf deprecation warnings, force UTF-8 source and
|
||||
# execution charsets so non-ASCII string literals (CJK in text-chunker.h,
|
||||
# prompt-builder.cpp) survive the compile without a BOM.
|
||||
# execution charsets so non-ASCII string literals (CJK language names,
|
||||
# punctuation tables in BPE and prompt builder) survive the compile
|
||||
# without a BOM.
|
||||
if(MSVC)
|
||||
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
|
||||
add_compile_options(/utf-8)
|
||||
|
||||
@@ -27,29 +27,6 @@
|
||||
// utf8.h: utf8_fopen, the path-UTF-8-aware fopen used below.
|
||||
#include "utf8.h"
|
||||
|
||||
// case-insensitive extension check
|
||||
static bool audio_io_ends_with(const char * str, const char * suffix) {
|
||||
int slen = (int) strlen(str);
|
||||
int xlen = (int) strlen(suffix);
|
||||
if (slen < xlen) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < xlen; i++) {
|
||||
char a = str[slen - xlen + i];
|
||||
char b = suffix[i];
|
||||
if (a >= 'A' && a <= 'Z') {
|
||||
a += 32;
|
||||
}
|
||||
if (b >= 'A' && b <= 'Z') {
|
||||
b += 32;
|
||||
}
|
||||
if (a != b) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load entire file into memory. Caller frees the returned pointer.
|
||||
static uint8_t * audio_io_load_file(const char * path, size_t * size_out) {
|
||||
*size_out = 0;
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
#pragma once
|
||||
// audio-postproc.h: TTS waveform post-processing
|
||||
//
|
||||
// Generic post-processing for neural TTS output: silence trimming
|
||||
// (pydub-strict bit-for-bit), fade-in/out, padding. Public functions take
|
||||
// and return float32 mono PCM in [-1, 1] at the pipeline sample rate.
|
||||
// Internal silence detection runs on int16 samples to match pydub.
|
||||
// Math reference: omnivoice/utils/audio.py (1:1 port).
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// RMS of an int16 slice [start, start + n) clamped to s16.size(). A slice that
|
||||
// extends past the end shrinks accordingly. Empty slices return 0.0, matching
|
||||
// pydub's AudioSegment.rms on empty segments.
|
||||
static double postproc_slice_rms_s16(const std::vector<int16_t> & s16, size_t start, size_t n) {
|
||||
size_t end = start + n;
|
||||
if (end > s16.size()) {
|
||||
end = s16.size();
|
||||
}
|
||||
|
||||
if (start >= end) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
int64_t ssq = 0;
|
||||
for (size_t i = start; i < end; i++) {
|
||||
int32_t s = s16[i];
|
||||
ssq += (int64_t) s * (int64_t) s;
|
||||
}
|
||||
|
||||
size_t cnt = end - start;
|
||||
return std::sqrt((double) ssq / (double) cnt);
|
||||
}
|
||||
|
||||
// Converts float32 [-1, 1] to int16 with the exact pydub recipe:
|
||||
// (audio * 32768.0).clip(-32768, 32767).astype(int16). Truncation toward 0,
|
||||
// matching numpy's astype(int16).
|
||||
static std::vector<int16_t> postproc_f32_to_s16(const std::vector<float> & a) {
|
||||
std::vector<int16_t> out(a.size());
|
||||
for (size_t i = 0; i < a.size(); i++) {
|
||||
double v = (double) a[i] * 32768.0;
|
||||
if (v > 32767.0) {
|
||||
v = 32767.0;
|
||||
}
|
||||
|
||||
if (v < -32768.0) {
|
||||
v = -32768.0;
|
||||
}
|
||||
|
||||
out[i] = (int16_t) v;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Inverse of postproc_f32_to_s16: int16 -> float32 via division by 32768.0.
|
||||
static std::vector<float> postproc_s16_to_f32(const std::vector<int16_t> & s16) {
|
||||
std::vector<float> out(s16.size());
|
||||
for (size_t i = 0; i < s16.size(); i++) {
|
||||
out[i] = (float) ((double) s16[i] / 32768.0);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// pydub.silence.detect_silence ported to int16 samples. seek_step and
|
||||
// min_silence_len are in samples. Returns inclusive ranges [start, end] in
|
||||
// samples where end = start + min_silence_len of the last hit, exactly as
|
||||
// pydub builds them.
|
||||
static std::vector<std::pair<int, int>> postproc_detect_silence(const std::vector<int16_t> & s16,
|
||||
int min_silence_len,
|
||||
double thresh_lin,
|
||||
int seek_step) {
|
||||
std::vector<std::pair<int, int>> ranges;
|
||||
int seg_len = (int) s16.size();
|
||||
|
||||
if (seg_len < min_silence_len) {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
int last_slice_start = seg_len - min_silence_len;
|
||||
|
||||
std::vector<int> starts;
|
||||
for (int i = 0; i <= last_slice_start; i += seek_step) {
|
||||
starts.push_back(i);
|
||||
}
|
||||
|
||||
if ((last_slice_start % seek_step) != 0) {
|
||||
starts.push_back(last_slice_start);
|
||||
}
|
||||
|
||||
std::vector<int> silence_starts;
|
||||
for (int i : starts) {
|
||||
double r = postproc_slice_rms_s16(s16, (size_t) i, (size_t) min_silence_len);
|
||||
if (r <= thresh_lin) {
|
||||
silence_starts.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (silence_starts.empty()) {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
int prev_i = silence_starts[0];
|
||||
int range_start = prev_i;
|
||||
|
||||
for (size_t k = 1; k < silence_starts.size(); k++) {
|
||||
int si = silence_starts[k];
|
||||
bool continuous = (si == prev_i + seek_step);
|
||||
bool has_gap = (si > prev_i + min_silence_len);
|
||||
|
||||
if (!continuous && has_gap) {
|
||||
ranges.push_back({ range_start, prev_i + min_silence_len });
|
||||
range_start = si;
|
||||
}
|
||||
|
||||
prev_i = si;
|
||||
}
|
||||
|
||||
ranges.push_back({ range_start, prev_i + min_silence_len });
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// pydub.silence.detect_nonsilent: invert detect_silence over [0, seg_len].
|
||||
static std::vector<std::pair<int, int>> postproc_detect_nonsilent(const std::vector<int16_t> & s16,
|
||||
int min_silence_len,
|
||||
double thresh_lin,
|
||||
int seek_step) {
|
||||
std::vector<std::pair<int, int>> nonsilent;
|
||||
int seg_len = (int) s16.size();
|
||||
auto silent = postproc_detect_silence(s16, min_silence_len, thresh_lin, seek_step);
|
||||
|
||||
if (silent.empty()) {
|
||||
nonsilent.push_back({ 0, seg_len });
|
||||
return nonsilent;
|
||||
}
|
||||
|
||||
if (silent.front().first == 0 && silent.front().second == seg_len) {
|
||||
return nonsilent;
|
||||
}
|
||||
|
||||
int prev_end = 0;
|
||||
int last_end = 0;
|
||||
|
||||
for (const auto & r : silent) {
|
||||
nonsilent.push_back({ prev_end, r.first });
|
||||
prev_end = r.second;
|
||||
last_end = r.second;
|
||||
}
|
||||
|
||||
if (last_end != seg_len) {
|
||||
nonsilent.push_back({ prev_end, seg_len });
|
||||
}
|
||||
|
||||
if (!nonsilent.empty() && nonsilent.front().first == 0 && nonsilent.front().second == 0) {
|
||||
nonsilent.erase(nonsilent.begin());
|
||||
}
|
||||
|
||||
return nonsilent;
|
||||
}
|
||||
|
||||
// pydub.silence.detect_leading_silence ported to int16. chunk_n is in samples.
|
||||
// Returns the sample index where the leading silence ends (clamped to len).
|
||||
static int postproc_detect_leading_silence(const std::vector<int16_t> & s16, double thresh_lin, int chunk_n) {
|
||||
int trim = 0;
|
||||
int seg_len = (int) s16.size();
|
||||
|
||||
while (trim < seg_len) {
|
||||
int slice_end = std::min(trim + chunk_n, seg_len);
|
||||
int n = slice_end - trim;
|
||||
double r = postproc_slice_rms_s16(s16, (size_t) trim, (size_t) n);
|
||||
|
||||
// pydub compares dBFS < threshold; in linear amplitude that is
|
||||
// r < thresh_lin (strict), since dBFS is monotonic in r and r=0
|
||||
// gives -inf which is always below any finite threshold.
|
||||
if (r >= thresh_lin) {
|
||||
break;
|
||||
}
|
||||
|
||||
trim += chunk_n;
|
||||
}
|
||||
|
||||
if (trim > seg_len) {
|
||||
trim = seg_len;
|
||||
}
|
||||
|
||||
return trim;
|
||||
}
|
||||
|
||||
// remove_silence: strict 1:1 port of omnivoice/utils/audio.py:remove_silence.
|
||||
// Removes mid silences longer than mid_sil_ms (kept down to mid_sil_ms via
|
||||
// pydub split_on_silence with keep_silence == mid_sil_ms), then trims the
|
||||
// leading and trailing silences leaving lead_sil_ms / trail_sil_ms intact.
|
||||
// thresh_db is the dBFS threshold (default -50 dBFS in upstream).
|
||||
static void remove_silence(std::vector<float> & a,
|
||||
int sr,
|
||||
int mid_sil_ms,
|
||||
int lead_sil_ms,
|
||||
int trail_sil_ms,
|
||||
double thresh_db) {
|
||||
if (a.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<int16_t> s16 = postproc_f32_to_s16(a);
|
||||
double thresh_lin = 32768.0 * std::pow(10.0, thresh_db / 20.0);
|
||||
int seek_step = sr / 100; // 10 ms
|
||||
|
||||
// Mid silence removal via split_on_silence + concat.
|
||||
if (mid_sil_ms > 0) {
|
||||
int min_sil_n = sr * mid_sil_ms / 1000;
|
||||
int keep_n = min_sil_n;
|
||||
|
||||
auto nonsilent = postproc_detect_nonsilent(s16, min_sil_n, thresh_lin, seek_step);
|
||||
|
||||
std::vector<std::pair<int, int>> output_ranges;
|
||||
output_ranges.reserve(nonsilent.size());
|
||||
for (const auto & r : nonsilent) {
|
||||
output_ranges.push_back({ r.first - keep_n, r.second + keep_n });
|
||||
}
|
||||
|
||||
// pydub pairwise overlap dedup: split overlap at the midpoint.
|
||||
for (size_t i = 0; i + 1 < output_ranges.size(); i++) {
|
||||
int last_end = output_ranges[i].second;
|
||||
int next_start = output_ranges[i + 1].first;
|
||||
if (next_start < last_end) {
|
||||
int mid = (last_end + next_start) / 2;
|
||||
output_ranges[i].second = mid;
|
||||
output_ranges[i + 1].first = mid;
|
||||
}
|
||||
}
|
||||
|
||||
// Concat clipped slices. Empty slices contribute nothing, matching
|
||||
// AudioSegment.silent(0) += seg semantics.
|
||||
std::vector<int16_t> out;
|
||||
out.reserve(s16.size());
|
||||
int seg_len = (int) s16.size();
|
||||
|
||||
for (const auto & r : output_ranges) {
|
||||
int cs = std::max(0, r.first);
|
||||
int ce = std::min(seg_len, r.second);
|
||||
if (cs < ce) {
|
||||
out.insert(out.end(), s16.begin() + cs, s16.begin() + ce);
|
||||
}
|
||||
}
|
||||
|
||||
s16 = std::move(out);
|
||||
}
|
||||
|
||||
// Edge trimming: leading then trailing via reverse trick.
|
||||
int chunk_n = sr / 100; // 10 ms
|
||||
|
||||
int trim_lead = postproc_detect_leading_silence(s16, thresh_lin, chunk_n);
|
||||
trim_lead = std::max(0, trim_lead - sr * lead_sil_ms / 1000);
|
||||
if (trim_lead > 0) {
|
||||
s16.erase(s16.begin(), s16.begin() + std::min(trim_lead, (int) s16.size()));
|
||||
}
|
||||
|
||||
std::reverse(s16.begin(), s16.end());
|
||||
|
||||
int trim_trail = postproc_detect_leading_silence(s16, thresh_lin, chunk_n);
|
||||
trim_trail = std::max(0, trim_trail - sr * trail_sil_ms / 1000);
|
||||
if (trim_trail > 0) {
|
||||
s16.erase(s16.begin(), s16.begin() + std::min(trim_trail, (int) s16.size()));
|
||||
}
|
||||
|
||||
std::reverse(s16.begin(), s16.end());
|
||||
|
||||
a = postproc_s16_to_f32(s16);
|
||||
}
|
||||
|
||||
// peak_normalize_half: rescale so peak amplitude becomes 0.5 (-6 dBFS).
|
||||
// Mirrors the no-ref branch of _post_process_audio in omnivoice.py.
|
||||
static void peak_normalize_half(std::vector<float> & a) {
|
||||
if (a.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float peak = 0.0f;
|
||||
for (float s : a) {
|
||||
float v = std::fabs(s);
|
||||
if (v > peak) {
|
||||
peak = v;
|
||||
}
|
||||
}
|
||||
|
||||
if (peak > 1e-6f) {
|
||||
float k = 0.5f / peak;
|
||||
for (float & s : a) {
|
||||
s *= k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fade_and_pad: linear fade-in / fade-out on the first and last fade_dur
|
||||
// seconds, then pad pad_dur seconds of silence on each side. 1:1 port of
|
||||
// fade_and_pad_audio in omnivoice/utils/audio.py.
|
||||
static void fade_and_pad(std::vector<float> & a, int sr, double fade_dur, double pad_dur) {
|
||||
if (a.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int fade_n = (int) (fade_dur * (double) sr);
|
||||
int pad_n = (int) (pad_dur * (double) sr);
|
||||
|
||||
if (fade_n > 0) {
|
||||
int k = std::min(fade_n, (int) a.size() / 2);
|
||||
if (k > 0) {
|
||||
int denom = std::max(k - 1, 1);
|
||||
|
||||
for (int i = 0; i < k; i++) {
|
||||
float w = (float) i / (float) denom;
|
||||
a[(size_t) i] *= w;
|
||||
}
|
||||
|
||||
for (int i = 0; i < k; i++) {
|
||||
float w = 1.0f - (float) i / (float) denom;
|
||||
a[a.size() - (size_t) k + (size_t) i] *= w;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pad_n > 0) {
|
||||
std::vector<float> padded((size_t) pad_n + a.size() + (size_t) pad_n, 0.0f);
|
||||
std::copy(a.begin(), a.end(), padded.begin() + pad_n);
|
||||
a = std::move(padded);
|
||||
}
|
||||
}
|
||||
|
||||
// cross_fade_chunks: concatenate audio chunks with a silence_dur gap split
|
||||
// into fade_out, pure silence, fade_in. 1:1 port of cross_fade_chunks in
|
||||
// omnivoice/utils/audio.py.
|
||||
static std::vector<float> cross_fade_chunks(const std::vector<std::vector<float>> & chunks,
|
||||
int sr,
|
||||
double silence_dur) {
|
||||
if (chunks.empty()) {
|
||||
return std::vector<float>();
|
||||
}
|
||||
|
||||
if (chunks.size() == 1) {
|
||||
return chunks[0];
|
||||
}
|
||||
|
||||
int total_n = (int) (silence_dur * (double) sr);
|
||||
int fade_n = total_n / 3;
|
||||
int silence_n = fade_n;
|
||||
|
||||
std::vector<float> merged = chunks[0];
|
||||
|
||||
for (size_t i = 1; i < chunks.size(); i++) {
|
||||
const auto & chunk = chunks[i];
|
||||
|
||||
// Fade-out tail of merged.
|
||||
int fout_n = std::min(fade_n, (int) merged.size());
|
||||
if (fout_n > 0) {
|
||||
int denom = std::max(fout_n - 1, 1);
|
||||
for (int j = 0; j < fout_n; j++) {
|
||||
float w = 1.0f - (float) j / (float) denom;
|
||||
merged[merged.size() - (size_t) fout_n + (size_t) j] *= w;
|
||||
}
|
||||
}
|
||||
|
||||
// Silence gap.
|
||||
if (silence_n > 0) {
|
||||
merged.insert(merged.end(), (size_t) silence_n, 0.0f);
|
||||
}
|
||||
|
||||
// Fade-in head of next chunk (worked on a copy to keep input const).
|
||||
std::vector<float> head = chunk;
|
||||
int fin_n = std::min(fade_n, (int) head.size());
|
||||
if (fin_n > 0) {
|
||||
int denom = std::max(fin_n - 1, 1);
|
||||
for (int j = 0; j < fin_n; j++) {
|
||||
float w = (float) j / (float) denom;
|
||||
head[(size_t) j] *= w;
|
||||
}
|
||||
}
|
||||
|
||||
merged.insert(merged.end(), head.begin(), head.end());
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
-28
@@ -6,7 +6,6 @@
|
||||
|
||||
#include "utf8.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
@@ -69,19 +68,6 @@ static void debug_dump_2d(const DebugDumper * d, const char * name, const float
|
||||
debug_dump(d, name, data, shape, 2);
|
||||
}
|
||||
|
||||
// Convenience: dump 3D tensor [d0, d1, d2].
|
||||
static void debug_dump_3d(const DebugDumper * d, const char * name, const float * data, int d0, int d1, int d2) {
|
||||
int shape[3] = { d0, d1, d2 };
|
||||
debug_dump(d, name, data, shape, 3);
|
||||
}
|
||||
|
||||
// Convenience: dump 4D tensor [d0, d1, d2, d3].
|
||||
static void
|
||||
debug_dump_4d(const DebugDumper * d, const char * name, const float * data, int d0, int d1, int d2, int d3) {
|
||||
int shape[4] = { d0, d1, d2, d3 };
|
||||
debug_dump(d, name, data, shape, 4);
|
||||
}
|
||||
|
||||
// Cast a stream of int32 values to f32 in place into a temporary buffer and
|
||||
// dump under the given name. Token comparisons are then expressed as cossim
|
||||
// over float values, with exact match recoverable via integer compare on the
|
||||
@@ -104,17 +90,3 @@ static void debug_dump_i32_as_f32(const DebugDumper * d,
|
||||
}
|
||||
debug_dump(d, name, buf.data(), shape, ndims);
|
||||
}
|
||||
|
||||
// Cosine similarity between two f32 arrays.
|
||||
static double debug_cosine_sim(const float * a, const float * b, int n) {
|
||||
double dot = 0, na = 0, nb = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
dot += (double) a[i] * (double) b[i];
|
||||
na += (double) a[i] * (double) a[i];
|
||||
nb += (double) b[i] * (double) b[i];
|
||||
}
|
||||
if (na < 1e-30 || nb < 1e-30) {
|
||||
return 0.0;
|
||||
}
|
||||
return dot / (sqrt(na) * sqrt(nb));
|
||||
}
|
||||
|
||||
@@ -359,93 +359,6 @@ static const void * gf_get_data(const GGUFModel & gf, const char * name) {
|
||||
return gf.mapping + gf.data_offset + offset;
|
||||
}
|
||||
|
||||
// Look up the native ggml type of a tensor stored in the GGUF, so callers
|
||||
// can mirror it on the backend allocation and let dtype-agnostic memcpy
|
||||
// handle the bytes. Aborts if the tensor is missing.
|
||||
static enum ggml_type gf_get_type(const GGUFModel & gf, const std::string & name) {
|
||||
struct ggml_tensor * src = ggml_get_tensor(gf.meta, name.c_str());
|
||||
if (!src) {
|
||||
qt_throw("[GGUF] tensor '%s' not in meta context", name.c_str());
|
||||
}
|
||||
return src->type;
|
||||
}
|
||||
|
||||
// Fuse Q, K, V projection weights into a single tensor [ne0, q_ne1 + k_ne1 + v_ne1].
|
||||
// Works for any quantized type since quantization is per-row (along ne[0]).
|
||||
// The fused tensor data is q rows || k rows || v rows (contiguous).
|
||||
static struct ggml_tensor * gf_load_qkv_fused(WeightCtx * wctx,
|
||||
const GGUFModel & gf,
|
||||
const std::string & q_name,
|
||||
const std::string & k_name,
|
||||
const std::string & v_name) {
|
||||
struct ggml_tensor * q_src = ggml_get_tensor(gf.meta, q_name.c_str());
|
||||
struct ggml_tensor * k_src = ggml_get_tensor(gf.meta, k_name.c_str());
|
||||
struct ggml_tensor * v_src = ggml_get_tensor(gf.meta, v_name.c_str());
|
||||
if (!q_src || !k_src || !v_src) {
|
||||
qt_throw("[GGUF] QKV tensor not found: %s / %s / %s", q_name.c_str(), k_name.c_str(), v_name.c_str());
|
||||
}
|
||||
// All must share ne[0] (input dim) and type - otherwise can't fuse
|
||||
GGML_ASSERT(q_src->ne[0] == k_src->ne[0] && k_src->ne[0] == v_src->ne[0]);
|
||||
if (q_src->type != k_src->type || k_src->type != v_src->type) {
|
||||
return NULL; // caller should fall back to separate loads
|
||||
}
|
||||
|
||||
int64_t ne0 = q_src->ne[0];
|
||||
int64_t fused_ne1 = q_src->ne[1] + k_src->ne[1] + v_src->ne[1];
|
||||
int64_t ne[2] = { ne0, fused_ne1 };
|
||||
struct ggml_tensor * fused = ggml_new_tensor(wctx->ctx, q_src->type, 2, ne);
|
||||
|
||||
size_t row_size = ggml_row_size(q_src->type, ne0);
|
||||
size_t q_bytes = q_src->ne[1] * row_size;
|
||||
size_t k_bytes = k_src->ne[1] * row_size;
|
||||
size_t v_bytes = v_src->ne[1] * row_size;
|
||||
|
||||
auto get_data = [&](const std::string & name) -> const void * {
|
||||
int64_t idx = gguf_find_tensor(gf.gguf, name.c_str());
|
||||
size_t off = gguf_get_tensor_offset(gf.gguf, idx);
|
||||
return gf.mapping + gf.data_offset + off;
|
||||
};
|
||||
|
||||
wctx->pending.push_back({ fused, get_data(q_name), q_bytes, 0 });
|
||||
wctx->pending.push_back({ fused, get_data(k_name), k_bytes, q_bytes });
|
||||
wctx->pending.push_back({ fused, get_data(v_name), v_bytes, q_bytes + k_bytes });
|
||||
return fused;
|
||||
}
|
||||
|
||||
// Fuse two projection weights [ne0, a_ne1 + b_ne1] when types match.
|
||||
// Returns NULL if types differ.
|
||||
static struct ggml_tensor * gf_load_pair_fused(WeightCtx * wctx,
|
||||
const GGUFModel & gf,
|
||||
const std::string & a_name,
|
||||
const std::string & b_name) {
|
||||
struct ggml_tensor * a_src = ggml_get_tensor(gf.meta, a_name.c_str());
|
||||
struct ggml_tensor * b_src = ggml_get_tensor(gf.meta, b_name.c_str());
|
||||
if (!a_src || !b_src) {
|
||||
return NULL;
|
||||
}
|
||||
if (a_src->ne[0] != b_src->ne[0] || a_src->type != b_src->type) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int64_t ne0 = a_src->ne[0];
|
||||
int64_t ne[2] = { ne0, a_src->ne[1] + b_src->ne[1] };
|
||||
struct ggml_tensor * fused = ggml_new_tensor(wctx->ctx, a_src->type, 2, ne);
|
||||
|
||||
size_t row_size = ggml_row_size(a_src->type, ne0);
|
||||
size_t a_bytes = a_src->ne[1] * row_size;
|
||||
size_t b_bytes = b_src->ne[1] * row_size;
|
||||
|
||||
auto get_data = [&](const std::string & name) -> const void * {
|
||||
int64_t idx = gguf_find_tensor(gf.gguf, name.c_str());
|
||||
size_t off = gguf_get_tensor_offset(gf.gguf, idx);
|
||||
return gf.mapping + gf.data_offset + off;
|
||||
};
|
||||
|
||||
wctx->pending.push_back({ fused, get_data(a_name), a_bytes, 0 });
|
||||
wctx->pending.push_back({ fused, get_data(b_name), b_bytes, a_bytes });
|
||||
return fused;
|
||||
}
|
||||
|
||||
// Read a uint32 KV value (returns 0 if not found)
|
||||
static uint32_t gf_get_u32(const GGUFModel & gf, const char * key) {
|
||||
int64_t idx = gguf_find_key(gf.gguf, key);
|
||||
|
||||
+5
-65
@@ -1,16 +1,11 @@
|
||||
#pragma once
|
||||
// philox.h Philox4x32-10 PRNG + Box-Muller normal distribution
|
||||
// philox.h Philox4x32-10 PRNG, uniform distribution
|
||||
//
|
||||
// Matches PyTorch CUDA torch.randn() output (cuRAND Philox4_32_10).
|
||||
// Zero dependencies beyond <cstdint>, <cmath>, <cstring>.
|
||||
//
|
||||
// CUDA kernel mapping (normal distribution):
|
||||
// element[k] = philox_normal4(seed, subsequence=k, offset=0)[0]
|
||||
// vals[1..3] discarded (one thread per element, one normal per thread).
|
||||
// Matches PyTorch CUDA torch.rand() output (cuRAND Philox4_32_10).
|
||||
// Used by the multinomial sampler in sampling.h to stay byte-exact
|
||||
// with the upstream Python pipeline. Zero dependencies beyond <cstdint>.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
// Philox constants (same as cuRAND / Random123)
|
||||
static constexpr uint32_t PHILOX_M0 = 0xD2511F53u;
|
||||
@@ -19,8 +14,7 @@ static constexpr uint32_t PHILOX_W0 = 0x9E3779B9u;
|
||||
static constexpr uint32_t PHILOX_W1 = 0xBB67AE85u;
|
||||
|
||||
// cuRAND uniform conversion
|
||||
static constexpr float CURAND_2POW32_INV = 2.3283064365386963e-10f; // 1 / 2^32
|
||||
static constexpr float CURAND_2POW32_INV_2PI = 1.4629180792671596e-09f; // 2*PI / 2^32
|
||||
static constexpr float CURAND_2POW32_INV = 2.3283064365386963e-10f; // 1 / 2^32
|
||||
|
||||
struct Philox4 {
|
||||
uint32_t x, y, z, w;
|
||||
@@ -81,60 +75,6 @@ static inline Philox4 philox4x32_10(Philox4 ctr, uint32_t seed_lo, uint32_t seed
|
||||
return ctr;
|
||||
}
|
||||
|
||||
// cuRAND Box-Muller: 2 uint32 -> 2 N(0,1)
|
||||
static inline void box_muller(uint32_t u0, uint32_t u1, float * n0, float * n1) {
|
||||
float u = (float) u0 * CURAND_2POW32_INV + (CURAND_2POW32_INV * 0.5f);
|
||||
float v = (float) u1 * CURAND_2POW32_INV_2PI + (CURAND_2POW32_INV_2PI * 0.5f);
|
||||
float s = sqrtf(-2.0f * logf(u));
|
||||
*n0 = s * sinf(v);
|
||||
*n1 = s * cosf(v);
|
||||
}
|
||||
|
||||
// Generate 4 N(0,1) for (seed, subsequence, offset)
|
||||
// counter = [offset_lo, offset_hi, subseq_lo, subseq_hi]
|
||||
static inline void philox_normal4(int64_t seed, int64_t subsequence, int64_t offset, float out[4]) {
|
||||
Philox4 ctr = {
|
||||
(uint32_t) (offset),
|
||||
(uint32_t) (offset >> 32),
|
||||
(uint32_t) (subsequence),
|
||||
(uint32_t) (subsequence >> 32),
|
||||
};
|
||||
uint32_t slo = (uint32_t) (seed);
|
||||
uint32_t shi = (uint32_t) ((uint64_t) seed >> 32);
|
||||
Philox4 r = philox4x32_10(ctr, slo, shi);
|
||||
box_muller(r.x, r.y, &out[0], &out[1]);
|
||||
box_muller(r.z, r.w, &out[2], &out[3]);
|
||||
}
|
||||
|
||||
// bf16 round-trip (match torch.bfloat16 precision)
|
||||
static inline float f32_to_bf16_to_f32(float x) {
|
||||
uint32_t bits;
|
||||
memcpy(&bits, &x, 4);
|
||||
bits += 0x7FFF + ((bits >> 16) & 1); // round-to-nearest-even
|
||||
bits &= 0xFFFF0000u;
|
||||
float y;
|
||||
memcpy(&y, &bits, 4);
|
||||
return y;
|
||||
}
|
||||
|
||||
// Fill array with N(0,1) matching torch.randn() on CUDA with bf16.
|
||||
//
|
||||
// Reproduces:
|
||||
// gen = torch.Generator(device="cuda").manual_seed(seed)
|
||||
// torch.randn([...], generator=gen, device="cuda", dtype=torch.bfloat16)
|
||||
//
|
||||
// PyTorch CUDA normal distribution: each element k gets its own Philox
|
||||
// subsequence and uses only the first Box-Muller output (val[0]).
|
||||
// vals[1..3] are discarded. This matches the CUDA kernel behavior where
|
||||
// grid = ceil(n / block_size), one element per thread.
|
||||
static inline void philox_randn(int64_t seed, float * out, int n, bool bf16_round = true) {
|
||||
for (int k = 0; k < n; k++) {
|
||||
float vals[4];
|
||||
philox_normal4(seed, k, 0, vals);
|
||||
out[k] = bf16_round ? f32_to_bf16_to_f32(vals[0]) : vals[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Fill array with uniform [0, 1) drawn from Philox4x32-10. Matches
|
||||
// PyTorch CUDA torch.rand kernels.
|
||||
//
|
||||
|
||||
+7
-18
@@ -16,6 +16,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
bool pipeline_codec_load(PipelineCodec * pc, const char * gguf_path, BackendPair bp) {
|
||||
@@ -69,13 +70,11 @@ bool pipeline_codec_load(PipelineCodec * pc, const char * gguf_path, BackendPair
|
||||
gf_close(&pc->gguf);
|
||||
return false;
|
||||
}
|
||||
pc->pre_conv_ctx = wctx.ctx;
|
||||
pc->pre_conv_buf = wctx.buffer;
|
||||
pc->pre_conv_wctx = std::move(wctx);
|
||||
}
|
||||
|
||||
if (!seanet_encoder_load(&pc->seanet, pc->gguf, pc->backend)) {
|
||||
ggml_backend_buffer_free(pc->pre_conv_buf);
|
||||
ggml_free(pc->pre_conv_ctx);
|
||||
wctx_free(&pc->pre_conv_wctx);
|
||||
dac_decoder_free(&pc->dac);
|
||||
upsample_stage_free(&pc->upsample);
|
||||
tok_trans_free(&pc->transformer);
|
||||
@@ -86,8 +85,7 @@ bool pipeline_codec_load(PipelineCodec * pc, const char * gguf_path, BackendPair
|
||||
|
||||
if (!enc_trans_load(&pc->enc_transformer, pc->gguf, pc->backend)) {
|
||||
seanet_encoder_free(&pc->seanet);
|
||||
ggml_backend_buffer_free(pc->pre_conv_buf);
|
||||
ggml_free(pc->pre_conv_ctx);
|
||||
wctx_free(&pc->pre_conv_wctx);
|
||||
dac_decoder_free(&pc->dac);
|
||||
upsample_stage_free(&pc->upsample);
|
||||
tok_trans_free(&pc->transformer);
|
||||
@@ -99,8 +97,7 @@ bool pipeline_codec_load(PipelineCodec * pc, const char * gguf_path, BackendPair
|
||||
if (!enc_down_load(&pc->enc_downsample, pc->gguf, pc->backend)) {
|
||||
enc_trans_free(&pc->enc_transformer);
|
||||
seanet_encoder_free(&pc->seanet);
|
||||
ggml_backend_buffer_free(pc->pre_conv_buf);
|
||||
ggml_free(pc->pre_conv_ctx);
|
||||
wctx_free(&pc->pre_conv_wctx);
|
||||
dac_decoder_free(&pc->dac);
|
||||
upsample_stage_free(&pc->upsample);
|
||||
tok_trans_free(&pc->transformer);
|
||||
@@ -113,8 +110,7 @@ bool pipeline_codec_load(PipelineCodec * pc, const char * gguf_path, BackendPair
|
||||
enc_down_free(&pc->enc_downsample);
|
||||
enc_trans_free(&pc->enc_transformer);
|
||||
seanet_encoder_free(&pc->seanet);
|
||||
ggml_backend_buffer_free(pc->pre_conv_buf);
|
||||
ggml_free(pc->pre_conv_ctx);
|
||||
wctx_free(&pc->pre_conv_wctx);
|
||||
dac_decoder_free(&pc->dac);
|
||||
upsample_stage_free(&pc->upsample);
|
||||
tok_trans_free(&pc->transformer);
|
||||
@@ -442,14 +438,7 @@ void pipeline_codec_free(PipelineCodec * pc) {
|
||||
enc_down_free(&pc->enc_downsample);
|
||||
enc_trans_free(&pc->enc_transformer);
|
||||
seanet_encoder_free(&pc->seanet);
|
||||
if (pc->pre_conv_buf) {
|
||||
ggml_backend_buffer_free(pc->pre_conv_buf);
|
||||
pc->pre_conv_buf = NULL;
|
||||
}
|
||||
if (pc->pre_conv_ctx) {
|
||||
ggml_free(pc->pre_conv_ctx);
|
||||
pc->pre_conv_ctx = NULL;
|
||||
}
|
||||
wctx_free(&pc->pre_conv_wctx);
|
||||
dac_decoder_free(&pc->dac);
|
||||
upsample_stage_free(&pc->upsample);
|
||||
tok_trans_free(&pc->transformer);
|
||||
|
||||
@@ -53,12 +53,11 @@ struct PipelineCodec {
|
||||
QwenUpsampleStage upsample;
|
||||
QwenDACDecoder dac;
|
||||
|
||||
// pre_conv: causal Conv1d k=3, 512 -> 1024. Loaded into a dedicated
|
||||
// weight ctx because it is the only module that does not own one.
|
||||
struct ggml_tensor * pre_conv_w; // [3, 512, 1024] f32
|
||||
struct ggml_tensor * pre_conv_b; // [1024] f32
|
||||
struct ggml_context * pre_conv_ctx;
|
||||
ggml_backend_buffer_t pre_conv_buf;
|
||||
// pre_conv: causal Conv1d k=3, 512 -> 1024. Owns a dedicated
|
||||
// WeightCtx because it is the only module without one.
|
||||
struct ggml_tensor * pre_conv_w; // [3, 512, 1024] f32
|
||||
struct ggml_tensor * pre_conv_b; // [1024] f32
|
||||
WeightCtx pre_conv_wctx;
|
||||
|
||||
// Encode side modules
|
||||
QwenSEANetEncoder seanet;
|
||||
|
||||
@@ -210,13 +210,6 @@ static void embed_text_range(const GGUFModel & gf,
|
||||
}
|
||||
}
|
||||
|
||||
// Append codec_embedding(id) to dst (one hidden-dim vector).
|
||||
static void embed_codec(const GGUFModel & gf, int id, int hidden_size, std::vector<float> & dst) {
|
||||
size_t old = dst.size();
|
||||
dst.resize(old + (size_t) hidden_size);
|
||||
embed_row_to_f32(gf, "talker.codec_embd.weight", id, hidden_size, dst.data() + old);
|
||||
}
|
||||
|
||||
// Vector add: a += b, length n.
|
||||
static void vec_add(float * a, const float * b, int n) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
#pragma once
|
||||
// text-chunker.h: punctuation-aware long-form text splitter for TTS
|
||||
//
|
||||
// chunk_text_punctuation: splits text on sentence-ending punctuation
|
||||
// (skipping abbreviation periods), then merges sentences into chunks of
|
||||
// at most chunk_len UTF-8 codepoints. Optional min_chunk_len merges
|
||||
// undersized chunks into a neighbour. Strings are UTF-8 in, UTF-8 out.
|
||||
// Comparison and length are codepoint-based, matching Python str
|
||||
// semantics.
|
||||
// Math reference: omnivoice/utils/text.py (1:1 port).
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Returns the byte length of the UTF-8 codepoint starting at b (1, 2, 3 or 4).
|
||||
// Falls back to 1 on invalid first bytes so iteration always advances.
|
||||
static inline int chunker_utf8_len(unsigned char b) {
|
||||
if ((b & 0x80) == 0x00) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ((b & 0xE0) == 0xC0) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if ((b & 0xF0) == 0xE0) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if ((b & 0xF8) == 0xF0) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sentence-ending punctuation. Mirrors SPLIT_PUNCTUATION in text.py.
|
||||
static const std::set<std::string> & chunker_split_punctuation() {
|
||||
static const std::set<std::string> s = {
|
||||
".",
|
||||
",",
|
||||
";",
|
||||
":",
|
||||
"!",
|
||||
"?",
|
||||
"\xe3\x80\x82", // U+3002 ideographic full stop
|
||||
"\xef\xbc\x8c", // U+FF0C fullwidth comma
|
||||
"\xef\xbc\x9b", // U+FF1B fullwidth semicolon
|
||||
"\xef\xbc\x9a", // U+FF1A fullwidth colon
|
||||
"\xef\xbc\x81", // U+FF01 fullwidth exclamation mark
|
||||
"\xef\xbc\x9f", // U+FF1F fullwidth question mark
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
// Closing marks attach to the preceding sentence. Mirrors CLOSING_MARKS.
|
||||
static const std::set<std::string> & chunker_closing_marks() {
|
||||
static const std::set<std::string> s = {
|
||||
"\"", "'", "]", ">",
|
||||
"\xe2\x80\x9c", // U+201C left double quotation mark
|
||||
"\xe2\x80\x9d", // U+201D right double quotation mark
|
||||
"\xe2\x80\x98", // U+2018 left single quotation mark
|
||||
"\xe2\x80\x99", // U+2019 right single quotation mark
|
||||
"\xef\xbc\x89", // U+FF09 fullwidth right parenthesis
|
||||
"\xe3\x80\x8b", // U+300B right double angle bracket
|
||||
"\xe3\x80\x8d", // U+300D right corner bracket
|
||||
"\xe3\x80\x91", // U+3011 right black lenticular bracket
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
// Abbreviations that suppress the period as a sentence break. ASCII only,
|
||||
// matched on the last whitespace-delimited word ending with the period.
|
||||
// Mirrors ABBREVIATIONS in text.py.
|
||||
static const std::set<std::string> & chunker_abbreviations() {
|
||||
static const std::set<std::string> s = {
|
||||
"Mr.", "Mrs.", "Ms.", "Dr.", "Prof.", "Sr.", "Jr.", "Rev.", "Fr.", "Hon.", "Pres.",
|
||||
"Gov.", "Capt.", "Gen.", "Sen.", "Rep.", "Col.", "Maj.", "Lt.", "Cmdr.", "Sgt.", "Cpl.",
|
||||
"Co.", "Corp.", "Inc.", "Ltd.", "Est.", "Dept.", "St.", "Ave.", "Blvd.", "Rd.", "Mt.",
|
||||
"Ft.", "No.", "Jan.", "Feb.", "Mar.", "Apr.", "Aug.", "Sep.", "Sept.", "Oct.", "Nov.",
|
||||
"Dec.", "i.e.", "e.g.", "vs.", "Vs.", "Etc.", "approx.", "fig.", "def.",
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
// Returns the last whitespace-delimited word of s, or s itself if no space.
|
||||
// Used to detect abbreviation periods.
|
||||
static std::string chunker_last_word(const std::string & s) {
|
||||
size_t e = s.find_last_not_of(" \t\n\r");
|
||||
if (e == std::string::npos) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::string trimmed = s.substr(0, e + 1);
|
||||
size_t sp = trimmed.find_last_of(" \t\n\r");
|
||||
if (sp == std::string::npos) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return trimmed.substr(sp + 1);
|
||||
}
|
||||
|
||||
// Strips leading and trailing ASCII whitespace from s.
|
||||
static std::string chunker_strip(const std::string & s) {
|
||||
size_t a = s.find_first_not_of(" \t\n\r");
|
||||
if (a == std::string::npos) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
size_t b = s.find_last_not_of(" \t\n\r");
|
||||
return s.substr(a, b - a + 1);
|
||||
}
|
||||
|
||||
// Splits text on sentence-ending punctuation (skipping abbreviations) and
|
||||
// merges sentences into chunks of at most chunk_len codepoints. If
|
||||
// min_chunk_len > 0, undersized chunks are merged with a neighbour.
|
||||
// Returns a list of stripped chunk strings (UTF-8). Empty chunks are dropped.
|
||||
//
|
||||
// Strict 1:1 port of chunk_text_punctuation in omnivoice/utils/text.py.
|
||||
static std::vector<std::string> chunk_text_punctuation(const std::string & text, int chunk_len, int min_chunk_len) {
|
||||
// Step 1: tokenise into UTF-8 codepoints, then split on punctuation.
|
||||
// sentences holds vectors of codepoints (each codepoint is a std::string).
|
||||
std::vector<std::vector<std::string>> sentences;
|
||||
std::vector<std::string> current;
|
||||
|
||||
const std::set<std::string> & split_set = chunker_split_punctuation();
|
||||
const std::set<std::string> & closing_set = chunker_closing_marks();
|
||||
const std::set<std::string> & abbrev_set = chunker_abbreviations();
|
||||
|
||||
const unsigned char * p = (const unsigned char *) text.data();
|
||||
const unsigned char * end = p + text.size();
|
||||
|
||||
while (p < end) {
|
||||
int n = chunker_utf8_len(*p);
|
||||
if (p + n > end) {
|
||||
n = (int) (end - p);
|
||||
}
|
||||
|
||||
std::string cp((const char *) p, (size_t) n);
|
||||
p += n;
|
||||
|
||||
bool is_split = split_set.count(cp) > 0;
|
||||
bool is_closing = closing_set.count(cp) > 0;
|
||||
|
||||
// Leading punctuation glues onto the previous sentence.
|
||||
if (current.empty() && !sentences.empty() && (is_split || is_closing)) {
|
||||
sentences.back().push_back(cp);
|
||||
continue;
|
||||
}
|
||||
|
||||
current.push_back(cp);
|
||||
|
||||
if (!is_split) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Period after an abbreviation does not break the sentence.
|
||||
bool is_abbreviation = false;
|
||||
if (cp == ".") {
|
||||
std::string joined;
|
||||
for (const auto & c : current) {
|
||||
joined += c;
|
||||
}
|
||||
|
||||
std::string last = chunker_last_word(joined);
|
||||
if (!last.empty() && abbrev_set.count(last) > 0) {
|
||||
is_abbreviation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_abbreviation) {
|
||||
sentences.push_back(current);
|
||||
current.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (!current.empty()) {
|
||||
sentences.push_back(current);
|
||||
}
|
||||
|
||||
// Step 2: greedy merge of sentences into chunks of at most chunk_len
|
||||
// codepoints. A sentence that does not fit starts a new chunk by itself,
|
||||
// even if it is longer than chunk_len.
|
||||
std::vector<std::vector<std::string>> merged;
|
||||
std::vector<std::string> cur_chunk;
|
||||
|
||||
for (const auto & sent : sentences) {
|
||||
if ((int) (cur_chunk.size() + sent.size()) <= chunk_len) {
|
||||
for (const auto & c : sent) {
|
||||
cur_chunk.push_back(c);
|
||||
}
|
||||
} else {
|
||||
if (!cur_chunk.empty()) {
|
||||
merged.push_back(cur_chunk);
|
||||
}
|
||||
|
||||
cur_chunk = sent;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cur_chunk.empty()) {
|
||||
merged.push_back(cur_chunk);
|
||||
}
|
||||
|
||||
// Step 3: merge undersized chunks. The first chunk, if short, is folded
|
||||
// into the second. Subsequent short chunks fold into the previous one.
|
||||
std::vector<std::vector<std::string>> finals;
|
||||
if (min_chunk_len > 0) {
|
||||
bool first_short = !merged.empty() && (int) merged[0].size() < min_chunk_len;
|
||||
|
||||
for (size_t i = 0; i < merged.size(); i++) {
|
||||
const auto & chunk = merged[i];
|
||||
|
||||
if (i == 1 && first_short) {
|
||||
for (const auto & c : chunk) {
|
||||
finals.back().push_back(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int) chunk.size() >= min_chunk_len) {
|
||||
finals.push_back(chunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (finals.empty()) {
|
||||
finals.push_back(chunk);
|
||||
} else {
|
||||
for (const auto & c : chunk) {
|
||||
finals.back().push_back(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finals = merged;
|
||||
}
|
||||
|
||||
// Step 4: join codepoints, strip whitespace, drop empty.
|
||||
std::vector<std::string> result;
|
||||
result.reserve(finals.size());
|
||||
|
||||
for (const auto & chunk : finals) {
|
||||
std::string joined;
|
||||
for (const auto & c : chunk) {
|
||||
joined += c;
|
||||
}
|
||||
|
||||
std::string stripped = chunker_strip(joined);
|
||||
if (!stripped.empty()) {
|
||||
result.push_back(stripped);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Counts UTF-8 codepoints in s. Used to derive the per-chunk character budget
|
||||
// from the average tokens-per-character of the full text, matching Python's
|
||||
// len(text) which counts codepoints.
|
||||
static int chunker_utf8_count(const std::string & text) {
|
||||
const unsigned char * p = (const unsigned char *) text.data();
|
||||
const unsigned char * end = p + text.size();
|
||||
int n = 0;
|
||||
|
||||
while (p < end) {
|
||||
int s = chunker_utf8_len(*p);
|
||||
if (p + s > end) {
|
||||
s = (int) (end - p);
|
||||
}
|
||||
|
||||
p += s;
|
||||
n += 1;
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
// Punctuation considered "terminal" by add_punctuation. Mirrors END_PUNCTUATION
|
||||
// in text.py. ASCII first, then UTF-8 byte sequences for fancy quotes, ellipsis
|
||||
// and Chinese variants.
|
||||
static const std::set<std::string> & chunker_end_punctuation() {
|
||||
static const std::set<std::string> s = {
|
||||
";",
|
||||
":",
|
||||
",",
|
||||
".",
|
||||
"!",
|
||||
"?",
|
||||
")",
|
||||
"]",
|
||||
"}",
|
||||
"\"",
|
||||
"'",
|
||||
"\xe2\x80\xa6", // U+2026 horizontal ellipsis
|
||||
"\xe2\x80\x9c", // U+201C left double quotation mark
|
||||
"\xe2\x80\x9d", // U+201D right double quotation mark
|
||||
"\xe2\x80\x98", // U+2018 left single quotation mark
|
||||
"\xe2\x80\x99", // U+2019 right single quotation mark
|
||||
"\xef\xbc\x9b", // U+FF1B fullwidth semicolon
|
||||
"\xef\xbc\x9a", // U+FF1A fullwidth colon
|
||||
"\xef\xbc\x8c", // U+FF0C fullwidth comma
|
||||
"\xe3\x80\x82", // U+3002 ideographic full stop
|
||||
"\xef\xbc\x81", // U+FF01 fullwidth exclamation mark
|
||||
"\xef\xbc\x9f", // U+FF1F fullwidth question mark
|
||||
"\xe3\x80\x81", // U+3001 ideographic comma
|
||||
"\xef\xbc\x89", // U+FF09 fullwidth right parenthesis
|
||||
"\xe3\x80\x91", // U+3011 right black lenticular bracket
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
// Returns the last UTF-8 codepoint of s as a std::string, or empty if s is
|
||||
// empty. Walks the byte sequence to find the start of the last codepoint.
|
||||
static std::string chunker_last_codepoint(const std::string & s) {
|
||||
if (s.empty()) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
size_t i = s.size();
|
||||
while (i > 0) {
|
||||
unsigned char b = (unsigned char) s[i - 1];
|
||||
if ((b & 0xC0) != 0x80) {
|
||||
return s.substr(i - 1);
|
||||
}
|
||||
|
||||
i--;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// Returns true if any codepoint of s falls inside the CJK Unified Ideographs
|
||||
// block (U+4E00..U+9FFF). Mirrors the Chinese-detection heuristic in
|
||||
// add_punctuation upstream.
|
||||
static bool chunker_contains_chinese(const std::string & s) {
|
||||
const unsigned char * p = (const unsigned char *) s.data();
|
||||
const unsigned char * end = p + s.size();
|
||||
|
||||
while (p < end) {
|
||||
int n = chunker_utf8_len(*p);
|
||||
if (p + n > end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (n == 3) {
|
||||
uint32_t cp =
|
||||
((uint32_t) (p[0] & 0x0F) << 12) | ((uint32_t) (p[1] & 0x3F) << 6) | ((uint32_t) (p[2] & 0x3F));
|
||||
if (cp >= 0x4E00 && cp <= 0x9FFF) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
p += n;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Strips text and appends a terminal punctuation if missing. Mirrors
|
||||
// add_punctuation in omnivoice/utils/text.py: appends "." for non-Chinese
|
||||
// text, and the ideographic full stop "。" for text containing CJK.
|
||||
static std::string add_punctuation(const std::string & text) {
|
||||
std::string s = chunker_strip(text);
|
||||
if (s.empty()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string last = chunker_last_codepoint(s);
|
||||
const auto & end_set = chunker_end_punctuation();
|
||||
if (end_set.count(last) > 0) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (chunker_contains_chinese(s)) {
|
||||
s += "\xe3\x80\x82"; // U+3002
|
||||
} else {
|
||||
s += ".";
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
Reference in New Issue
Block a user