The Audio File Processor (AFP) is decibri's offline audio source. It runs a recording through the same conditioning chain the live microphone uses, and, because a recording has an end, it can also score the whole file for speech in one pass. The class is called File, in both Python and Node.js.
Where a Microphone hands you conditioned audio as it arrives, a File hands you conditioned audio from something you already have: a WAV on disk, or samples already in memory. The options are the same options, with the same names and the same ranges, and the chunks come out in the same shape. Like the rest of the engine it is keyless and local: no API key, no account, no network call.
A File does exactly two things, and each one reads the source from beginning to end:
File. See the complete example.
Open a WAV, turn on the conditioning stages you want, and read conditioned chunks:
import decibri
with decibri.File("clip.wav", denoise="fastenhancer-t", agc=-18) as file:
for chunk in file:
handle(chunk) # conditioned int16 PCM bytes
const { File } = require('decibri');
const file = await File.open('clip.wav', { denoise: 'fastenhancer-t', agc: -18 });
for await (const chunk of file) {
handle(chunk); // Buffer of conditioned Int16 LE PCM
}
file.close();
Leave an option unset to keep that stage off. With every stage off, a File simply reads the recording, downmixes it to mono, and resamples it to your target rate.
File. The web File API defines one. Import decibri's explicitly with const { File } = require('decibri'), or reference it as decibri.File, so the two do not shadow each other.
There are three ways in, and the right one depends on where the audio is and whether you are on an event loop.
| Situation | Python | Node.js |
|---|---|---|
| A WAV on disk, blocking is fine | decibri.File(path) |
new File(path) |
| A WAV on disk, inside async code | await decibri.AsyncFile.open(path) |
await File.open(path) |
| Samples already in memory | decibri.File.buffer(samples, input_rate=...) |
File.buffer(samples, { inputRate: ... }) |
Two spellings are easy to mix up. In Python, File.open(path) is a plain alternate spelling of File(path) and returns the same object; the async form is a separate class, AsyncFile. In Node.js, File.open() is the async factory and reads the file off the event loop, while File.buffer() is synchronous because no I/O is involved.
The buffer constructor takes mono samples in the range -1.0 to 1.0: a list of floats or a one-channel NumPy array with a floating dtype in Python, a Float32Array in Node.js. Raw samples carry no header, so their native rate has to be passed explicitly on input_rate (inputRate in Node.js). It is required, and range-checked from 1,000 to 384,000 exactly as the target rate is.
import decibri
# samples: mono floats in -1.0 to 1.0, at their own native rate
file = decibri.File.buffer(samples, input_rate=48000, sample_rate=16000)
for chunk in file:
handle(chunk)
const { File } = require('decibri');
// samples: a Float32Array of mono samples in -1.0 to 1.0, at their own rate
const file = File.buffer(samples, { inputRate: 48000, sampleRate: 16000 });
file.on('data', (chunk) => handle(chunk));
The path constructors read WAV. Two encodings are supported, and both are accepted inside the WAVE_FORMAT_EXTENSIBLE container as well as the plain one, which matters because several common encoders emit extensible by default:
Anything else, 8-bit and 24-bit PCM among them, is rejected at construction with the message invalid WAV file: unsupported encoding; 16-bit PCM or 32-bit float required. For those, and for any other codec, decode the audio yourself and pass the samples to the buffer constructor: the conditioning behaves identically either way.
Two more things worth knowing before you rely on them. Multi-channel WAVs are accepted and downmixed to mono, where the Microphone rejects multi-channel input outright. And a header-only WAV is valid rather than an error: it constructs, reports its rates, yields no chunks, and analyses to an empty report.
A File takes the same five conditioning options as a Microphone, with the same names, the same ranges, and the same fixed order: DC removal → denoise → high-pass → AGC → limiter. Every stage is off by default. The Audio Capture Engine page documents what each stage does and how to choose its value; everything there applies to a File unchanged.
file = decibri.File(
"clip.wav",
sample_rate=16000,
dc_removal=True,
denoise="fastenhancer-t",
highpass=100,
agc=-18,
limiter=-1.0,
)
const file = await File.open('clip.wav', {
sampleRate: 16000,
dcRemoval: true,
denoise: 'fastenhancer-t',
highpass: 100,
agc: -18,
limiter: -1.0,
});
As on the microphone, voice activity detection reads the signal before the conditioning chain, so turning on enhancement does not change what counts as speech. It only changes the audio you receive.
A full chunk is 1,600 samples at the target rate, in both bindings. That is 3,200 bytes as int16 and 6,400 bytes as float32. The final chunk is shorter whenever the total does not divide evenly.
The count is fixed and the rate is not, so the chunk duration moves with the target rate: 100 ms at 16 kHz, 200 ms at 8 kHz, and about 33 ms at 48 kHz. Size buffers and reason about latency from the duration, not the count.
Two rates are visible on the instance. sample_rate (sampleRate) is the target rate every delivered chunk carries. input_rate (inputRate) is the source's own rate, read from the WAV header or taken from the value you passed to the buffer constructor. They differ whenever the recording is resampled.
duration × rate will come up short. The one case with no tail is the identity case: source rate equal to target rate, with denoise off, gives exactly the input sample count.
In Node.js, the total number of bytes is exact but the number of chunks depends on how you consume the stream: a for await loop and a 'data' listener over the same recording produce the same total in a different number of pieces, because Readable coalesces its internal buffer. Treat the total as exact and the chunk count as a property of the consumption style, not of decibri.
VAD is opt-in through the vad option, exactly as on the Microphone: pass "silero" or "energy" for the defaults, or a config object to tune the threshold and the holdoff. Without it, a File just conditions audio.
While you are streaming, Python exposes the speaking state as the is_speaking property and the raw score as vad_score. Node.js emits 'speech' and 'silence' events and exposes the raw score as vadScore.
import decibri
with decibri.File("clip.wav", vad="silero") as file:
for chunk in file:
if file.is_speaking:
send_to_stt(chunk) # file.vad_score holds the raw score
const { File } = require('decibri');
const file = await File.open('clip.wav', { vad: 'silero' });
let speaking = false;
file.on('speech', () => { speaking = true; });
file.on('silence', () => { speaking = false; });
file.on('data', (chunk) => {
if (speaking) {
sendToStt(chunk); // file.vadScore holds the raw score
}
});
The holdoff on a File is measured in file time, not wall-clock time. A recording processes faster than real time, so a wall-clock timer would collapse the reported speech timing; measuring it in sample positions means a file reports the same state sequence a live stream of the identical audio would.
Three different things carry a speech signal here, and they are not interchangeable:
| What you want | What to use |
|---|---|
| The speech regions of a whole recording | analyze().segments |
| Per-window scores across a whole recording | analyze().scores |
| The speech score while streaming chunks | vad_score (Python), vadScore (Node.js) |
| The debounced speech state while streaming | is_speaking (Python), the 'speech' and 'silence' events (Node.js) |
analyze() runs the recording once and returns a report of where the speech is. It requires vad="silero": a File built without a VAD configuration is refused with analysis requires VAD; construct the File with a vad configuration, and energy mode has no whole-recording analysis. analyse() is the same call under the other spelling.
The report has exactly two parts, both in seconds of file time:
scores: one entry per analysis window, carrying start, end, the window's speech score, and whether that score met the threshold. Windows tile the recording from zero, contiguous and non-overlapping, 0.032 seconds each. A trailing remainder shorter than one window is left unscored.segments: the merged speech regions, each with a start and an end. Consecutive speech windows whose silence gaps fall within the holdoff collapse into one segment, and a segment ends at its last speech window rather than at the holdoff expiry.import decibri
report = decibri.File("clip.wav", vad="silero").analyze()
for segment in report.segments:
print(f"speech from {segment.start:.3f}s to {segment.end:.3f}s")
for window in report.scores:
print(window.start, window.end, window.vad_score, window.is_speech)
const { File } = require('decibri');
const file = await File.open('clip.wav', { vad: 'silero' });
const report = await file.analyze();
for (const segment of report.segments) {
console.log(`speech from ${segment.start}s to ${segment.end}s`);
}
for (const window of report.scores) {
console.log(window.start, window.end, window.vadScore, window.isSpeech);
}
The threshold and the holdoff shape the result: raising the threshold trims the segment edges, and a long holdoff merges neighbouring segments into one. Both are set on the vad config object at construction, so changing them means constructing a new File.
This is the two-pass shape the single-pass rule leads to: analyse one File to find the speech, then construct a second File over the same recording to stream the conditioned audio.
import decibri
# Pass one: where is the speech? This File is consumed by the analysis.
report = decibri.File("clip.wav", vad="silero").analyze()
# Pass two: a second File over the same recording, conditioned and streamed.
# chunk.timestamp is the chunk's position in seconds of file time, which is
# the same clock report.segments is measured on.
with decibri.File(
"clip.wav",
denoise="fastenhancer-t",
agc=-18,
limiter=-1.0,
) as file:
for chunk in file.iter_with_metadata():
if any(s.start <= chunk.timestamp <= s.end for s in report.segments):
send_to_stt(chunk.data)
const { File } = require('decibri');
// Pass one: where is the speech? This File is consumed by the analysis.
const scan = await File.open('clip.wav', { vad: 'silero' });
const report = await scan.analyze();
// Pass two: a second File over the same recording, conditioned and streamed.
const file = await File.open('clip.wav', {
denoise: 'fastenhancer-t',
agc: -18,
limiter: -1.0,
});
let position = 0; // seconds of file time, the clock report.segments uses
for await (const chunk of file) {
const inSpeech = report.segments.some(
(s) => s.start <= position && position <= s.end
);
if (inSpeech) {
sendToStt(chunk);
}
position += chunk.length / 2 / file.sampleRate; // int16: 2 bytes per sample
}
file.close();
Constructing a second File over the same source is the supported way to do both. Neither pass waits on real time, so the second one costs a second read of the recording rather than a second playback of it.
The full constructor surface, the metadata iteration, and the error tables live on the binding reference pages:
File and AsyncFile classes, including read_with_metadata(), iter_with_metadata(), as_ndarray, and the exception surface.File stream, its FileOptions surface, the speech events, and the error classes with their code values.AFP is a native stage of the engine, so it is available in the Python and Node.js builds but not in the browser build. See the Browser API.