@royalti/syynk 0.1.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/dist/index.js ADDED
@@ -0,0 +1,587 @@
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
+ AuthorizationError: () => AuthorizationError,
25
+ NetworkError: () => NetworkError,
26
+ NotFoundError: () => NotFoundError,
27
+ RateLimitError: () => RateLimitError,
28
+ ServerError: () => ServerError,
29
+ SyynkClient: () => SyynkClient,
30
+ SyynkError: () => SyynkError,
31
+ TimeoutError: () => TimeoutError,
32
+ ValidationError: () => ValidationError,
33
+ isRateLimitError: () => isRateLimitError,
34
+ isRetryableError: () => isRetryableError,
35
+ isSyynkError: () => isSyynkError
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/errors.ts
40
+ var SyynkError = class extends Error {
41
+ /** Error code for programmatic handling */
42
+ code;
43
+ /** HTTP status code */
44
+ status;
45
+ /** Additional error details from the API */
46
+ details;
47
+ constructor(message, code, status, details) {
48
+ super(message);
49
+ this.name = "SyynkError";
50
+ this.code = code;
51
+ this.status = status;
52
+ this.details = details;
53
+ if (Error.captureStackTrace) {
54
+ Error.captureStackTrace(this, this.constructor);
55
+ }
56
+ }
57
+ /**
58
+ * Convert error to a plain object for serialization
59
+ */
60
+ toJSON() {
61
+ return {
62
+ name: this.name,
63
+ message: this.message,
64
+ code: this.code,
65
+ status: this.status,
66
+ details: this.details
67
+ };
68
+ }
69
+ };
70
+ var AuthenticationError = class extends SyynkError {
71
+ constructor(message = "Invalid or missing API key", details) {
72
+ super(message, "AUTHENTICATION_ERROR", 401, details);
73
+ this.name = "AuthenticationError";
74
+ }
75
+ };
76
+ var AuthorizationError = class extends SyynkError {
77
+ constructor(message = "Insufficient permissions for this operation", details) {
78
+ super(message, "AUTHORIZATION_ERROR", 403, details);
79
+ this.name = "AuthorizationError";
80
+ }
81
+ };
82
+ var RateLimitError = class extends SyynkError {
83
+ /** Unix timestamp when the rate limit resets */
84
+ resetAt;
85
+ /** Seconds until the rate limit resets */
86
+ retryAfter;
87
+ constructor(message = "Rate limit exceeded", resetAt, details) {
88
+ super(message, "RATE_LIMIT_ERROR", 429, details);
89
+ this.name = "RateLimitError";
90
+ this.resetAt = resetAt;
91
+ this.retryAfter = Math.max(0, Math.ceil(resetAt - Date.now() / 1e3));
92
+ }
93
+ /**
94
+ * Get the Date object for when the rate limit resets
95
+ */
96
+ getResetDate() {
97
+ return new Date(this.resetAt * 1e3);
98
+ }
99
+ toJSON() {
100
+ return {
101
+ ...super.toJSON(),
102
+ resetAt: this.resetAt,
103
+ retryAfter: this.retryAfter
104
+ };
105
+ }
106
+ };
107
+ var ValidationError = class extends SyynkError {
108
+ /** Field-level validation errors */
109
+ fieldErrors;
110
+ constructor(message = "Invalid request data", details, fieldErrors) {
111
+ super(message, "VALIDATION_ERROR", 400, details);
112
+ this.name = "ValidationError";
113
+ this.fieldErrors = fieldErrors;
114
+ }
115
+ toJSON() {
116
+ return {
117
+ ...super.toJSON(),
118
+ fieldErrors: this.fieldErrors
119
+ };
120
+ }
121
+ };
122
+ var NotFoundError = class extends SyynkError {
123
+ /** Type of resource that wasn't found */
124
+ resourceType;
125
+ /** ID of the resource that wasn't found */
126
+ resourceId;
127
+ constructor(message = "Resource not found", resourceType, resourceId, details) {
128
+ super(message, "NOT_FOUND_ERROR", 404, details);
129
+ this.name = "NotFoundError";
130
+ this.resourceType = resourceType;
131
+ this.resourceId = resourceId;
132
+ }
133
+ toJSON() {
134
+ return {
135
+ ...super.toJSON(),
136
+ resourceType: this.resourceType,
137
+ resourceId: this.resourceId
138
+ };
139
+ }
140
+ };
141
+ var ServerError = class extends SyynkError {
142
+ /** Request ID for debugging */
143
+ requestId;
144
+ constructor(message = "Internal server error", status = 500, requestId, details) {
145
+ super(message, "SERVER_ERROR", status, details);
146
+ this.name = "ServerError";
147
+ this.requestId = requestId;
148
+ }
149
+ toJSON() {
150
+ return {
151
+ ...super.toJSON(),
152
+ requestId: this.requestId
153
+ };
154
+ }
155
+ };
156
+ var NetworkError = class extends SyynkError {
157
+ /** The original error that caused this network error */
158
+ cause;
159
+ constructor(message = "Network request failed", cause) {
160
+ super(message, "NETWORK_ERROR", 0);
161
+ this.name = "NetworkError";
162
+ this.cause = cause;
163
+ }
164
+ toJSON() {
165
+ return {
166
+ ...super.toJSON(),
167
+ cause: this.cause?.message
168
+ };
169
+ }
170
+ };
171
+ var TimeoutError = class extends SyynkError {
172
+ /** The timeout duration in milliseconds */
173
+ timeoutMs;
174
+ constructor(message = "Request timed out", timeoutMs) {
175
+ super(message, "TIMEOUT_ERROR", 0);
176
+ this.name = "TimeoutError";
177
+ this.timeoutMs = timeoutMs;
178
+ }
179
+ toJSON() {
180
+ return {
181
+ ...super.toJSON(),
182
+ timeoutMs: this.timeoutMs
183
+ };
184
+ }
185
+ };
186
+ function isSyynkError(error) {
187
+ return error instanceof SyynkError;
188
+ }
189
+ function isRateLimitError(error) {
190
+ return error instanceof RateLimitError;
191
+ }
192
+ function isRetryableError(error) {
193
+ if (error instanceof RateLimitError) return true;
194
+ if (error instanceof NetworkError) return true;
195
+ if (error instanceof TimeoutError) return true;
196
+ if (error instanceof ServerError && error.status >= 500) return true;
197
+ return false;
198
+ }
199
+
200
+ // src/client.ts
201
+ var DEFAULT_BASE_URL = "https://syynk.to";
202
+ var DEFAULT_TIMEOUT = 3e4;
203
+ var DEFAULT_MAX_RETRIES = 3;
204
+ var INITIAL_RETRY_DELAY = 1e3;
205
+ var SyynkClient = class {
206
+ apiKey;
207
+ baseUrl;
208
+ timeout;
209
+ maxRetries;
210
+ /** Last rate limit info received from the API */
211
+ lastRateLimitInfo = null;
212
+ /**
213
+ * Create a new Syynk API client
214
+ *
215
+ * @param options - Client configuration options
216
+ * @throws {Error} If apiKey is not provided
217
+ */
218
+ constructor(options) {
219
+ if (!options.apiKey) {
220
+ throw new Error("API key is required");
221
+ }
222
+ this.apiKey = options.apiKey;
223
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
224
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
225
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
226
+ }
227
+ /**
228
+ * Get the current rate limit information
229
+ *
230
+ * @returns The last rate limit info received, or null if no requests have been made
231
+ */
232
+ getRateLimitInfo() {
233
+ return this.lastRateLimitInfo;
234
+ }
235
+ /**
236
+ * Transcribe audio from a URL
237
+ *
238
+ * @param options - Transcription options
239
+ * @returns Transcription result with segments and words
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * const result = await client.transcribe({
244
+ * audioUrl: 'https://example.com/song.mp3',
245
+ * language: 'en',
246
+ * });
247
+ * console.log(`Transcribed ${result.segments.length} segments`);
248
+ * ```
249
+ */
250
+ async transcribe(options) {
251
+ return this.request("/api/v1/transcribe", {
252
+ method: "POST",
253
+ body: JSON.stringify({
254
+ audioUrl: options.audioUrl,
255
+ language: options.language,
256
+ projectName: options.projectName
257
+ })
258
+ });
259
+ }
260
+ /**
261
+ * Transcribe audio from a file upload
262
+ *
263
+ * Works in both Node.js and browser environments.
264
+ *
265
+ * @param file - Audio file as Blob/File (browser) or ArrayBuffer/Uint8Array (Node.js Buffer works as it extends Uint8Array)
266
+ * @param options - Optional transcription options
267
+ * @returns Transcription result with segments and words
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * // Browser
272
+ * const file = document.querySelector('input[type="file"]').files[0];
273
+ * const result = await client.transcribeFile(file);
274
+ *
275
+ * // Node.js
276
+ * const buffer = fs.readFileSync('song.mp3');
277
+ * const result = await client.transcribeFile(buffer, { filename: 'song.mp3' });
278
+ * ```
279
+ */
280
+ async transcribeFile(file, options) {
281
+ const formData = new FormData();
282
+ const filename = options?.filename ?? (file instanceof Blob && "name" in file ? file.name : null) ?? "audio";
283
+ if (file instanceof Blob) {
284
+ formData.append("file", file, filename);
285
+ } else {
286
+ const blob = new Blob([file]);
287
+ formData.append("file", blob, filename);
288
+ }
289
+ if (options?.language) {
290
+ formData.append("language", options.language);
291
+ }
292
+ if (options?.projectName) {
293
+ formData.append("projectName", options.projectName);
294
+ }
295
+ return this.request("/api/v1/transcribe/upload", {
296
+ method: "POST",
297
+ body: formData
298
+ // Don't set Content-Type header - let the browser set it with boundary
299
+ });
300
+ }
301
+ /**
302
+ * Export segments to a specific format
303
+ *
304
+ * @param options - Export options including format and segments
305
+ * @returns Export result with content and metadata
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * const result = await client.export({
310
+ * format: 'lrc',
311
+ * segments: transcription.segments,
312
+ * projectName: 'My Song',
313
+ * });
314
+ *
315
+ * // Save the file
316
+ * fs.writeFileSync(`output.${result.extension}`, result.content);
317
+ * ```
318
+ */
319
+ async export(options) {
320
+ return this.request("/api/v1/export", {
321
+ method: "POST",
322
+ body: JSON.stringify({
323
+ format: options.format,
324
+ segments: options.segments,
325
+ words: options.words,
326
+ projectName: options.projectName
327
+ })
328
+ });
329
+ }
330
+ /**
331
+ * Get information about all available export formats
332
+ *
333
+ * @returns Array of format information objects
334
+ *
335
+ * @example
336
+ * ```typescript
337
+ * const formats = await client.getFormats();
338
+ * const wordTimingFormats = formats.filter(f => f.supportsWordTiming);
339
+ * ```
340
+ */
341
+ async getFormats() {
342
+ return this.request("/api/v1/formats", {
343
+ method: "GET"
344
+ });
345
+ }
346
+ /**
347
+ * List projects with optional filtering and pagination
348
+ *
349
+ * @param options - Listing options
350
+ * @returns Paginated list of projects
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * const result = await client.listProjects({
355
+ * status: 'ready',
356
+ * limit: 10,
357
+ * });
358
+ * console.log(`Found ${result.total} projects`);
359
+ * ```
360
+ */
361
+ async listProjects(options) {
362
+ const params = new URLSearchParams();
363
+ if (options?.limit !== void 0) {
364
+ params.set("limit", String(options.limit));
365
+ }
366
+ if (options?.offset !== void 0) {
367
+ params.set("offset", String(options.offset));
368
+ }
369
+ if (options?.status) {
370
+ params.set("status", options.status);
371
+ }
372
+ if (options?.type) {
373
+ params.set("type", options.type);
374
+ }
375
+ const queryString = params.toString();
376
+ const path = queryString ? `/api/v1/projects?${queryString}` : "/api/v1/projects";
377
+ return this.request(path, {
378
+ method: "GET"
379
+ });
380
+ }
381
+ /**
382
+ * Get a single project by ID
383
+ *
384
+ * @param id - Project ID
385
+ * @param options - Options for including segments/words
386
+ * @returns Project with segments and words
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * const project = await client.getProject('project-id', {
391
+ * includeWords: true,
392
+ * });
393
+ * console.log(`Project has ${project.segments.length} segments`);
394
+ * ```
395
+ */
396
+ async getProject(id, options) {
397
+ const params = new URLSearchParams();
398
+ if (options?.includeWords !== void 0) {
399
+ params.set("includeWords", String(options.includeWords));
400
+ }
401
+ if (options?.includeSegments !== void 0) {
402
+ params.set("includeSegments", String(options.includeSegments));
403
+ }
404
+ const queryString = params.toString();
405
+ const path = queryString ? `/api/v1/projects/${id}?${queryString}` : `/api/v1/projects/${id}`;
406
+ return this.request(path, {
407
+ method: "GET"
408
+ });
409
+ }
410
+ /**
411
+ * Delete a project by ID
412
+ *
413
+ * @param id - Project ID
414
+ *
415
+ * @example
416
+ * ```typescript
417
+ * await client.deleteProject('project-id');
418
+ * ```
419
+ */
420
+ async deleteProject(id) {
421
+ await this.request(`/api/v1/projects/${id}`, {
422
+ method: "DELETE"
423
+ });
424
+ }
425
+ /**
426
+ * Make an authenticated API request with automatic retry
427
+ *
428
+ * @param path - API path (starting with /)
429
+ * @param options - Fetch request options
430
+ * @returns Parsed response data
431
+ */
432
+ async request(path, options, retryCount = 0) {
433
+ const url = `${this.baseUrl}${path}`;
434
+ const headers = {
435
+ Authorization: `Bearer ${this.apiKey}`,
436
+ Accept: "application/json"
437
+ };
438
+ if (!(options.body instanceof FormData)) {
439
+ headers["Content-Type"] = "application/json";
440
+ }
441
+ const requestHeaders = {
442
+ ...headers,
443
+ ...options.headers
444
+ };
445
+ const controller = new AbortController();
446
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
447
+ try {
448
+ const response = await fetch(url, {
449
+ ...options,
450
+ headers: requestHeaders,
451
+ signal: controller.signal
452
+ });
453
+ clearTimeout(timeoutId);
454
+ this.parseRateLimitHeaders(response.headers);
455
+ if (response.ok) {
456
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
457
+ return void 0;
458
+ }
459
+ const json = await response.json();
460
+ if (json && typeof json === "object" && "data" in json) {
461
+ return json.data;
462
+ }
463
+ return json;
464
+ }
465
+ const error = await this.handleErrorResponse(response);
466
+ if (isRetryableError(error) && retryCount < this.maxRetries) {
467
+ const delay = this.calculateRetryDelay(error, retryCount);
468
+ await this.sleep(delay);
469
+ return this.request(path, options, retryCount + 1);
470
+ }
471
+ throw error;
472
+ } catch (err) {
473
+ clearTimeout(timeoutId);
474
+ if (err instanceof Error && err.name === "AbortError") {
475
+ const timeoutError = new TimeoutError(
476
+ `Request timed out after ${this.timeout}ms`,
477
+ this.timeout
478
+ );
479
+ if (retryCount < this.maxRetries) {
480
+ const delay = this.calculateRetryDelay(timeoutError, retryCount);
481
+ await this.sleep(delay);
482
+ return this.request(path, options, retryCount + 1);
483
+ }
484
+ throw timeoutError;
485
+ }
486
+ if (err instanceof TypeError && err.message.includes("fetch")) {
487
+ const networkError = new NetworkError("Network request failed", err);
488
+ if (retryCount < this.maxRetries) {
489
+ const delay = this.calculateRetryDelay(networkError, retryCount);
490
+ await this.sleep(delay);
491
+ return this.request(path, options, retryCount + 1);
492
+ }
493
+ throw networkError;
494
+ }
495
+ if (err instanceof SyynkError) {
496
+ throw err;
497
+ }
498
+ throw new NetworkError(
499
+ err instanceof Error ? err.message : "Unknown error occurred",
500
+ err instanceof Error ? err : void 0
501
+ );
502
+ }
503
+ }
504
+ /**
505
+ * Parse rate limit headers from response
506
+ */
507
+ parseRateLimitHeaders(headers) {
508
+ const limit = headers.get("X-RateLimit-Limit");
509
+ const remaining = headers.get("X-RateLimit-Remaining");
510
+ const reset = headers.get("X-RateLimit-Reset");
511
+ if (limit && remaining && reset) {
512
+ this.lastRateLimitInfo = {
513
+ limit: parseInt(limit, 10),
514
+ remaining: parseInt(remaining, 10),
515
+ reset: parseInt(reset, 10)
516
+ };
517
+ }
518
+ }
519
+ /**
520
+ * Handle error responses and convert to typed errors
521
+ */
522
+ async handleErrorResponse(response) {
523
+ let errorData = null;
524
+ try {
525
+ errorData = await response.json();
526
+ } catch {
527
+ }
528
+ const message = errorData?.error?.message ?? response.statusText;
529
+ const code = errorData?.error?.code ?? "UNKNOWN_ERROR";
530
+ const details = errorData?.error?.details;
531
+ switch (response.status) {
532
+ case 400:
533
+ return new ValidationError(message, details);
534
+ case 401:
535
+ return new AuthenticationError(message, details);
536
+ case 403:
537
+ return new AuthorizationError(message, details);
538
+ case 404:
539
+ return new NotFoundError(message, void 0, void 0, details);
540
+ case 429: {
541
+ const resetHeader = response.headers.get("X-RateLimit-Reset");
542
+ const resetAt = resetHeader ? parseInt(resetHeader, 10) : Math.floor(Date.now() / 1e3) + 60;
543
+ return new RateLimitError(message, resetAt, details);
544
+ }
545
+ default:
546
+ if (response.status >= 500) {
547
+ const requestId = response.headers.get("X-Request-Id") ?? void 0;
548
+ return new ServerError(message, response.status, requestId, details);
549
+ }
550
+ return new SyynkError(message, code, response.status, details);
551
+ }
552
+ }
553
+ /**
554
+ * Calculate retry delay with exponential backoff
555
+ */
556
+ calculateRetryDelay(error, retryCount) {
557
+ if (error instanceof RateLimitError && error.retryAfter > 0) {
558
+ const jitter2 = Math.random() * 1e3;
559
+ return error.retryAfter * 1e3 + jitter2;
560
+ }
561
+ const baseDelay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount);
562
+ const jitter = Math.random() * baseDelay * 0.25;
563
+ return Math.min(baseDelay + jitter, 3e4);
564
+ }
565
+ /**
566
+ * Sleep for the specified duration
567
+ */
568
+ sleep(ms) {
569
+ return new Promise((resolve) => setTimeout(resolve, ms));
570
+ }
571
+ };
572
+ // Annotate the CommonJS export names for ESM import in node:
573
+ 0 && (module.exports = {
574
+ AuthenticationError,
575
+ AuthorizationError,
576
+ NetworkError,
577
+ NotFoundError,
578
+ RateLimitError,
579
+ ServerError,
580
+ SyynkClient,
581
+ SyynkError,
582
+ TimeoutError,
583
+ ValidationError,
584
+ isRateLimitError,
585
+ isRetryableError,
586
+ isSyynkError
587
+ });