DEV Community

SOP Work Pod
SOP Work Pod

Posted on

Designing a Small TypeScript Model for Noise-Aware Focus Sessions

Designing a Small TypeScript Model for Noise-Aware Focus Sessions

Software teams often build focus timers as if time were the only input: start a 25-minute session, count down, and notify the user. In shared offices, however, interruption risk also depends on the acoustic environment. A timer cannot measure privacy perfectly, but it can turn a few observable signals into a useful, explainable recommendation.

This article builds a deliberately small TypeScript model for that job. The goal is not acoustic certification. The goal is a testable domain function that a web application can use without hiding important assumptions.

Start with a narrow domain model

We will use three inputs:

  • ambientDb: the background sound level near the user.
  • speechDb: the approximate level of nearby speech.
  • enclosureReductionDb: an estimated reduction provided by the current workspace.

The third input is intentionally generic. A product page can help us understand the physical entities a facilities application may represent; for example, I used a modular office-pod product context to define the boundary between a room, an enclosure, and a workstation. The calculator itself remains vendor-neutral and makes no product-performance claim.

export type FocusEnvironment = {
  ambientDb: number;
  speechDb: number;
  enclosureReductionDb: number;
};

export type FocusRecommendation = {
  score: number;
  sessionMinutes: 15 | 25 | 45;
  reason: string;
};
Enter fullscreen mode Exit fullscreen mode

The union type for sessionMinutes is useful because it prevents the rest of the application from inventing unsupported timer lengths.

Validate at the boundary

Sensor values, form fields, and imported facility data are all untrusted inputs. Reject impossible values before calculating anything.

function assertFiniteRange(
  name: string,
  value: number,
  min: number,
  max: number,
): void {
  if (!Number.isFinite(value) || value < min || value > max) {
    throw new RangeError(`${name} must be between ${min} and ${max}`);
  }
}

export function validateEnvironment(input: FocusEnvironment): void {
  assertFiniteRange("ambientDb", input.ambientDb, 0, 140);
  assertFiniteRange("speechDb", input.speechDb, 0, 140);
  assertFiniteRange(
    "enclosureReductionDb",
    input.enclosureReductionDb,
    0,
    60,
  );
}
Enter fullscreen mode Exit fullscreen mode

These ranges are application guardrails, not a replacement for calibrated measurements. Keeping that distinction in the code comments and UI copy prevents a convenient heuristic from being mistaken for engineering analysis.

Keep the scoring rule explainable

For a lightweight recommendation, calculate the speech level remaining after the estimated reduction and compare it with ambient sound.

export function recommendFocusSession(
  input: FocusEnvironment,
): FocusRecommendation {
  validateEnvironment(input);

  const effectiveSpeechDb = Math.max(
    0,
    input.speechDb - input.enclosureReductionDb,
  );
  const speechMarginDb = effectiveSpeechDb - input.ambientDb;

  const score = Math.round(
    Math.max(0, Math.min(100, 75 - speechMarginDb * 4)),
  );

  if (score >= 75) {
    return {
      score,
      sessionMinutes: 45,
      reason: "Low modeled interruption risk",
    };
  }

  if (score >= 45) {
    return {
      score,
      sessionMinutes: 25,
      reason: "Moderate modeled interruption risk",
    };
  }

  return {
    score,
    sessionMinutes: 15,
    reason: "High modeled interruption risk",
  };
}
Enter fullscreen mode Exit fullscreen mode

This model is intentionally simple. Decibels are logarithmic, and real speech privacy involves frequency, distance, absorption, leakage paths, and measurement uncertainty. A small app should state that limitation instead of presenting a precise-looking number as truth.

The advantage of the simple rule is traceability. A user can see why the recommendation changed, and a developer can change the coefficient or thresholds without rewriting the UI.

Lock the behavior with tests

The most valuable tests sit at domain boundaries: invalid input, clamping, and recommendation thresholds.

import { describe, expect, it } from "vitest";
import { recommendFocusSession } from "./focus-model";

describe("recommendFocusSession", () => {
  it("recommends a longer session when modeled speech is well controlled", () => {
    expect(
      recommendFocusSession({
        ambientDb: 42,
        speechDb: 62,
        enclosureReductionDb: 25,
      }),
    ).toEqual({
      score: 95,
      sessionMinutes: 45,
      reason: "Low modeled interruption risk",
    });
  });

  it("rejects non-finite sensor values", () => {
    expect(() =>
      recommendFocusSession({
        ambientDb: Number.NaN,
        speechDb: 60,
        enclosureReductionDb: 10,
      }),
    ).toThrow(RangeError);
  });

  it("never returns a score outside 0 to 100", () => {
    const result = recommendFocusSession({
      ambientDb: 10,
      speechDb: 120,
      enclosureReductionDb: 0,
    });

    expect(result.score).toBe(0);
    expect(result.sessionMinutes).toBe(15);
  });
});
Enter fullscreen mode Exit fullscreen mode

The first case also documents the arithmetic. Effective speech is 37 dB, the margin is -5 dB, and the score is 95.

Treat recommendations as events, not truth

In a production application, store the inputs and model version next to each recommendation:

type FocusRecommendationEvent = {
  modelVersion: "focus-score-v1";
  recordedAt: string;
  environment: FocusEnvironment;
  recommendation: FocusRecommendation;
};
Enter fullscreen mode Exit fullscreen mode

Versioning matters because thresholds will change after user feedback. Without it, historical analytics mix different algorithms and become difficult to interpret.

The UI should also let the user override the recommendation. A person may prefer a short session even in a quiet environment, or a long session while using headphones. The model is decision support, not an authority.

A practical next step

Before adding machine learning, collect a small set of anonymous, consented outcomes: the recommendation, the chosen session length, and whether the session was interrupted. That data can show whether the heuristic is useful. If it is not, a more complicated model will only make the mistake harder to explain.

Small domain functions are a good place to start because they are portable, testable, and honest about uncertainty. The timer UI can change; the browser sensor API can change; the scoring rule can evolve. The contract between them stays clear.

Disclosure: I used an AI writing assistant to help organize and edit this article. The code, calculations, and technical claims were manually reviewed.

Top comments (0)