termux-tts 1.6.0 → 1.7.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/README.md +0 -36
- package/bin/cli.js +4 -0
- package/binding_node/index.js +129 -125
- package/binding_node/test_node.js +44 -40
- package/index.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,42 +127,6 @@ main();
|
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
-
## Distributed Clustering & Memory Pooling (AMEVA-Cluster)
|
|
131
|
-
|
|
132
|
-
Termux-TTS natively integrates with **AMEVA-Cluster** (`pip install ameva-cluster`) for distributed speech synthesis across interconnected mobile nodes.
|
|
133
|
-
|
|
134
|
-
### 1. Install Cluster Runtime
|
|
135
|
-
```bash
|
|
136
|
-
pip install ameva-cluster
|
|
137
|
-
# or Node.js:
|
|
138
|
-
npm install @ameva/cluster
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### 2. Launch Worker Node on Remote Phone
|
|
142
|
-
```bash
|
|
143
|
-
# On remote worker device (e.g. Galaxy A53):
|
|
144
|
-
ameva-cluster worker --port 50052
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### 3. Distributed Speech Synthesis via Master Node
|
|
148
|
-
```bash
|
|
149
|
-
# Master CLI offloading neural vocoder synthesis:
|
|
150
|
-
termux-tts synthesize "Clustering active across distributed phone fleet." \
|
|
151
|
-
--rpc 192.0.2.10:50052,192.0.2.11:50052 \
|
|
152
|
-
-o cluster_output.wav
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
```python
|
|
156
|
-
import termux_tts as tts
|
|
157
|
-
|
|
158
|
-
# Python SDK Distributed TTS
|
|
159
|
-
with tts.load(cluster_rpc_servers="192.0.2.10:50052,192.0.2.11:50052") as engine:
|
|
160
|
-
result = engine.synthesize("Distributed TTS voice synthesis active.", output="distributed.wav")
|
|
161
|
-
print(f"Elapsed: {result.elapsed_ms:.1f}ms")
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
130
|
## Official Documentation & Benchmarks
|
|
167
131
|
- [Official Architecture & API Reference](https://uno-km.vercel.app/lib/tts/)
|
|
168
132
|
- [Ecosystem Metrics & Registry Stats](https://uno-km.vercel.app/foundation/metrics)
|
package/bin/cli.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
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
|
+
*/
|
|
2
6
|
/**
|
|
3
7
|
* AMEVA Standard Node.js CLI Runner for termux_tts.
|
|
4
8
|
* Automatically resolves Python 3 environment and dispatches to python -m termux_tts.
|
package/binding_node/index.js
CHANGED
|
@@ -1,125 +1,129 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const
|
|
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
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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-tts Node.js SDK
|
|
7
|
+
* Secure On-Device Speech Synthesis Binding (CWE-94 Remediation).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
class TTSEngine {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.language = options.language || 'ko';
|
|
16
|
+
this.preset = options.preset || 'balanced';
|
|
17
|
+
this.sampleRate = options.sampleRate || 22050;
|
|
18
|
+
this.engine = options.engine || 'auto';
|
|
19
|
+
this.device = options.device || (options.gpu ? 'gpu' : 'auto');
|
|
20
|
+
this.tier = options.tier || null;
|
|
21
|
+
this.model = options.model || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async synthesize(text, options = {}) {
|
|
25
|
+
if (!text || typeof text !== 'string' || !text.trim()) {
|
|
26
|
+
throw new Error('TTSInferenceError: Input text cannot be empty.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const output = options.output || path.join(process.cwd(), 'output.wav');
|
|
30
|
+
const speed = typeof options.speed === 'number' ? options.speed : 1.0;
|
|
31
|
+
const preset = options.preset || this.preset;
|
|
32
|
+
const engineType = options.engine || this.engine;
|
|
33
|
+
const deviceType = options.device || (options.gpu ? 'gpu' : this.device);
|
|
34
|
+
const tierType = options.tier || this.tier;
|
|
35
|
+
const modelPath = options.model || this.model;
|
|
36
|
+
|
|
37
|
+
const payload = JSON.stringify({
|
|
38
|
+
text: text,
|
|
39
|
+
output: output,
|
|
40
|
+
language: this.language,
|
|
41
|
+
sample_rate: this.sampleRate,
|
|
42
|
+
speed: speed,
|
|
43
|
+
preset: preset,
|
|
44
|
+
engine: engineType,
|
|
45
|
+
device: deviceType,
|
|
46
|
+
tier: tierType,
|
|
47
|
+
model: modelPath
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const pythonDriver = `
|
|
52
|
+
import sys, json
|
|
53
|
+
import termux_tts as tts
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
data = json.loads(sys.stdin.read())
|
|
57
|
+
engine = tts.load(
|
|
58
|
+
model=data.get('model'),
|
|
59
|
+
language=data.get('language', 'ko'),
|
|
60
|
+
preset=data.get('preset', 'balanced'),
|
|
61
|
+
sample_rate=data.get('sample_rate', 22050),
|
|
62
|
+
engine=data.get('engine', 'auto'),
|
|
63
|
+
device=data.get('device', 'auto'),
|
|
64
|
+
tier=data.get('tier', None)
|
|
65
|
+
)
|
|
66
|
+
res = engine.synthesize(data['text'], output=data['output'], speed=float(data.get('speed', 1.0)))
|
|
67
|
+
result = {
|
|
68
|
+
'status': 'SUCCESS',
|
|
69
|
+
'text': res.text,
|
|
70
|
+
'outputPath': data['output'],
|
|
71
|
+
'durationSec': res.duration_sec,
|
|
72
|
+
'elapsedMs': res.elapsed_ms,
|
|
73
|
+
'rtf': res.rtf,
|
|
74
|
+
'sampleRate': res.sample_rate,
|
|
75
|
+
'backend': res.backend,
|
|
76
|
+
'modelName': getattr(res, 'model_name', 'default')
|
|
77
|
+
}
|
|
78
|
+
sys.stdout.write(json.dumps(result))
|
|
79
|
+
except Exception as e:
|
|
80
|
+
sys.stderr.write(f"TTSInferenceError: {str(e)}")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
`;
|
|
83
|
+
const pyBin = process.env.PYTHON_BIN || (process.platform === 'win32' ? 'py' : 'python3');
|
|
84
|
+
const proc = spawn(pyBin, ['-c', pythonDriver], {
|
|
85
|
+
env: { ...process.env, PYTHONPATH: path.join(__dirname, '..') }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let stdout = '';
|
|
89
|
+
let stderr = '';
|
|
90
|
+
|
|
91
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
92
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
93
|
+
|
|
94
|
+
proc.on('close', (code) => {
|
|
95
|
+
if (code !== 0) {
|
|
96
|
+
return reject(new Error(`TTSInferenceError (${code}): ${stderr.trim() || stdout.trim()}`));
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(stdout);
|
|
100
|
+
if (parsed.status === 'SUCCESS') {
|
|
101
|
+
resolve(parsed);
|
|
102
|
+
} else {
|
|
103
|
+
reject(new Error(`TTSInferenceError: Unexpected execution status in ${stdout}`));
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
reject(new Error(`TTSParseError: Failed to parse Python worker JSON response: ${stdout}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
proc.on('error', (err) => {
|
|
111
|
+
reject(new Error(`TTSProcessError: Failed to spawn python3 worker: ${err.message}`));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Write JSON payload securely to stdin and close stream
|
|
115
|
+
proc.stdin.write(payload);
|
|
116
|
+
proc.stdin.end();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
close() {
|
|
121
|
+
// Resource clean up
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
TTSEngine,
|
|
127
|
+
load: (opts) => new TTSEngine(opts),
|
|
128
|
+
version: "0.1.0"
|
|
129
|
+
};
|
|
@@ -1,40 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
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
|
-
console.
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
4
|
+
*/
|
|
5
|
+
const tts = require('./index.js');
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
async function testNode() {
|
|
11
|
+
console.log('[*] Testing termux-tts Node.js SDK...');
|
|
12
|
+
const engine = tts.load({ language: 'ko' });
|
|
13
|
+
assert.strictEqual(engine.language, 'ko');
|
|
14
|
+
|
|
15
|
+
// 1. Valid synthesis test
|
|
16
|
+
const outWav = path.join(__dirname, 'test_node_out.wav');
|
|
17
|
+
const res = await engine.synthesize("노드 JS SDK 보안 무결성 합성 검증입니다.", {
|
|
18
|
+
output: outWav,
|
|
19
|
+
speed: 1.0
|
|
20
|
+
});
|
|
21
|
+
assert.strictEqual(res.status, 'SUCCESS');
|
|
22
|
+
assert(res.durationSec > 0.3);
|
|
23
|
+
assert(fs.existsSync(outWav));
|
|
24
|
+
assert(fs.statSync(outWav).size > 1000);
|
|
25
|
+
fs.unlinkSync(outWav);
|
|
26
|
+
console.log(`[PASS] Node.js Synthesis: ${res.durationSec}s in ${res.elapsedMs}ms`);
|
|
27
|
+
|
|
28
|
+
// 2. Security payload test (Command/Code Injection Attack payload)
|
|
29
|
+
const attackPayload = 'Hello"""\nimport os\nos.system("echo HACKED")\n"""World';
|
|
30
|
+
const attackOut = path.join(__dirname, 'test_attack_out.wav');
|
|
31
|
+
const attackRes = await engine.synthesize(attackPayload, {
|
|
32
|
+
output: attackOut
|
|
33
|
+
});
|
|
34
|
+
assert.strictEqual(attackRes.status, 'SUCCESS');
|
|
35
|
+
if (fs.existsSync(attackOut)) {
|
|
36
|
+
fs.unlinkSync(attackOut);
|
|
37
|
+
}
|
|
38
|
+
console.log('[PASS] Node.js RCE Injection Defense verified.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
testNode().catch((err) => {
|
|
42
|
+
console.error('[FAIL]', err);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
package/index.js
CHANGED
package/package.json
CHANGED