readback 0.0.0-alpha.4 → 0.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +51 -29
  2. package/dist/bin/index.js +78 -0
  3. package/dist/package.json +59 -0
  4. package/dist/src/asr-task.js +14 -0
  5. package/dist/src/asr-worker.js +37 -0
  6. package/dist/src/bandpass.js +57 -0
  7. package/dist/src/banner.js +19 -0
  8. package/dist/src/capture.js +315 -0
  9. package/dist/src/classifier-manager.js +117 -0
  10. package/dist/src/classify-atc-pilot.js +18 -0
  11. package/dist/src/cleaner.js +36 -0
  12. package/dist/src/console.js +11 -0
  13. package/dist/src/generated/airlines.js +5768 -0
  14. package/dist/src/generated/nav-data.js +203401 -0
  15. package/dist/src/generated/waypoint-data.js +203401 -0
  16. package/dist/src/model-manager.js +90 -0
  17. package/dist/src/normalization/airlines.js +5773 -0
  18. package/dist/src/normalization/altitude.js +134 -0
  19. package/dist/src/normalization/atc-phrases.js +315 -0
  20. package/dist/src/normalization/callsigns.js +87 -0
  21. package/dist/src/normalization/flight-level.js +26 -0
  22. package/dist/src/normalization/frequency.js +30 -0
  23. package/dist/src/normalization/heading.js +46 -0
  24. package/dist/src/normalization/headings.js +42 -0
  25. package/dist/src/normalization/keywords.js +141 -0
  26. package/dist/src/normalization/nav-data.js +252 -0
  27. package/dist/src/normalization/numbers.js +405 -0
  28. package/dist/src/normalization/phonetic.js +56 -0
  29. package/dist/src/normalization/qnh.js +23 -0
  30. package/dist/src/normalization/runway.js +35 -0
  31. package/dist/src/normalization/speed.js +66 -0
  32. package/dist/src/normalization/squawk.js +23 -0
  33. package/dist/src/normalization/tokenizer.js +33 -0
  34. package/dist/src/normalization/waypoint-data.js +203401 -0
  35. package/dist/src/normalization/waypoints.js +183 -0
  36. package/dist/src/presentation/renderer.js +134 -0
  37. package/dist/src/progress-manager.js +42 -0
  38. package/dist/src/prompt-manager.js +114 -0
  39. package/dist/src/setup-manager.js +167 -0
  40. package/dist/src/startup-cleanup.js +18 -0
  41. package/dist/src/status-line.js +297 -0
  42. package/dist/src/types.js +21 -0
  43. package/dist/src/vad-worker.js +171 -0
  44. package/dist/src/whisper-worker.js +46 -0
  45. package/models/silero_vad_v6.onnx +0 -0
  46. package/package.json +21 -12
  47. package/bin/index.ts +0 -68
  48. package/src/capture.ts +0 -242
  49. package/src/cleaner.ts +0 -35
  50. package/src/console.ts +0 -12
  51. package/src/model-manager.ts +0 -230
  52. package/src/normalization/atc-phrases.ts +0 -385
  53. package/src/normalization/callsigns.ts +0 -164
  54. package/src/normalization/keywords.ts +0 -137
  55. package/src/normalization/numbers.ts +0 -197
  56. package/src/normalization/phonetic.ts +0 -58
  57. package/src/normalization/tokenizer.ts +0 -26
  58. package/src/normalization/waypoints.ts +0 -194
  59. package/src/presentation/renderer.ts +0 -92
  60. package/src/types.ts +0 -45
  61. package/src/vad-worker.ts +0 -26
  62. package/src/whisper-worker.ts +0 -37
package/README.md CHANGED
@@ -1,28 +1,42 @@
1
- # 🎧 readback
1
+ ```
2
+ █▀▄ █▀▀ █▀█ █▀▄ █▀▄ █▀█ █▀▀ █ █
3
+ █▀▄ █▀▀ █▀█ █ █ █▀▄ █▀█ █ █▀▄
4
+ █ █ █ █ █ █ █ █ █ █ █ █ █ █
5
+ ▀ ▀ ▀▀▀ ▀ ▀ ▀▀ ▀▀ ▀ ▀ ▀▀▀ ▀ ▀
6
+ ```
2
7
 
3
8
  **readback** is a real-time ATC (Air Traffic Control) transcription tool that captures system audio, transcribes it using local ATC-fine-tuned [Whisper](https://en.wikipedia.org/wiki/Whisper_(speech_recognition_system)) models, and formats the output with aviation-specific syntax highlighting. Useful for flight simmers on [VATSIM](https://vatsim.net) and [IVAO](https://www.ivao.aero) networks who (like me) struggle with following ATC communications.
4
9
 
5
- ## ⚠️ Disclaimer
10
+ > **Disclaimer**: Transcription is far from perfect. The AI will make mistakes with fast speech, accents, background noise, and similar-sounding words. Always verify critical information (altitudes, headings, frequencies) and ask ATC to repeat if unsure. Never use for real-world aviation.
11
+
12
+ ## Installation
13
+
14
+ ### Step 1: Build tools (only if prebuilt binaries are unavailable)
15
+
16
+ Most users won’t need this. It’s only required if npm can’t find a compatible prebuilt binary for a native dependency (e.g. `smart-whisper`, `native-recorder-nodejs`) and falls back to building from source.
17
+
18
+ **macOS**: `xcode-select --install`
6
19
 
7
- **Transcription is far from perfect.** The AI will make mistakes with fast speech, accents, background noise, and similar-sounding words. Always verify critical information (altitudes, headings, frequencies) and ask ATC to repeat if unsure. Never use for real-world aviation.
20
+ **Windows**: Install Visual Studio Build Tools with the C++ workload (MSVC).
8
21
 
9
- ## 🚀 Installation
22
+ **Linux (Debian/Ubuntu)**: `sudo apt-get install build-essential python3`
10
23
 
11
- ### Step 1: Install build tools for native modules:
24
+ ### Step 2 (Linux only): System audio capture tools
12
25
 
13
- **macOS**: Install Xcode Command Line Tools with `xcode-select --install`
26
+ On Linux, readback captures system audio by spawning either PipeWire (`pw-cat`) or PulseAudio (`parec`).
14
27
 
15
- **Windows**: Install Visual Studio Build Tools with C++ workload from visualstudio.microsoft.com
28
+ Install one of the following:
16
29
 
17
- **Linux**: Install build essentials with `sudo apt-get install build-essential cmake` (Debian/Ubuntu) or equivalent for your distribution
30
+ - PipeWire: `sudo apt install pipewire-bin` (provides `pw-cat`)
31
+ - PulseAudio: `sudo apt install pulseaudio-utils` (provides `parec`)
18
32
 
19
- ### Step 2: Install Node.js
33
+ ### Step 3: Install Node.js
20
34
 
21
- You need Node.js version 22.18.0 or higher installed on your system:
35
+ You need Node.js version 22 or higher installed on your system:
22
36
 
23
- 1. **Download Node.js**: Go to https://nodejs.org/en/download/ and download the **LTS version** (Long Term Support)
24
- 2. **Install**: Run the downloaded installer
25
- 3. **Verify**: Open Terminal, type `node --version` and hit the enter key (you should see something like `v24.x.x`)
37
+ 3.1. **Download Node.js**: Go to https://nodejs.org/en/download/ and download the **LTS version** (Long Term Support)
38
+ 3.2. **Install**: Run the downloaded installer
39
+ 3.3. **Verify**: Open Terminal, type `node --version` and hit the enter key (you should see something like `v24.x.x`)
26
40
 
27
41
  ### Step 3: Install readback globally
28
42
 
@@ -34,32 +48,40 @@ npm install -g readback
34
48
 
35
49
  This installs readback globally so you can run it from anywhere.
36
50
 
51
+ If the instal fails with node-gyp errors, install Step 1 tools and retry.
52
+
37
53
  ---
38
54
 
39
- ## 🎮 Usage
55
+ ## Usage
40
56
 
41
57
  In your terminal, run:
42
58
  ```bash
43
59
  readback
44
60
  ```
45
61
 
46
- The first time you run readback, it will check if a model exists in `~/.readback/models` and ask you to download a Whisper model (Medium ~1.5GB or Large ~3GB). Models are stored in `~/.readback/models`.
62
+ ### First time run
63
+
64
+ The first time you run readback, it will check if necessary models exist on your machine and guide you through the download process. Models are stored in `~/.readback/models`.
65
+
66
+ After all models are installed, readback will attempt to capture your system audio. This may trigger an operating system security prompt asking for microphone or audio recording permissions.
67
+
68
+ Please grant this permission. Although readback listens to output (what you hear), operating systems often classify this as "Microphone" or "Screen Recording" access. Once granted, you may need to restart your terminal or the application.
69
+
70
+ ### Options
47
71
 
48
- > 💡I recommend using **medium** since it's faster and the quality difference to large is negligible.
49
-
50
- You can bring your own model too:
72
+ To list all available options run:
51
73
 
52
74
  ```bash
53
- MODEL_PATH=~/path/to/your-model-ggml.bin readback
75
+ readback --help
54
76
  ```
55
77
 
56
78
  ### Exiting
57
79
 
58
- Press `Ctrl+C`
80
+ Press <kbd>Ctrl</kbd> + <kbd>C</kbd>
59
81
 
60
82
  ---
61
83
 
62
- ## ⬆️️ Updating
84
+ ## Updating
63
85
 
64
86
  Open Terminal and run:
65
87
 
@@ -69,36 +91,36 @@ npm update -g readback
69
91
 
70
92
  ---
71
93
 
72
- ## 🗑️ Uninstalling
94
+ ## Uninstalling
73
95
 
74
96
  ### Uninstall npm package
75
97
 
76
98
  Open Terminal and run:
77
99
 
78
100
  ```bash
79
- npm uninstall -g readback
101
+ readback --purge && npm uninstall -g readback
80
102
  ```
81
103
 
82
- Delete the `.readback` folder in your home directory.
104
+ This will first delete the `.readback` folder in your home directory and then uninstall the globally installed npm module.
83
105
 
84
106
  ---
85
107
 
86
- ## 📝 License
108
+ ## License
87
109
 
88
110
  MIT License - See repository for details
89
111
 
90
112
  ---
91
113
 
92
- ## 🙏 Credits
114
+ ## Credits
93
115
 
94
- This project would not be possible without the ATC-fine-tuned models provided by [Jack Tol](https://huggingface.co/jacktol).
116
+ This project would not be possible without the ATC-fine-tuned Whisper models provided by [Jack Tol](https://huggingface.co/jacktol).
95
117
 
96
118
  ---
97
119
 
98
- ## 🐛 Issues and contributions
120
+ ## Issues and contributions
99
121
 
100
122
  Found a bug or want to add features? Submit issues or pull requests on the [GitHub repository](https://github.com/borisdiakur/readback).
101
123
 
102
124
  ---
103
125
 
104
- **👋🏻 Happy flying!**
126
+ Happy flying!
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import "../src/banner.js";
3
+ import { Command } from "commander";
4
+ import { styleText } from "node:util";
5
+ import { ensureAllModels, purgeData } from "../src/setup-manager.js";
6
+ import { cleanTranscript } from "../src/cleaner.js";
7
+ import { startCapture } from "../src/capture.js";
8
+ import { startupCleanup } from "../src/startup-cleanup.js";
9
+ import packageJson from "../package.json" with { type: "json" };
10
+ import { BottomStatusLine } from "../src/status-line.js";
11
+ import { printTranscript } from "../src/presentation/renderer.js";
12
+ await startupCleanup();
13
+ const program = new Command();
14
+ program
15
+ .version(packageJson.version)
16
+ .description(`${packageJson.name}\n${packageJson.description}`)
17
+ .option("--debug", "Enable debug output showing token stream")
18
+ .option("--raw", "Disable all cleaning and formatting")
19
+ .option("--no-altitude", "Disable altitude formatting")
20
+ .option("--no-callsigns", "Disable callsign detection and normalization")
21
+ .option("--no-colors", "Disable coloring")
22
+ .option("--no-fl", "Disable flight level abbreviation")
23
+ .option("--no-frequency", "Disable frequency formatting")
24
+ .option("--no-heading", "Disable heading formatting")
25
+ .option("--no-keywords", "Disable keyword highlighting")
26
+ .option("--no-numbers", "Disable number-word conversion")
27
+ .option("--no-phonetic", "Disable phonetic formatting")
28
+ .option("--no-qnh", "Disable qnh formatting")
29
+ .option("--no-runways", "Disable runway formatting")
30
+ .option("--no-speed", "Disable speed formatting")
31
+ .option("--no-squawk", "Disable squawk formatting")
32
+ .option("--no-waypoints", "Disable waypoint formatting")
33
+ .option("--purge", "Delete all data including downloaded models and exit")
34
+ .helpOption("-h, --help", "Display this help text")
35
+ .parse(process.argv);
36
+ const opts = program.opts();
37
+ if (opts.purge) {
38
+ purgeData();
39
+ process.exit(0);
40
+ }
41
+ let sigintSeen = false;
42
+ process.on("SIGINT", () => {
43
+ if (sigintSeen)
44
+ return;
45
+ sigintSeen = true;
46
+ console.log("\nBye!");
47
+ process.exit(0);
48
+ });
49
+ const paths = await ensureAllModels();
50
+ const status = new BottomStatusLine();
51
+ startCapture(async (text) => {
52
+ if (opts.raw) {
53
+ console.log(text);
54
+ return;
55
+ }
56
+ const result = cleanTranscript(text, {
57
+ altitude: opts.altitude,
58
+ callsigns: opts.callsigns,
59
+ colors: opts.colors,
60
+ debug: opts.debug,
61
+ fl: opts.fl,
62
+ frequency: opts.frequency,
63
+ heading: opts.heading,
64
+ keywords: opts.keywords,
65
+ numbers: opts.numbers,
66
+ phonetic: opts.phonetic,
67
+ qnh: opts.qnh,
68
+ raw: opts.raw,
69
+ runways: opts.runways,
70
+ speed: opts.speed,
71
+ squawk: opts.squawk,
72
+ waypoints: opts.waypoints,
73
+ });
74
+ if (opts.debug) {
75
+ console.log(" " + styleText("dim", JSON.stringify(result.tokens)));
76
+ }
77
+ printTranscript(result.output, status);
78
+ }, status, paths.asr);
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "readback",
3
+ "version": "0.0.0-alpha.6",
4
+ "description": "Transcribes ATC transmissions into readable text.",
5
+ "keywords": [
6
+ "ATC",
7
+ "aviation"
8
+ ],
9
+ "homepage": "https://github.com/borisdiakur/readback#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/borisdiakur/readback/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/borisdiakur/readback.git"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Boris Diakur (https://borisdiakur.de)",
19
+ "type": "module",
20
+ "main": "dist/bin/index.js",
21
+ "bin": {
22
+ "readback": "dist/bin/index.js"
23
+ },
24
+ "files": [
25
+ "dist/",
26
+ "models/",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "start": "node ./dist/bin/index.js",
32
+ "start:debug": "node ./dist/bin/index.js --debug",
33
+ "build": "tsc",
34
+ "build:start": "tsc && npm start",
35
+ "build:watch": "tsc --watch",
36
+ "extractNavData": "node scripts/extract-nav-data.ts",
37
+ "test": "node --test tests/**/*.spec.ts",
38
+ "test:cover": "node --experimental-test-coverage --test tests/**/*.spec.ts"
39
+ },
40
+ "dependencies": {
41
+ "cli-progress": "^3.12.0",
42
+ "cmake-js": "^8.0.0",
43
+ "commander": "^14.0.3",
44
+ "native-recorder-nodejs": "^1.2.0",
45
+ "onnxruntime-node": "^1.24.1",
46
+ "smart-whisper": "^0.8.1",
47
+ "speex-resampler": "^3.0.1",
48
+ "wrap-ansi": "^9.0.2"
49
+ },
50
+ "devDependencies": {
51
+ "@types/cli-progress": "^3.11.6",
52
+ "@types/node": "^25.2.2",
53
+ "prettier": "^3.8.1",
54
+ "typescript": "^5.9.3"
55
+ },
56
+ "engines": {
57
+ "node": ">=22"
58
+ }
59
+ }
@@ -0,0 +1,14 @@
1
+ import { Whisper } from "smart-whisper";
2
+ let whisper = null;
3
+ let loadedPath = null;
4
+ export default async function run({ audio, asrModelPath }) {
5
+ if (!whisper || loadedPath !== asrModelPath) {
6
+ whisper = new Whisper(asrModelPath, { gpu: true });
7
+ loadedPath = asrModelPath;
8
+ }
9
+ const audioData = new Float32Array(audio);
10
+ const task = await whisper.transcribe(audioData, { language: "en" });
11
+ const result = await task.result;
12
+ const text = result.map((s) => s.text.trim().toLowerCase()).join(" ");
13
+ return { text };
14
+ }
@@ -0,0 +1,37 @@
1
+ import { Whisper } from "smart-whisper";
2
+ const ASR_MODEL_PATH = process.env.ASR_MODEL_PATH;
3
+ if (!ASR_MODEL_PATH) {
4
+ throw new Error("ASR_MODEL_PATH environment variable not set");
5
+ }
6
+ let whisper;
7
+ async function init() {
8
+ try {
9
+ whisper = new Whisper(ASR_MODEL_PATH, { gpu: true });
10
+ if (process.send)
11
+ process.send({ ready: true });
12
+ }
13
+ catch (err) {
14
+ console.error(" ASR worker init failed:", err);
15
+ process.exit(1);
16
+ }
17
+ }
18
+ process.on("message", async (msg) => {
19
+ if (!msg.audio || msg.audio.length === 0)
20
+ return;
21
+ try {
22
+ const audioData = new Float32Array(msg.audio);
23
+ const task = await whisper.transcribe(audioData, {
24
+ language: "en",
25
+ });
26
+ const result = await task.result;
27
+ const text = result.map((s) => s.text.trim().toLowerCase()).join(" ");
28
+ if (process.send)
29
+ process.send({ text: text, seq: msg.seq });
30
+ }
31
+ catch (err) {
32
+ console.error(" ASR worker processing error:", err);
33
+ if (process.send)
34
+ process.send({ text: "", seq: msg.seq, error: String(err) });
35
+ }
36
+ });
37
+ init();
@@ -0,0 +1,57 @@
1
+ export class BandpassFilter {
2
+ x1 = 0;
3
+ x2 = 0;
4
+ y1 = 0;
5
+ y2 = 0;
6
+ // Coefficients
7
+ b0 = 0;
8
+ b1 = 0;
9
+ b2 = 0;
10
+ a1 = 0;
11
+ a2 = 0;
12
+ constructor(sampleRate, centerFreq, q) {
13
+ this.recalculate(sampleRate, centerFreq, q);
14
+ }
15
+ recalculate(sampleRate, centerFreq, q) {
16
+ // Standard BPF (Constant Peak Gain) implementation
17
+ const w0 = (2 * Math.PI * centerFreq) / sampleRate;
18
+ const alpha = Math.sin(w0) / (2 * q);
19
+ const a0 = 1 + alpha;
20
+ this.b0 = alpha / a0;
21
+ this.b1 = 0;
22
+ this.b2 = -alpha / a0;
23
+ this.a1 = (-2 * Math.cos(w0)) / a0;
24
+ this.a2 = (1 - alpha) / a0;
25
+ // Reset state on recalc (optional, avoids pop)
26
+ this.reset();
27
+ }
28
+ reset() {
29
+ this.x1 = 0;
30
+ this.x2 = 0;
31
+ this.y1 = 0;
32
+ this.y2 = 0;
33
+ }
34
+ process(input) {
35
+ const output = new Float32Array(input.length);
36
+ // Local copy for speed
37
+ let { x1, x2, y1, y2 } = this;
38
+ const { b0, b1, b2, a1, a2 } = this;
39
+ for (let i = 0; i < input.length; i++) {
40
+ const x = input[i];
41
+ // Direct Form I or II (this is DFI)
42
+ const y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
43
+ output[i] = y;
44
+ // Shift
45
+ x2 = x1;
46
+ x1 = x;
47
+ y2 = y1;
48
+ y1 = y;
49
+ }
50
+ // Save state
51
+ this.x1 = x1;
52
+ this.x2 = x2;
53
+ this.y1 = y1;
54
+ this.y2 = y2;
55
+ return output;
56
+ }
57
+ }
@@ -0,0 +1,19 @@
1
+ import { styleText } from "node:util";
2
+ import packageJson from "../package.json" with { type: "json" };
3
+ const g = (t) => styleText(["dim", "white"], t);
4
+ const w = (t) => styleText(["whiteBright"], t);
5
+ const a = (t) => t
6
+ .split("")
7
+ .map((c, i) => i < 2 ||
8
+ i > t.length - 2 ||
9
+ i === Math.floor(t.length / 2) - 1 ||
10
+ i === Math.floor(t.length / 2) + 1 ||
11
+ (i + 1) % 2 === 0
12
+ ? g(c)
13
+ : w(c))
14
+ .join("");
15
+ const v = styleText("dim", " v" + packageJson.version);
16
+ console.log(a("█▀▄ █▀▀ █▀█ █▀▄ █▀▄ █▀█ █▀▀ █ █"));
17
+ console.log(a("█▀▄ █▀▀ █▀█ █ █ █▀▄ █▀█ █ █▀▄"));
18
+ console.log(a("█ █ █ █ █ █ █ █ █ █ █ █ █ █"));
19
+ console.log(a("▀ ▀ ▀▀▀ ▀ ▀ ▀▀ ▀▀ ▀ ▀ ▀▀▀ ▀ ▀") + v);