termux-stt 2.0.4 → 2.1.0
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.
- package/LICENSE +176 -21
- package/README.md +84 -84
- package/README.pypi.md +44 -44
- package/bin/cli.js +55 -51
- package/bin/termux-stt.js +55 -51
- package/index.d.ts +72 -68
- package/index.js +53 -49
- package/lib/engine.js +96 -92
- package/lib/hybrid.js +68 -64
- package/lib/sherpa.js +101 -97
- package/lib/whisper.js +83 -79
- package/package.json +86 -86
package/bin/termux-stt.js
CHANGED
|
@@ -1,51 +1,55 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (fs.existsSync(
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
const
|
|
18
|
-
if (fs.existsSync(
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* AMEVA Standard Node.js CLI Runner for termux_stt.
|
|
8
|
+
* Automatically resolves Python 3 environment and dispatches to python -m termux_stt.
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { spawn, execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
function findPython() {
|
|
14
|
+
if (process.env.PYTHON && fs.existsSync(process.env.PYTHON)) {
|
|
15
|
+
return process.env.PYTHON;
|
|
16
|
+
}
|
|
17
|
+
const termuxBin = '/data/data/com.termux/files/usr/bin/python3';
|
|
18
|
+
if (fs.existsSync(termuxBin)) {
|
|
19
|
+
return termuxBin;
|
|
20
|
+
}
|
|
21
|
+
const termuxBinAlt = '/data/data/com.termux/files/usr/bin/python';
|
|
22
|
+
if (fs.existsSync(termuxBinAlt)) {
|
|
23
|
+
return termuxBinAlt;
|
|
24
|
+
}
|
|
25
|
+
const candidates = ['python3', 'python'];
|
|
26
|
+
for (const cmd of candidates) {
|
|
27
|
+
try {
|
|
28
|
+
const checkCmd = process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
|
|
29
|
+
const res = execSync(checkCmd, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
30
|
+
if (res) return cmd;
|
|
31
|
+
} catch (_) {}
|
|
32
|
+
}
|
|
33
|
+
return 'python3';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pythonBin = findPython();
|
|
37
|
+
const args = ['-m', 'termux_stt.platform.installer', ...process.argv.slice(2)];
|
|
38
|
+
|
|
39
|
+
const child = spawn(pythonBin, args, {
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
env: process.env
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
child.on('error', (err) => {
|
|
45
|
+
console.error(`[${'termux_stt'}] Failed to spawn python process (${pythonBin}):`, err.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
child.on('exit', (code, signal) => {
|
|
50
|
+
if (signal) {
|
|
51
|
+
process.kill(process.pid, signal);
|
|
52
|
+
} else {
|
|
53
|
+
process.exit(code || 0);
|
|
54
|
+
}
|
|
55
|
+
});
|
package/index.d.ts
CHANGED
|
@@ -1,68 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
4
|
+
*/
|
|
5
|
+
export interface EngineOptions {
|
|
6
|
+
model?: string;
|
|
7
|
+
lang?: string;
|
|
8
|
+
device?: 'auto' | 'vulkan' | 'gpu' | 'cpu';
|
|
9
|
+
threads?: number;
|
|
10
|
+
vad?: boolean;
|
|
11
|
+
vadThreshold?: number;
|
|
12
|
+
quantization?: string;
|
|
13
|
+
numSpeakers?: number;
|
|
14
|
+
customModelPath?: string;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Segment {
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
text: string;
|
|
22
|
+
speaker?: string;
|
|
23
|
+
confidence?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TranscriptResult {
|
|
27
|
+
text: string;
|
|
28
|
+
segments: Segment[];
|
|
29
|
+
language?: string;
|
|
30
|
+
duration?: number;
|
|
31
|
+
speakers?: string[];
|
|
32
|
+
|
|
33
|
+
toJson(): string;
|
|
34
|
+
toSrt(): string;
|
|
35
|
+
toVtt(): string;
|
|
36
|
+
toRttm(fileId?: string): string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export abstract class Engine {
|
|
40
|
+
options: EngineOptions;
|
|
41
|
+
constructor(options?: EngineOptions);
|
|
42
|
+
abstract transcribe(audioPath: string, options?: any): Promise<TranscriptResult>;
|
|
43
|
+
abstract diarize(audioPath: string, numSpeakers?: number): Promise<TranscriptResult>;
|
|
44
|
+
getInfo(): Record<string, any>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class WhisperEngine extends Engine {
|
|
48
|
+
transcribe(audioPath: string, options?: any): Promise<TranscriptResult>;
|
|
49
|
+
diarize(audioPath: string, numSpeakers?: number): Promise<TranscriptResult>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class SherpaEngine extends Engine {
|
|
53
|
+
transcribe(audioPath: string, options?: any): Promise<TranscriptResult>;
|
|
54
|
+
diarize(audioPath: string, numSpeakers?: number): Promise<TranscriptResult>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class HybridEngine extends Engine {
|
|
58
|
+
transcribe(audioPath: string, options?: any): Promise<TranscriptResult>;
|
|
59
|
+
diarize(audioPath: string, numSpeakers?: number): Promise<TranscriptResult>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createEngine(
|
|
63
|
+
engineName?: 'whisper' | 'sherpa' | 'hybrid' | string,
|
|
64
|
+
options?: EngineOptions
|
|
65
|
+
): Engine;
|
|
66
|
+
|
|
67
|
+
export class TermuxSTT {
|
|
68
|
+
engine: Engine;
|
|
69
|
+
constructor(options?: EngineOptions & { engine?: string });
|
|
70
|
+
transcribe(filePath: string, options?: any): Promise<TranscriptResult>;
|
|
71
|
+
diarize(filePath: string, options?: any): Promise<TranscriptResult>;
|
|
72
|
+
}
|
package/index.js
CHANGED
|
@@ -1,49 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
case '
|
|
18
|
-
return new
|
|
19
|
-
case '
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* termux-stt Node.js entry point (v2.0.0)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { Engine, TranscriptResult, Segment, formatTime } = require('./lib/engine');
|
|
10
|
+
const { WhisperEngine } = require('./lib/whisper');
|
|
11
|
+
const { SherpaEngine } = require('./lib/sherpa');
|
|
12
|
+
const { HybridEngine } = require('./lib/hybrid');
|
|
13
|
+
|
|
14
|
+
function createEngine(engineName = 'whisper', options = {}) {
|
|
15
|
+
const name = String(engineName).toLowerCase();
|
|
16
|
+
switch (name) {
|
|
17
|
+
case 'whisper':
|
|
18
|
+
return new WhisperEngine(options);
|
|
19
|
+
case 'sherpa':
|
|
20
|
+
return new SherpaEngine(options);
|
|
21
|
+
case 'hybrid':
|
|
22
|
+
return new HybridEngine(options);
|
|
23
|
+
case 'vosk':
|
|
24
|
+
throw new Error("Vosk has been deprecated and completely removed in v2.0.0. Please use 'sherpa' (SenseVoice) or 'whisper'.");
|
|
25
|
+
default:
|
|
26
|
+
throw new Error(`Unknown engine: ${engineName}. Available: whisper, sherpa, hybrid`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class TermuxSTT {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
const engineName = options.engine || 'whisper';
|
|
33
|
+
this.engine = createEngine(engineName, options);
|
|
34
|
+
}
|
|
35
|
+
transcribe(filePath, options = {}) {
|
|
36
|
+
return this.engine.transcribe(filePath, options);
|
|
37
|
+
}
|
|
38
|
+
diarize(filePath, options = {}) {
|
|
39
|
+
return this.engine.diarize(filePath, options);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
TermuxSTT,
|
|
45
|
+
createEngine,
|
|
46
|
+
Engine,
|
|
47
|
+
TranscriptResult,
|
|
48
|
+
Segment,
|
|
49
|
+
WhisperEngine,
|
|
50
|
+
SherpaEngine,
|
|
51
|
+
HybridEngine,
|
|
52
|
+
formatTime
|
|
53
|
+
};
|
package/lib/engine.js
CHANGED
|
@@ -1,92 +1,96 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async
|
|
75
|
-
throw new Error('
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
throw new Error('
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* termux-stt - Engine Abstraction for Node.js
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class TranscriptResult {
|
|
10
|
+
constructor(text, segments = [], language = 'ko', duration = 0) {
|
|
11
|
+
this.text = text;
|
|
12
|
+
this.segments = segments;
|
|
13
|
+
this.language = language;
|
|
14
|
+
this.duration = duration;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toJson() {
|
|
18
|
+
return JSON.stringify(this, null, 2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toSrt() {
|
|
22
|
+
return this.segments.map((seg, i) => {
|
|
23
|
+
const start = formatTime(seg.start);
|
|
24
|
+
const end = formatTime(seg.end);
|
|
25
|
+
return `${i + 1}\n${start} --> ${end}\n${seg.text}\n`;
|
|
26
|
+
}).join('\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
toVtt() {
|
|
30
|
+
const body = this.segments.map((seg) => {
|
|
31
|
+
const start = formatTime(seg.start, true);
|
|
32
|
+
const end = formatTime(seg.end, true);
|
|
33
|
+
return `${start} --> ${end}\n${seg.text}\n`;
|
|
34
|
+
}).join('\n');
|
|
35
|
+
return `WEBVTT\n\n${body}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toRttm(fileId = 'audio') {
|
|
39
|
+
return this.segments.map((seg) => {
|
|
40
|
+
const spk = seg.speaker || 'Speaker_0';
|
|
41
|
+
const dur = (seg.end - seg.start).toFixed(2);
|
|
42
|
+
return `SPEAKER ${fileId} 1 ${seg.start.toFixed(2)} ${dur} <NA> <NA> ${spk} <NA> <NA>`;
|
|
43
|
+
}).join('\n');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class Segment {
|
|
48
|
+
constructor(start, end, text, speaker = null, confidence = null) {
|
|
49
|
+
this.start = start;
|
|
50
|
+
this.end = end;
|
|
51
|
+
this.text = text;
|
|
52
|
+
this.speaker = speaker;
|
|
53
|
+
this.confidence = confidence;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatTime(seconds, isVtt = false) {
|
|
58
|
+
const s = Math.max(0, seconds || 0);
|
|
59
|
+
const hrs = Math.floor(s / 3600);
|
|
60
|
+
const mins = Math.floor((s % 3600) / 60);
|
|
61
|
+
const secs = Math.floor(s % 60);
|
|
62
|
+
const ms = Math.floor((s % 1) * 1000);
|
|
63
|
+
|
|
64
|
+
const pad = (n, z = 2) => String(n).padStart(z, '0');
|
|
65
|
+
const sep = isVtt ? '.' : ',';
|
|
66
|
+
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}${sep}${pad(ms, 3)}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class Engine {
|
|
70
|
+
constructor(config = {}) {
|
|
71
|
+
this.config = config;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async transcribe(audioPath, options = {}) {
|
|
75
|
+
throw new Error('transcribe() must be implemented by subclass');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async diarize(audioPath, numSpeakers = 2) {
|
|
79
|
+
throw new Error('diarize() must be implemented by subclass');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
streamMic(duration = null) {
|
|
83
|
+
throw new Error('streamMic() must be implemented by subclass');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getInfo() {
|
|
87
|
+
return { name: this.constructor.name, config: this.config };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
Engine,
|
|
93
|
+
TranscriptResult,
|
|
94
|
+
Segment,
|
|
95
|
+
formatTime
|
|
96
|
+
};
|