jyoti / tinyasr /codec_streaming_v1.py
voidash's picture
jyoti 2.3 kbps speech codec: development candidate C (p=0.50) + variant B (p=0.25), inference bundle
b1882a9 verified
Raw History Blame Contribute Delete
5.8 kB
"""Bounded inference for the frozen TinyCall-inspired codec.
Finite context is recomputed on each 20 ms hop. This favors a small, auditable
state over speed; model parameters and caller-owned input/output are excluded
from the persistent state accounting. Only batch-one CPU inference is supported.
"""
import math
import torch
class StreamingEncoder:
hop = 320
# Encoder receptive field is 1438 samples. Four complete past hops are
# sufficient, and preserving hop alignment preserves every strided phase.
history_samples = 1280
def __init__(self, codec):
if next(codec.parameters()).device.type != 'cpu':
raise ValueError('Streaming v1 supports CPU inference only')
self.codec = codec
self.history = torch.empty(0)
self.pending = torch.empty(0)
self.samples = 0
self.frames = 0
self.finished = False
@property
def state_bytes(self):
return sum(t.untyped_storage().nbytes() for t in [self.history, self.pending])
def _frame(self, hop):
context = torch.cat((self.history, hop))
features = self.codec.encoder(context[None])[..., -1:]
_, codes = self.codec.quantizer(features)
self.history = context[-self.history_samples:].clone()
self.frames += 1
return codes
@torch.inference_mode()
def push(self, audio):
if self.finished:
raise ValueError('Encoder stream already finished')
if audio.ndim != 1 or audio.device.type != 'cpu' or audio.dtype != torch.float32:
raise ValueError('Expected one-dimensional CPU float32 waveform')
if not torch.isfinite(audio).all():
raise ValueError('Nonfinite waveform')
self.samples += audio.numel()
# The temporary concatenation is input-sized, but no view of it survives
# this call; persistent storage is independent of the input duration.
data = torch.cat((self.pending, audio))
complete = data.numel() // self.hop * self.hop
frames = [self._frame(data[i:i + self.hop]) for i in range(0, complete, self.hop)]
self.pending = data[complete:].clone()
return torch.cat(frames, -1) if frames else torch.empty((1, 4, 6, 0), dtype=torch.long)
@torch.inference_mode()
def finish(self):
if self.finished or not self.samples:
raise ValueError('Empty or already finished encoder stream')
self.finished = True
if self.pending.numel():
# Let each strided layer pad its own final partial activation, as
# in whole-waveform inference. Padding the raw waveform instead
# would generate biased hidden activations beyond its true end.
result = self._frame(self.pending)
else:
result = torch.empty((1, 4, 6, 0), dtype=torch.long)
self.pending = torch.empty(0)
return result
class StreamingDecoder:
hop = 320
history_frames = 30
def __init__(self, codec):
if next(codec.parameters()).device.type != 'cpu':
raise ValueError('Streaming v1 supports CPU inference only')
self.codec = codec
self.history = torch.empty((1, 64, 0))
self.previous_right = torch.empty(0)
window = codec.decoder.window
self.denominator = window[:self.hop].square() + window[self.hop:].square()
self.frames = 0
self.emitted = 0
self.finished = False
@property
def state_bytes(self):
return sum(t.untyped_storage().nbytes() for t in [self.history, self.previous_right, self.denominator])
def _frame(self, features):
context = torch.cat((self.history, features), -1)
decoder = self.codec.decoder
values = decoder.spectrum(decoder.blocks(decoder.input(context)))[..., -1]
log_magnitude, phase = values.chunk(2, dim=1)
magnitude = log_magnitude.clamp(-10, 5).exp()
phase = math.pi * phase.tanh()
spectrum = torch.complex(magnitude * phase.cos(), magnitude * phase.sin())
frame = torch.fft.irfft(spectrum, n=decoder.n_fft)[0] * decoder.window
self.history = context[..., -self.history_frames:].clone()
output = ((self.previous_right + frame[:self.hop]) / self.denominator
if self.previous_right.numel() else torch.empty(0))
self.previous_right = frame[self.hop:].clone()
self.emitted += output.numel()
return output
@torch.inference_mode()
def push(self, codes):
if self.finished:
raise ValueError('Decoder stream already finished')
if codes.ndim != 4 or codes.shape[:3] != (1, 4, 6) or codes.dtype != torch.long or codes.device.type != 'cpu':
raise ValueError('Expected CPU int64 codes [1,4,6,frames]')
frames = []
for i in range(codes.shape[-1]):
features = self.codec.quantizer.decode(codes[..., i:i + 1])
frames.append(self._frame(features))
self.frames += 1
return torch.cat(frames) if frames else torch.empty(0)
@torch.inference_mode()
def finish(self, samples):
if self.finished or not self.frames:
raise ValueError('Empty or already finished decoder stream')
if not isinstance(samples, int) or not (self.frames - 1) * self.hop < samples <= self.frames * self.hop:
raise ValueError('Sample count inconsistent with received code frames')
self.finished = True
# Match the offline decoder's single terminal zero latent. It is not a
# quantized code and does not carry an undeclared transmitted feature.
remaining = samples - self.emitted
output = self._frame(torch.zeros((1, 64, 1)))[:remaining]
self.emitted = samples
return output