termux-tts 1.5.5 → 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/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/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