mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 01:00:04 +02:00
Bound captured PCM to the ASR normalized amplitude range
This commit is contained in:
1 parent
3190877587
commit
019b8132bb
6 files changed
+51
-5
No files matched your search
+5
-2
@@ -15,8 +15,11 @@ cancellation; this component does not implement focus or delivery policy.
|
||||
The adapter requires an existing absolute, owner-private `$XDG_RUNTIME_DIR`
|
||||
(no symlink at the final component), creates its own 0700 `mkdtemp` directory,
|
||||
and writes only `clip.raw` with `O_EXCL|O_NOFOLLOW`, mode 0600. Clips are
|
||||
3200..320000 finite float samples, mono 16 kHz, stored as little-endian IEEE
|
||||
float32 (0.2..20 s). Files are unlinked after replies or shutdown, and the
|
||||
3200..320000 finite float samples in `[-1, 1]`, mono 16 kHz, stored as
|
||||
little-endian IEEE float32 (0.2..20 s). The microphone adapter saturates finite
|
||||
capture/resampler overshoot to this range without rescaling the rest of the
|
||||
clip; NaN/infinity are rejected. `Worker::submit` independently rejects samples
|
||||
outside this contract before writing a clip or request. Files are unlinked after replies or shutdown, and the
|
||||
private directory is removed. Private clips are not encrypted against the
|
||||
account owner/root; do not use an untrusted runtime directory. The caller
|
||||
should pass a trusted interpreter and script. By default audio/transcripts are
|
||||
|
||||
+6
-1
@@ -47,8 +47,13 @@ void Audio::drain() {
|
||||
if (bytes % sizeof(float)) throw std::runtime_error("Misaligned microphone samples");
|
||||
if (recording_) {
|
||||
const auto retain = std::min(std::size_t(bytes) / sizeof(float), 320000 - pcm_.size());
|
||||
for (std::size_t i = 0; i < retain; ++i)
|
||||
for (std::size_t i = 0; i < retain; ++i) {
|
||||
if (!std::isfinite(chunk[i])) throw std::runtime_error("Invalid microphone samples");
|
||||
// Floating-point capture/resampling can exceed full scale. The
|
||||
// ASR API requires normalized PCM; saturate peaks, never scale
|
||||
// the whole clip or silently turn nonfinite input into speech.
|
||||
chunk[i] = std::clamp(chunk[i], -1.0f, 1.0f);
|
||||
}
|
||||
pcm_.insert(pcm_.end(), chunk.begin(), chunk.begin() + retain);
|
||||
}
|
||||
last_data_ = std::chrono::steady_clock::now();
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ public:
|
||||
void prepare(); // open/resume once, while app is ready; never retain idle samples
|
||||
void start(); // arm the already-running stream without touching the device
|
||||
bool poll(); // drain even while idle; true at 20s limit; throws on loss/stall
|
||||
std::vector<float> finish();
|
||||
std::vector<float> finish(); // finite PCM saturated to [-1, 1] for the ASR API
|
||||
void cancel(); // discard current clip, leave the device running
|
||||
void close(); // release device on shutdown or capture failure
|
||||
bool recording() const { return recording_; }
|
||||
|
||||
+3
-1
@@ -353,7 +353,9 @@ void Worker::submit(uint64_t id, const std::vector<float>& pcm) {
|
||||
auto& s = *state_;
|
||||
if (!ready() || s.pending) throw std::logic_error("worker not ready or request already pending");
|
||||
if (pcm.size() < 3200 || pcm.size() > 320000) throw std::invalid_argument("clip must be 0.2..20 seconds at 16 kHz");
|
||||
for (float value : pcm) if (!std::isfinite(value)) throw std::invalid_argument("nonfinite PCM sample");
|
||||
for (float value : pcm)
|
||||
if (!std::isfinite(value) || value < -1.0f || value > 1.0f)
|
||||
throw std::invalid_argument("PCM samples must be finite and in [-1, 1]");
|
||||
const std::string path = s.dir + "/clip.raw";
|
||||
int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
if (fd < 0) throw std::runtime_error("cannot create exclusive private clip");
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
@@ -52,6 +53,31 @@ int main() {
|
||||
feed(0.9f, 100); audio.start(); feed(0.4f, 3200);
|
||||
clip = audio.finish(); assert(clip.size() == 3200 && clip.front() == 0.4f);
|
||||
assert(opens == 1 && closes == 0 && resumes == 1);
|
||||
// Real float capture can exceed full scale. Both poll and finish must
|
||||
// saturate finite peaks without changing ordinary samples or clip size.
|
||||
audio.start();
|
||||
feed(1.01f, 1600); feed(-1.25f, 1600); audio.poll();
|
||||
feed(std::numeric_limits<float>::max(), 1);
|
||||
feed(-std::numeric_limits<float>::max(), 1);
|
||||
feed(1.f, 1); feed(-1.f, 1); feed(.25f, 1); feed(0.f, 1);
|
||||
clip = audio.finish();
|
||||
assert(clip.size() == 3206);
|
||||
assert(std::all_of(clip.begin(), clip.begin() + 1600, [](float x) { return x == 1.f; }));
|
||||
assert(std::all_of(clip.begin() + 1600, clip.begin() + 3200, [](float x) { return x == -1.f; }));
|
||||
assert(clip[3200] == 1.f && clip[3201] == -1.f && clip[3202] == 1.f && clip[3203] == -1.f);
|
||||
assert(clip[3204] == .25f && clip[3205] == 0.f);
|
||||
for (float invalid : {std::numeric_limits<float>::infinity(),
|
||||
-std::numeric_limits<float>::infinity(),
|
||||
std::numeric_limits<float>::quiet_NaN()}) {
|
||||
audio.start(); feed(.2f, 3200); feed(invalid, 1);
|
||||
bool rejected = false;
|
||||
try { (void)audio.finish(); } catch (const std::runtime_error&) { rejected = true; }
|
||||
assert(rejected && !audio.recording() && current->queued.empty());
|
||||
audio.start(); feed(.3f, 3200);
|
||||
clip = audio.finish();
|
||||
assert(clip.size() == 3200 && clip.front() == .3f); // no failed clip tail leaks
|
||||
}
|
||||
assert(opens == 1 && closes == 0 && resumes == 1);
|
||||
removed = true;
|
||||
bool failed = false;
|
||||
try { audio.poll(); } catch (const std::runtime_error&) { failed = true; }
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <sys/stat.h>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -40,6 +41,15 @@ int main(int argc, char** argv) {
|
||||
try { worker.submit(1, std::vector<float>(3199)); }
|
||||
catch (const std::invalid_argument&) { rejected = true; }
|
||||
assert(rejected);
|
||||
for (float value : {1.01f, -1.01f, std::numeric_limits<float>::infinity(),
|
||||
std::numeric_limits<float>::quiet_NaN()}) {
|
||||
auto invalid = clip; invalid.back() = value;
|
||||
rejected = false;
|
||||
try { worker.submit(99, invalid); }
|
||||
catch (const std::invalid_argument&) { rejected = true; }
|
||||
assert(rejected && worker.ready()); // rejection happens before clip/IPC mutation
|
||||
}
|
||||
clip.front() = -1.f; clip.back() = 1.f; // full-scale boundaries are valid
|
||||
worker.submit(100, clip);
|
||||
rejected = false;
|
||||
try { worker.submit(101, clip); }
|
||||
|
||||
Reference in new issue
Block a user