Add bounded dictation state and persistent offline Redux worker

This commit is contained in:
baketnk committed 2026-09-24 11:44:24 -04:00
1 parent f79db54ca9
commit 0cb2991f02
13 files changed
+1132

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 FrameYap contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -0,0 +1 @@
"""Standalone FrameYap offline CPU worker (no model imports on package import)."""
+9
View File
@@ -0,0 +1,9 @@
"""Pinned public moondream/parakeet-redux artifacts; weights are never in Git."""
REVISION = "fad622f25f303105c20d70e201bcc477c88b620c"
FILES = {
"model.safetensors": (177774490, "78ec25733ee0d0c1586d1346fc86db9d0c2e436e3a8ab1d32a82d1bb8f848d21"),
"config.json": (12988, "503c653b2e3bb788adbcb04f5abdee532d958686564081baeed133ff10143f6e"),
"ternary.json": (57970, "1221c6d3ce901ffe09c089da758a8db8b76189f80cff41c5afc244fc61e2051d"),
"tokenizer.json": (1159960, "bd321b096832a3f270bd3b2a88823957920f1a5c5ada71114a26ea729d0cbe91"),
"README.md": (8533, "a8b327f983a8b8ff262ff7bead3a791fbed9350632002af8db85ab5cd84cdaa5"),
}
+191
View File
@@ -0,0 +1,191 @@
"""Persistent local Redux worker. No imports of model dependencies until explicit start.
Wire protocol: unsigned LE32 payload length (max 65536), type byte, unsigned
LE64 request id for T/R/E; Y (ready) and F (load failure) have no id. T refers
to the fixed clip.raw in the private directory. R/E carry <=4096 UTF-8 bytes.
Only one T may be outstanding. No stdout other than frames; stderr is suppressed
by the native launcher. This module never captures audio or delivers input.
"""
import argparse
import hashlib
import os
from pathlib import Path
import stat
import struct
import sys
MAX_FRAME = 65536
MAX_TEXT = 4096
MIN_SAMPLES = 3200
MAX_SAMPLES = 320000
try:
from .model_files import REVISION, FILES
except ImportError: # direct executable script
from model_files import REVISION, FILES
def read_exact(fd, count):
parts = bytearray()
while len(parts) < count:
part = os.read(fd, count - len(parts))
if not part:
if not parts:
return None
raise ValueError("truncated worker frame")
parts.extend(part)
return bytes(parts)
def read_frame(fd):
header = read_exact(fd, 4)
if header is None:
return None
size, = struct.unpack("<I", header)
if not 1 <= size <= MAX_FRAME:
raise ValueError("invalid frame length")
payload = read_exact(fd, size)
if payload is None:
raise ValueError("truncated frame")
return payload
def send_frame(fd, kind, data=b""):
payload = kind + data
if len(payload) > MAX_FRAME:
raise ValueError("oversize response")
frame = struct.pack("<I", len(payload)) + payload
while frame:
written = os.write(fd, frame)
if written <= 0:
raise OSError("closed worker pipe")
frame = frame[written:]
def private_dir(path):
st = os.lstat(path)
if not stat.S_ISDIR(st.st_mode) or st.st_uid != os.geteuid() or st.st_mode & 0o077:
raise ValueError("clip directory must be private and owned by current user")
def local_model(path):
"""Require a real local directory with weight file(s), never a hub identifier."""
root = Path(path)
if not root.is_absolute() or not root.is_dir() or root.is_symlink():
raise ValueError("absolute local model directory required")
for name, (size, expected) in FILES.items():
file = root / name
if not file.is_file() or file.is_symlink() or file.stat().st_size != size:
raise ValueError("pinned local Redux model weights missing or incomplete")
digest = hashlib.sha256()
with file.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
if digest.hexdigest() != expected:
raise ValueError("local Redux model does not match pinned revision")
return str(root)
def load_model(path, threads):
# Set before importing moondream/torch/huggingface dependencies, even when
# invoked directly without the native adapter.
os.environ["HF_HUB_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"
os.environ["HF_DATASETS_OFFLINE"] = "1"
for key in ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS"):
os.environ[key] = str(threads)
os.environ["CUDA_VISIBLE_DEVICES"] = ""
os.environ["TOKENIZERS_PARALLELISM"] = "false"
directory = local_model(path)
import torch
torch.set_num_threads(threads)
torch.set_num_interop_threads(1)
import moondream as md # noqa: explicit lazy import
return md.photon("moondream/parakeet-redux", model_path=directory,
device="cpu", cpu_threads=threads)
def read_clip(directory):
# No symlinks; the parent wrote an exclusive, mode-0600 file. Keep private
# audio out of logs and do not follow a swapped filename.
fd = os.open(os.path.join(directory, "clip.raw"), os.O_RDONLY | os.O_NOFOLLOW)
try:
st = os.fstat(fd)
if not stat.S_ISREG(st.st_mode) or st.st_uid != os.geteuid() or st.st_mode & 0o077:
raise ValueError("unsafe clip")
if not MIN_SAMPLES * 4 <= st.st_size <= MAX_SAMPLES * 4 or st.st_size % 4:
raise ValueError("invalid clip length")
raw = read_exact(fd, st.st_size)
if raw is None or os.read(fd, 1):
raise ValueError("clip changed during read")
finally:
os.close(fd)
import numpy as np # deferred until the explicit request
pcm = np.frombuffer(raw, dtype="<f4")
if not np.isfinite(pcm).all():
raise ValueError("invalid PCM")
return pcm
def run(model, directory, input_fd=0, output_fd=1):
private_dir(directory)
send_frame(output_fd, b"Y")
while True:
frame = read_frame(input_fd)
if frame is None:
return
if len(frame) != 9 or frame[0:1] != b"T":
raise ValueError("invalid request")
request_id = frame[1:9]
try:
result = model.transcribe(audio=read_clip(directory), sample_rate=16000)
text = result["text"]
if not isinstance(text, str):
raise ValueError("invalid model response")
encoded = text.encode("utf-8", errors="strict")
if len(encoded) > MAX_TEXT:
raise ValueError("transcript exceeds 4096 bytes")
send_frame(output_fd, b"R", request_id + encoded)
except Exception:
# Model exceptions may include audio or transcripts. Do not log or
# forward them to the overlay; request-local failure only.
send_frame(output_fd, b"E", request_id + b"transcription failed")
def main(argv=None):
parser = argparse.ArgumentParser(description="FrameYap local offline Redux worker")
parser.add_argument("--model", required=True)
parser.add_argument("--threads", type=int, default=2)
parser.add_argument("--clip-dir", required=True)
args = parser.parse_args(argv)
if not 1 <= args.threads <= 64:
parser.error("threads must be 1..64")
# Native libraries sometimes print directly to fd 1. Keep those bytes out of
# the framed channel, not merely Python's sys.stdout wrapper.
protocol_fd = os.dup(1)
os.set_inheritable(protocol_fd, False)
with open(os.devnull, "wb") as null:
os.dup2(null.fileno(), 1)
os.dup2(null.fileno(), 2)
try:
try:
private_dir(args.clip_dir)
# The full hash validation occurs exactly once in load_model.
if not Path(args.model).is_dir():
raise ValueError("missing model")
except Exception:
send_frame(protocol_fd, b"F", b"M")
return 1
try:
model = load_model(args.model, args.threads)
except Exception:
send_frame(protocol_fd, b"F", b"D")
return 1
run(model, args.clip_dir, output_fd=protocol_fd)
return 0
finally:
os.close(protocol_fd)
if __name__ == "__main__":
sys.exit(main())
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Opt-in local CPU/IPC benchmark of a supplied nonprivate WAV. Never captures or types."""
import argparse
import array
import os
from pathlib import Path
import select
import statistics
import struct
import subprocess
import tempfile
import time
import wave
def receive(fd, timeout):
end = time.monotonic() + timeout
def exact(count):
data = bytearray()
while len(data) < count:
remaining = end - time.monotonic()
if remaining <= 0 or not select.select([fd], [], [], remaining)[0]:
raise TimeoutError("worker deadline")
chunk = os.read(fd, count - len(data))
if not chunk:
raise RuntimeError("worker exited")
data.extend(chunk)
return bytes(data)
size, = struct.unpack("<I", exact(4))
if not 1 <= size <= 65536:
raise ValueError("bad frame")
return exact(size)
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--python", required=True)
p.add_argument("--worker", type=Path, required=True)
p.add_argument("--model", type=Path, required=True)
p.add_argument("--wav", type=Path, required=True, help="nonprivate PCM16 mono 16kHz WAV, <=20s")
p.add_argument("--threads", type=int, choices=[2, 4], default=2)
p.add_argument("--repeats", type=int, default=5)
p.add_argument("--show-text", action="store_true", help="explicitly print this nonprivate fixture's transcript")
args = p.parse_args()
if not 1 <= args.repeats <= 20:
p.error("repeats must be 1..20")
with wave.open(str(args.wav), "rb") as wav:
if (wav.getnchannels(), wav.getsampwidth(), wav.getframerate()) != (1, 2, 16000) or not 3200 <= wav.getnframes() <= 320000:
p.error("expected 0.2..20s mono PCM16 at 16kHz")
raw = wav.readframes(wav.getnframes())
pcm = array.array("f", (x[0] / 32768.0 for x in struct.iter_unpack("<h", raw)))
if __import__("sys").byteorder != "little":
pcm.byteswap()
runtime = os.environ.get("XDG_RUNTIME_DIR")
if not runtime:
p.error("XDG_RUNTIME_DIR required")
with tempfile.TemporaryDirectory(prefix="frameyap-benchmark-", dir=runtime) as directory:
start = time.monotonic()
child = subprocess.Popen([args.python, str(args.worker.absolute()), "--model", str(args.model.absolute()),
"--threads", str(args.threads), "--clip-dir", directory],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
try:
ready = receive(child.stdout.fileno(), 120)
if ready != b"Y":
raise RuntimeError(f"worker warmup rejected: {ready!r}")
print(f"load_seconds={time.monotonic() - start:.3f}", flush=True)
times = []
for ident in range(1, args.repeats + 1):
clip = Path(directory) / "clip.raw"
fd = os.open(clip, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "wb") as output:
output.write(pcm.tobytes())
start = time.monotonic()
child.stdin.write(struct.pack("<IcQ", 9, b"T", ident))
child.stdin.flush()
reply = receive(child.stdout.fileno(), 60)
elapsed = time.monotonic() - start
if len(reply) < 9 or reply[:1] != b"R" or struct.unpack("<Q", reply[1:9])[0] != ident:
raise RuntimeError("worker transcription failed or wrong correlation")
text = reply[9:].decode("utf-8")
clip.unlink()
times.append(elapsed)
print(f"request={ident} seconds={elapsed:.3f} bytes={len(reply)-9}", flush=True)
if args.show_text and ident == 1:
print(f"public_fixture_transcript={text}", flush=True)
print(f"threads={args.threads} median_seconds={statistics.median(times):.3f} max_seconds={max(times):.3f}")
finally:
child.stdin.close()
try:
child.wait(timeout=2)
except subprocess.TimeoutExpired:
child.terminate()
try:
child.wait(timeout=2)
except subprocess.TimeoutExpired:
child.kill()
child.wait()
child.stdout.close()
if __name__ == "__main__":
main()
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Explicit setup only: fetch the pinned ~178 MB Redux weights; never used by runtime."""
import argparse
import hashlib
import os
from pathlib import Path
import sys
import tempfile
import urllib.request
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "python"))
from frameyap.model_files import FILES, REVISION
def matches(path, size, digest):
if not path.is_file() or path.is_symlink() or path.stat().st_size != size:
return False
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest() == digest
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--destination", type=Path, required=True)
args = parser.parse_args()
dest = args.destination.expanduser().absolute()
if dest.is_symlink():
parser.error("destination must not be a symlink")
dest.mkdir(mode=0o700, parents=True, exist_ok=True)
for name, (size, digest) in FILES.items():
target = dest / name
if matches(target, size, digest):
continue
if target.exists() or target.is_symlink():
parser.error(f"existing mismatched file: {target}; move it aside explicitly")
url = f"https://huggingface.co/moondream/parakeet-redux/resolve/{REVISION}/{name}"
fd, temp = tempfile.mkstemp(prefix=".download-", dir=dest)
try:
with os.fdopen(fd, "wb") as output, urllib.request.urlopen(url, timeout=60) as source:
total = 0
while chunk := source.read(1024 * 1024):
total += len(chunk)
if total > size:
raise ValueError("download exceeded pinned size")
output.write(chunk)
if not matches(Path(temp), size, digest):
raise ValueError(f"pinned SHA-256/size mismatch: {name}")
os.replace(temp, target)
finally:
Path(temp).unlink(missing_ok=True)
print(f"Pinned Redux {REVISION} verified in {dest}")
print("Model attribution: moondream/parakeet-redux, CC-BY-4.0; see downloaded README.md.")
if __name__ == "__main__":
main()
+60
View File
@@ -0,0 +1,60 @@
#include "core.hpp"
#include <stdexcept>
namespace frameyap {
std::string literal_text(std::string_view input) {
if (input.size() > 4096) throw std::runtime_error("Transcript exceeds 4096 bytes");
std::string out;
for (std::size_t i = 0; i < input.size();) {
const auto start = i;
const auto first = static_cast<unsigned char>(input[i++]);
uint32_t cp = first;
int continuation = 0;
uint32_t minimum = 0;
if (first >= 0xc2 && first <= 0xdf) { cp = first & 31; continuation = 1; minimum = 0x80; }
else if (first >= 0xe0 && first <= 0xef) { cp = first & 15; continuation = 2; minimum = 0x800; }
else if (first >= 0xf0 && first <= 0xf4) { cp = first & 7; continuation = 3; minimum = 0x10000; }
else if (first >= 0x80) throw std::runtime_error("Invalid UTF-8 transcript");
while (continuation--) {
if (i == input.size()) throw std::runtime_error("Truncated UTF-8 transcript");
auto byte = static_cast<unsigned char>(input[i++]);
if ((byte & 0xc0) != 0x80) throw std::runtime_error("Invalid UTF-8 transcript");
cp = (cp << 6) | (byte & 63);
}
if (cp < minimum || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff))
throw std::runtime_error("Invalid UTF-8 codepoint");
if (cp == '\n' || cp == '\r' || cp == '\t' || cp == 0x2028 || cp == 0x2029) {
out += ' ';
} else {
if (cp < 32 || (cp >= 0x7f && cp <= 0x9f))
throw std::runtime_error("Control characters rejected");
out.append(input.substr(start, i - start));
}
}
if (out.find_first_not_of(' ') == std::string::npos) out.clear();
return out;
}
void Session::ready() { if (state_ == State::Warming) state_ = State::Ready; }
bool Session::record() {
if (state_ != State::Ready && state_ != State::Queued) return false;
++id_; text_.clear(); state_ = State::Recording; return true;
}
bool Session::finish(std::size_t samples) {
if (state_ != State::Recording) return false;
if (samples < 3200 || samples > 320000) { cancel(); return false; }
state_ = State::Transcribing; return true;
}
bool Session::reply(uint64_t id, std::string_view text) {
if (id != id_ || state_ != State::Transcribing) return false;
text_ = literal_text(text);
state_ = text_.empty() ? State::Ready : State::Review;
return true;
}
std::optional<std::string> Session::take_insert() {
if (state_ != State::Review) return std::nullopt;
auto text = std::move(text_); text_.clear(); state_ = State::Queued;
return text;
}
void Session::cancel() { ++id_; text_.clear(); state_ = State::Ready; }
void Session::fail() { ++id_; text_.clear(); state_ = State::Error; }
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
namespace frameyap {
// Throws on malformed UTF-8, controls, or >4096 bytes. Never interprets commands.
std::string literal_text(std::string_view input);
enum class State { Warming, Ready, Recording, Transcribing, Review, Queued, Error };
// Hardware-free delivery ledger. A request can be consumed only once, including
// ambiguous transport failures. Explicit Insert authorizes CURRENT seat focus.
class Session {
public:
State state() const { return state_; }
const std::string& text() const { return text_; }
uint64_t id() const { return id_; }
void ready();
bool record();
bool finish(std::size_t samples);
bool reply(uint64_t id, std::string_view text);
std::optional<std::string> take_insert();
void cancel();
void fail();
private:
State state_ = State::Warming;
uint64_t id_ = 0;
std::string text_;
};
}
+296
View File
@@ -0,0 +1,296 @@
#include "worker.hpp"
#include <array>
#include <cerrno>
#include <chrono>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <stdexcept>
#include <string_view>
#include <system_error>
#include <utility>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <spawn.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
extern char** environ;
namespace frameyap {
namespace {
using Clock = std::chrono::steady_clock;
constexpr size_t max_frame = 65536;
constexpr size_t max_text = 4096;
void close_fd(int& fd) { if (fd >= 0) { ::close(fd); fd = -1; } }
void put32(unsigned char* p, uint32_t n) {
for (int i = 0; i < 4; ++i) p[i] = static_cast<unsigned char>(n >> (8 * i));
}
void put64(unsigned char* p, uint64_t n) {
for (int i = 0; i < 8; ++i) p[i] = static_cast<unsigned char>(n >> (8 * i));
}
uint32_t get32(const unsigned char* p) {
uint32_t n = 0;
for (int i = 0; i < 4; ++i) n |= uint32_t(p[i]) << (8 * i);
return n;
}
uint64_t get64(const unsigned char* p) {
uint64_t n = 0;
for (int i = 0; i < 8; ++i) n |= uint64_t(p[i]) << (8 * i);
return n;
}
bool valid_utf8(std::string_view text) {
for (size_t i = 0; i < text.size();) {
unsigned char first = static_cast<unsigned char>(text[i]);
if (first == 0) return false;
if (first < 0x80) { ++i; continue; }
unsigned count = first >= 0xc2 && first <= 0xdf ? 2 :
first >= 0xe0 && first <= 0xef ? 3 :
first >= 0xf0 && first <= 0xf4 ? 4 : 0;
if (!count || i + count > text.size()) return false;
for (unsigned j = 1; j < count; ++j)
if ((static_cast<unsigned char>(text[i + j]) & 0xc0) != 0x80) return false;
unsigned char second = static_cast<unsigned char>(text[i + 1]);
if ((first == 0xe0 && second < 0xa0) || (first == 0xed && second >= 0xa0) ||
(first == 0xf0 && second < 0x90) || (first == 0xf4 && second >= 0x90)) return false;
i += count;
}
return true;
}
void check_runtime(const char* path) {
if (!path || path[0] != '/') throw std::runtime_error("XDG_RUNTIME_DIR must be an absolute private directory");
struct stat st{};
if (::lstat(path, &st) || !S_ISDIR(st.st_mode) || st.st_uid != ::geteuid() || (st.st_mode & 0077))
throw std::runtime_error("XDG_RUNTIME_DIR must be owned by this user and inaccessible to others (no symlinks)");
}
// Blocking SIGPIPE only on this thread avoids changing the host application's signal disposition.
ssize_t safe_write(int fd, const void* data, size_t size) {
sigset_t mask{}, prior{}, pending{};
sigemptyset(&mask); sigaddset(&mask, SIGPIPE);
if (pthread_sigmask(SIG_BLOCK, &mask, &prior)) throw std::runtime_error("cannot block SIGPIPE");
sigpending(&pending);
bool was_pending = sigismember(&pending, SIGPIPE);
ssize_t result = ::write(fd, data, size);
int saved = errno;
if (result < 0 && saved == EPIPE && !was_pending) {
timespec zero{};
::sigtimedwait(&mask, nullptr, &zero);
}
pthread_sigmask(SIG_SETMASK, &prior, nullptr);
errno = saved;
return result;
}
void write_all(int fd, const unsigned char* data, size_t size) {
while (size) {
ssize_t n = safe_write(fd, data, size);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) throw std::runtime_error("worker pipe write failed");
data += n; size -= static_cast<size_t>(n);
}
}
} // namespace
struct Worker::State {
pid_t pid = -1;
int to_child = -1, from_child = -1;
std::string dir;
bool loaded = false;
std::optional<uint64_t> pending;
Clock::time_point deadline{};
std::vector<unsigned char> input;
std::chrono::milliseconds warmup_timeout{120000}, request_timeout{60000};
};
Worker::Worker(std::chrono::milliseconds warmup, std::chrono::milliseconds request)
: state_(std::make_unique<State>()) {
if (warmup.count() <= 0 || request.count() <= 0) throw std::invalid_argument("positive worker deadlines required");
state_->warmup_timeout = warmup;
state_->request_timeout = request;
}
Worker::~Worker() { stop(); }
void Worker::stop() {
auto& s = *state_;
close_fd(s.to_child);
close_fd(s.from_child);
if (s.pid > 0) {
::kill(s.pid, SIGTERM); // Only our direct child, never a process group.
auto until = Clock::now() + std::chrono::milliseconds(500);
int status = 0;
while (::waitpid(s.pid, &status, WNOHANG) == 0 && Clock::now() < until)
::usleep(10000);
if (::waitpid(s.pid, &status, WNOHANG) == 0) {
::kill(s.pid, SIGKILL);
while (::waitpid(s.pid, &status, 0) < 0 && errno == EINTR) {}
}
s.pid = -1;
}
if (!s.dir.empty()) {
::unlink((s.dir + "/clip.raw").c_str());
::rmdir(s.dir.c_str());
s.dir.clear();
}
s.input.clear(); s.pending.reset(); s.loaded = false;
}
void Worker::start(const std::string& python, const std::string& script,
const std::string& model, int threads) {
if (state_->pid > 0) throw std::logic_error("worker already started");
if (python.empty() || script.empty() || model.empty() || threads < 1 || threads > 64)
throw std::invalid_argument("python, script, local model and 1..64 threads required");
check_runtime(::getenv("XDG_RUNTIME_DIR"));
try {
// The model's internal files are checked by the child before loading, never fetched.
std::string pattern = std::string(::getenv("XDG_RUNTIME_DIR")) + "/frameyap-XXXXXX";
std::vector<char> tmp(pattern.begin(), pattern.end()); tmp.push_back('\0');
if (!::mkdtemp(tmp.data())) throw std::runtime_error("cannot create private clip directory");
state_->dir = tmp.data();
::chmod(state_->dir.c_str(), 0700);
int in[2]{-1,-1}, out[2]{-1,-1};
if (::pipe2(in, O_CLOEXEC) || ::pipe2(out, O_CLOEXEC)) {
close_fd(in[0]); close_fd(in[1]); close_fd(out[0]); close_fd(out[1]);
throw std::runtime_error("cannot create worker pipes");
}
std::string thread_arg = std::to_string(threads);
// OpenVR/SDL already have threads. posix_spawn avoids allocations and
// setenv in a forked child of a multithreaded process.
std::vector<std::string> environment;
for (char** e = environ; *e; ++e) {
std::string_view entry(*e);
if (entry.starts_with("PYTHONDONTWRITEBYTECODE=")) continue;
environment.emplace_back(*e);
}
environment.emplace_back("PYTHONDONTWRITEBYTECODE=1");
std::vector<char*> envp;
for (auto& value : environment) envp.push_back(value.data());
envp.push_back(nullptr);
const char* args[] = {python.c_str(), script.c_str(), "--model", model.c_str(),
"--threads", thread_arg.c_str(), "--clip-dir", state_->dir.c_str(), nullptr};
posix_spawn_file_actions_t actions;
int error = posix_spawn_file_actions_init(&actions);
if (error) {
close_fd(in[0]); close_fd(in[1]); close_fd(out[0]); close_fd(out[1]);
throw std::runtime_error("cannot initialize worker spawn");
}
error = posix_spawn_file_actions_adddup2(&actions, in[0], STDIN_FILENO);
if (!error) error = posix_spawn_file_actions_adddup2(&actions, out[1], STDOUT_FILENO);
if (!error) error = posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null", O_WRONLY, 0);
for (int fd : std::array<int, 4>{in[0], in[1], out[0], out[1]})
if (!error && fd > STDERR_FILENO) error = posix_spawn_file_actions_addclose(&actions, fd);
pid_t pid = -1;
if (!error) error = posix_spawnp(&pid, python.c_str(), &actions, nullptr,
const_cast<char* const*>(args), envp.data());
posix_spawn_file_actions_destroy(&actions);
if (error) {
close_fd(in[0]); close_fd(in[1]); close_fd(out[0]); close_fd(out[1]);
throw std::runtime_error("cannot launch configured Python worker");
}
close_fd(in[0]); close_fd(out[1]);
state_->pid = pid; state_->to_child = in[1]; state_->from_child = out[0];
int flags = ::fcntl(state_->from_child, F_GETFL);
if (flags < 0 || ::fcntl(state_->from_child, F_SETFL, flags | O_NONBLOCK))
throw std::runtime_error("cannot set nonblocking worker pipe");
state_->deadline = Clock::now() + state_->warmup_timeout;
} catch (...) { stop(); throw; }
}
bool Worker::ready() const { return state_->pid > 0 && state_->loaded; }
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");
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");
try {
// IEEE-754 binary32 encoded explicitly little-endian, independent of host byte order.
std::array<unsigned char, 4096> bytes{};
size_t index = 0;
for (float value : pcm) {
uint32_t bits; static_assert(sizeof bits == sizeof value);
std::memcpy(&bits, &value, sizeof bits);
put32(bytes.data() + index, bits); index += 4;
if (index == bytes.size()) { write_all(fd, bytes.data(), index); index = 0; }
}
if (index) write_all(fd, bytes.data(), index);
if (::close(fd)) { fd = -1; throw std::runtime_error("clip close failed"); }
fd = -1;
unsigned char request[13]{};
put32(request, 9); request[4] = 'T'; put64(request + 5, id);
// Pipe is empty (single outstanding request); 13 bytes <= PIPE_BUF.
write_all(s.to_child, request, sizeof request);
s.pending = id;
s.deadline = Clock::now() + s.request_timeout;
} catch (...) {
if (fd >= 0) ::close(fd);
::unlink(path.c_str());
stop(); // Any partial pipe write means protocol synchronization is unknown.
throw;
}
}
std::optional<WorkerReply> Worker::poll() {
auto& s = *state_;
if (s.pid <= 0) return std::nullopt;
try {
if ((!s.loaded || s.pending) && Clock::now() > s.deadline)
throw std::runtime_error(s.loaded ? "worker transcription timed out" : "worker warmup timed out");
unsigned char block[4096];
bool eof = false;
for (;;) {
ssize_t n = ::read(s.from_child, block, sizeof block);
if (n > 0) {
s.input.insert(s.input.end(), block, block + n);
if (s.input.size() > max_frame + 4) throw std::runtime_error("worker frame exceeded limit");
continue;
}
if (n == 0) { eof = true; break; }
if (errno == EINTR) continue;
if (errno != EAGAIN && errno != EWOULDBLOCK) throw std::runtime_error("worker pipe read failed");
break;
}
if (s.input.size() >= 4) {
auto size = get32(s.input.data());
if (size > max_frame || size < 1) throw std::runtime_error("invalid worker frame length");
if (s.input.size() >= 4 + size) {
if (s.input.size() != 4 + size) throw std::runtime_error("unexpected extra worker frame");
char type = static_cast<char>(s.input[4]);
if (type == 'Y' && size == 1 && !s.loaded && !s.pending) {
if (eof) throw std::runtime_error("worker exited after warmup");
s.loaded = true; s.input.clear(); return std::nullopt;
}
if (type == 'F' && !s.loaded) {
if (size == 2 && s.input[5] == 'M')
throw std::runtime_error("worker missing local model weights or private clip directory");
throw std::runtime_error("worker failed to load local CPU model/runtime");
}
if ((type != 'R' && type != 'E') || size < 9 || !s.loaded || !s.pending ||
get64(s.input.data() + 5) != *s.pending || size - 9 > max_text)
throw std::runtime_error("invalid or stale worker reply");
WorkerReply reply{*s.pending, {}, {}};
std::string text(reinterpret_cast<const char*>(s.input.data() + 13), size - 9);
if (!valid_utf8(text)) throw std::runtime_error("invalid worker UTF-8 reply");
if (type == 'R') reply.text = std::move(text);
else reply.error = std::move(text);
s.pending.reset(); s.input.clear();
::unlink((s.dir + "/clip.raw").c_str());
return reply;
}
}
if (eof) throw std::runtime_error("worker exited or closed its pipe");
int status{};
if (::waitpid(s.pid, &status, WNOHANG) == s.pid) {
s.pid = -1;
throw std::runtime_error("worker exited unexpectedly");
}
return std::nullopt;
} catch (...) { stop(); throw; }
}
} // namespace frameyap
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <cstdint>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace frameyap {
struct WorkerReply {
uint64_t id;
std::string text;
std::string error;
};
class Worker {
public:
explicit Worker(std::chrono::milliseconds warmup_timeout = std::chrono::seconds(120),
std::chrono::milliseconds request_timeout = std::chrono::seconds(60));
~Worker();
Worker(const Worker&) = delete;
Worker& operator=(const Worker&) = delete;
void start(const std::string& python, const std::string& script,
const std::string& model, int threads = 2);
bool ready() const;
void submit(uint64_t id, const std::vector<float>& pcm);
std::optional<WorkerReply> poll();
void stop();
private:
struct State;
std::unique_ptr<State> state_;
};
} // namespace frameyap
+31
View File
@@ -0,0 +1,31 @@
#include "core.hpp"
#include <cstdlib>
#include <iostream>
#include <stdexcept>
using namespace frameyap;
#define CHECK(x) do { if (!(x)) { std::cerr << "line " << __LINE__ << ": " #x "\n"; std::exit(1); } } while (0)
void rejects(std::string text) { try { literal_text(text); } catch (const std::runtime_error&) { return; } CHECK(false); }
int main() {
CHECK(literal_text("Hello 世界 😀") == "Hello 世界 😀");
CHECK(literal_text("one\r\ntwo\tthree\xe2\x80\xa8") == "one two three ");
CHECK(literal_text("submit; $(echo x)") == "submit; $(echo x)");
CHECK(literal_text(" \t\n").empty());
CHECK(literal_text(std::string(4096, 'a')).size() == 4096);
rejects(std::string(4097, 'a')); rejects(std::string("a\0b", 3));
rejects("\x1b[0m"); rejects("\xc0\xaf"); rejects("\xed\xa0\x80");
rejects("\xf4\x90\x80\x80"); rejects("\xe2\x82"); rejects("\xc2\x85");
Session s;
CHECK(!s.record()); s.ready(); CHECK(s.record());
CHECK(!s.finish(3199)); CHECK(s.state() == State::Ready);
CHECK(s.record()); auto cancelled = s.id(); CHECK(s.finish(3200));
CHECK(!s.record()); s.cancel(); CHECK(!s.reply(cancelled, "stale"));
CHECK(s.record()); auto current = s.id(); CHECK(s.finish(320000));
CHECK(!s.reply(current - 1, "stale")); CHECK(s.reply(current, "text\n"));
CHECK(!s.reply(current, "duplicate")); CHECK(!s.record());
CHECK(s.take_insert() == "text "); CHECK(!s.take_insert());
CHECK(!s.reply(current, "again")); CHECK(s.record()); CHECK(!s.finish(320001));
CHECK(s.record()); CHECK(s.finish(16000)); CHECK(s.reply(s.id(), ""));
CHECK(s.state() == State::Ready); CHECK(!s.take_insert());
CHECK(s.record()); CHECK(s.finish(16000)); s.fail(); CHECK(!s.reply(s.id(), "bad"));
std::cout << "core checks passed\n";
}
+167
View File
@@ -0,0 +1,167 @@
"""Hardware-free tests, and a deliberately fake child for native IPC tests.
Run: python3 -m unittest discover -s tests -p test_worker.py
Native fixture: python3 tests/test_worker.py --model ok --threads 2 --clip-dir DIR
"""
import os
from pathlib import Path
import struct
import sys
import tempfile
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "python"))
from frameyap import worker
class FakeModel:
def __init__(self):
self.calls = []
def transcribe(self, audio, sample_rate):
self.calls.append((audio, sample_rate))
return {"text": "héllo 世界"}
def fake_child():
# This branch intentionally never imports numpy, torch, moondream or a mic.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--model", required=True)
parser.add_argument("--threads", required=True)
parser.add_argument("--clip-dir", required=True)
args = parser.parse_args()
if args.model == "fail":
worker.send_frame(1, b"F")
return
if args.model == "crash":
return
if args.model == "hang-warm":
import time
time.sleep(130)
return
worker.send_frame(1, b"Y")
while True:
msg = worker.read_frame(0)
if msg is None:
return
if args.model == "hang":
import time
time.sleep(70)
continue
assert len(msg) == 9 and msg[:1] == b"T"
clip = Path(args.clip_dir) / "clip.raw"
assert clip.stat().st_size == 3200 * 4
ident = msg[1:9]
if args.model == "stale":
ident = struct.pack("<Q", struct.unpack("<Q", ident)[0] + 1)
if args.model == "oversized-frame":
os.write(1, struct.pack("<I", 65537))
continue
text = b"a" * 4097 if args.model == "long" else "héllo 世界".encode()
worker.send_frame(1, b"R", ident + text)
if args.model == "duplicate":
worker.send_frame(1, b"R", ident + text)
class WorkerTests(unittest.TestCase):
def test_framing_and_bounds(self):
r, w = os.pipe()
try:
worker.send_frame(w, b"T", struct.pack("<Q", 23))
self.assertEqual(worker.read_frame(r), b"T" + struct.pack("<Q", 23))
os.write(w, struct.pack("<I", 65537))
with self.assertRaises(ValueError):
worker.read_frame(r)
finally:
os.close(r)
os.close(w)
def test_fake_transcription_correlated_and_private(self):
with tempfile.TemporaryDirectory() as path:
os.chmod(path, 0o700)
clip = Path(path) / "clip.raw"
clip.write_bytes(struct.pack("<3200f", *([0.0] * 3200)))
os.chmod(clip, 0o600)
model = FakeModel()
with patch.object(worker, "read_clip", return_value=[0.0] * 3200):
in_r, in_w = os.pipe()
out_r, out_w = os.pipe()
try:
worker.send_frame(in_w, b"T", struct.pack("<Q", 42))
os.close(in_w)
in_w = -1
worker.run(model, path, in_r, out_w)
self.assertEqual(worker.read_frame(out_r), b"Y")
reply = worker.read_frame(out_r)
self.assertEqual(reply, b"R" + struct.pack("<Q", 42) + "héllo 世界".encode())
self.assertEqual(model.calls[0][1], 16000)
finally:
os.close(in_r)
if in_w >= 0:
os.close(in_w)
os.close(out_r)
os.close(out_w)
def test_missing_weights_no_dependency_import(self):
with tempfile.TemporaryDirectory() as path:
with self.assertRaisesRegex(ValueError, "weights missing"):
worker.local_model(path)
with patch.dict(sys.modules, {"moondream": None}):
with self.assertRaises(ValueError):
worker.load_model(path, 2)
self.assertEqual(os.environ["HF_HUB_OFFLINE"], "1")
def test_pinned_model_hashes_and_symlinks(self):
import hashlib
with tempfile.TemporaryDirectory() as path:
root = Path(path)
(root / "model.safetensors").write_bytes(b"fixture")
digest = hashlib.sha256(b"fixture").hexdigest()
with patch.object(worker, "FILES", {"model.safetensors": (7, digest)}):
self.assertEqual(worker.local_model(path), path)
(root / "model.safetensors").write_bytes(b"changed")
with self.assertRaisesRegex(ValueError, "pinned revision"):
worker.local_model(path)
(root / "model.safetensors").unlink()
(root / "other").write_bytes(b"fixture")
(root / "model.safetensors").symlink_to(root / "other")
with self.assertRaises(ValueError):
worker.local_model(path)
def test_unsafe_clip_and_oversize_text(self):
with tempfile.TemporaryDirectory() as path:
os.chmod(path, 0o700)
outside = Path(path) / "outside"
outside.write_bytes(b"0" * 12800)
os.symlink(outside, Path(path) / "clip.raw")
with self.assertRaises(OSError):
worker.read_clip(path)
(Path(path) / "clip.raw").unlink()
class Large:
def transcribe(self, **kwargs):
return {"text": "a" * 4097}
with patch.object(worker, "read_clip", return_value=[]):
in_r, in_w = os.pipe()
out_r, out_w = os.pipe()
try:
worker.send_frame(in_w, b"T", struct.pack("<Q", 7))
os.close(in_w)
in_w = -1
worker.run(Large(), path, in_r, out_w)
self.assertEqual(worker.read_frame(out_r), b"Y")
self.assertEqual(worker.read_frame(out_r), b"E" + struct.pack("<Q", 7) + b"transcription failed")
finally:
os.close(in_r)
if in_w >= 0:
os.close(in_w)
os.close(out_r)
os.close(out_w)
if __name__ == "__main__":
if "--clip-dir" in sys.argv:
fake_child()
else:
unittest.main()
+131
View File
@@ -0,0 +1,131 @@
#include "worker.hpp"
#include <cassert>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <functional>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
#include <unistd.h>
using namespace frameyap;
using namespace std::chrono_literals;
static void until(const std::function<bool()>& fn) {
auto deadline = std::chrono::steady_clock::now() + 4s;
while (!fn()) {
if (std::chrono::steady_clock::now() >= deadline) throw std::runtime_error("fixture timed out");
std::this_thread::sleep_for(5ms);
}
}
int main(int argc, char** argv) {
// CMake should pass its Python interpreter and the absolute test_worker.py path.
if (argc != 3) return 2;
char runtime[] = "/tmp/frameyap-test-XXXXXX";
if (!::mkdtemp(runtime)) return 3;
::setenv("XDG_RUNTIME_DIR", runtime, 1);
try {
std::vector<float> clip(3200, 0.25f);
Worker worker;
worker.start(argv[1], argv[2], "ok", 2);
assert(!worker.ready());
until([&] { worker.poll(); return worker.ready(); });
bool rejected = false;
try { worker.submit(1, std::vector<float>(3199)); }
catch (const std::invalid_argument&) { rejected = true; }
assert(rejected);
worker.submit(100, clip);
rejected = false;
try { worker.submit(101, clip); }
catch (const std::logic_error&) { rejected = true; }
assert(rejected);
std::optional<WorkerReply> reply;
until([&] { reply = worker.poll(); return reply.has_value(); });
assert(reply->id == 100 && reply->text == "héllo 世界" && reply->error.empty());
worker.submit(101, clip);
until([&] { reply = worker.poll(); return reply.has_value(); });
assert(reply->id == 101);
worker.stop();
assert(!worker.ready());
for (const auto& entry : std::filesystem::directory_iterator(runtime)) {
(void)entry;
throw std::runtime_error("private directory leaked");
}
worker.start(argv[1], argv[2], "fail", 2);
bool failed = false;
until([&] {
try { worker.poll(); }
catch (const std::runtime_error&) { failed = true; }
return failed;
});
assert(!worker.ready());
worker.start(argv[1], argv[2], "stale", 2);
until([&] { worker.poll(); return worker.ready(); });
worker.submit(4, clip);
failed = false;
until([&] {
try { worker.poll(); }
catch (const std::runtime_error&) { failed = true; }
return failed;
});
for (const char* mode : {"long", "oversized-frame", "duplicate"}) {
worker.start(argv[1], argv[2], mode, 2);
until([&] { worker.poll(); return worker.ready(); });
worker.submit(5, clip);
failed = false;
until([&] {
try { worker.poll(); }
catch (const std::runtime_error&) { failed = true; }
return failed;
});
assert(!worker.ready());
}
// Cancellation must not wait for inference or leak the private clip.
worker.start(argv[1], argv[2], "hang", 2);
until([&] { worker.poll(); return worker.ready(); });
worker.submit(6, clip);
auto cancel_start = std::chrono::steady_clock::now();
worker.stop();
assert(std::chrono::steady_clock::now() - cancel_start < 2s);
assert(std::filesystem::is_empty(runtime));
worker.start(argv[1], argv[2], "hang-warm", 2);
worker.stop();
assert(std::filesystem::is_empty(runtime));
worker.start(argv[1], argv[2], "crash", 2);
failed = false;
until([&] {
try { worker.poll(); }
catch (const std::runtime_error&) { failed = true; }
return failed;
});
worker.stop();
Worker deadlines(2s, 50ms);
deadlines.start(argv[1], argv[2], "hang", 2);
until([&] { deadlines.poll(); return deadlines.ready(); });
deadlines.submit(9, clip);
failed = false;
until([&] {
try { deadlines.poll(); }
catch (const std::runtime_error& e) { failed = std::string(e.what()).find("timed out") != std::string::npos; }
return failed;
});
assert(std::filesystem::is_empty(runtime));
Worker warm_deadline(50ms, 2s);
warm_deadline.start(argv[1], argv[2], "hang-warm", 2);
failed = false;
until([&] {
try { warm_deadline.poll(); }
catch (const std::runtime_error& e) { failed = std::string(e.what()).find("timed out") != std::string::npos; }
return failed;
});
assert(std::filesystem::is_empty(runtime));
std::filesystem::remove(runtime);
} catch (...) {
std::filesystem::remove(runtime);
throw;
}
}