mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 00:00:05 +02:00
233 lines
9.5 KiB
Python
233 lines
9.5 KiB
Python
"""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 default, or captured privately with explicit --advanced-debug. This module never captures audio or delivers input.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
import struct
|
|
import sys
|
|
import traceback
|
|
|
|
MAX_FRAME = 65536
|
|
MAX_TEXT = 4096
|
|
MIN_SAMPLES = 3200
|
|
MAX_SAMPLES = 320000
|
|
try:
|
|
from .model_files import DEFAULT_MANIFEST_DIR, ManifestError, check_model, load_backends
|
|
except ImportError: # direct executable script
|
|
from model_files import DEFAULT_MANIFEST_DIR, ManifestError, check_model, load_backends
|
|
|
|
|
|
class LocalModelError(ValueError):
|
|
"""Missing, incomplete or wrong pinned local weights (never a download cue)."""
|
|
|
|
|
|
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, manifest_dir=DEFAULT_MANIFEST_DIR):
|
|
"""Check Redux against its manifest before loading any inference libraries."""
|
|
try:
|
|
backend = load_backends(manifest_dir)["redux"]
|
|
except (ManifestError, KeyError) as error:
|
|
raise LocalModelError("pinned Redux manifest missing or invalid") from error
|
|
result = check_model(backend, path)
|
|
if result["state"] != "installed_verified":
|
|
raise LocalModelError("pinned local Redux model weights missing, unsafe or mismatched: " + result["reason"])
|
|
return str(Path(path))
|
|
|
|
|
|
def load_model(path, threads, manifest_dir=DEFAULT_MANIFEST_DIR):
|
|
# 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, manifest_dir)
|
|
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 safe_request_error(stage, error):
|
|
"""Only fixed labels: never stringify exceptions, paths, audio or transcripts."""
|
|
if stage not in ("audio", "inference", "response"):
|
|
stage = "unknown"
|
|
# Use built-in categories, not an arbitrary exception class name supplied by
|
|
# a model/runtime. Subclasses are reduced to their safe built-in category.
|
|
categories = (MemoryError, ImportError, OSError, UnicodeError, TypeError,
|
|
ValueError, KeyError, IndexError, RuntimeError)
|
|
category = next((kind.__name__ for kind in categories if isinstance(error, kind)), "Exception")
|
|
return f"transcription failed [{stage}: {category}]".encode("ascii")
|
|
|
|
|
|
def run(model, directory, input_fd=0, output_fd=1, advanced_debug=False):
|
|
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]
|
|
stage = "audio"
|
|
try:
|
|
audio = read_clip(directory)
|
|
if advanced_debug:
|
|
print(f"request {int.from_bytes(request_id, 'little')}: audio samples={len(audio)} rate=16000", file=sys.stderr, flush=True)
|
|
stage = "inference"
|
|
result = model.transcribe(audio=audio, sample_rate=16000)
|
|
stage = "response"
|
|
text = result["text"]
|
|
if not isinstance(text, str):
|
|
raise TypeError("invalid model response")
|
|
encoded = text.encode("utf-8", errors="strict")
|
|
if len(encoded) > MAX_TEXT:
|
|
raise ValueError("transcript exceeds 4096 bytes")
|
|
if advanced_debug:
|
|
print(f"request {int.from_bytes(request_id, 'little')}: transcript={text!r}", file=sys.stderr, flush=True)
|
|
send_frame(output_fd, b"R", request_id + encoded)
|
|
except Exception as error:
|
|
if advanced_debug:
|
|
print(f"request {int.from_bytes(request_id, 'little')}: failure stage={stage}", file=sys.stderr, flush=True)
|
|
traceback.print_exc(file=sys.stderr)
|
|
# Only fixed stage/category labels cross IPC. Exception messages and
|
|
# tracebacks may contain private audio/text and remain suppressed.
|
|
send_frame(output_fd, b"E", request_id + safe_request_error(stage, error))
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description="FrameYap local offline Redux worker")
|
|
parser.add_argument("--model", required=True)
|
|
parser.add_argument("--manifest-dir", type=Path, default=DEFAULT_MANIFEST_DIR)
|
|
parser.add_argument("--threads", type=int, default=2)
|
|
parser.add_argument("--clip-dir", required=True)
|
|
parser.add_argument("--advanced-debug", action="store_true",
|
|
help="log full exceptions/runtime output and transcripts to stderr; may contain private speech")
|
|
args = parser.parse_args(argv)
|
|
# Dispatcher uses this explicit protocol preference for the pinned Redux
|
|
# launcher, rather than adding flags to arbitrary backend commands.
|
|
args.advanced_debug = args.advanced_debug or os.environ.get("FRAMEYAP_ADVANCED_DEBUG") == "1"
|
|
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)
|
|
if args.advanced_debug:
|
|
# Native parent supplies a bounded private diagnostic sink. Model/native
|
|
# stdout must still never corrupt the duplicated framed protocol fd.
|
|
os.dup2(2, 1)
|
|
print("FrameYap advanced debugging ON: private speech/text/paths may be logged; no raw clip archive.", file=sys.stderr, flush=True)
|
|
else:
|
|
with open(os.devnull, "wb") as null:
|
|
os.dup2(null.fileno(), 1)
|
|
os.dup2(null.fileno(), 2)
|
|
try:
|
|
try:
|
|
private_dir(args.clip_dir)
|
|
except Exception:
|
|
send_frame(protocol_fd, b"F", b"M")
|
|
return 1
|
|
try:
|
|
model = load_model(args.model, args.threads, args.manifest_dir)
|
|
except LocalModelError:
|
|
if args.advanced_debug: traceback.print_exc(file=sys.stderr)
|
|
send_frame(protocol_fd, b"F", b"M")
|
|
return 1
|
|
except ImportError:
|
|
if args.advanced_debug: traceback.print_exc(file=sys.stderr)
|
|
send_frame(protocol_fd, b"F", b"I")
|
|
return 1
|
|
except Exception:
|
|
if args.advanced_debug: traceback.print_exc(file=sys.stderr)
|
|
send_frame(protocol_fd, b"F", b"D")
|
|
return 1
|
|
if args.advanced_debug:
|
|
print("Local model ready", file=sys.stderr, flush=True)
|
|
run(model, args.clip_dir, output_fd=protocol_fd, advanced_debug=args.advanced_debug)
|
|
return 0
|
|
finally:
|
|
os.close(protocol_fd)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|