mia-narrative 1.0.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 +149 -0
- package/bin/mia-narrative.ts +86 -0
- package/dist/bin/mia-narrative.d.ts +3 -0
- package/dist/bin/mia-narrative.d.ts.map +1 -0
- package/dist/bin/mia-narrative.js +34 -0
- package/dist/bin/mia-narrative.js.map +1 -0
- package/dist/src/audio/processor.d.ts +6 -0
- package/dist/src/audio/processor.d.ts.map +1 -0
- package/dist/src/audio/processor.js +72 -0
- package/dist/src/audio/processor.js.map +1 -0
- package/dist/src/commands/generate.d.ts +19 -0
- package/dist/src/commands/generate.d.ts.map +1 -0
- package/dist/src/commands/generate.js +104 -0
- package/dist/src/commands/generate.js.map +1 -0
- package/dist/src/commands/voices.d.ts +8 -0
- package/dist/src/commands/voices.d.ts.map +1 -0
- package/dist/src/commands/voices.js +60 -0
- package/dist/src/commands/voices.js.map +1 -0
- package/dist/src/config/defaults.d.ts +54 -0
- package/dist/src/config/defaults.d.ts.map +1 -0
- package/dist/src/config/defaults.js +30 -0
- package/dist/src/config/defaults.js.map +1 -0
- package/dist/src/config/voices.d.ts +15 -0
- package/dist/src/config/voices.d.ts.map +1 -0
- package/dist/src/config/voices.js +79 -0
- package/dist/src/config/voices.js.map +1 -0
- package/dist/src/engines/ElevenLabsEngine.d.ts +10 -0
- package/dist/src/engines/ElevenLabsEngine.d.ts.map +1 -0
- package/dist/src/engines/ElevenLabsEngine.js +78 -0
- package/dist/src/engines/ElevenLabsEngine.js.map +1 -0
- package/dist/src/engines/SystemTtsEngine.d.ts +9 -0
- package/dist/src/engines/SystemTtsEngine.d.ts.map +1 -0
- package/dist/src/engines/SystemTtsEngine.js +56 -0
- package/dist/src/engines/SystemTtsEngine.js.map +1 -0
- package/dist/src/engines/base.d.ts +23 -0
- package/dist/src/engines/base.d.ts.map +1 -0
- package/dist/src/engines/base.js +3 -0
- package/dist/src/engines/base.js.map +1 -0
- package/dist/src/engines/factory.d.ts +6 -0
- package/dist/src/engines/factory.d.ts.map +1 -0
- package/dist/src/engines/factory.js +20 -0
- package/dist/src/engines/factory.js.map +1 -0
- package/dist/src/engines/piper.d.ts +12 -0
- package/dist/src/engines/piper.d.ts.map +1 -0
- package/dist/src/engines/piper.js +118 -0
- package/dist/src/engines/piper.js.map +1 -0
- package/dist/src/utils/file-reader.d.ts +5 -0
- package/dist/src/utils/file-reader.d.ts.map +1 -0
- package/dist/src/utils/file-reader.js +26 -0
- package/dist/src/utils/file-reader.js.map +1 -0
- package/dist/src/utils/logger.d.ts +10 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +27 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/package.json +35 -0
- package/src/audio/processor.ts +94 -0
- package/src/commands/generate.ts +144 -0
- package/src/commands/voices.ts +68 -0
- package/src/config/defaults.ts +41 -0
- package/src/config/voices.ts +89 -0
- package/src/engines/ElevenLabsEngine.ts +81 -0
- package/src/engines/SystemTtsEngine.ts +61 -0
- package/src/engines/base.ts +26 -0
- package/src/engines/factory.ts +28 -0
- package/src/engines/piper.ts +134 -0
- package/src/types/say.d.ts +26 -0
- package/src/utils/file-reader.ts +25 -0
- package/src/utils/logger.ts +33 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
|
+
import { writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import { Logger } from '../utils/logger.js';
|
|
7
|
+
import { TTSEngine, TTSEngineConfig, GenerateAudioOptions, Voice } from './base.js';
|
|
8
|
+
import { getVoiceProfile, VOICE_PROFILES } from '../config/voices.js';
|
|
9
|
+
|
|
10
|
+
export class PiperEngine extends TTSEngine {
|
|
11
|
+
private piperPath: string = 'piper';
|
|
12
|
+
private modelPath: string = '';
|
|
13
|
+
|
|
14
|
+
async initialize(config: TTSEngineConfig): Promise<void> {
|
|
15
|
+
if (config.piperPath) {
|
|
16
|
+
this.piperPath = config.piperPath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (config.modelPath) {
|
|
20
|
+
this.modelPath = config.modelPath;
|
|
21
|
+
} else {
|
|
22
|
+
this.modelPath = process.env.MIA_NARRATIVE_PIPER_MODEL || '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Logger.debug(`Piper engine initialized with model path: ${this.modelPath}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async generateAudio(options: GenerateAudioOptions): Promise<Buffer> {
|
|
29
|
+
const { text, voiceId } = options;
|
|
30
|
+
|
|
31
|
+
const voiceProfile = getVoiceProfile(voiceId);
|
|
32
|
+
if (!voiceProfile) {
|
|
33
|
+
throw new Error(`Piper voice '${voiceId}' not found.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!text || text.trim().length === 0) {
|
|
37
|
+
throw new Error('Text cannot be empty');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const tempDir = tmpdir();
|
|
41
|
+
const uid = randomUUID();
|
|
42
|
+
const textFile = join(tempDir, `piper-input-${uid}.txt`);
|
|
43
|
+
const audioFile = join(tempDir, `piper-output-${uid}.wav`);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
writeFileSync(textFile, text, 'utf-8');
|
|
47
|
+
await this.runPiper(textFile, audioFile, voiceProfile.piperModel);
|
|
48
|
+
const audioBuffer = readFileSync(audioFile);
|
|
49
|
+
return audioBuffer;
|
|
50
|
+
} finally {
|
|
51
|
+
try {
|
|
52
|
+
unlinkSync(textFile);
|
|
53
|
+
Logger.debug(`Cleaned up: ${textFile}`);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
unlinkSync(audioFile);
|
|
59
|
+
Logger.debug(`Cleaned up: ${audioFile}`);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// ignore
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getVoices(): Promise<Voice[]> {
|
|
67
|
+
return Object.values(VOICE_PROFILES).map(v => ({ id: v.id, name: `${v.name} (${v.style})` }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async isAvailable(): Promise<boolean> {
|
|
71
|
+
try {
|
|
72
|
+
if (this.piperPath === 'piper') {
|
|
73
|
+
execSync('which piper', { stdio: 'pipe' });
|
|
74
|
+
} else {
|
|
75
|
+
const { existsSync } = await import('fs');
|
|
76
|
+
if (!existsSync(this.piperPath)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getName(): string {
|
|
87
|
+
return 'piper';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private runPiper(
|
|
91
|
+
textFile: string,
|
|
92
|
+
outputFile: string,
|
|
93
|
+
model: string
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const args = [
|
|
97
|
+
'--model',
|
|
98
|
+
model,
|
|
99
|
+
'--output-file',
|
|
100
|
+
outputFile,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const piper = spawn(this.piperPath, args, { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
104
|
+
|
|
105
|
+
const textContent = readFileSync(textFile, 'utf-8');
|
|
106
|
+
piper.stdin.write(textContent);
|
|
107
|
+
piper.stdin.end();
|
|
108
|
+
|
|
109
|
+
let stderr = '';
|
|
110
|
+
const MAX_STDERR_SIZE = 2048;
|
|
111
|
+
|
|
112
|
+
piper.stderr.on('data', (data) => {
|
|
113
|
+
const chunk = data.toString();
|
|
114
|
+
stderr = (stderr + chunk).slice(-MAX_STDERR_SIZE);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
piper.on('close', (code) => {
|
|
118
|
+
if (code === 0) {
|
|
119
|
+
resolve();
|
|
120
|
+
} else {
|
|
121
|
+
reject(new Error(`Piper failed (exit code ${code}): ${stderr}`));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
piper.on('error', (error) => {
|
|
126
|
+
reject(
|
|
127
|
+
new Error(
|
|
128
|
+
`Failed to start Piper: ${error.message}. Ensure Piper is installed and in your PATH.`
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare module 'say' {
|
|
2
|
+
type errorCallback = (err: string | null) => void;
|
|
3
|
+
type getInstalledVoicesCallback = (err: string | null, voices: string[]) => void;
|
|
4
|
+
|
|
5
|
+
const say: {
|
|
6
|
+
speak(
|
|
7
|
+
text: string,
|
|
8
|
+
voice?: string | null,
|
|
9
|
+
speed?: number,
|
|
10
|
+
callback?: errorCallback
|
|
11
|
+
): void;
|
|
12
|
+
|
|
13
|
+
stop(callback?: errorCallback): void;
|
|
14
|
+
|
|
15
|
+
export(
|
|
16
|
+
text: string,
|
|
17
|
+
voice: string | null,
|
|
18
|
+
speed: number,
|
|
19
|
+
filename: string,
|
|
20
|
+
callback?: errorCallback
|
|
21
|
+
): void;
|
|
22
|
+
|
|
23
|
+
getInstalledVoices(callback?: getInstalledVoicesCallback): void;
|
|
24
|
+
};
|
|
25
|
+
export default say;
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { Logger } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
export class FileReader {
|
|
6
|
+
static readTextFile(filePath: string): string {
|
|
7
|
+
try {
|
|
8
|
+
const absolutePath = resolve(filePath);
|
|
9
|
+
return readFileSync(absolutePath, "utf-8");
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error instanceof Error) {
|
|
12
|
+
Logger.error(`Failed to read file: ${error.message}`);
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static fileExists(filePath: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
return existsSync(filePath);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
export class Logger {
|
|
4
|
+
static info(message: string): void {
|
|
5
|
+
console.log(chalk.blue("ℹ"), message);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
static success(message: string): void {
|
|
9
|
+
console.log(chalk.green("✓"), message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static error(message: string): void {
|
|
13
|
+
console.error(chalk.red("✗"), message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static warn(message: string): void {
|
|
17
|
+
console.log(chalk.yellow("⚠"), message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static debug(message: string): void {
|
|
21
|
+
if (process.env.DEBUG) {
|
|
22
|
+
console.log(chalk.gray("→"), message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static log(message: string): void {
|
|
27
|
+
console.log(message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static clear(): void {
|
|
31
|
+
console.clear();
|
|
32
|
+
}
|
|
33
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"moduleResolution": "node",
|
|
17
|
+
"typeRoots": ["./node_modules/@types", "./src/types"]
|
|
18
|
+
},
|
|
19
|
+
"include": ["bin/**/*", "src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist"]
|
|
21
|
+
}
|