@thinkable-labs/audio-enhancer 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.
@@ -0,0 +1,3 @@
1
+ export declare function isolateAudio(audioPath: string, apiKey: string): Promise<Buffer>;
2
+ export declare function changeVoice(audioBuffer: Buffer, voiceId: string, apiKey: string): Promise<Buffer>;
3
+ //# sourceMappingURL=elevenlabs.d.ts.map
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isolateAudio = isolateAudio;
37
+ exports.changeVoice = changeVoice;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const https = __importStar(require("https"));
41
+ // ─── Audio Isolation ────────────────────────────────────────────────────────
42
+ // POST https://api.elevenlabs.io/v1/audio-isolation
43
+ // Multipart form: audio file
44
+ // Returns: audio stream
45
+ async function isolateAudio(audioPath, apiKey) {
46
+ const fileBuffer = fs.readFileSync(audioPath);
47
+ const fileName = path.basename(audioPath);
48
+ const boundary = `----FormBoundary${Date.now()}`;
49
+ const parts = [];
50
+ // File part
51
+ parts.push(Buffer.from(`--${boundary}\r\n` +
52
+ `Content-Disposition: form-data; name="audio"; filename="${fileName}"\r\n` +
53
+ `Content-Type: audio/mpeg\r\n\r\n`));
54
+ parts.push(fileBuffer);
55
+ parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
56
+ const body = Buffer.concat(parts);
57
+ return new Promise((resolve, reject) => {
58
+ const req = https.request({
59
+ hostname: "api.elevenlabs.io",
60
+ path: "/v1/audio-isolation",
61
+ method: "POST",
62
+ headers: {
63
+ "xi-api-key": apiKey,
64
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
65
+ "Content-Length": body.length,
66
+ },
67
+ }, (res) => {
68
+ const chunks = [];
69
+ res.on("data", (chunk) => chunks.push(chunk));
70
+ res.on("end", () => {
71
+ const result = Buffer.concat(chunks);
72
+ if (res.statusCode && res.statusCode >= 400) {
73
+ reject(new Error(`Audio isolation failed (${res.statusCode}): ${result.toString().substring(0, 200)}`));
74
+ return;
75
+ }
76
+ resolve(result);
77
+ });
78
+ });
79
+ req.on("error", reject);
80
+ req.write(body);
81
+ req.end();
82
+ });
83
+ }
84
+ // ─── Speech to Speech (Voice Change) ───────────────────────────────────────
85
+ // POST https://api.elevenlabs.io/v1/speech-to-speech/{voice_id}
86
+ // Multipart form: audio file + model_id
87
+ // Returns: audio stream
88
+ async function changeVoice(audioBuffer, voiceId, apiKey) {
89
+ const boundary = `----FormBoundary${Date.now()}`;
90
+ const parts = [];
91
+ // Audio file part
92
+ parts.push(Buffer.from(`--${boundary}\r\n` +
93
+ `Content-Disposition: form-data; name="audio"; filename="audio.mp3"\r\n` +
94
+ `Content-Type: audio/mpeg\r\n\r\n`));
95
+ parts.push(audioBuffer);
96
+ parts.push(Buffer.from("\r\n"));
97
+ // Model ID part
98
+ parts.push(Buffer.from(`--${boundary}\r\n` +
99
+ `Content-Disposition: form-data; name="model_id"\r\n\r\n` +
100
+ `eleven_english_sts_v2\r\n`));
101
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
102
+ const body = Buffer.concat(parts);
103
+ return new Promise((resolve, reject) => {
104
+ const req = https.request({
105
+ hostname: "api.elevenlabs.io",
106
+ path: `/v1/speech-to-speech/${voiceId}`,
107
+ method: "POST",
108
+ headers: {
109
+ "xi-api-key": apiKey,
110
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
111
+ "Content-Length": body.length,
112
+ },
113
+ }, (res) => {
114
+ const chunks = [];
115
+ res.on("data", (chunk) => chunks.push(chunk));
116
+ res.on("end", () => {
117
+ const result = Buffer.concat(chunks);
118
+ if (res.statusCode && res.statusCode >= 400) {
119
+ reject(new Error(`Voice change failed (${res.statusCode}): ${result.toString().substring(0, 200)}`));
120
+ return;
121
+ }
122
+ resolve(result);
123
+ });
124
+ });
125
+ req.on("error", reject);
126
+ req.write(body);
127
+ req.end();
128
+ });
129
+ }
130
+ //# sourceMappingURL=elevenlabs.js.map
@@ -0,0 +1,34 @@
1
+ export interface EnhanceOptions {
2
+ apiKey: string;
3
+ ffmpegPath: string;
4
+ outputDir: string;
5
+ }
6
+ export interface EnhanceItem {
7
+ videoPath: string;
8
+ voiceId: string;
9
+ }
10
+ export type ItemStatus = "pending" | "extracting" | "isolating" | "voice-changing" | "success" | "error";
11
+ export interface ItemResult {
12
+ item: EnhanceItem;
13
+ videoName: string;
14
+ outputPath: string;
15
+ status: ItemStatus;
16
+ error?: string;
17
+ }
18
+ export interface BatchEnhanceResult {
19
+ results: ItemResult[];
20
+ totalCount: number;
21
+ successCount: number;
22
+ errorCount: number;
23
+ }
24
+ export type ProgressCallback = (update: {
25
+ current: number;
26
+ total: number;
27
+ videoName: string;
28
+ status: ItemStatus;
29
+ outputPath?: string;
30
+ error?: string;
31
+ }) => void;
32
+ export declare function enhanceVideo(item: EnhanceItem, options: EnhanceOptions): Promise<ItemResult>;
33
+ export declare function enhanceBatch(items: EnhanceItem[], options: EnhanceOptions, onProgress?: ProgressCallback): Promise<BatchEnhanceResult>;
34
+ //# sourceMappingURL=enhancer.d.ts.map
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.enhanceVideo = enhanceVideo;
37
+ exports.enhanceBatch = enhanceBatch;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const extractor_1 = require("./extractor");
41
+ const elevenlabs_1 = require("./elevenlabs");
42
+ // ─── Enhance a single video ────────────────────────────────────────────────
43
+ async function enhanceVideo(item, options) {
44
+ const videoName = path.basename(item.videoPath, path.extname(item.videoPath));
45
+ const outputPath = path.join(options.outputDir, `${videoName}.mp3`);
46
+ let tempAudioPath = null;
47
+ try {
48
+ // Step 1: Extract audio from video
49
+ tempAudioPath = await (0, extractor_1.extractAudio)(item.videoPath, options.ffmpegPath);
50
+ // Step 2: Isolate voice (remove background noise)
51
+ const isolatedAudio = await (0, elevenlabs_1.isolateAudio)(tempAudioPath, options.apiKey);
52
+ // Step 3: Voice change
53
+ const finalAudio = await (0, elevenlabs_1.changeVoice)(isolatedAudio, item.voiceId, options.apiKey);
54
+ // Save final audio
55
+ if (!fs.existsSync(options.outputDir)) {
56
+ fs.mkdirSync(options.outputDir, { recursive: true });
57
+ }
58
+ fs.writeFileSync(outputPath, finalAudio);
59
+ return {
60
+ item,
61
+ videoName,
62
+ outputPath,
63
+ status: "success",
64
+ };
65
+ }
66
+ catch (err) {
67
+ return {
68
+ item,
69
+ videoName,
70
+ outputPath,
71
+ status: "error",
72
+ error: err?.message || String(err),
73
+ };
74
+ }
75
+ finally {
76
+ // Clean up temp file
77
+ if (tempAudioPath && fs.existsSync(tempAudioPath)) {
78
+ try {
79
+ fs.unlinkSync(tempAudioPath);
80
+ }
81
+ catch { }
82
+ }
83
+ }
84
+ }
85
+ // ─── Batch enhance ─────────────────────────────────────────────────────────
86
+ async function enhanceBatch(items, options, onProgress) {
87
+ const results = [];
88
+ let successCount = 0;
89
+ let errorCount = 0;
90
+ if (!fs.existsSync(options.outputDir)) {
91
+ fs.mkdirSync(options.outputDir, { recursive: true });
92
+ }
93
+ for (let i = 0; i < items.length; i++) {
94
+ const item = items[i];
95
+ const videoName = path.basename(item.videoPath, path.extname(item.videoPath));
96
+ // Notify: extracting
97
+ onProgress?.({
98
+ current: i + 1,
99
+ total: items.length,
100
+ videoName,
101
+ status: "extracting",
102
+ });
103
+ let tempAudioPath = null;
104
+ try {
105
+ // Step 1: Extract audio
106
+ tempAudioPath = await (0, extractor_1.extractAudio)(item.videoPath, options.ffmpegPath);
107
+ // Notify: isolating
108
+ onProgress?.({
109
+ current: i + 1,
110
+ total: items.length,
111
+ videoName,
112
+ status: "isolating",
113
+ });
114
+ // Step 2: Isolate voice
115
+ const isolatedAudio = await (0, elevenlabs_1.isolateAudio)(tempAudioPath, options.apiKey);
116
+ // Notify: voice changing
117
+ onProgress?.({
118
+ current: i + 1,
119
+ total: items.length,
120
+ videoName,
121
+ status: "voice-changing",
122
+ });
123
+ // Step 3: Voice change
124
+ const finalAudio = await (0, elevenlabs_1.changeVoice)(isolatedAudio, item.voiceId, options.apiKey);
125
+ // Save
126
+ const outputPath = path.join(options.outputDir, `${videoName}.mp3`);
127
+ fs.writeFileSync(outputPath, finalAudio);
128
+ successCount++;
129
+ results.push({ item, videoName, outputPath, status: "success" });
130
+ onProgress?.({
131
+ current: i + 1,
132
+ total: items.length,
133
+ videoName,
134
+ status: "success",
135
+ outputPath,
136
+ });
137
+ }
138
+ catch (err) {
139
+ errorCount++;
140
+ const outputPath = path.join(options.outputDir, `${videoName}.mp3`);
141
+ const errorMsg = err?.message || String(err);
142
+ results.push({ item, videoName, outputPath, status: "error", error: errorMsg });
143
+ onProgress?.({
144
+ current: i + 1,
145
+ total: items.length,
146
+ videoName,
147
+ status: "error",
148
+ error: errorMsg,
149
+ });
150
+ }
151
+ finally {
152
+ if (tempAudioPath && fs.existsSync(tempAudioPath)) {
153
+ try {
154
+ fs.unlinkSync(tempAudioPath);
155
+ }
156
+ catch { }
157
+ }
158
+ }
159
+ }
160
+ return { results, totalCount: items.length, successCount, errorCount };
161
+ }
162
+ //# sourceMappingURL=enhancer.js.map
@@ -0,0 +1,2 @@
1
+ export declare function extractAudio(videoPath: string, ffmpegPath: string): Promise<string>;
2
+ //# sourceMappingURL=extractor.d.ts.map
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.extractAudio = extractAudio;
37
+ const child_process_1 = require("child_process");
38
+ const path = __importStar(require("path"));
39
+ const fs = __importStar(require("fs"));
40
+ const os = __importStar(require("os"));
41
+ function extractAudio(videoPath, ffmpegPath) {
42
+ return new Promise((resolve, reject) => {
43
+ const outputPath = path.join(os.tmpdir(), `audio-enhance-${Date.now()}.mp3`);
44
+ (0, child_process_1.execFile)(ffmpegPath, [
45
+ "-i", videoPath,
46
+ "-vn", // no video
47
+ "-acodec", "libmp3lame",
48
+ "-ab", "192k",
49
+ "-ar", "44100",
50
+ "-y", // overwrite
51
+ outputPath,
52
+ ], { timeout: 60000 }, (error, _stdout, stderr) => {
53
+ if (error) {
54
+ reject(new Error(`FFmpeg failed: ${stderr || error.message}`));
55
+ return;
56
+ }
57
+ if (!fs.existsSync(outputPath)) {
58
+ reject(new Error("FFmpeg produced no output file"));
59
+ return;
60
+ }
61
+ resolve(outputPath);
62
+ });
63
+ });
64
+ }
65
+ //# sourceMappingURL=extractor.js.map
@@ -0,0 +1,5 @@
1
+ export { extractAudio } from "./extractor";
2
+ export { isolateAudio, changeVoice } from "./elevenlabs";
3
+ export { enhanceVideo, enhanceBatch } from "./enhancer";
4
+ export type { EnhanceOptions, EnhanceItem, ItemStatus, ItemResult, BatchEnhanceResult, ProgressCallback, } from "./enhancer";
5
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enhanceBatch = exports.enhanceVideo = exports.changeVoice = exports.isolateAudio = exports.extractAudio = void 0;
4
+ var extractor_1 = require("./extractor");
5
+ Object.defineProperty(exports, "extractAudio", { enumerable: true, get: function () { return extractor_1.extractAudio; } });
6
+ var elevenlabs_1 = require("./elevenlabs");
7
+ Object.defineProperty(exports, "isolateAudio", { enumerable: true, get: function () { return elevenlabs_1.isolateAudio; } });
8
+ Object.defineProperty(exports, "changeVoice", { enumerable: true, get: function () { return elevenlabs_1.changeVoice; } });
9
+ var enhancer_1 = require("./enhancer");
10
+ Object.defineProperty(exports, "enhanceVideo", { enumerable: true, get: function () { return enhancer_1.enhanceVideo; } });
11
+ Object.defineProperty(exports, "enhanceBatch", { enumerable: true, get: function () { return enhancer_1.enhanceBatch; } });
12
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@thinkable-labs/audio-enhancer",
3
+ "version": "1.0.0",
4
+ "description": "Extract audio from video, isolate voice with ElevenLabs, apply voice change",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/**/*.js",
9
+ "dist/**/*.d.ts"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "prepublishOnly": "tsc"
14
+ },
15
+ "license": "UNLICENSED",
16
+ "dependencies": {
17
+ "form-data": "^4.0.1"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^25.5.0",
21
+ "typescript": "^5.9.3"
22
+ }
23
+ }