cleanup: remove dead code and DRY pre_conv release via wctx_free

This commit is contained in:
Pascal
2026-05-14 23:27:09 +02:00
parent dda50c2225
commit f9f4a0c820
10 changed files with 20 additions and 1005 deletions
+3 -2
View File
@@ -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)
-23
View File
@@ -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;
-387
View File
@@ -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
View File
@@ -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));
}
-87
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+5 -6
View File
@@ -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;
-7
View File
@@ -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++) {
-382
View File
@@ -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;
}