Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ bargekit fixtures --json

## `bargekit tune`

Analyze a fixture and print threshold guidance.
Analyze a fixture and print threshold guidance. The report separates the highest
ambient level below the configured speech threshold (`peakNoiseLevel`) from the
average level of frames classified as speech (`averageSpeechLevel`).

```sh
bargekit tune --fixture tests/fixtures/long_utterance.json
Expand Down
6 changes: 5 additions & 1 deletion src/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function analyzeFixtureForThresholds(fixture, overrides = {}) {
let noiseFrames = 0;
let quietFrames = 0;
let peakLevel = 0;
let peakNoiseLevel = 0;

for (const sample of fixture.samples) {
peakLevel = Math.max(peakLevel, sample.level);
Expand All @@ -125,6 +126,7 @@ export function analyzeFixtureForThresholds(fixture, overrides = {}) {
continue;
}

peakNoiseLevel = Math.max(peakNoiseLevel, sample.level);
if (sample.level >= config.noiseFloorThreshold) {
noiseFrames += 1;
continue;
Expand All @@ -139,7 +141,8 @@ export function analyzeFixtureForThresholds(fixture, overrides = {}) {
.reduce((sum, sample) => sum + sample.level, 0) / speechFrames;

const suggestedProfile = Object.entries(PRESET_PROFILES).reduce((best, [name, profile]) => {
const distance = Math.abs(profile.speechThreshold - averageSpeechLevel || config.speechThreshold);
const comparisonLevel = averageSpeechLevel || config.speechThreshold;
const distance = Math.abs(profile.speechThreshold - comparisonLevel);
if (!best || distance < best.distance) {
return { name, distance };
}
Expand All @@ -152,6 +155,7 @@ export function analyzeFixtureForThresholds(fixture, overrides = {}) {
noiseFrames,
quietFrames,
peakLevel,
peakNoiseLevel,
averageSpeechLevel,
suggestedProfile,
explanation: speechFrames === 0
Expand Down
7 changes: 5 additions & 2 deletions src/tuning.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { analyzeFixtureForThresholds } from './fixtures.js';

export function recommendThresholds(fixtures, options = {}) {
const analyses = fixtures.map((fixture) => analyzeFixtureForThresholds(fixture, options));
const peaks = analyses.map((analysis) => analysis.peakLevel).filter((level) => level > 0).sort((a, b) => a - b);
const noisePeaks = analyses
.map((analysis) => analysis.peakNoiseLevel)
.filter((level) => level > 0)
.sort((a, b) => a - b);
const speechAverages = analyses
.map((analysis) => analysis.averageSpeechLevel)
.filter((level) => level > 0)
.sort((a, b) => a - b);

const noiseCeiling = percentile(peaks, 0.5) || DEFAULT_CONFIG.noiseFloorThreshold;
const noiseCeiling = percentile(noisePeaks, 0.5) || DEFAULT_CONFIG.noiseFloorThreshold;
const speechFloor = percentile(speechAverages, 0.25) || DEFAULT_CONFIG.speechThreshold;
const speechThreshold = clamp(round(Math.max(speechFloor * 0.82, noiseCeiling + 0.18)), 0.2, 0.9);
const noiseFloorThreshold = clamp(round(Math.min(noiseCeiling + 0.04, speechThreshold - 0.1)), 0.04, 0.5);
Expand Down
1 change: 1 addition & 0 deletions test/fixtures.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ test('tuning analysis explains threshold decisions and suggests a profile', () =
});

assert.equal(analysis.speechFrames, 0);
assert.equal(analysis.peakNoiseLevel, 0.23);
assert.match(analysis.explanation, /No speech crossed/);
assert.ok(analysis.suggestedProfile);
});
Expand Down
28 changes: 24 additions & 4 deletions test/tuning.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { SYNTHETIC_FIXTURES, recommendThresholds } from '../src/index.js';
import {
SYNTHETIC_FIXTURES,
analyzeFixtureForThresholds,
createSyntheticFixture,
recommendThresholds
} from '../src/index.js';

test('threshold recommendation returns safe ordered thresholds and rationale', () => {
test('threshold recommendation separates ambient peaks from speech evidence', () => {
const result = recommendThresholds([SYNTHETIC_FIXTURES.keyboard_noise, SYNTHETIC_FIXTURES.long_utterance]);
assert.ok(result.config.speechThreshold > result.config.noiseFloorThreshold);
assert.ok(result.closestProfile);
assert.equal(result.config.speechThreshold, 0.59);
assert.equal(result.config.noiseFloorThreshold, 0.27);
assert.equal(result.analyses[0].peakNoiseLevel, 0.23);
assert.ok(Math.abs(result.analyses[1].averageSpeechLevel - 0.72) < Number.EPSILON * 4);
assert.equal(result.closestProfile, 'laptop_speakers');
assert.match(result.rationale, /speechThreshold/);
});

test('profile selection preserves zero distance for an exact speech threshold match', () => {
const fixture = createSyntheticFixture('exact-wired-headset', {
baseline: 0.04,
segments: [{ startMs: 100, endMs: 300, level: 0.5 }]
});

const analysis = analyzeFixtureForThresholds(fixture, { speechThreshold: 0.5 });

assert.equal(analysis.averageSpeechLevel, 0.5);
assert.equal(analysis.suggestedProfile, 'wired_headset');
});
Loading