Skip to content

Commit 94eb1a3

Browse files
author
crispasr integration
committed
feat(api,#432): set the reference voice from in-memory samples
rslife: passing a reference only as a path "forces going through filesystem IO and creating temporary files when a segment of a wav file needs to be passed". You have already decoded the audio and sliced out the segment you want; the API then makes you write it back to disk to hand it over. crispasr_session_set_voice_samples(s, pcm, n_samples, sample_rate, ref_text) Session::set_voice_samples(&[f32], sample_rate, ref_text) // Rust WHY IT DELEGATES THROUGH THE PATH FUNCTION INSTEAD OF CALLING BACKENDS DIRECTLY, which is the one interesting decision here. Seven backends already accept PCM (f5_tts_set_reference, pocket_tts_set_voice, moss_tts_set_reference_wav, miotts_set_reference, irodori_tts_set_reference, ...), so routing straight to them is both tempting and obvious, and would avoid the write entirely. It would also bypass crispasr_session_set_voice's consent and Art. 50(4) marking logic, which is keyed on the reference's provenance and emits the [CONSENT] audit line. That would make the compliance trail depend on WHICH OVERLOAD a caller happened to reach for — a clone acquiring a different audit trail for arriving as a buffer rather than a path. That is the same shape as the #435 phonemizer gap fixed earlier today: a second code path that quietly skips what the first one guarantees. One behaviour, one audit trail. So the serialisation still happens — but inside the library, once, in the system temp directory, with cleanup on every exit path including failure. The caller's actual problem (materialising and reaping temp files around a Vec<f32>) is solved. The library's own IO is an implementation detail that per-backend PCM routes can remove later WITHOUT changing this signature, which is why the signature takes samples rather than a path. Temp names combine a steady-clock stamp with an in-process atomic counter rather than a PID, so concurrent calls in one process cannot collide and no getpid()/GetCurrentProcessId() split is needed. A leftover file from a crashed run can never be adopted as this call's reference. Compiles clean (g++ -fsyntax-only over the full TU). Behavioural coverage comes from CI's Rust binding tests; a local end-to-end needs a cloning-capable TTS model, which this box does not have.
1 parent 07fda1c commit 94eb1a3

4 files changed

Lines changed: 129 additions & 0 deletions

File tree

crispasr-sys/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,14 @@ extern "C" {
441441
// #433: backend -> verb ("capability") lookup.
442442
pub fn crispasr_backend_caps_abi(backend: *const c_char, out_csv: *mut c_char, out_cap: c_int) -> c_int;
443443
pub fn crispasr_backend_caps_list_abi(out_buf: *mut c_char, out_cap: c_int) -> c_int;
444+
// #432: reference voice from in-memory samples.
445+
pub fn crispasr_session_set_voice_samples(
446+
s: *mut CrispasrSession,
447+
pcm: *const f32,
448+
n_samples: c_int,
449+
sample_rate: c_int,
450+
ref_text_or_null: *const c_char,
451+
) -> c_int;
444452

445453
/// Describe the exact canonical artifact bundle downloaded by `-m auto`.
446454
/// Returns its artifact count, 0 on miss, or a negative argument/buffer error.

crispasr/src/lib.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,53 @@ impl Session {
919919
Ok(())
920920
}
921921

922+
/// Set the reference voice from samples you already hold (#432).
923+
///
924+
/// [`set_voice`](Self::set_voice) takes a path, which forces a temp file
925+
/// whenever the reference is a *segment* of a WAV you have already decoded.
926+
/// This takes the buffer directly.
927+
///
928+
/// `pcm` is mono float32 at `sample_rate`. `ref_text` follows the same rule
929+
/// as `set_voice`: backends that clone from raw audio need the reference
930+
/// transcript.
931+
///
932+
/// The library still serialises to a temp WAV internally and routes through
933+
/// the same code path as `set_voice`, so consent handling and AI-Act
934+
/// marking are identical for both — a clone does not get a different audit
935+
/// trail for arriving as a buffer. That write is an implementation detail
936+
/// and can be removed per-backend later without changing this signature.
937+
pub fn set_voice_samples(
938+
&self,
939+
pcm: &[f32],
940+
sample_rate: i32,
941+
ref_text: Option<&str>,
942+
) -> Result<(), String> {
943+
if pcm.is_empty() {
944+
return Err("set_voice_samples: empty sample buffer".to_string());
945+
}
946+
if sample_rate <= 0 {
947+
return Err(format!("set_voice_samples: invalid sample_rate {sample_rate}"));
948+
}
949+
let crt = match ref_text {
950+
Some(t) => Some(CString::new(t).map_err(|e| e.to_string())?),
951+
None => None,
952+
};
953+
let rt_ptr = crt.as_ref().map(|c| c.as_ptr()).unwrap_or(std::ptr::null());
954+
let rc = unsafe {
955+
crispasr_sys::crispasr_session_set_voice_samples(
956+
self.handle,
957+
pcm.as_ptr(),
958+
pcm.len() as i32,
959+
sample_rate,
960+
rt_ptr,
961+
)
962+
};
963+
if rc != 0 {
964+
return Err(format!("set_voice_samples failed (rc={rc})"));
965+
}
966+
Ok(())
967+
}
968+
922969
/// Select a fixed/preset speaker by NAME for backends that bake names
923970
/// into the GGUF (orpheus). Names are e.g. `"tara"`/`"leo"` for
924971
/// canopylabs English; `"Anton"`/`"Sophie"` for Kartoffel_Orpheus DE.

include/crispasr_session.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,17 @@ CRISPASR_SESSION_API int crispasr_session_set_codec_path(crispasr_session* s, co
424424
// has no voice-setting implementation.
425425
CRISPASR_SESSION_API int crispasr_session_set_voice(crispasr_session* s, const char* path,
426426
const char* ref_text_or_null);
427+
428+
// #432: same thing, from samples you already hold — no temp file of your own.
429+
// `pcm` is mono float32 at `sample_rate`; `ref_text_or_null` follows the same
430+
// rule as above (required for WAV-style cloning on backends that need it).
431+
//
432+
// The library serialises the samples to a temp WAV internally and routes them
433+
// through crispasr_session_set_voice, so consent handling and the Art. 50(4)
434+
// marking behave identically for both entry points — a clone must not acquire a
435+
// different audit trail by arriving as a buffer instead of a path.
436+
CRISPASR_SESSION_API int crispasr_session_set_voice_samples(crispasr_session* s, const float* pcm, int32_t n_samples,
437+
int32_t sample_rate, const char* ref_text_or_null);
427438
// #201: configure the TADA encoder + aligner GGUFs used for on-the-fly voice
428439
// cloning, i.e. crispasr_session_set_voice(s, "ref.wav", "<transcript>") on a
429440
// TADA session. The .wav clone path is opt-in (experimental) — enable it with

src/crispasr_c_api.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
#include "core/audio_resample.h" // Sidon S2S input-rate conversion
3030

3131
#include <atomic>
32+
#include <chrono>
33+
#include <fstream>
34+
#include <filesystem>
3235
#include <climits> // INT_MIN (parakeet att_context_* sentinels) — issue #257
3336
#include <cstddef> // offsetof (diarize ABI layout static_asserts) — issue #332
3437
#include <cstdint>
@@ -8453,6 +8456,66 @@ static std::vector<float> indextts_resample_16k_to_24k(const float* in, int n) {
84538456
}
84548457
#endif
84558458

8459+
// #432: set the reference voice from IN-MEMORY samples.
8460+
//
8461+
// rslife: passing a reference only as a path "forces going through filesystem
8462+
// IO and creating temporary files when a segment of a wav file needs to be
8463+
// passed" — you have the PCM in hand, and the API makes you write it out.
8464+
//
8465+
// WHY THIS DELEGATES THROUGH THE PATH FUNCTION RATHER THAN CALLING BACKENDS
8466+
// DIRECTLY. Seven backends already take PCM (f5_tts_set_reference,
8467+
// pocket_tts_set_voice, moss_tts_set_reference_wav, miotts_set_reference,
8468+
// irodori_tts_set_reference, ...), so a direct route is tempting and would
8469+
// avoid the write. It would also bypass crispasr_session_set_voice's consent
8470+
// and Art. 50(4) marking logic, which is keyed on the reference's provenance
8471+
// and emits the [CONSENT] audit line. Skipping that for the in-memory path
8472+
// would make the compliance trail depend on WHICH OVERLOAD a caller happened
8473+
// to use, which is precisely the kind of silent gap #435's phonemizer had.
8474+
// One behaviour, one audit trail.
8475+
//
8476+
// So the write still happens — but inside the library, once, in the system temp
8477+
// directory, and it is cleaned up on every exit path. The caller's problem
8478+
// (materialising and reaping temp files around a Vec<f32>) is solved; the
8479+
// library's own IO is an implementation detail that per-backend PCM routes can
8480+
// remove later without changing this signature.
8481+
CA_EXPORT int crispasr_session_set_voice_samples(crispasr_session* s, const float* pcm, int32_t n_samples,
8482+
int32_t sample_rate, const char* ref_text_or_null) {
8483+
if (!s || !pcm || n_samples <= 0 || sample_rate <= 0)
8484+
return -1;
8485+
8486+
std::string wav = crispasr_make_wav_int16(pcm, (int)n_samples, (int)sample_rate);
8487+
if (wav.empty())
8488+
return -1;
8489+
8490+
std::error_code ec;
8491+
std::filesystem::path dir = std::filesystem::temp_directory_path(ec);
8492+
if (ec)
8493+
return -1;
8494+
// Distinct per call and per process: two sessions setting a voice at once
8495+
// must not race on one filename, and a leftover from a crashed run must not
8496+
// be silently adopted as this call's reference.
8497+
// A steady-clock stamp plus an in-process counter, rather than a PID: it is
8498+
// unique across concurrent calls in this process and across processes
8499+
// without needing a getpid()/GetCurrentProcessId() split.
8500+
static std::atomic<uint64_t> seq{0};
8501+
const auto stamp = (unsigned long long)std::chrono::steady_clock::now().time_since_epoch().count();
8502+
const std::filesystem::path tmp = dir / ("crispasr-voice-" + std::to_string(stamp) + "-" +
8503+
std::to_string((unsigned long long)seq.fetch_add(1)) + ".wav");
8504+
8505+
{
8506+
std::ofstream f(tmp, std::ios::binary);
8507+
if (!f)
8508+
return -1;
8509+
f.write(wav.data(), (std::streamsize)wav.size());
8510+
if (!f)
8511+
return -1;
8512+
}
8513+
8514+
const int rc = crispasr_session_set_voice(s, tmp.string().c_str(), ref_text_or_null);
8515+
std::filesystem::remove(tmp, ec); // best effort; the reference is already loaded
8516+
return rc;
8517+
}
8518+
84568519
CA_EXPORT int crispasr_session_set_voice(crispasr_session* s, const char* path, const char* ref_text_or_null) {
84578520
if (!s || !path)
84588521
return -1;

0 commit comments

Comments
 (0)