@vdegenne/gemini-tts-proxy 0.0.0-stage → 0.1.2
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/lib/access.d.ts +6 -0
- package/lib/access.d.ts.map +1 -0
- package/lib/access.js +37 -0
- package/lib/access.js.map +1 -0
- package/lib/auth.d.ts +3 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +27 -0
- package/lib/auth.js.map +1 -0
- package/lib/cache.d.ts +9 -0
- package/lib/cache.d.ts.map +1 -0
- package/lib/cache.js +192 -0
- package/lib/cache.js.map +1 -0
- package/lib/clean-cache.d.ts +2 -0
- package/lib/clean-cache.d.ts.map +1 -0
- package/lib/clean-cache.js +95 -0
- package/lib/clean-cache.js.map +1 -0
- package/lib/config.d.ts +10 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +22 -0
- package/lib/config.js.map +1 -0
- package/lib/core.d.ts +15 -0
- package/lib/core.d.ts.map +1 -0
- package/lib/core.js +59 -0
- package/lib/core.js.map +1 -0
- package/lib/ffmpeg.d.ts +11 -0
- package/lib/ffmpeg.d.ts.map +1 -0
- package/lib/ffmpeg.js +113 -0
- package/lib/ffmpeg.js.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +239 -0
- package/lib/index.js.map +1 -0
- package/lib/stats.d.ts +3 -0
- package/lib/stats.d.ts.map +1 -0
- package/lib/stats.js +80 -0
- package/lib/stats.js.map +1 -0
- package/lib/tts.d.ts +31 -0
- package/lib/tts.d.ts.map +1 -0
- package/lib/tts.js +134 -0
- package/lib/tts.js.map +1 -0
- package/package.json +55 -5
- package/README.md +0 -3
package/lib/access.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare class AccessError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
constructor(message: string, status: number);
|
|
4
|
+
}
|
|
5
|
+
export declare function resolveApiKey(clientApiKey?: string, authorization?: string): Promise<string>;
|
|
6
|
+
//# sourceMappingURL=access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAGA,qBAAa,WAAY,SAAQ,KAAK;aAGpB,MAAM,EAAE,MAAM;IAF/B,YACC,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,MAAM,EAG9B;CACD;AAED,wBAAsB,aAAa,CAClC,YAAY,CAAC,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAgCjB"}
|
package/lib/access.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isFirebaseConfigured, verifyPremiumUser } from './auth.js';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
export class AccessError extends Error {
|
|
4
|
+
constructor(message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function resolveApiKey(clientApiKey, authorization) {
|
|
10
|
+
const providedKey = clientApiKey?.trim();
|
|
11
|
+
// A client-provided key takes precedence and bypasses Firebase.
|
|
12
|
+
if (providedKey)
|
|
13
|
+
return providedKey;
|
|
14
|
+
const serverApiKey = config.apiKey?.trim();
|
|
15
|
+
if (!serverApiKey) {
|
|
16
|
+
throw new AccessError('Gemini API key required', 401);
|
|
17
|
+
}
|
|
18
|
+
// Without Firebase credentials, the server key is sufficient.
|
|
19
|
+
if (!isFirebaseConfigured())
|
|
20
|
+
return serverApiKey;
|
|
21
|
+
if (!authorization?.startsWith('Bearer ')) {
|
|
22
|
+
throw new AccessError('Authentication required', 401);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const isPremium = await verifyPremiumUser(authorization.slice(7));
|
|
26
|
+
if (!isPremium) {
|
|
27
|
+
throw new AccessError('Premium subscription required', 403);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof AccessError)
|
|
32
|
+
throw error;
|
|
33
|
+
throw new AccessError('Invalid authentication token', 401);
|
|
34
|
+
}
|
|
35
|
+
return serverApiKey;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,oBAAoB,EAAE,iBAAiB,EAAC,MAAM,WAAW,CAAA;AACjE,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAA;AAElC,MAAM,OAAO,WAAY,SAAQ,KAAK;IACrC,YACC,OAAe,EACC,MAAc;QAE9B,KAAK,CAAC,OAAO,CAAC,CAAA;sBAFE,MAAM;IAGvB,CAAC;CACD;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,YAAqB,EACrB,aAAsB;IAEtB,MAAM,WAAW,GAAG,YAAY,EAAE,IAAI,EAAE,CAAA;IAExC,gEAAgE;IAChE,IAAI,WAAW;QAAE,OAAO,WAAW,CAAA;IAEnC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,CAAA;IAE1C,IAAI,CAAC,YAAY,EAAE,CAAC;QACnB,MAAM,IAAI,WAAW,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAA;IACtD,CAAC;IAED,8DAA8D;IAC9D,IAAI,CAAC,oBAAoB,EAAE;QAAE,OAAO,YAAY,CAAA;IAEhD,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,WAAW,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAA;IACtD,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,SAAS,GAAG,MAAM,iBAAiB,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;QAEjE,IAAI,CAAC,SAAS,EAAE,CAAC;YAChB,MAAM,IAAI,WAAW,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAA;QAC5D,CAAC;IACF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,KAAK,YAAY,WAAW;YAAE,MAAM,KAAK,CAAA;QAE7C,MAAM,IAAI,WAAW,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAA;IAC3D,CAAC;IAED,OAAO,YAAY,CAAA;AACpB,CAAC"}
|
package/lib/auth.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAUA,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AA2BD,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGvE"}
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { cert, getApps, initializeApp } from 'firebase-admin/app';
|
|
3
|
+
import { getAuth } from 'firebase-admin/auth';
|
|
4
|
+
import { config } from './config.js';
|
|
5
|
+
let auth;
|
|
6
|
+
export function isFirebaseConfigured() {
|
|
7
|
+
return existsSync(config.serviceAccountPath);
|
|
8
|
+
}
|
|
9
|
+
function getFirebaseAuth() {
|
|
10
|
+
if (auth)
|
|
11
|
+
return auth;
|
|
12
|
+
if (!isFirebaseConfigured()) {
|
|
13
|
+
throw new Error('Firebase service account is not configured');
|
|
14
|
+
}
|
|
15
|
+
const serviceAccount = JSON.parse(readFileSync(config.serviceAccountPath, 'utf8'));
|
|
16
|
+
const app = getApps().find((app) => app.name === 'gemini-tts-proxy') ??
|
|
17
|
+
initializeApp({
|
|
18
|
+
credential: cert(serviceAccount),
|
|
19
|
+
}, 'gemini-tts-proxy');
|
|
20
|
+
auth = getAuth(app);
|
|
21
|
+
return auth;
|
|
22
|
+
}
|
|
23
|
+
export async function verifyPremiumUser(token) {
|
|
24
|
+
const decodedToken = await getFirebaseAuth().verifyIdToken(token, true);
|
|
25
|
+
return decodedToken.premium === true;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=auth.js.map
|
package/lib/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAGlD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,EAAE,OAAO,EAAa,MAAM,qBAAqB,CAAA;AAExD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,IAAI,IAAsB,CAAA;AAE1B,MAAM,UAAU,oBAAoB;IACnC,OAAO,UAAU,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAA;AAC7C,CAAC;AAED,SAAS,eAAe;IACvB,IAAI,IAAI;QAAE,OAAO,IAAI,CAAA;IAErB,IAAI,CAAC,oBAAoB,EAAE,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;IAC9D,CAAC;IAED,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAChC,YAAY,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAC7B,CAAA;IAEnB,MAAM,GAAG,GACR,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,kBAAkB,CAAC;QACxD,aAAa,CACZ;YACC,UAAU,EAAE,IAAI,CAAC,cAAc,CAAC;SAChC,EACD,kBAAkB,CAClB,CAAA;IAEF,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;IAEnB,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAa;IACpD,MAAM,YAAY,GAAG,MAAM,eAAe,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACvE,OAAO,YAAY,CAAC,OAAO,KAAK,IAAI,CAAA;AACrC,CAAC"}
|
package/lib/cache.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { type SpeechOptions } from './core.js';
|
|
3
|
+
export interface CachedAudio {
|
|
4
|
+
body: Buffer | Readable;
|
|
5
|
+
size: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function getDiskPath(hash: string): string;
|
|
8
|
+
export declare function getSpeech(options: SpeechOptions): Promise<CachedAudio>;
|
|
9
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAKtC,OAAO,EAA6B,KAAK,aAAa,EAAE,MAAM,WAAW,CAAA;AAIzE,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;CACZ;AAWD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAkLD,wBAAsB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAsD5E"}
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, rename, stat, unlink } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { GoogleGenAI } from '@google/genai';
|
|
6
|
+
import { config } from './config.js';
|
|
7
|
+
import { getAudioPath, getCacheKey } from './core.js';
|
|
8
|
+
import { encodePcmToMp3 } from './ffmpeg.js';
|
|
9
|
+
import { recordUsage } from './stats.js';
|
|
10
|
+
const memoryCache = new Map();
|
|
11
|
+
const inFlight = new Map();
|
|
12
|
+
let memoryCacheSize = 0;
|
|
13
|
+
export function getDiskPath(hash) {
|
|
14
|
+
return join(config.cacheDir, getAudioPath(hash));
|
|
15
|
+
}
|
|
16
|
+
function getMemory(hash) {
|
|
17
|
+
const entry = memoryCache.get(hash);
|
|
18
|
+
if (!entry)
|
|
19
|
+
return undefined;
|
|
20
|
+
// Refresh insertion order to implement LRU.
|
|
21
|
+
memoryCache.delete(hash);
|
|
22
|
+
memoryCache.set(hash, entry);
|
|
23
|
+
return entry.data;
|
|
24
|
+
}
|
|
25
|
+
function setMemory(hash, data) {
|
|
26
|
+
if (data.length > config.memoryCacheMaxBytes)
|
|
27
|
+
return;
|
|
28
|
+
const previous = memoryCache.get(hash);
|
|
29
|
+
if (previous) {
|
|
30
|
+
memoryCacheSize -= previous.data.length;
|
|
31
|
+
memoryCache.delete(hash);
|
|
32
|
+
}
|
|
33
|
+
while (memoryCacheSize + data.length > config.memoryCacheMaxBytes) {
|
|
34
|
+
const oldest = memoryCache.keys().next().value;
|
|
35
|
+
if (oldest === undefined)
|
|
36
|
+
break;
|
|
37
|
+
const entry = memoryCache.get(oldest);
|
|
38
|
+
memoryCacheSize -= entry.data.length;
|
|
39
|
+
memoryCache.delete(oldest);
|
|
40
|
+
}
|
|
41
|
+
memoryCache.set(hash, { data });
|
|
42
|
+
memoryCacheSize += data.length;
|
|
43
|
+
}
|
|
44
|
+
async function getDiskAudio(hash) {
|
|
45
|
+
const path = getDiskPath(hash);
|
|
46
|
+
let fileSize;
|
|
47
|
+
try {
|
|
48
|
+
fileSize = (await stat(path)).size;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT') {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
if (fileSize <= config.memoryCacheMaxBytes) {
|
|
57
|
+
const data = await readFile(path);
|
|
58
|
+
setMemory(hash, data);
|
|
59
|
+
return { body: data, size: data.length };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
body: createReadStream(path),
|
|
63
|
+
size: fileSize,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function synthesize(options, hash) {
|
|
67
|
+
const path = getDiskPath(hash);
|
|
68
|
+
const directory = join(path, '..');
|
|
69
|
+
const tempPath = `${path}.${randomUUID()}.tmp`;
|
|
70
|
+
if (!options.apiKey) {
|
|
71
|
+
throw new Error('No Gemini API key was provided');
|
|
72
|
+
}
|
|
73
|
+
await mkdir(directory, { recursive: true });
|
|
74
|
+
try {
|
|
75
|
+
const ai = new GoogleGenAI({
|
|
76
|
+
apiKey: options.apiKey,
|
|
77
|
+
httpOptions: {
|
|
78
|
+
apiVersion: 'v1beta',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const response = await ai.models.generateContentStream({
|
|
82
|
+
model: options.model,
|
|
83
|
+
contents: [
|
|
84
|
+
{
|
|
85
|
+
role: 'user',
|
|
86
|
+
parts: [{ text: options.input }],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
config: {
|
|
90
|
+
responseModalities: ['AUDIO'],
|
|
91
|
+
systemInstruction: options.systemInstruction,
|
|
92
|
+
speechConfig: {
|
|
93
|
+
languageCode: options.languageCode,
|
|
94
|
+
voiceConfig: {
|
|
95
|
+
voice: options.voice,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const chunks = [];
|
|
101
|
+
let pcmFormat;
|
|
102
|
+
for await (const chunk of response) {
|
|
103
|
+
const parts = chunk.candidates?.[0]?.content?.parts ?? [];
|
|
104
|
+
for (const part of parts) {
|
|
105
|
+
const inlineData = part.inlineData;
|
|
106
|
+
if (!inlineData?.data)
|
|
107
|
+
continue;
|
|
108
|
+
const mimeType = inlineData.mimeType ?? '';
|
|
109
|
+
const match = mimeType.match(/^audio\/L(\d+)(?:;rate=(\d+))?/i);
|
|
110
|
+
if (!match) {
|
|
111
|
+
throw new Error(`Unsupported Gemini audio format: ${mimeType}`);
|
|
112
|
+
}
|
|
113
|
+
const format = {
|
|
114
|
+
bitsPerSample: Number(match[1]),
|
|
115
|
+
sampleRate: Number(match[2] ?? 24000),
|
|
116
|
+
channels: 1,
|
|
117
|
+
};
|
|
118
|
+
if (format.bitsPerSample !== 16) {
|
|
119
|
+
throw new Error(`Unsupported PCM bit depth: ${format.bitsPerSample}`);
|
|
120
|
+
}
|
|
121
|
+
if (pcmFormat) {
|
|
122
|
+
if (pcmFormat.sampleRate !== format.sampleRate ||
|
|
123
|
+
pcmFormat.bitsPerSample !== format.bitsPerSample ||
|
|
124
|
+
pcmFormat.channels !== format.channels) {
|
|
125
|
+
throw new Error('Gemini returned inconsistent audio formats');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
pcmFormat = format;
|
|
130
|
+
}
|
|
131
|
+
chunks.push(Buffer.from(inlineData.data, 'base64'));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (chunks.length === 0 || !pcmFormat) {
|
|
135
|
+
throw new Error('Gemini returned no audio data');
|
|
136
|
+
}
|
|
137
|
+
await encodePcmToMp3(Buffer.concat(chunks), tempPath, pcmFormat);
|
|
138
|
+
await rename(tempPath, path);
|
|
139
|
+
const fileSize = (await stat(path)).size;
|
|
140
|
+
if (fileSize <= config.memoryCacheMaxBytes) {
|
|
141
|
+
try {
|
|
142
|
+
setMemory(hash, await readFile(path));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// The disk cache remains usable if memory caching fails.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
await unlink(tempPath).catch(() => { });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function getSpeech(options) {
|
|
154
|
+
const hash = await getCacheKey(options);
|
|
155
|
+
let audio = getMemory(hash);
|
|
156
|
+
if (audio) {
|
|
157
|
+
recordUsage(hash);
|
|
158
|
+
return { body: audio, size: audio.length };
|
|
159
|
+
}
|
|
160
|
+
const cached = await getDiskAudio(hash);
|
|
161
|
+
if (cached) {
|
|
162
|
+
recordUsage(hash);
|
|
163
|
+
return cached;
|
|
164
|
+
}
|
|
165
|
+
let pending = inFlight.get(hash);
|
|
166
|
+
if (!pending) {
|
|
167
|
+
pending = synthesize(options, hash);
|
|
168
|
+
inFlight.set(hash, pending);
|
|
169
|
+
void pending.then(() => {
|
|
170
|
+
if (inFlight.get(hash) === pending) {
|
|
171
|
+
inFlight.delete(hash);
|
|
172
|
+
}
|
|
173
|
+
}, () => {
|
|
174
|
+
if (inFlight.get(hash) === pending) {
|
|
175
|
+
inFlight.delete(hash);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
await pending;
|
|
180
|
+
audio = getMemory(hash);
|
|
181
|
+
if (audio) {
|
|
182
|
+
recordUsage(hash);
|
|
183
|
+
return { body: audio, size: audio.length };
|
|
184
|
+
}
|
|
185
|
+
const result = await getDiskAudio(hash);
|
|
186
|
+
if (!result) {
|
|
187
|
+
throw new Error('Audio was generated but is missing from the disk cache');
|
|
188
|
+
}
|
|
189
|
+
recordUsage(hash);
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=cache.js.map
|
package/lib/cache.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clean-cache.d.ts","sourceRoot":"","sources":["../src/clean-cache.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, stat, unlink, writeFile, } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { config } from './config.js';
|
|
5
|
+
const gib = 1024 ** 3;
|
|
6
|
+
const maxBytes = Number(process.env.DISK_CACHE_MAX_BYTES ?? 5 * gib);
|
|
7
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
8
|
+
throw new Error('DISK_CACHE_MAX_BYTES must be a positive integer');
|
|
9
|
+
}
|
|
10
|
+
const audioDir = join(config.cacheDir, 'audio');
|
|
11
|
+
const statsPath = join(config.cacheDir, 'stats.json');
|
|
12
|
+
function getAudioPath(hash) {
|
|
13
|
+
return join(audioDir, hash.slice(0, 2), hash.slice(2, 4), `${hash}.mp3`);
|
|
14
|
+
}
|
|
15
|
+
async function main() {
|
|
16
|
+
let stats;
|
|
17
|
+
try {
|
|
18
|
+
const content = await readFile(statsPath, 'utf8');
|
|
19
|
+
const parsed = JSON.parse(content);
|
|
20
|
+
if (parsed === null ||
|
|
21
|
+
typeof parsed !== 'object' ||
|
|
22
|
+
Array.isArray(parsed)) {
|
|
23
|
+
throw new Error('Invalid stats.json format');
|
|
24
|
+
}
|
|
25
|
+
stats = parsed;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === 'ENOENT') {
|
|
29
|
+
console.log('No stats.json found. Nothing to clean.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
const entries = Object.entries(stats).filter(([hash, count]) => /^[a-f0-9]{64}$/.test(hash) && Number.isSafeInteger(count) && count >= 0);
|
|
35
|
+
const files = [];
|
|
36
|
+
let totalBytes = 0;
|
|
37
|
+
for (const [hash, uses] of entries) {
|
|
38
|
+
const path = getAudioPath(hash);
|
|
39
|
+
try {
|
|
40
|
+
const info = await stat(path);
|
|
41
|
+
if (!info.isFile())
|
|
42
|
+
continue;
|
|
43
|
+
files.push({
|
|
44
|
+
hash,
|
|
45
|
+
path,
|
|
46
|
+
size: info.size,
|
|
47
|
+
uses,
|
|
48
|
+
mtimeMs: info.mtimeMs,
|
|
49
|
+
});
|
|
50
|
+
totalBytes += info.size;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code !== 'ENOENT') {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
// Remove stale statistics for files that no longer exist.
|
|
57
|
+
delete stats[hash];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const initialBytes = totalBytes;
|
|
61
|
+
// Least-used files first; oldest files break ties.
|
|
62
|
+
files.sort((a, b) => a.uses - b.uses || a.mtimeMs - b.mtimeMs);
|
|
63
|
+
let deletedFiles = 0;
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
if (totalBytes <= maxBytes)
|
|
66
|
+
break;
|
|
67
|
+
await unlink(file.path);
|
|
68
|
+
totalBytes -= file.size;
|
|
69
|
+
delete stats[file.hash];
|
|
70
|
+
deletedFiles++;
|
|
71
|
+
console.log(`Deleted ${file.hash}.mp3 (${file.size} bytes, ${file.uses} uses)`);
|
|
72
|
+
}
|
|
73
|
+
if (deletedFiles > 0 || files.length !== entries.length) {
|
|
74
|
+
await mkdir(dirname(statsPath), { recursive: true });
|
|
75
|
+
const tempPath = `${statsPath}.${randomUUID()}.tmp`;
|
|
76
|
+
try {
|
|
77
|
+
await writeFile(tempPath, JSON.stringify(stats), 'utf8');
|
|
78
|
+
await rename(tempPath, statsPath);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await unlink(tempPath).catch(() => { });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
console.log(`Cache: ${(initialBytes / gib).toFixed(2)} GiB → ` +
|
|
85
|
+
`${(totalBytes / gib).toFixed(2)} GiB`);
|
|
86
|
+
console.log(`Deleted ${deletedFiles} file(s).`);
|
|
87
|
+
if (totalBytes > maxBytes) {
|
|
88
|
+
console.log('Cache remains above the limit. Some audio files may not be tracked in stats.json.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
main().catch((error) => {
|
|
92
|
+
console.error('Cache cleanup failed:', error);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
});
|
|
95
|
+
//# sourceMappingURL=clean-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clean-cache.js","sourceRoot":"","sources":["../src/clean-cache.ts"],"names":[],"mappings":"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"}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const config: {
|
|
2
|
+
readonly host: string;
|
|
3
|
+
readonly port: number;
|
|
4
|
+
readonly apiKey: string | undefined;
|
|
5
|
+
readonly serviceAccountPath: string;
|
|
6
|
+
readonly cacheDir: string;
|
|
7
|
+
readonly memoryCacheMaxBytes: number;
|
|
8
|
+
readonly statsFlushIntervalMs: number;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,MAAM;aAClB,IAAI;aACJ,IAAI;aAEJ,MAAM;aAEN,kBAAkB;aAIlB,QAAQ;aAER,mBAAmB;aAEnB,oBAAoB;CACX,CAAA"}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
function getPositiveInteger(name, fallback) {
|
|
3
|
+
const value = process.env[name];
|
|
4
|
+
if (value === undefined)
|
|
5
|
+
return fallback;
|
|
6
|
+
const parsed = Number(value);
|
|
7
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
8
|
+
throw new Error(`${name} must be a positive integer`);
|
|
9
|
+
}
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
const mib = 1024 * 1024;
|
|
13
|
+
export const config = {
|
|
14
|
+
host: process.env.HOST ?? '127.0.0.1',
|
|
15
|
+
port: getPositiveInteger('PORT', 3000),
|
|
16
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
17
|
+
serviceAccountPath: resolve(process.env.FIREBASE_SERVICE_ACCOUNT ?? 'serviceAccountKey.json'),
|
|
18
|
+
cacheDir: resolve(process.env.CACHE_DIR ?? 'data/gemini'),
|
|
19
|
+
memoryCacheMaxBytes: getPositiveInteger('MEMORY_CACHE_MAX_BYTES', 100 * mib),
|
|
20
|
+
statsFlushIntervalMs: getPositiveInteger('STATS_FLUSH_INTERVAL_MS', 30_000),
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAEjC,SAAS,kBAAkB,CAAC,IAAY,EAAE,QAAgB;IACzD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAE/B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAA;IAExC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAE5B,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,6BAA6B,CAAC,CAAA;IACtD,CAAC;IAED,OAAO,MAAM,CAAA;AACd,CAAC;AAED,MAAM,GAAG,GAAG,IAAI,GAAG,IAAI,CAAA;AAEvB,MAAM,CAAC,MAAM,MAAM,GAAG;IACrB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,WAAW;IACrC,IAAI,EAAE,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC;IAEtC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;IAElC,kBAAkB,EAAE,OAAO,CAC1B,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,wBAAwB,CAChE;IAED,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,aAAa,CAAC;IAEzD,mBAAmB,EAAE,kBAAkB,CAAC,wBAAwB,EAAE,GAAG,GAAG,GAAG,CAAC;IAE5E,oBAAoB,EAAE,kBAAkB,CAAC,yBAAyB,EAAE,MAAM,CAAC;CAClE,CAAA"}
|
package/lib/core.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const GEMINI_TTS_MODELS: readonly ['gemini-2.5-flash-preview-tts', 'gemini-2.5-pro-preview-tts', 'gemini-3.1-flash-tts-preview', 'gemini-3.8-flash-tts', 'gemini-3.8-flash-lite-tts'];
|
|
2
|
+
export type GeminiTtsModel = (typeof GEMINI_TTS_MODELS)[number];
|
|
3
|
+
export declare const GEMINI_TTS_VOICES: readonly ['Achernar', 'Achird', 'Algenib', 'Algieba', 'Alnilam', 'Aoede', 'Autonoe', 'Callirrhoe', 'Charon', 'Despina', 'Enceladus', 'Erinome', 'Fenrir', 'Gacrux', 'Iapetus', 'Kore', 'Laomedeia', 'Leda', 'Orus', 'Puck', 'Pulcherrima', 'Rasalgethi', 'Sadachbia', 'Sadaltager', 'Schedar', 'Sulafat', 'Umbriel', 'Vindemiatrix', 'Zephyr', 'Zubenelgenubi'];
|
|
4
|
+
export type GeminiTtsVoice = (typeof GEMINI_TTS_VOICES)[number];
|
|
5
|
+
export interface SpeechOptions {
|
|
6
|
+
input: string;
|
|
7
|
+
model: GeminiTtsModel;
|
|
8
|
+
voice: string;
|
|
9
|
+
languageCode: string;
|
|
10
|
+
systemInstruction?: string;
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function getCacheKey(options: SpeechOptions): Promise<string>;
|
|
14
|
+
export declare function getAudioPath(hash: string): string;
|
|
15
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,YAC7B,8BAA8B,EAC9B,4BAA4B,EAC5B,8BAA8B,EAC9B,sBAAsB,EACtB,2BAA2B,CAClB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/D,eAAO,MAAM,iBAAiB,YAC7B,UAAU,EACV,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACT,OAAO,EACP,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,WAAW,EACX,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,MAAM,EACN,WAAW,EACX,MAAM,EACN,MAAM,EACN,MAAM,EACN,aAAa,EACb,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACT,SAAS,EACT,cAAc,EACd,QAAQ,EACR,eAAe,CACN,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/D,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBzE;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD"}
|
package/lib/core.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const GEMINI_TTS_MODELS = [
|
|
2
|
+
'gemini-2.5-flash-preview-tts',
|
|
3
|
+
'gemini-2.5-pro-preview-tts',
|
|
4
|
+
'gemini-3.1-flash-tts-preview',
|
|
5
|
+
'gemini-3.8-flash-tts',
|
|
6
|
+
'gemini-3.8-flash-lite-tts',
|
|
7
|
+
];
|
|
8
|
+
export const GEMINI_TTS_VOICES = [
|
|
9
|
+
'Achernar',
|
|
10
|
+
'Achird',
|
|
11
|
+
'Algenib',
|
|
12
|
+
'Algieba',
|
|
13
|
+
'Alnilam',
|
|
14
|
+
'Aoede',
|
|
15
|
+
'Autonoe',
|
|
16
|
+
'Callirrhoe',
|
|
17
|
+
'Charon',
|
|
18
|
+
'Despina',
|
|
19
|
+
'Enceladus',
|
|
20
|
+
'Erinome',
|
|
21
|
+
'Fenrir',
|
|
22
|
+
'Gacrux',
|
|
23
|
+
'Iapetus',
|
|
24
|
+
'Kore',
|
|
25
|
+
'Laomedeia',
|
|
26
|
+
'Leda',
|
|
27
|
+
'Orus',
|
|
28
|
+
'Puck',
|
|
29
|
+
'Pulcherrima',
|
|
30
|
+
'Rasalgethi',
|
|
31
|
+
'Sadachbia',
|
|
32
|
+
'Sadaltager',
|
|
33
|
+
'Schedar',
|
|
34
|
+
'Sulafat',
|
|
35
|
+
'Umbriel',
|
|
36
|
+
'Vindemiatrix',
|
|
37
|
+
'Zephyr',
|
|
38
|
+
'Zubenelgenubi',
|
|
39
|
+
];
|
|
40
|
+
export async function getCacheKey(options) {
|
|
41
|
+
const payload = JSON.stringify([
|
|
42
|
+
options.input,
|
|
43
|
+
options.model,
|
|
44
|
+
options.voice,
|
|
45
|
+
options.languageCode,
|
|
46
|
+
options.systemInstruction ?? '',
|
|
47
|
+
]);
|
|
48
|
+
const data = new TextEncoder().encode(payload);
|
|
49
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
50
|
+
return Array.from(new Uint8Array(hash))
|
|
51
|
+
.map(function (byte) {
|
|
52
|
+
return byte.toString(16).padStart(2, '0');
|
|
53
|
+
})
|
|
54
|
+
.join('');
|
|
55
|
+
}
|
|
56
|
+
export function getAudioPath(hash) {
|
|
57
|
+
return `audio/${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash}.mp3`;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=core.js.map
|
package/lib/core.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAChC,8BAA8B;IAC9B,4BAA4B;IAC5B,8BAA8B;IAC9B,sBAAsB;IACtB,2BAA2B;CAClB,CAAA;AAIV,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAChC,UAAU;IACV,QAAQ;IACR,SAAS;IACT,SAAS;IACT,SAAS;IACT,OAAO;IACP,SAAS;IACT,YAAY;IACZ,QAAQ;IACR,SAAS;IACT,WAAW;IACX,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,MAAM;IACN,WAAW;IACX,MAAM;IACN,MAAM;IACN,MAAM;IACN,aAAa;IACb,YAAY;IACZ,WAAW;IACX,YAAY;IACZ,SAAS;IACT,SAAS;IACT,SAAS;IACT,cAAc;IACd,QAAQ;IACR,eAAe;CACN,CAAA;AAaV,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAsB;IACvD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;QAC9B,OAAO,CAAC,KAAK;QACb,OAAO,CAAC,KAAK;QACb,OAAO,CAAC,KAAK;QACb,OAAO,CAAC,YAAY;QACpB,OAAO,CAAC,iBAAiB,IAAI,EAAE;KAC/B,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC9C,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;IAExD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;SACrC,GAAG,CAAC,UAAU,IAAI;QAClB,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC1C,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC,CAAA;AACX,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY;IACxC,OAAO,SAAS,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,MAAM,CAAA;AACnE,CAAC"}
|
package/lib/ffmpeg.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface FFmpegInfo {
|
|
2
|
+
version: string;
|
|
3
|
+
}
|
|
4
|
+
export interface PcmFormat {
|
|
5
|
+
sampleRate: number;
|
|
6
|
+
bitsPerSample: number;
|
|
7
|
+
channels: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function checkFFmpeg(): Promise<FFmpegInfo>;
|
|
10
|
+
export declare function encodePcmToMp3(pcm: Buffer, outputPath: string, format: PcmFormat): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=ffmpeg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../src/ffmpeg.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,MAAM,CAAA;CACf;AACD,MAAM,WAAW,SAAS;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;CAChB;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,UAAU,CAAC,CAiFvD;AAED,wBAAsB,cAAc,CACnC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,GACf,OAAO,CAAC,IAAI,CAAC,CAyDf"}
|