@luanpoppe/ai 1.1.5 → 1.1.6

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.
Files changed (151) hide show
  1. package/.env.example +13 -1
  2. package/README.md +795 -0
  3. package/dist/@types/embedding-model-names.d.ts +11 -0
  4. package/dist/@types/embedding-model-names.d.ts.map +1 -0
  5. package/dist/@types/embedding-model-names.js +3 -0
  6. package/dist/@types/embedding-model-names.js.map +1 -0
  7. package/dist/@types/model-names.d.ts +5 -1
  8. package/dist/@types/model-names.d.ts.map +1 -1
  9. package/dist/@types/openrouter-provider.d.ts +25 -0
  10. package/dist/@types/openrouter-provider.d.ts.map +1 -0
  11. package/dist/@types/openrouter-provider.js +3 -0
  12. package/dist/@types/openrouter-provider.js.map +1 -0
  13. package/dist/ai.d.ts +7 -0
  14. package/dist/ai.d.ts.map +1 -1
  15. package/dist/ai.js +56 -5
  16. package/dist/ai.js.map +1 -1
  17. package/dist/audio/gemini-transcription.d.ts +4 -0
  18. package/dist/audio/gemini-transcription.d.ts.map +1 -0
  19. package/dist/audio/gemini-transcription.js +37 -0
  20. package/dist/audio/gemini-transcription.js.map +1 -0
  21. package/dist/audio/index.d.ts +33 -0
  22. package/dist/audio/index.d.ts.map +1 -0
  23. package/dist/audio/index.js +43 -0
  24. package/dist/audio/index.js.map +1 -0
  25. package/dist/audio/openrouter-client.d.ts +2 -0
  26. package/dist/audio/openrouter-client.d.ts.map +1 -0
  27. package/dist/audio/openrouter-client.js +7 -0
  28. package/dist/audio/openrouter-client.js.map +1 -0
  29. package/dist/audio/speech-openai.d.ts +4 -0
  30. package/dist/audio/speech-openai.d.ts.map +1 -0
  31. package/dist/audio/speech-openai.js +65 -0
  32. package/dist/audio/speech-openai.js.map +1 -0
  33. package/dist/audio/speech-openrouter.d.ts +4 -0
  34. package/dist/audio/speech-openrouter.d.ts.map +1 -0
  35. package/dist/audio/speech-openrouter.js +89 -0
  36. package/dist/audio/speech-openrouter.js.map +1 -0
  37. package/dist/audio/transcription-openai.d.ts +11 -0
  38. package/dist/audio/transcription-openai.d.ts.map +1 -0
  39. package/dist/audio/transcription-openai.js +204 -0
  40. package/dist/audio/transcription-openai.js.map +1 -0
  41. package/dist/audio/transcription-openrouter.d.ts +4 -0
  42. package/dist/audio/transcription-openrouter.d.ts.map +1 -0
  43. package/dist/audio/transcription-openrouter.js +44 -0
  44. package/dist/audio/transcription-openrouter.js.map +1 -0
  45. package/dist/audio/types.d.ts +92 -0
  46. package/dist/audio/types.d.ts.map +1 -0
  47. package/dist/audio/types.js +6 -0
  48. package/dist/audio/types.js.map +1 -0
  49. package/dist/audio/utils.d.ts +5 -0
  50. package/dist/audio/utils.d.ts.map +1 -0
  51. package/dist/audio/utils.js +25 -0
  52. package/dist/audio/utils.js.map +1 -0
  53. package/dist/embeddings/embed-gemini.d.ts +3 -0
  54. package/dist/embeddings/embed-gemini.d.ts.map +1 -0
  55. package/dist/embeddings/embed-gemini.js +38 -0
  56. package/dist/embeddings/embed-gemini.js.map +1 -0
  57. package/dist/embeddings/embed-openai.d.ts +3 -0
  58. package/dist/embeddings/embed-openai.d.ts.map +1 -0
  59. package/dist/embeddings/embed-openai.js +44 -0
  60. package/dist/embeddings/embed-openai.js.map +1 -0
  61. package/dist/embeddings/embed-openrouter.d.ts +3 -0
  62. package/dist/embeddings/embed-openrouter.d.ts.map +1 -0
  63. package/dist/embeddings/embed-openrouter.js +47 -0
  64. package/dist/embeddings/embed-openrouter.js.map +1 -0
  65. package/dist/embeddings/index.d.ts +19 -0
  66. package/dist/embeddings/index.d.ts.map +1 -0
  67. package/dist/embeddings/index.js +36 -0
  68. package/dist/embeddings/index.js.map +1 -0
  69. package/dist/embeddings/route.d.ts +12 -0
  70. package/dist/embeddings/route.d.ts.map +1 -0
  71. package/dist/embeddings/route.js +42 -0
  72. package/dist/embeddings/route.js.map +1 -0
  73. package/dist/embeddings/types.d.ts +37 -0
  74. package/dist/embeddings/types.d.ts.map +1 -0
  75. package/dist/embeddings/types.js +6 -0
  76. package/dist/embeddings/types.js.map +1 -0
  77. package/dist/embeddings/utils.d.ts +6 -0
  78. package/dist/embeddings/utils.d.ts.map +1 -0
  79. package/dist/embeddings/utils.js +25 -0
  80. package/dist/embeddings/utils.js.map +1 -0
  81. package/dist/index.d.ts +11 -2
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +16 -3
  84. package/dist/index.js.map +1 -1
  85. package/dist/langchain/audio-transcription.d.ts +8 -13
  86. package/dist/langchain/audio-transcription.d.ts.map +1 -1
  87. package/dist/langchain/audio-transcription.js +8 -92
  88. package/dist/langchain/audio-transcription.js.map +1 -1
  89. package/dist/langchain/messages.d.ts +6 -1
  90. package/dist/langchain/messages.d.ts.map +1 -1
  91. package/dist/langchain/messages.js +35 -7
  92. package/dist/langchain/messages.js.map +1 -1
  93. package/dist/langchain/models.d.ts +38 -0
  94. package/dist/langchain/models.d.ts.map +1 -1
  95. package/dist/langchain/models.js +63 -4
  96. package/dist/langchain/models.js.map +1 -1
  97. package/dist/langchain/structured-output.d.ts +9 -0
  98. package/dist/langchain/structured-output.d.ts.map +1 -0
  99. package/dist/langchain/structured-output.js +70 -0
  100. package/dist/langchain/structured-output.js.map +1 -0
  101. package/dist/langchain/tools.d.ts +1 -1
  102. package/dist/utils/openrouter-client.d.ts +4 -0
  103. package/dist/utils/openrouter-client.d.ts.map +1 -0
  104. package/dist/utils/openrouter-client.js +16 -0
  105. package/dist/utils/openrouter-client.js.map +1 -0
  106. package/docs/LANGCHAIN_WHISPER_WINDOWS_BUG.md +2 -0
  107. package/package.json +4 -4
  108. package/src/@types/embedding-model-names.ts +36 -0
  109. package/src/@types/model-names.ts +12 -1
  110. package/src/@types/openrouter-provider.ts +27 -0
  111. package/src/ai.ts +89 -10
  112. package/src/audio/gemini-transcription.ts +47 -0
  113. package/src/audio/index.ts +101 -0
  114. package/src/audio/openrouter-client.ts +4 -0
  115. package/src/audio/speech-openai.ts +79 -0
  116. package/src/audio/speech-openrouter.ts +121 -0
  117. package/src/audio/transcription-openai.ts +259 -0
  118. package/src/audio/transcription-openrouter.ts +59 -0
  119. package/src/audio/types.ts +127 -0
  120. package/src/audio/utils.ts +20 -0
  121. package/src/embeddings/embed-gemini.ts +54 -0
  122. package/src/embeddings/embed-openai.ts +53 -0
  123. package/src/embeddings/embed-openrouter.ts +64 -0
  124. package/src/embeddings/index.ts +83 -0
  125. package/src/embeddings/route.ts +62 -0
  126. package/src/embeddings/types.ts +53 -0
  127. package/src/embeddings/utils.ts +21 -0
  128. package/src/index.ts +51 -5
  129. package/src/langchain/audio-transcription.ts +19 -90
  130. package/src/langchain/messages.ts +84 -12
  131. package/src/langchain/models.ts +130 -3
  132. package/src/langchain/structured-output.ts +77 -0
  133. package/src/utils/openrouter-client.ts +10 -0
  134. package/tests/e2e/README.md +53 -0
  135. package/tests/e2e/ai-audio-multiprovider.test.ts +118 -0
  136. package/tests/e2e/ai-deepseek-structured-output.test.ts +133 -0
  137. package/tests/e2e/ai-embeddings.test.ts +87 -0
  138. package/tests/e2e/ai-local-models.test.ts +65 -0
  139. package/tests/unit/audio/index.test.ts +17 -0
  140. package/tests/unit/audio/speech-openai.test.ts +74 -0
  141. package/tests/unit/audio/speech-openrouter.test.ts +88 -0
  142. package/tests/unit/audio/transcription-openai.test.ts +130 -0
  143. package/tests/unit/audio/transcription-openrouter.test.ts +82 -0
  144. package/tests/unit/embeddings/embed-gemini.test.ts +52 -0
  145. package/tests/unit/embeddings/embed-openai.test.ts +68 -0
  146. package/tests/unit/embeddings/embed-openrouter.test.ts +62 -0
  147. package/tests/unit/embeddings/index.test.ts +55 -0
  148. package/tests/unit/index.test.ts +209 -0
  149. package/tests/unit/langchain/messages.test.ts +37 -0
  150. package/tests/unit/langchain/models.test.ts +223 -1
  151. package/tests/unit/langchain/structured-output.test.ts +49 -0
@@ -0,0 +1,101 @@
1
+ import type { AudioBuffer } from "../@types/audio";
2
+ import type {
3
+ GeminiTranscriptionOptions,
4
+ OpenAITranscriptionDetailed,
5
+ OpenAITranscriptionOptions,
6
+ OpenRouterSpeechOptions,
7
+ OpenRouterTranscriptionOptions,
8
+ OpenRouterTranscriptionResult,
9
+ OpenAISpeechOptions,
10
+ SpeechResult,
11
+ } from "./types";
12
+ import {
13
+ transcribeDetailedOpenAI,
14
+ transcribeDiarizedOpenAI,
15
+ transcribeFileDetailedOpenAI,
16
+ transcribeFileWithWhisperOpenAI,
17
+ transcribeToSrtOpenAI,
18
+ transcribeToVttOpenAI,
19
+ transcribeWithWhisperOpenAI,
20
+ translateOpenAI,
21
+ } from "./transcription-openai";
22
+ import { transcribeOpenRouter } from "./transcription-openrouter";
23
+ import { speakOpenAI, speakOpenAIStream } from "./speech-openai";
24
+ import { speakOpenRouter, speakOpenRouterStream } from "./speech-openrouter";
25
+ import { transcribeWithGeminiPrompt } from "./gemini-transcription";
26
+
27
+ export type {
28
+ WhisperModel,
29
+ OpenAITranscriptionOptions,
30
+ OpenAITranscriptionResponseFormat,
31
+ OpenAITranscriptionDetailed,
32
+ OpenRouterTranscriptionOptions,
33
+ OpenRouterTranscriptionResult,
34
+ OpenAISpeechOptions,
35
+ OpenAISpeechModel,
36
+ OpenRouterSpeechOptions,
37
+ SpeechResult,
38
+ TranscriptionWord,
39
+ TranscriptionSegment,
40
+ DiarizedSegment,
41
+ GeminiTranscriptionOptions,
42
+ } from "./types";
43
+
44
+ export { ANTHROPIC_NO_NATIVE_AUDIO } from "./types";
45
+
46
+ export class AIAudio {
47
+ static transcribeDetailedOpenAI = transcribeDetailedOpenAI;
48
+ static transcribeWithWhisper = transcribeWithWhisperOpenAI;
49
+ static transcribeFileWithWhisper = transcribeFileWithWhisperOpenAI;
50
+ static transcribeToSrtOpenAI = transcribeToSrtOpenAI;
51
+ static transcribeToVttOpenAI = transcribeToVttOpenAI;
52
+ static transcribeDiarizedOpenAI = transcribeDiarizedOpenAI;
53
+ static translateOpenAI = translateOpenAI;
54
+ static transcribeOpenRouter = transcribeOpenRouter;
55
+ static speakOpenAI = speakOpenAI;
56
+ static speakOpenAIStream = speakOpenAIStream;
57
+ static speakOpenRouter = speakOpenRouter;
58
+ static speakOpenRouterStream = speakOpenRouterStream;
59
+ static transcribeWithGeminiPrompt = transcribeWithGeminiPrompt;
60
+
61
+ /** @deprecated Use AIAudio.transcribeWithWhisper */
62
+ static transcribeWithWhisperLegacy(
63
+ audioBuffer: AudioBuffer,
64
+ options?: OpenAITranscriptionOptions,
65
+ openAIApiKey?: string,
66
+ ): Promise<string> {
67
+ return transcribeWithWhisperOpenAI(audioBuffer, options, openAIApiKey);
68
+ }
69
+
70
+ static async transcribeFileDetailedOpenAI(
71
+ filePath: string,
72
+ options?: OpenAITranscriptionOptions,
73
+ openAIApiKey?: string,
74
+ ): Promise<OpenAITranscriptionDetailed> {
75
+ return transcribeFileDetailedOpenAI(filePath, options, openAIApiKey);
76
+ }
77
+
78
+ static async transcribeDetailedOpenRouter(
79
+ audioBuffer: AudioBuffer,
80
+ options: OpenRouterTranscriptionOptions,
81
+ openRouterApiKey?: string,
82
+ ): Promise<OpenRouterTranscriptionResult> {
83
+ return transcribeOpenRouter(audioBuffer, options, openRouterApiKey);
84
+ }
85
+
86
+ static async speak(
87
+ text: string,
88
+ options: OpenAISpeechOptions & { provider?: "openai" },
89
+ openAIApiKey?: string,
90
+ ): Promise<SpeechResult> {
91
+ return speakOpenAI(text, options, openAIApiKey);
92
+ }
93
+
94
+ static async speakViaOpenRouter(
95
+ text: string,
96
+ options: OpenRouterSpeechOptions,
97
+ openRouterApiKey?: string,
98
+ ): Promise<SpeechResult> {
99
+ return speakOpenRouter(text, options, openRouterApiKey);
100
+ }
101
+ }
@@ -0,0 +1,4 @@
1
+ export {
2
+ OPENROUTER_API_BASE,
3
+ createOpenRouterOpenAIClient,
4
+ } from "../utils/openrouter-client";
@@ -0,0 +1,79 @@
1
+ import OpenAI from "openai";
2
+ import type { OpenAISpeechOptions, SpeechResult } from "./types";
3
+
4
+ const CONTENT_TYPE_BY_FORMAT: Record<string, string> = {
5
+ mp3: "audio/mpeg",
6
+ opus: "audio/opus",
7
+ aac: "audio/aac",
8
+ flac: "audio/flac",
9
+ wav: "audio/wav",
10
+ pcm: "audio/pcm",
11
+ };
12
+
13
+ function validateSpeechOptions(options: OpenAISpeechOptions): void {
14
+ const model = options.model ?? "gpt-4o-mini-tts";
15
+ if (
16
+ options.instructions &&
17
+ model !== "gpt-4o-mini-tts" &&
18
+ model !== "gpt-4o-mini-tts-2025-12-15"
19
+ ) {
20
+ throw new Error(
21
+ "[@luanpoppe/ai] instructions em TTS só é suportado por modelos gpt-4o-mini-tts.",
22
+ );
23
+ }
24
+ }
25
+
26
+ function createOpenAIClient(apiKey?: string): OpenAI {
27
+ if (!apiKey) {
28
+ throw new Error("OpenAI API key is not passed in the speech parameters");
29
+ }
30
+ return new OpenAI({ apiKey });
31
+ }
32
+
33
+ export async function speakOpenAI(
34
+ text: string,
35
+ options: OpenAISpeechOptions,
36
+ openAIApiKey?: string,
37
+ ): Promise<SpeechResult> {
38
+ validateSpeechOptions(options);
39
+ const openai = createOpenAIClient(openAIApiKey);
40
+ const responseFormat = options.responseFormat ?? "mp3";
41
+ const model = options.model ?? "gpt-4o-mini-tts";
42
+
43
+ const response = await openai.audio.speech.create({
44
+ model,
45
+ voice: options.voice as OpenAI.Audio.SpeechCreateParams["voice"],
46
+ input: text,
47
+ response_format: responseFormat,
48
+ ...(options.speed !== undefined ? { speed: options.speed } : {}),
49
+ ...(options.instructions ? { instructions: options.instructions } : {}),
50
+ });
51
+
52
+ const arrayBuffer = await response.arrayBuffer();
53
+ return {
54
+ audio: Buffer.from(arrayBuffer),
55
+ contentType: CONTENT_TYPE_BY_FORMAT[responseFormat] ?? "application/octet-stream",
56
+ };
57
+ }
58
+
59
+ export async function speakOpenAIStream(
60
+ text: string,
61
+ options: OpenAISpeechOptions,
62
+ openAIApiKey?: string,
63
+ ): Promise<ReadableStream<Uint8Array>> {
64
+ validateSpeechOptions(options);
65
+ const openai = createOpenAIClient(openAIApiKey);
66
+ const responseFormat = options.responseFormat ?? "mp3";
67
+ const model = options.model ?? "gpt-4o-mini-tts";
68
+
69
+ const stream = await openai.audio.speech.create({
70
+ model,
71
+ voice: options.voice as OpenAI.Audio.SpeechCreateParams["voice"],
72
+ input: text,
73
+ response_format: responseFormat,
74
+ ...(options.speed !== undefined ? { speed: options.speed } : {}),
75
+ ...(options.instructions ? { instructions: options.instructions } : {}),
76
+ });
77
+
78
+ return stream.body as ReadableStream<Uint8Array>;
79
+ }
@@ -0,0 +1,121 @@
1
+ import type { OpenRouterSpeechOptions, SpeechResult } from "./types";
2
+ import { createOpenRouterOpenAIClient } from "./openrouter-client";
3
+ import { resolveOpenRouterProvider } from "../langchain/models";
4
+
5
+ const CONTENT_TYPE_BY_FORMAT: Record<string, string> = {
6
+ mp3: "audio/mpeg",
7
+ pcm: "audio/pcm",
8
+ };
9
+
10
+ function isGeminiTtsModel(model: string): boolean {
11
+ return /gemini/i.test(model) && /tts/i.test(model);
12
+ }
13
+
14
+ function resolveOpenRouterSpeechFormat(
15
+ model: string,
16
+ responseFormat?: OpenRouterSpeechOptions["responseFormat"],
17
+ ): "mp3" | "pcm" {
18
+ if (responseFormat) return responseFormat;
19
+ // Gemini TTS no OpenRouter aceita apenas pcm (ver docs OR /audio/speech)
20
+ if (isGeminiTtsModel(model)) return "pcm";
21
+ return "mp3";
22
+ }
23
+
24
+ function validateOpenRouterSpeechOptions(options: OpenRouterSpeechOptions): void {
25
+ if (!options.model) {
26
+ throw new Error("[@luanpoppe/ai] model é obrigatório para speakOpenRouter.");
27
+ }
28
+ if (!options.voice) {
29
+ throw new Error("[@luanpoppe/ai] voice é obrigatório para speakOpenRouter.");
30
+ }
31
+ }
32
+
33
+ export async function speakOpenRouter(
34
+ text: string,
35
+ options: OpenRouterSpeechOptions,
36
+ openRouterApiKey?: string,
37
+ ): Promise<SpeechResult> {
38
+ validateOpenRouterSpeechOptions(options);
39
+ if (!openRouterApiKey) {
40
+ throw new Error(
41
+ "OpenRouter API key is not passed in the speech parameters",
42
+ );
43
+ }
44
+
45
+ const client = createOpenRouterOpenAIClient(openRouterApiKey);
46
+ const responseFormat = resolveOpenRouterSpeechFormat(
47
+ options.model,
48
+ options.responseFormat,
49
+ );
50
+ const provider = resolveOpenRouterProvider(options.model, {
51
+ openRouterProvider: options.openRouterProvider,
52
+ openRouterAllowAllProviders: options.openRouterAllowAllProviders,
53
+ });
54
+
55
+ const body: Record<string, unknown> = {
56
+ model: options.model,
57
+ input: text,
58
+ voice: options.voice,
59
+ response_format: responseFormat,
60
+ };
61
+
62
+ if (options.speed !== undefined) body.speed = options.speed;
63
+ if (provider || options.instructions) {
64
+ const mergedProvider: Record<string, unknown> = { ...(provider ?? {}) };
65
+ if (options.instructions) {
66
+ mergedProvider.options = {
67
+ openai: { instructions: options.instructions },
68
+ };
69
+ }
70
+ body.provider = mergedProvider;
71
+ }
72
+
73
+ const response = await client.audio.speech.create(
74
+ body as unknown as Parameters<typeof client.audio.speech.create>[0],
75
+ );
76
+
77
+ const arrayBuffer = await response.arrayBuffer();
78
+ return {
79
+ audio: Buffer.from(arrayBuffer),
80
+ contentType: CONTENT_TYPE_BY_FORMAT[responseFormat] ?? "application/octet-stream",
81
+ };
82
+ }
83
+
84
+ export async function speakOpenRouterStream(
85
+ text: string,
86
+ options: OpenRouterSpeechOptions,
87
+ openRouterApiKey?: string,
88
+ ): Promise<ReadableStream<Uint8Array>> {
89
+ validateOpenRouterSpeechOptions(options);
90
+ if (!openRouterApiKey) {
91
+ throw new Error(
92
+ "OpenRouter API key is not passed in the speech parameters",
93
+ );
94
+ }
95
+
96
+ const client = createOpenRouterOpenAIClient(openRouterApiKey);
97
+ const responseFormat = resolveOpenRouterSpeechFormat(
98
+ options.model,
99
+ options.responseFormat,
100
+ );
101
+ const provider = resolveOpenRouterProvider(options.model, {
102
+ openRouterProvider: options.openRouterProvider,
103
+ openRouterAllowAllProviders: options.openRouterAllowAllProviders,
104
+ });
105
+
106
+ const body: Record<string, unknown> = {
107
+ model: options.model,
108
+ input: text,
109
+ voice: options.voice,
110
+ response_format: responseFormat,
111
+ };
112
+
113
+ if (options.speed !== undefined) body.speed = options.speed;
114
+ if (provider) body.provider = provider;
115
+
116
+ const stream = await client.audio.speech.create(
117
+ body as unknown as Parameters<typeof client.audio.speech.create>[0],
118
+ );
119
+
120
+ return stream.body as ReadableStream<Uint8Array>;
121
+ }
@@ -0,0 +1,259 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import OpenAI from "openai";
4
+ import { toFile } from "openai";
5
+ import type { AudioBuffer } from "../@types/audio";
6
+ import type {
7
+ OpenAITranscriptionDetailed,
8
+ OpenAITranscriptionOptions,
9
+ TranscriptionSegment,
10
+ TranscriptionWord,
11
+ DiarizedSegment,
12
+ } from "./types";
13
+ import { getAudioExtension, toAudioBuffer } from "./utils";
14
+
15
+ function createOpenAIClient(apiKey?: string): OpenAI {
16
+ if (apiKey) {
17
+ return new OpenAI({ apiKey });
18
+ }
19
+ return new OpenAI();
20
+ }
21
+
22
+ async function createAudioFile(
23
+ audioBuffer: AudioBuffer,
24
+ format?: OpenAITranscriptionOptions["format"],
25
+ ) {
26
+ const buffer = toAudioBuffer(audioBuffer);
27
+ const extension = getAudioExtension(format);
28
+ const fileName = `whisper-${Date.now()}.${extension}`;
29
+ return toFile(buffer, fileName);
30
+ }
31
+
32
+ function validateTranscriptionOptions(options: OpenAITranscriptionOptions): void {
33
+ const model = options.model ?? "whisper-1";
34
+ const format = options.responseFormat ?? "text";
35
+
36
+ if (
37
+ options.timestampGranularities?.length &&
38
+ (model !== "whisper-1" || format !== "verbose_json")
39
+ ) {
40
+ throw new Error(
41
+ "[@luanpoppe/ai] timestampGranularities só é suportado com model whisper-1 e responseFormat verbose_json.",
42
+ );
43
+ }
44
+
45
+ if (model === "gpt-4o-transcribe-diarize" && format === "diarized_json") {
46
+ if (!options.chunkingStrategy) {
47
+ options.chunkingStrategy = "auto";
48
+ }
49
+ }
50
+ }
51
+
52
+ function mapVerboseResponse(
53
+ response: Record<string, unknown>,
54
+ ): OpenAITranscriptionDetailed {
55
+ const words = Array.isArray(response.words)
56
+ ? (response.words as TranscriptionWord[])
57
+ : undefined;
58
+ const segments = Array.isArray(response.segments)
59
+ ? (response.segments as TranscriptionSegment[])
60
+ : undefined;
61
+
62
+ const detailed: OpenAITranscriptionDetailed = {
63
+ text: String(response.text ?? ""),
64
+ };
65
+ if (typeof response.language === "string") {
66
+ detailed.language = response.language;
67
+ }
68
+ if (typeof response.duration === "number") {
69
+ detailed.duration = response.duration;
70
+ }
71
+ if (words) detailed.words = words;
72
+ if (segments) detailed.segments = segments;
73
+ return detailed;
74
+ }
75
+
76
+ function mapDiarizedResponse(
77
+ response: Record<string, unknown>,
78
+ ): OpenAITranscriptionDetailed {
79
+ const segments = Array.isArray(response.segments)
80
+ ? (response.segments as Array<Record<string, unknown>>)
81
+ : [];
82
+
83
+ const speakers: DiarizedSegment[] = segments.map((seg) => ({
84
+ speaker: String(seg.speaker ?? "unknown"),
85
+ start: Number(seg.start ?? 0),
86
+ end: Number(seg.end ?? 0),
87
+ text: String(seg.text ?? ""),
88
+ }));
89
+
90
+ return {
91
+ text: String(response.text ?? speakers.map((s) => s.text).join(" ")),
92
+ speakers,
93
+ segments: speakers.map((s, id) => ({
94
+ id,
95
+ start: s.start,
96
+ end: s.end,
97
+ text: s.text,
98
+ })),
99
+ };
100
+ }
101
+
102
+ function mapTranscriptionResponse(
103
+ response: unknown,
104
+ responseFormat: OpenAITranscriptionOptions["responseFormat"],
105
+ ): OpenAITranscriptionDetailed {
106
+ if (typeof response === "string") {
107
+ return { text: response };
108
+ }
109
+
110
+ const obj = response as Record<string, unknown>;
111
+ if (responseFormat === "diarized_json") {
112
+ return mapDiarizedResponse(obj);
113
+ }
114
+ if (responseFormat === "verbose_json") {
115
+ return mapVerboseResponse(obj);
116
+ }
117
+ return { text: String(obj.text ?? "") };
118
+ }
119
+
120
+ export async function transcribeDetailedOpenAI(
121
+ audioBuffer: AudioBuffer,
122
+ options: OpenAITranscriptionOptions = {},
123
+ openAIApiKey?: string,
124
+ ): Promise<OpenAITranscriptionDetailed> {
125
+ validateTranscriptionOptions(options);
126
+
127
+ const openai = createOpenAIClient(openAIApiKey);
128
+ const file = await createAudioFile(audioBuffer, options.format);
129
+ const responseFormat = options.responseFormat ?? "verbose_json";
130
+ const model = options.model ?? "whisper-1";
131
+
132
+ const params: OpenAI.Audio.TranscriptionCreateParams = {
133
+ file,
134
+ model,
135
+ response_format: responseFormat as OpenAI.Audio.TranscriptionCreateParams["response_format"],
136
+ };
137
+
138
+ if (options.languageIn2Digits) params.language = options.languageIn2Digits;
139
+ if (options.prompt) params.prompt = options.prompt;
140
+ if (options.temperature !== undefined) params.temperature = options.temperature;
141
+ if (options.timestampGranularities) {
142
+ params.timestamp_granularities = options.timestampGranularities;
143
+ }
144
+ if (options.chunkingStrategy) {
145
+ (params as OpenAI.Audio.TranscriptionCreateParams & {
146
+ chunking_strategy?: unknown;
147
+ }).chunking_strategy = options.chunkingStrategy;
148
+ }
149
+
150
+ const response = await openai.audio.transcriptions.create(params);
151
+ return mapTranscriptionResponse(response, responseFormat);
152
+ }
153
+
154
+ export async function transcribeWithWhisperOpenAI(
155
+ audioBuffer: AudioBuffer,
156
+ options: OpenAITranscriptionOptions = {},
157
+ openAIApiKey?: string,
158
+ ): Promise<string> {
159
+ const detailed = await transcribeDetailedOpenAI(
160
+ audioBuffer,
161
+ { ...options, responseFormat: options.responseFormat ?? "text" },
162
+ openAIApiKey,
163
+ );
164
+ return detailed.text;
165
+ }
166
+
167
+ export async function transcribeToSrtOpenAI(
168
+ audioBuffer: AudioBuffer,
169
+ options: Omit<OpenAITranscriptionOptions, "responseFormat"> = {},
170
+ openAIApiKey?: string,
171
+ ): Promise<string> {
172
+ const detailed = await transcribeDetailedOpenAI(
173
+ audioBuffer,
174
+ { ...options, model: options.model ?? "whisper-1", responseFormat: "srt" },
175
+ openAIApiKey,
176
+ );
177
+ return detailed.text;
178
+ }
179
+
180
+ export async function transcribeToVttOpenAI(
181
+ audioBuffer: AudioBuffer,
182
+ options: Omit<OpenAITranscriptionOptions, "responseFormat"> = {},
183
+ openAIApiKey?: string,
184
+ ): Promise<string> {
185
+ const detailed = await transcribeDetailedOpenAI(
186
+ audioBuffer,
187
+ { ...options, model: options.model ?? "whisper-1", responseFormat: "vtt" },
188
+ openAIApiKey,
189
+ );
190
+ return detailed.text;
191
+ }
192
+
193
+ export async function transcribeDiarizedOpenAI(
194
+ audioBuffer: AudioBuffer,
195
+ options: Omit<OpenAITranscriptionOptions, "model" | "responseFormat"> = {},
196
+ openAIApiKey?: string,
197
+ ): Promise<OpenAITranscriptionDetailed> {
198
+ return transcribeDetailedOpenAI(
199
+ audioBuffer,
200
+ {
201
+ ...options,
202
+ model: "gpt-4o-transcribe-diarize",
203
+ responseFormat: "diarized_json",
204
+ chunkingStrategy: options.chunkingStrategy ?? "auto",
205
+ },
206
+ openAIApiKey,
207
+ );
208
+ }
209
+
210
+ export async function translateOpenAI(
211
+ audioBuffer: AudioBuffer,
212
+ options: Omit<OpenAITranscriptionOptions, "responseFormat"> = {},
213
+ openAIApiKey?: string,
214
+ ): Promise<string> {
215
+ const openai = createOpenAIClient(openAIApiKey);
216
+ const file = await createAudioFile(audioBuffer, options.format);
217
+
218
+ const response = await openai.audio.translations.create({
219
+ file,
220
+ model: "whisper-1",
221
+ ...(options.prompt ? { prompt: options.prompt } : {}),
222
+ ...(options.temperature !== undefined
223
+ ? { temperature: options.temperature }
224
+ : {}),
225
+ });
226
+
227
+ return typeof response === "string" ? response : response.text;
228
+ }
229
+
230
+ export async function transcribeFileDetailedOpenAI(
231
+ filePath: string,
232
+ options: OpenAITranscriptionOptions = {},
233
+ openAIApiKey?: string,
234
+ ): Promise<OpenAITranscriptionDetailed> {
235
+ if (!fs.existsSync(filePath)) {
236
+ throw new Error(`Arquivo não encontrado: ${filePath}`);
237
+ }
238
+ const audioBuffer = fs.readFileSync(filePath);
239
+ const format =
240
+ options.format ?? (path.extname(filePath).replace(/^\./, "") || "mp3");
241
+ return transcribeDetailedOpenAI(
242
+ audioBuffer,
243
+ { ...options, format },
244
+ openAIApiKey,
245
+ );
246
+ }
247
+
248
+ export async function transcribeFileWithWhisperOpenAI(
249
+ filePath: string,
250
+ options: OpenAITranscriptionOptions = {},
251
+ openAIApiKey?: string,
252
+ ): Promise<string> {
253
+ const detailed = await transcribeFileDetailedOpenAI(
254
+ filePath,
255
+ options,
256
+ openAIApiKey,
257
+ );
258
+ return detailed.text;
259
+ }
@@ -0,0 +1,59 @@
1
+ import type { AudioBuffer } from "../@types/audio";
2
+ import { resolveOpenRouterProvider } from "../langchain/models";
3
+ import type {
4
+ OpenRouterTranscriptionOptions,
5
+ OpenRouterTranscriptionResult,
6
+ } from "./types";
7
+ import { bufferToBase64, getAudioExtension } from "./utils";
8
+
9
+ const OPENROUTER_TRANSCRIPTIONS_URL =
10
+ "https://openrouter.ai/api/v1/audio/transcriptions";
11
+
12
+ export async function transcribeOpenRouter(
13
+ audioBuffer: AudioBuffer,
14
+ options: OpenRouterTranscriptionOptions,
15
+ openRouterApiKey?: string,
16
+ ): Promise<OpenRouterTranscriptionResult> {
17
+ if (!openRouterApiKey) {
18
+ throw new Error(
19
+ "OpenRouter API key is not passed in the transcription parameters",
20
+ );
21
+ }
22
+
23
+ const format = getAudioExtension(options.format);
24
+ const provider = resolveOpenRouterProvider(options.model, {
25
+ openRouterProvider: options.openRouterProvider,
26
+ openRouterAllowAllProviders: options.openRouterAllowAllProviders,
27
+ });
28
+
29
+ const body: Record<string, unknown> = {
30
+ model: options.model,
31
+ input_audio: {
32
+ data: bufferToBase64(audioBuffer),
33
+ format,
34
+ },
35
+ };
36
+
37
+ if (options.language) body.language = options.language;
38
+ if (options.temperature !== undefined) body.temperature = options.temperature;
39
+ if (provider) body.provider = provider;
40
+
41
+ const response = await fetch(OPENROUTER_TRANSCRIPTIONS_URL, {
42
+ method: "POST",
43
+ headers: {
44
+ Authorization: `Bearer ${openRouterApiKey}`,
45
+ "Content-Type": "application/json",
46
+ },
47
+ body: JSON.stringify(body),
48
+ });
49
+
50
+ if (!response.ok) {
51
+ const errText = await response.text();
52
+ throw new Error(
53
+ `[@luanpoppe/ai] OpenRouter STT falhou (${response.status}): ${errText}`,
54
+ );
55
+ }
56
+
57
+ const data = (await response.json()) as OpenRouterTranscriptionResult;
58
+ return data;
59
+ }