youtube-audio-transcript-api 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 ADDED
@@ -0,0 +1,258 @@
1
+ <p align="center">
2
+ <img src="https://youtubetranscript.dev/logo.svg" alt="YouTubeTranscript.dev" width="60" />
3
+ </p>
4
+
5
+ <h1 align="center">youtube-transcript-api</h1>
6
+
7
+ <p align="center">
8
+ Official Node.js / TypeScript SDK for the <a href="https://youtubetranscript.dev">YouTubeTranscript.dev</a> API (V2).
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/youtube-transcript-api"><img src="https://img.shields.io/npm/v/youtube-transcript-api" alt="npm" /></a>
13
+ <a href="https://youtubetranscript.dev"><img src="https://img.shields.io/badge/API-v2-brightgreen" alt="API Version" /></a>
14
+ <a href="https://github.com/youtubetranscript/youtube-transcript-api/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/youtube-transcript-api" alt="License" /></a>
15
+ </p>
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install youtube-transcript-api
23
+ ```
24
+
25
+ ```bash
26
+ pnpm add youtube-transcript-api
27
+ ```
28
+
29
+ ```bash
30
+ yarn add youtube-transcript-api
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```typescript
36
+ import { YouTubeTranscript } from "youtube-transcript-api";
37
+
38
+ const yt = new YouTubeTranscript({ apiKey: "your_api_key" });
39
+
40
+ // Simple — just pass a video ID or URL
41
+ const result = await yt.getTranscript("dQw4w9WgXcQ");
42
+ console.log(result.data?.transcript.text);
43
+ ```
44
+
45
+ Get your API key at **[youtubetranscript.dev](https://youtubetranscript.dev)**
46
+
47
+ ## Features
48
+
49
+ - ✅ Full V2 API coverage — transcribe, batch, jobs, polling
50
+ - ✅ TypeScript-first with complete type definitions
51
+ - ✅ Zero dependencies — uses native `fetch` (Node 18+)
52
+ - ✅ Typed errors for every API error code
53
+ - ✅ Built-in polling helpers for async ASR jobs
54
+ - ✅ ESM and CommonJS support
55
+
56
+ ## Usage
57
+
58
+ ### Basic Transcription
59
+
60
+ ```typescript
61
+ import { YouTubeTranscript } from "youtube-transcript-api";
62
+
63
+ const yt = new YouTubeTranscript({ apiKey: "your_api_key" });
64
+
65
+ // By video ID
66
+ const result = await yt.getTranscript("dQw4w9WgXcQ");
67
+
68
+ // By URL
69
+ const result = await yt.getTranscript("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
70
+
71
+ // With translation
72
+ const result = await yt.getTranscript("dQw4w9WgXcQ", "es");
73
+ ```
74
+
75
+ ### Full Options
76
+
77
+ ```typescript
78
+ const result = await yt.transcribe({
79
+ video: "dQw4w9WgXcQ",
80
+ language: "fr",
81
+ source: "manual",
82
+ format: {
83
+ timestamp: true,
84
+ paragraphs: true,
85
+ words: false,
86
+ },
87
+ });
88
+
89
+ console.log(result.status); // "completed"
90
+ console.log(result.data?.transcript.text); // Full transcript text
91
+ console.log(result.data?.transcript.language); // "fr"
92
+ console.log(result.data?.transcript.segments); // Timestamped segments
93
+ console.log(result.credits_used); // Credits consumed
94
+ ```
95
+
96
+ ### Batch Processing (up to 100 videos)
97
+
98
+ ```typescript
99
+ const result = await yt.batch({
100
+ video_ids: [
101
+ "dQw4w9WgXcQ",
102
+ "jNQXAC9IVRw",
103
+ "9bZkp7q19f0",
104
+ ],
105
+ format: { timestamp: true },
106
+ });
107
+
108
+ console.log(result.summary);
109
+ // { total: 3, succeeded: 3, failed: 0, processing: 0 }
110
+
111
+ for (const item of result.results) {
112
+ console.log(`${item.data?.video_id}: ${item.data?.transcript.text.slice(0, 100)}...`);
113
+ }
114
+ ```
115
+
116
+ ### ASR Audio Transcription (Async)
117
+
118
+ For videos without captions, use ASR with a webhook or polling:
119
+
120
+ ```typescript
121
+ // Option 1: Webhook (recommended for production)
122
+ const result = await yt.transcribe({
123
+ video: "VIDEO_ID",
124
+ source: "asr",
125
+ allow_asr: true,
126
+ webhook_url: "https://yoursite.com/webhook",
127
+ });
128
+ // result.status === "processing"
129
+ // result.job_id === "job_abc123"
130
+
131
+ // Option 2: Poll until complete
132
+ const result = await yt.transcribe({
133
+ video: "VIDEO_ID",
134
+ source: "asr",
135
+ allow_asr: true,
136
+ });
137
+
138
+ if (result.job_id) {
139
+ const final = await yt.waitForJob(result.job_id, {
140
+ interval: 5000, // poll every 5s
141
+ maxAttempts: 60, // give up after 5 minutes
142
+ });
143
+ console.log(final.data?.transcript.text);
144
+ }
145
+ ```
146
+
147
+ ### ASR Confirmation Flow
148
+
149
+ V2 requires explicit confirmation before ASR charges. If you don't set `allow_asr: true` and captions aren't available:
150
+
151
+ ```typescript
152
+ const result = await yt.transcribe({
153
+ video: "VIDEO_WITHOUT_CAPTIONS",
154
+ source: "asr",
155
+ // allow_asr not set
156
+ });
157
+
158
+ if (result.status === "requires_asr_confirmation") {
159
+ console.log(result.estimated_credits); // e.g. 5
160
+ console.log(result.duration_minutes); // e.g. 7.5
161
+ console.log(result.suggestion); // "Set allow_asr=true to proceed"
162
+
163
+ // User confirms → retry with allow_asr
164
+ const confirmed = await yt.transcribe({
165
+ video: "VIDEO_WITHOUT_CAPTIONS",
166
+ source: "asr",
167
+ allow_asr: true,
168
+ webhook_url: "https://yoursite.com/webhook",
169
+ });
170
+ }
171
+ ```
172
+
173
+ ### Job & Batch Polling
174
+
175
+ ```typescript
176
+ // Poll a single job
177
+ const job = await yt.getJob("job_abc123");
178
+
179
+ // Poll with format options
180
+ const job = await yt.getJob("job_abc123", {
181
+ include_segments: true,
182
+ include_paragraphs: true,
183
+ });
184
+
185
+ // Poll a batch
186
+ const batch = await yt.getBatch("batch_abc123");
187
+
188
+ // Auto-poll until done
189
+ const completed = await yt.waitForJob("job_abc123");
190
+ const completedBatch = await yt.waitForBatch("batch_abc123");
191
+ ```
192
+
193
+ ## Error Handling
194
+
195
+ Every API error maps to a typed exception:
196
+
197
+ ```typescript
198
+ import {
199
+ YouTubeTranscript,
200
+ InvalidRequestError,
201
+ AuthenticationError,
202
+ InsufficientCreditsError,
203
+ NoCaptionsError,
204
+ RateLimitError,
205
+ } from "youtube-transcript-api";
206
+
207
+ try {
208
+ await yt.getTranscript("invalid");
209
+ } catch (error) {
210
+ if (error instanceof AuthenticationError) {
211
+ console.log("Bad API key");
212
+ } else if (error instanceof InsufficientCreditsError) {
213
+ console.log("Top up at https://youtubetranscript.dev/pricing");
214
+ } else if (error instanceof NoCaptionsError) {
215
+ console.log("No captions — try source: 'asr' with allow_asr: true");
216
+ } else if (error instanceof RateLimitError) {
217
+ console.log(`Rate limited. Retry after ${error.retryAfter}s`);
218
+ } else if (error instanceof InvalidRequestError) {
219
+ console.log(`Bad request: ${error.message}`);
220
+ }
221
+ }
222
+ ```
223
+
224
+ | Error Class | HTTP Status | When |
225
+ |---|---|---|
226
+ | `InvalidRequestError` | 400 | Invalid JSON, missing fields, bad video ID |
227
+ | `AuthenticationError` | 401 | Missing or invalid API key |
228
+ | `InsufficientCreditsError` | 402 | Not enough credits |
229
+ | `NoCaptionsError` | 404 | No captions and ASR not used |
230
+ | `RateLimitError` | 429 | Too many requests |
231
+ | `YouTubeTranscriptError` | Other | Server errors, unexpected responses |
232
+
233
+ ## Configuration
234
+
235
+ ```typescript
236
+ const yt = new YouTubeTranscript({
237
+ apiKey: "your_api_key", // Required
238
+ baseUrl: "https://...", // Override API base URL
239
+ timeout: 60_000, // Request timeout in ms (default: 30s)
240
+ });
241
+ ```
242
+
243
+ ## Requirements
244
+
245
+ - Node.js 18+ (uses native `fetch`)
246
+ - API key from [youtubetranscript.dev](https://youtubetranscript.dev)
247
+
248
+ ## Links
249
+
250
+ - 🌐 [Website](https://youtubetranscript.dev)
251
+ - 📖 [Full API Docs](https://youtubetranscript.dev/api-docs)
252
+ - 📐 [OpenAPI Spec](https://youtubetranscript.dev/api-docs#openapi)
253
+ - 💰 [Pricing](https://youtubetranscript.dev/pricing)
254
+ - 🐛 [Issues](https://github.com/youtubetranscript/youtube-transcript-api/issues)
255
+
256
+ ## License
257
+
258
+ MIT — see [LICENSE](./LICENSE) for details.
@@ -0,0 +1,208 @@
1
+ /** Control which extra fields are included in the response. */
2
+ interface FormatOptions {
3
+ /** Include timestamped segments */
4
+ timestamp?: boolean;
5
+ /** Include paragraph groupings */
6
+ paragraphs?: boolean;
7
+ /** Include word-level timestamps */
8
+ words?: boolean;
9
+ }
10
+ /** Caption source preference. */
11
+ type Source = "auto" | "manual" | "asr";
12
+ interface TranscribeRequest {
13
+ /** YouTube URL or 11-character video ID */
14
+ video: string;
15
+ /** ISO 639-1 language code (e.g. "es", "fr"). Omit for best available. */
16
+ language?: string;
17
+ /** Caption source: "auto" (default), "manual", or "asr" */
18
+ source?: Source;
19
+ /** Explicitly confirm ASR usage when captions are unavailable */
20
+ allow_asr?: boolean;
21
+ /** Control extra output fields */
22
+ format?: FormatOptions;
23
+ /** URL to receive async results. Required for source="asr". */
24
+ webhook_url?: string;
25
+ }
26
+ interface BatchRequest {
27
+ /** Array of YouTube URLs or 11-character video IDs (max 100) */
28
+ video_ids: string[];
29
+ /** ISO 639-1 language code */
30
+ language?: string;
31
+ /** Caption source preference */
32
+ source?: Source;
33
+ /** Explicitly confirm ASR usage */
34
+ allow_asr?: boolean;
35
+ /** Control extra output fields */
36
+ format?: FormatOptions;
37
+ /** URL to receive async results */
38
+ webhook_url?: string;
39
+ }
40
+ interface TranscriptPayload {
41
+ /** Full transcript text */
42
+ text: string;
43
+ /** Detected / returned language */
44
+ language: string;
45
+ /** Source of the transcript: manual captions, auto, or asr */
46
+ source: string;
47
+ /** Timestamped segments (when format.timestamp = true) */
48
+ segments?: Record<string, unknown>[];
49
+ /** Paragraph groupings (when format.paragraphs = true) */
50
+ paragraphs?: Record<string, unknown>[];
51
+ /** Word-level timestamps (when format.words = true) */
52
+ words?: Record<string, unknown>[];
53
+ }
54
+ type TranscribeStatus = "completed" | "processing" | "failed" | "requires_asr_confirmation";
55
+ interface TranscribeResponse {
56
+ request_id: string;
57
+ status: TranscribeStatus;
58
+ data?: {
59
+ video_id: string;
60
+ transcript: TranscriptPayload;
61
+ video_title?: string | null;
62
+ };
63
+ error?: string;
64
+ job_id?: string | null;
65
+ estimated_credits?: number;
66
+ duration_minutes?: number | null;
67
+ suggestion?: string;
68
+ credits_used?: number;
69
+ }
70
+ type BatchStatus = "completed" | "partial" | "processing";
71
+ interface BatchResponse {
72
+ batch_id: string;
73
+ status: BatchStatus;
74
+ results: TranscribeResponse[];
75
+ summary?: {
76
+ total: number;
77
+ succeeded: number;
78
+ failed: number;
79
+ processing: number;
80
+ };
81
+ credits_used?: number;
82
+ }
83
+ interface ErrorResponse {
84
+ error: string;
85
+ message: string;
86
+ details?: Record<string, unknown>;
87
+ }
88
+ interface YouTubeTranscriptOptions {
89
+ /** Your API key from https://youtubetranscript.dev/dashboard */
90
+ apiKey: string;
91
+ /** Base URL override (default: https://youtubetranscript.dev/api/v2) */
92
+ baseUrl?: string;
93
+ /** Request timeout in ms (default: 30000) */
94
+ timeout?: number;
95
+ }
96
+
97
+ declare class YouTubeTranscript {
98
+ private readonly apiKey;
99
+ private readonly baseUrl;
100
+ private readonly timeout;
101
+ /**
102
+ * Create a new YouTubeTranscript client.
103
+ *
104
+ * ```ts
105
+ * const yt = new YouTubeTranscript({ apiKey: "your_api_key" });
106
+ * ```
107
+ *
108
+ * Get your API key at https://youtubetranscript.dev
109
+ */
110
+ constructor(options: YouTubeTranscriptOptions);
111
+ /**
112
+ * Transcribe a single YouTube video.
113
+ *
114
+ * ```ts
115
+ * const result = await yt.transcribe({ video: "dQw4w9WgXcQ" });
116
+ * console.log(result.data?.transcript.text);
117
+ * ```
118
+ */
119
+ transcribe(request: TranscribeRequest): Promise<TranscribeResponse>;
120
+ /**
121
+ * Shorthand: transcribe a video by URL or ID with minimal config.
122
+ *
123
+ * ```ts
124
+ * const result = await yt.getTranscript("dQw4w9WgXcQ");
125
+ * ```
126
+ */
127
+ getTranscript(video: string, language?: string): Promise<TranscribeResponse>;
128
+ /**
129
+ * Transcribe multiple videos in a single request (up to 100).
130
+ *
131
+ * ```ts
132
+ * const result = await yt.batch({
133
+ * video_ids: ["dQw4w9WgXcQ", "jNQXAC9IVRw"],
134
+ * });
135
+ * ```
136
+ */
137
+ batch(request: BatchRequest): Promise<BatchResponse>;
138
+ /**
139
+ * Poll an async transcription job (ASR).
140
+ *
141
+ * ```ts
142
+ * const status = await yt.getJob("job_abc123");
143
+ * ```
144
+ */
145
+ getJob(jobId: string, options?: {
146
+ include_segments?: boolean;
147
+ include_paragraphs?: boolean;
148
+ include_words?: boolean;
149
+ }): Promise<TranscribeResponse>;
150
+ /**
151
+ * Poll a batch job.
152
+ *
153
+ * ```ts
154
+ * const status = await yt.getBatch("batch_abc123");
155
+ * ```
156
+ */
157
+ getBatch(batchId: string): Promise<BatchResponse>;
158
+ /**
159
+ * Poll a job until it completes or fails.
160
+ *
161
+ * ```ts
162
+ * const result = await yt.waitForJob("job_abc123", {
163
+ * interval: 5000,
164
+ * maxAttempts: 60,
165
+ * });
166
+ * ```
167
+ */
168
+ waitForJob(jobId: string, options?: {
169
+ interval?: number;
170
+ maxAttempts?: number;
171
+ }): Promise<TranscribeResponse>;
172
+ /**
173
+ * Poll a batch until it completes.
174
+ */
175
+ waitForBatch(batchId: string, options?: {
176
+ interval?: number;
177
+ maxAttempts?: number;
178
+ }): Promise<BatchResponse>;
179
+ private post;
180
+ private get;
181
+ private request;
182
+ private createError;
183
+ }
184
+
185
+ declare class YouTubeTranscriptError extends Error {
186
+ readonly status: number;
187
+ readonly errorCode: string;
188
+ readonly details?: Record<string, unknown>;
189
+ constructor(status: number, body: ErrorResponse);
190
+ }
191
+ declare class InvalidRequestError extends YouTubeTranscriptError {
192
+ constructor(body: ErrorResponse);
193
+ }
194
+ declare class AuthenticationError extends YouTubeTranscriptError {
195
+ constructor(body: ErrorResponse);
196
+ }
197
+ declare class InsufficientCreditsError extends YouTubeTranscriptError {
198
+ constructor(body: ErrorResponse);
199
+ }
200
+ declare class NoCaptionsError extends YouTubeTranscriptError {
201
+ constructor(body: ErrorResponse);
202
+ }
203
+ declare class RateLimitError extends YouTubeTranscriptError {
204
+ readonly retryAfter?: number;
205
+ constructor(body: ErrorResponse, retryAfter?: number);
206
+ }
207
+
208
+ export { AuthenticationError, type BatchRequest, type BatchResponse, type BatchStatus, type ErrorResponse, type FormatOptions, InsufficientCreditsError, InvalidRequestError, NoCaptionsError, RateLimitError, type Source, type TranscribeRequest, type TranscribeResponse, type TranscribeStatus, type TranscriptPayload, YouTubeTranscript, YouTubeTranscriptError, type YouTubeTranscriptOptions };
@@ -0,0 +1,208 @@
1
+ /** Control which extra fields are included in the response. */
2
+ interface FormatOptions {
3
+ /** Include timestamped segments */
4
+ timestamp?: boolean;
5
+ /** Include paragraph groupings */
6
+ paragraphs?: boolean;
7
+ /** Include word-level timestamps */
8
+ words?: boolean;
9
+ }
10
+ /** Caption source preference. */
11
+ type Source = "auto" | "manual" | "asr";
12
+ interface TranscribeRequest {
13
+ /** YouTube URL or 11-character video ID */
14
+ video: string;
15
+ /** ISO 639-1 language code (e.g. "es", "fr"). Omit for best available. */
16
+ language?: string;
17
+ /** Caption source: "auto" (default), "manual", or "asr" */
18
+ source?: Source;
19
+ /** Explicitly confirm ASR usage when captions are unavailable */
20
+ allow_asr?: boolean;
21
+ /** Control extra output fields */
22
+ format?: FormatOptions;
23
+ /** URL to receive async results. Required for source="asr". */
24
+ webhook_url?: string;
25
+ }
26
+ interface BatchRequest {
27
+ /** Array of YouTube URLs or 11-character video IDs (max 100) */
28
+ video_ids: string[];
29
+ /** ISO 639-1 language code */
30
+ language?: string;
31
+ /** Caption source preference */
32
+ source?: Source;
33
+ /** Explicitly confirm ASR usage */
34
+ allow_asr?: boolean;
35
+ /** Control extra output fields */
36
+ format?: FormatOptions;
37
+ /** URL to receive async results */
38
+ webhook_url?: string;
39
+ }
40
+ interface TranscriptPayload {
41
+ /** Full transcript text */
42
+ text: string;
43
+ /** Detected / returned language */
44
+ language: string;
45
+ /** Source of the transcript: manual captions, auto, or asr */
46
+ source: string;
47
+ /** Timestamped segments (when format.timestamp = true) */
48
+ segments?: Record<string, unknown>[];
49
+ /** Paragraph groupings (when format.paragraphs = true) */
50
+ paragraphs?: Record<string, unknown>[];
51
+ /** Word-level timestamps (when format.words = true) */
52
+ words?: Record<string, unknown>[];
53
+ }
54
+ type TranscribeStatus = "completed" | "processing" | "failed" | "requires_asr_confirmation";
55
+ interface TranscribeResponse {
56
+ request_id: string;
57
+ status: TranscribeStatus;
58
+ data?: {
59
+ video_id: string;
60
+ transcript: TranscriptPayload;
61
+ video_title?: string | null;
62
+ };
63
+ error?: string;
64
+ job_id?: string | null;
65
+ estimated_credits?: number;
66
+ duration_minutes?: number | null;
67
+ suggestion?: string;
68
+ credits_used?: number;
69
+ }
70
+ type BatchStatus = "completed" | "partial" | "processing";
71
+ interface BatchResponse {
72
+ batch_id: string;
73
+ status: BatchStatus;
74
+ results: TranscribeResponse[];
75
+ summary?: {
76
+ total: number;
77
+ succeeded: number;
78
+ failed: number;
79
+ processing: number;
80
+ };
81
+ credits_used?: number;
82
+ }
83
+ interface ErrorResponse {
84
+ error: string;
85
+ message: string;
86
+ details?: Record<string, unknown>;
87
+ }
88
+ interface YouTubeTranscriptOptions {
89
+ /** Your API key from https://youtubetranscript.dev/dashboard */
90
+ apiKey: string;
91
+ /** Base URL override (default: https://youtubetranscript.dev/api/v2) */
92
+ baseUrl?: string;
93
+ /** Request timeout in ms (default: 30000) */
94
+ timeout?: number;
95
+ }
96
+
97
+ declare class YouTubeTranscript {
98
+ private readonly apiKey;
99
+ private readonly baseUrl;
100
+ private readonly timeout;
101
+ /**
102
+ * Create a new YouTubeTranscript client.
103
+ *
104
+ * ```ts
105
+ * const yt = new YouTubeTranscript({ apiKey: "your_api_key" });
106
+ * ```
107
+ *
108
+ * Get your API key at https://youtubetranscript.dev
109
+ */
110
+ constructor(options: YouTubeTranscriptOptions);
111
+ /**
112
+ * Transcribe a single YouTube video.
113
+ *
114
+ * ```ts
115
+ * const result = await yt.transcribe({ video: "dQw4w9WgXcQ" });
116
+ * console.log(result.data?.transcript.text);
117
+ * ```
118
+ */
119
+ transcribe(request: TranscribeRequest): Promise<TranscribeResponse>;
120
+ /**
121
+ * Shorthand: transcribe a video by URL or ID with minimal config.
122
+ *
123
+ * ```ts
124
+ * const result = await yt.getTranscript("dQw4w9WgXcQ");
125
+ * ```
126
+ */
127
+ getTranscript(video: string, language?: string): Promise<TranscribeResponse>;
128
+ /**
129
+ * Transcribe multiple videos in a single request (up to 100).
130
+ *
131
+ * ```ts
132
+ * const result = await yt.batch({
133
+ * video_ids: ["dQw4w9WgXcQ", "jNQXAC9IVRw"],
134
+ * });
135
+ * ```
136
+ */
137
+ batch(request: BatchRequest): Promise<BatchResponse>;
138
+ /**
139
+ * Poll an async transcription job (ASR).
140
+ *
141
+ * ```ts
142
+ * const status = await yt.getJob("job_abc123");
143
+ * ```
144
+ */
145
+ getJob(jobId: string, options?: {
146
+ include_segments?: boolean;
147
+ include_paragraphs?: boolean;
148
+ include_words?: boolean;
149
+ }): Promise<TranscribeResponse>;
150
+ /**
151
+ * Poll a batch job.
152
+ *
153
+ * ```ts
154
+ * const status = await yt.getBatch("batch_abc123");
155
+ * ```
156
+ */
157
+ getBatch(batchId: string): Promise<BatchResponse>;
158
+ /**
159
+ * Poll a job until it completes or fails.
160
+ *
161
+ * ```ts
162
+ * const result = await yt.waitForJob("job_abc123", {
163
+ * interval: 5000,
164
+ * maxAttempts: 60,
165
+ * });
166
+ * ```
167
+ */
168
+ waitForJob(jobId: string, options?: {
169
+ interval?: number;
170
+ maxAttempts?: number;
171
+ }): Promise<TranscribeResponse>;
172
+ /**
173
+ * Poll a batch until it completes.
174
+ */
175
+ waitForBatch(batchId: string, options?: {
176
+ interval?: number;
177
+ maxAttempts?: number;
178
+ }): Promise<BatchResponse>;
179
+ private post;
180
+ private get;
181
+ private request;
182
+ private createError;
183
+ }
184
+
185
+ declare class YouTubeTranscriptError extends Error {
186
+ readonly status: number;
187
+ readonly errorCode: string;
188
+ readonly details?: Record<string, unknown>;
189
+ constructor(status: number, body: ErrorResponse);
190
+ }
191
+ declare class InvalidRequestError extends YouTubeTranscriptError {
192
+ constructor(body: ErrorResponse);
193
+ }
194
+ declare class AuthenticationError extends YouTubeTranscriptError {
195
+ constructor(body: ErrorResponse);
196
+ }
197
+ declare class InsufficientCreditsError extends YouTubeTranscriptError {
198
+ constructor(body: ErrorResponse);
199
+ }
200
+ declare class NoCaptionsError extends YouTubeTranscriptError {
201
+ constructor(body: ErrorResponse);
202
+ }
203
+ declare class RateLimitError extends YouTubeTranscriptError {
204
+ readonly retryAfter?: number;
205
+ constructor(body: ErrorResponse, retryAfter?: number);
206
+ }
207
+
208
+ export { AuthenticationError, type BatchRequest, type BatchResponse, type BatchStatus, type ErrorResponse, type FormatOptions, InsufficientCreditsError, InvalidRequestError, NoCaptionsError, RateLimitError, type Source, type TranscribeRequest, type TranscribeResponse, type TranscribeStatus, type TranscriptPayload, YouTubeTranscript, YouTubeTranscriptError, type YouTubeTranscriptOptions };
package/dist/index.js ADDED
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AuthenticationError: () => AuthenticationError,
24
+ InsufficientCreditsError: () => InsufficientCreditsError,
25
+ InvalidRequestError: () => InvalidRequestError,
26
+ NoCaptionsError: () => NoCaptionsError,
27
+ RateLimitError: () => RateLimitError,
28
+ YouTubeTranscript: () => YouTubeTranscript,
29
+ YouTubeTranscriptError: () => YouTubeTranscriptError
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/errors.ts
34
+ var YouTubeTranscriptError = class extends Error {
35
+ constructor(status, body) {
36
+ super(body.message || body.error);
37
+ this.name = "YouTubeTranscriptError";
38
+ this.status = status;
39
+ this.errorCode = body.error;
40
+ this.details = body.details;
41
+ }
42
+ };
43
+ var InvalidRequestError = class extends YouTubeTranscriptError {
44
+ constructor(body) {
45
+ super(400, body);
46
+ this.name = "InvalidRequestError";
47
+ }
48
+ };
49
+ var AuthenticationError = class extends YouTubeTranscriptError {
50
+ constructor(body) {
51
+ super(401, body);
52
+ this.name = "AuthenticationError";
53
+ }
54
+ };
55
+ var InsufficientCreditsError = class extends YouTubeTranscriptError {
56
+ constructor(body) {
57
+ super(402, body);
58
+ this.name = "InsufficientCreditsError";
59
+ }
60
+ };
61
+ var NoCaptionsError = class extends YouTubeTranscriptError {
62
+ constructor(body) {
63
+ super(404, body);
64
+ this.name = "NoCaptionsError";
65
+ }
66
+ };
67
+ var RateLimitError = class extends YouTubeTranscriptError {
68
+ constructor(body, retryAfter) {
69
+ super(429, body);
70
+ this.name = "RateLimitError";
71
+ this.retryAfter = retryAfter;
72
+ }
73
+ };
74
+
75
+ // src/client.ts
76
+ var DEFAULT_BASE_URL = "https://youtubetranscript.dev/api/v2";
77
+ var DEFAULT_TIMEOUT = 3e4;
78
+ var YouTubeTranscript = class {
79
+ /**
80
+ * Create a new YouTubeTranscript client.
81
+ *
82
+ * ```ts
83
+ * const yt = new YouTubeTranscript({ apiKey: "your_api_key" });
84
+ * ```
85
+ *
86
+ * Get your API key at https://youtubetranscript.dev
87
+ */
88
+ constructor(options) {
89
+ if (!options.apiKey) {
90
+ throw new Error(
91
+ "API key is required. Get one at https://youtubetranscript.dev"
92
+ );
93
+ }
94
+ this.apiKey = options.apiKey;
95
+ this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
96
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
97
+ }
98
+ // ---------------------------------------------------
99
+ // Public methods
100
+ // ---------------------------------------------------
101
+ /**
102
+ * Transcribe a single YouTube video.
103
+ *
104
+ * ```ts
105
+ * const result = await yt.transcribe({ video: "dQw4w9WgXcQ" });
106
+ * console.log(result.data?.transcript.text);
107
+ * ```
108
+ */
109
+ async transcribe(request) {
110
+ return this.post("/transcribe", request);
111
+ }
112
+ /**
113
+ * Shorthand: transcribe a video by URL or ID with minimal config.
114
+ *
115
+ * ```ts
116
+ * const result = await yt.getTranscript("dQw4w9WgXcQ");
117
+ * ```
118
+ */
119
+ async getTranscript(video, language) {
120
+ return this.transcribe({ video, language });
121
+ }
122
+ /**
123
+ * Transcribe multiple videos in a single request (up to 100).
124
+ *
125
+ * ```ts
126
+ * const result = await yt.batch({
127
+ * video_ids: ["dQw4w9WgXcQ", "jNQXAC9IVRw"],
128
+ * });
129
+ * ```
130
+ */
131
+ async batch(request) {
132
+ return this.post("/batch", request);
133
+ }
134
+ /**
135
+ * Poll an async transcription job (ASR).
136
+ *
137
+ * ```ts
138
+ * const status = await yt.getJob("job_abc123");
139
+ * ```
140
+ */
141
+ async getJob(jobId, options) {
142
+ const params = new URLSearchParams();
143
+ if (options?.include_segments) params.set("include_segments", "true");
144
+ if (options?.include_paragraphs) params.set("include_paragraphs", "true");
145
+ if (options?.include_words) params.set("include_words", "true");
146
+ const query = params.toString();
147
+ const path = `/jobs/${jobId}${query ? `?${query}` : ""}`;
148
+ return this.get(path);
149
+ }
150
+ /**
151
+ * Poll a batch job.
152
+ *
153
+ * ```ts
154
+ * const status = await yt.getBatch("batch_abc123");
155
+ * ```
156
+ */
157
+ async getBatch(batchId) {
158
+ return this.get(`/batch/${batchId}`);
159
+ }
160
+ /**
161
+ * Poll a job until it completes or fails.
162
+ *
163
+ * ```ts
164
+ * const result = await yt.waitForJob("job_abc123", {
165
+ * interval: 5000,
166
+ * maxAttempts: 60,
167
+ * });
168
+ * ```
169
+ */
170
+ async waitForJob(jobId, options) {
171
+ const interval = options?.interval || 5e3;
172
+ const maxAttempts = options?.maxAttempts || 60;
173
+ for (let i = 0; i < maxAttempts; i++) {
174
+ const result = await this.getJob(jobId);
175
+ if (result.status === "completed" || result.status === "failed") {
176
+ return result;
177
+ }
178
+ await new Promise((resolve) => setTimeout(resolve, interval));
179
+ }
180
+ throw new Error(
181
+ `Job ${jobId} did not complete after ${maxAttempts} attempts`
182
+ );
183
+ }
184
+ /**
185
+ * Poll a batch until it completes.
186
+ */
187
+ async waitForBatch(batchId, options) {
188
+ const interval = options?.interval || 5e3;
189
+ const maxAttempts = options?.maxAttempts || 60;
190
+ for (let i = 0; i < maxAttempts; i++) {
191
+ const result = await this.getBatch(batchId);
192
+ if (result.status === "completed") {
193
+ return result;
194
+ }
195
+ await new Promise((resolve) => setTimeout(resolve, interval));
196
+ }
197
+ throw new Error(
198
+ `Batch ${batchId} did not complete after ${maxAttempts} attempts`
199
+ );
200
+ }
201
+ // ---------------------------------------------------
202
+ // HTTP helpers
203
+ // ---------------------------------------------------
204
+ async post(path, body) {
205
+ return this.request("POST", path, body);
206
+ }
207
+ async get(path) {
208
+ return this.request("GET", path);
209
+ }
210
+ async request(method, path, body) {
211
+ const url = `${this.baseUrl}${path}`;
212
+ const controller = new AbortController();
213
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
214
+ try {
215
+ const response = await fetch(url, {
216
+ method,
217
+ headers: {
218
+ Authorization: `Bearer ${this.apiKey}`,
219
+ "Content-Type": "application/json",
220
+ "User-Agent": "youtube-transcript-sdk/1.0.0"
221
+ },
222
+ body: body ? JSON.stringify(body) : void 0,
223
+ signal: controller.signal
224
+ });
225
+ const data = await response.json();
226
+ if (!response.ok) {
227
+ throw this.createError(response.status, data, response.headers);
228
+ }
229
+ return data;
230
+ } catch (error) {
231
+ if (error instanceof YouTubeTranscriptError) throw error;
232
+ if (error.name === "AbortError") {
233
+ throw new Error(`Request to ${path} timed out after ${this.timeout}ms`);
234
+ }
235
+ throw error;
236
+ } finally {
237
+ clearTimeout(timeoutId);
238
+ }
239
+ }
240
+ createError(status, body, headers) {
241
+ switch (status) {
242
+ case 400:
243
+ return new InvalidRequestError(body);
244
+ case 401:
245
+ return new AuthenticationError(body);
246
+ case 402:
247
+ return new InsufficientCreditsError(body);
248
+ case 404:
249
+ return new NoCaptionsError(body);
250
+ case 429: {
251
+ const retryAfter = headers.get("Retry-After");
252
+ return new RateLimitError(
253
+ body,
254
+ retryAfter ? parseInt(retryAfter, 10) : void 0
255
+ );
256
+ }
257
+ default:
258
+ return new YouTubeTranscriptError(status, body);
259
+ }
260
+ }
261
+ };
262
+ // Annotate the CommonJS export names for ESM import in node:
263
+ 0 && (module.exports = {
264
+ AuthenticationError,
265
+ InsufficientCreditsError,
266
+ InvalidRequestError,
267
+ NoCaptionsError,
268
+ RateLimitError,
269
+ YouTubeTranscript,
270
+ YouTubeTranscriptError
271
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,238 @@
1
+ // src/errors.ts
2
+ var YouTubeTranscriptError = class extends Error {
3
+ constructor(status, body) {
4
+ super(body.message || body.error);
5
+ this.name = "YouTubeTranscriptError";
6
+ this.status = status;
7
+ this.errorCode = body.error;
8
+ this.details = body.details;
9
+ }
10
+ };
11
+ var InvalidRequestError = class extends YouTubeTranscriptError {
12
+ constructor(body) {
13
+ super(400, body);
14
+ this.name = "InvalidRequestError";
15
+ }
16
+ };
17
+ var AuthenticationError = class extends YouTubeTranscriptError {
18
+ constructor(body) {
19
+ super(401, body);
20
+ this.name = "AuthenticationError";
21
+ }
22
+ };
23
+ var InsufficientCreditsError = class extends YouTubeTranscriptError {
24
+ constructor(body) {
25
+ super(402, body);
26
+ this.name = "InsufficientCreditsError";
27
+ }
28
+ };
29
+ var NoCaptionsError = class extends YouTubeTranscriptError {
30
+ constructor(body) {
31
+ super(404, body);
32
+ this.name = "NoCaptionsError";
33
+ }
34
+ };
35
+ var RateLimitError = class extends YouTubeTranscriptError {
36
+ constructor(body, retryAfter) {
37
+ super(429, body);
38
+ this.name = "RateLimitError";
39
+ this.retryAfter = retryAfter;
40
+ }
41
+ };
42
+
43
+ // src/client.ts
44
+ var DEFAULT_BASE_URL = "https://youtubetranscript.dev/api/v2";
45
+ var DEFAULT_TIMEOUT = 3e4;
46
+ var YouTubeTranscript = class {
47
+ /**
48
+ * Create a new YouTubeTranscript client.
49
+ *
50
+ * ```ts
51
+ * const yt = new YouTubeTranscript({ apiKey: "your_api_key" });
52
+ * ```
53
+ *
54
+ * Get your API key at https://youtubetranscript.dev
55
+ */
56
+ constructor(options) {
57
+ if (!options.apiKey) {
58
+ throw new Error(
59
+ "API key is required. Get one at https://youtubetranscript.dev"
60
+ );
61
+ }
62
+ this.apiKey = options.apiKey;
63
+ this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
64
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
65
+ }
66
+ // ---------------------------------------------------
67
+ // Public methods
68
+ // ---------------------------------------------------
69
+ /**
70
+ * Transcribe a single YouTube video.
71
+ *
72
+ * ```ts
73
+ * const result = await yt.transcribe({ video: "dQw4w9WgXcQ" });
74
+ * console.log(result.data?.transcript.text);
75
+ * ```
76
+ */
77
+ async transcribe(request) {
78
+ return this.post("/transcribe", request);
79
+ }
80
+ /**
81
+ * Shorthand: transcribe a video by URL or ID with minimal config.
82
+ *
83
+ * ```ts
84
+ * const result = await yt.getTranscript("dQw4w9WgXcQ");
85
+ * ```
86
+ */
87
+ async getTranscript(video, language) {
88
+ return this.transcribe({ video, language });
89
+ }
90
+ /**
91
+ * Transcribe multiple videos in a single request (up to 100).
92
+ *
93
+ * ```ts
94
+ * const result = await yt.batch({
95
+ * video_ids: ["dQw4w9WgXcQ", "jNQXAC9IVRw"],
96
+ * });
97
+ * ```
98
+ */
99
+ async batch(request) {
100
+ return this.post("/batch", request);
101
+ }
102
+ /**
103
+ * Poll an async transcription job (ASR).
104
+ *
105
+ * ```ts
106
+ * const status = await yt.getJob("job_abc123");
107
+ * ```
108
+ */
109
+ async getJob(jobId, options) {
110
+ const params = new URLSearchParams();
111
+ if (options?.include_segments) params.set("include_segments", "true");
112
+ if (options?.include_paragraphs) params.set("include_paragraphs", "true");
113
+ if (options?.include_words) params.set("include_words", "true");
114
+ const query = params.toString();
115
+ const path = `/jobs/${jobId}${query ? `?${query}` : ""}`;
116
+ return this.get(path);
117
+ }
118
+ /**
119
+ * Poll a batch job.
120
+ *
121
+ * ```ts
122
+ * const status = await yt.getBatch("batch_abc123");
123
+ * ```
124
+ */
125
+ async getBatch(batchId) {
126
+ return this.get(`/batch/${batchId}`);
127
+ }
128
+ /**
129
+ * Poll a job until it completes or fails.
130
+ *
131
+ * ```ts
132
+ * const result = await yt.waitForJob("job_abc123", {
133
+ * interval: 5000,
134
+ * maxAttempts: 60,
135
+ * });
136
+ * ```
137
+ */
138
+ async waitForJob(jobId, options) {
139
+ const interval = options?.interval || 5e3;
140
+ const maxAttempts = options?.maxAttempts || 60;
141
+ for (let i = 0; i < maxAttempts; i++) {
142
+ const result = await this.getJob(jobId);
143
+ if (result.status === "completed" || result.status === "failed") {
144
+ return result;
145
+ }
146
+ await new Promise((resolve) => setTimeout(resolve, interval));
147
+ }
148
+ throw new Error(
149
+ `Job ${jobId} did not complete after ${maxAttempts} attempts`
150
+ );
151
+ }
152
+ /**
153
+ * Poll a batch until it completes.
154
+ */
155
+ async waitForBatch(batchId, options) {
156
+ const interval = options?.interval || 5e3;
157
+ const maxAttempts = options?.maxAttempts || 60;
158
+ for (let i = 0; i < maxAttempts; i++) {
159
+ const result = await this.getBatch(batchId);
160
+ if (result.status === "completed") {
161
+ return result;
162
+ }
163
+ await new Promise((resolve) => setTimeout(resolve, interval));
164
+ }
165
+ throw new Error(
166
+ `Batch ${batchId} did not complete after ${maxAttempts} attempts`
167
+ );
168
+ }
169
+ // ---------------------------------------------------
170
+ // HTTP helpers
171
+ // ---------------------------------------------------
172
+ async post(path, body) {
173
+ return this.request("POST", path, body);
174
+ }
175
+ async get(path) {
176
+ return this.request("GET", path);
177
+ }
178
+ async request(method, path, body) {
179
+ const url = `${this.baseUrl}${path}`;
180
+ const controller = new AbortController();
181
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
182
+ try {
183
+ const response = await fetch(url, {
184
+ method,
185
+ headers: {
186
+ Authorization: `Bearer ${this.apiKey}`,
187
+ "Content-Type": "application/json",
188
+ "User-Agent": "youtube-transcript-sdk/1.0.0"
189
+ },
190
+ body: body ? JSON.stringify(body) : void 0,
191
+ signal: controller.signal
192
+ });
193
+ const data = await response.json();
194
+ if (!response.ok) {
195
+ throw this.createError(response.status, data, response.headers);
196
+ }
197
+ return data;
198
+ } catch (error) {
199
+ if (error instanceof YouTubeTranscriptError) throw error;
200
+ if (error.name === "AbortError") {
201
+ throw new Error(`Request to ${path} timed out after ${this.timeout}ms`);
202
+ }
203
+ throw error;
204
+ } finally {
205
+ clearTimeout(timeoutId);
206
+ }
207
+ }
208
+ createError(status, body, headers) {
209
+ switch (status) {
210
+ case 400:
211
+ return new InvalidRequestError(body);
212
+ case 401:
213
+ return new AuthenticationError(body);
214
+ case 402:
215
+ return new InsufficientCreditsError(body);
216
+ case 404:
217
+ return new NoCaptionsError(body);
218
+ case 429: {
219
+ const retryAfter = headers.get("Retry-After");
220
+ return new RateLimitError(
221
+ body,
222
+ retryAfter ? parseInt(retryAfter, 10) : void 0
223
+ );
224
+ }
225
+ default:
226
+ return new YouTubeTranscriptError(status, body);
227
+ }
228
+ }
229
+ };
230
+ export {
231
+ AuthenticationError,
232
+ InsufficientCreditsError,
233
+ InvalidRequestError,
234
+ NoCaptionsError,
235
+ RateLimitError,
236
+ YouTubeTranscript,
237
+ YouTubeTranscriptError
238
+ };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "youtube-audio-transcript-api",
3
+ "version": "1.0.0",
4
+ "description": "Official Node.js/TypeScript SDK for the YouTubeTranscript.dev API — extract, transcribe, and translate YouTube videos at scale.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
22
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "lint": "tsc --noEmit",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "youtube",
30
+ "transcript",
31
+ "youtube-transcript",
32
+ "youtube-api",
33
+ "subtitles",
34
+ "captions",
35
+ "speech-to-text",
36
+ "asr",
37
+ "transcription",
38
+ "video-to-text",
39
+ "youtube-transcript-api",
40
+ "ai",
41
+ "batch",
42
+ "webhook"
43
+ ],
44
+ "author": "YouTubeTranscript.dev",
45
+ "license": "MIT",
46
+ "homepage": "https://youtubetranscript.dev",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/youtubetranscript/youtube-transcript-api"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/youtubetranscript/youtube-transcript-api/issues"
53
+ },
54
+ "engines": {
55
+ "node": ">=18.0.0"
56
+ },
57
+ "devDependencies": {
58
+ "tsup": "^8.0.0",
59
+ "typescript": "^5.4.0",
60
+ "vitest": "^2.0.0"
61
+ }
62
+ }