@runnerpro/backend 1.34.1 → 1.35.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/lib/cjs/index.js +8 -2
- package/lib/cjs/mediaProcessing/audio.js +169 -155
- package/lib/cjs/mediaProcessing/azureSpeech.js +114 -0
- package/lib/cjs/mediaProcessing/azureTranscribe.js +73 -0
- package/lib/cjs/mediaProcessing/correccionTranscripcion.js +313 -0
- package/lib/cjs/mediaProcessing/ffmpegUtils.js +41 -2
- package/lib/cjs/mediaProcessing/index.js +157 -32
- package/lib/cjs/mediaProcessing/video.js +4 -4
- package/lib/cjs/types/index.d.ts +3 -2
- package/lib/cjs/types/index.d.ts.map +1 -1
- package/lib/cjs/types/mediaProcessing/audio.d.ts +31 -36
- package/lib/cjs/types/mediaProcessing/audio.d.ts.map +1 -1
- package/lib/cjs/types/mediaProcessing/azureSpeech.d.ts +38 -0
- package/lib/cjs/types/mediaProcessing/azureSpeech.d.ts.map +1 -0
- package/lib/cjs/types/mediaProcessing/azureTranscribe.d.ts +26 -0
- package/lib/cjs/types/mediaProcessing/azureTranscribe.d.ts.map +1 -0
- package/lib/cjs/types/mediaProcessing/correccionTranscripcion.d.ts +49 -0
- package/lib/cjs/types/mediaProcessing/correccionTranscripcion.d.ts.map +1 -0
- package/lib/cjs/types/mediaProcessing/ffmpegUtils.d.ts +20 -1
- package/lib/cjs/types/mediaProcessing/ffmpegUtils.d.ts.map +1 -1
- package/lib/cjs/types/mediaProcessing/index.d.ts +42 -8
- package/lib/cjs/types/mediaProcessing/index.d.ts.map +1 -1
- package/lib/cjs/types/mediaProcessing/video.d.ts +2 -1
- package/lib/cjs/types/mediaProcessing/video.d.ts.map +1 -1
- package/package.json +1 -2
package/lib/cjs/index.js
CHANGED
|
@@ -24,8 +24,8 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.saveDoneStructuraWorkout = exports.computeStreakStats = exports.getUnlockedAchievements = exports.evaluateAchievementsBounded = exports.evaluateAchievements = exports.saveWorkoutLaps = exports.saveWorkoutAplication = exports.getPlanificacionPrueba7dias = exports.sendWorkoutToWatch = exports.getDefaultWorkoutImage = exports.generateShareMap = exports.reduceSizeImage = exports.getLetter = exports.getNumberByLetter = exports.appendSheet = exports.writeSheet = exports.findCellByValue = exports.readSheet = exports.NOTION_DATABASES_ID = exports.notionEditPage = exports.notionAddPage = exports.notionGetDatabase = exports.notionGetUsers = exports.getCountNotificaciones = exports.chatExposed = exports.chatApi = exports.chat = exports.getExerciseTranslatedDescription = exports.useTranslation = exports.LANGUAGES = exports.translate = exports.CHANNEL_SLACK = exports.notifySlack = exports.fetchIA = exports.err = exports.sendMail = exports.pool = exports.toPgArray = exports.batchQuery = exports.longRunningQuery = exports.queryWithClient = exports.query = exports.sleep = exports.sendNotification = exports.TIPO_POR_DEFECTO = exports.urlPublica = exports.Archivo = exports.Contenedor = exports.StorageAzure = exports.achievementsCatalog = void 0;
|
|
27
|
-
exports.
|
|
28
|
-
exports.calculateCost = exports.MODEL_PRICING = exports.AISTUDIO_PREFIX = exports.BEDROCK_CLAUDE_HAIKU = void 0;
|
|
27
|
+
exports.FALLBACK_MODELS = exports.PRODUCTION_MODELS = exports.AZURE_FALLBACK_MODELS = exports.AZURE_PRIMARY_MODELS = exports.AZURE_DEEPSEEK_V4_FLASH = exports.AZURE_DEEPSEEK_V4_PRO = exports.AZURE_GPT_5_4_NANO = exports.AZURE_GPT_5_4_MINI = exports.AZURE_GPT_5_6_LUNA = exports.AZURE_GPT_5_6_TERRA = exports.AZURE_GPT_5_6_SOL = exports.AZURE_GPT_5_5 = exports.AZURE_DEEPSEEK_V4_FLASH_DEPLOYMENT = exports.AZURE_DEEPSEEK_V4_PRO_DEPLOYMENT = exports.AZURE_GPT_5_4_NANO_DEPLOYMENT = exports.AZURE_GPT_5_4_MINI_DEPLOYMENT = exports.AZURE_GPT_5_6_LUNA_DEPLOYMENT = exports.AZURE_GPT_5_6_TERRA_DEPLOYMENT = exports.AZURE_GPT_5_6_SOL_DEPLOYMENT = exports.AZURE_GPT_5_5_DEPLOYMENT = exports.AZURE_PREFIX = exports.GOOGLE_MODELS = exports.AI_MODELS = exports.MODEL_TIER = exports.runWithModels = exports.runWithCostTracking = exports.createModelFromString = exports.createAzureModelFromString = exports.createBedrockModelFromString = exports.createGoogleModelFromString = exports.createGoogleModel = exports.generateText = exports.generateObject = exports.MARCADORES_AUDIO = exports.MARCADOR_NO_TRANSCRITO = exports.MARCADOR_SIN_VOZ = exports.GLOSARIO_RUNNING = exports.corregirTranscripcion = exports.reprocessMediaByIds = exports.reprocessRecentMedia = exports.processMediaFile = exports.analyzeFile = exports.describeTextFile = exports.describePdf = exports.describeVideo = exports.describeImage = exports.transcribeAudio = exports.updateSenderView = exports.saveResponseTime = exports.getZone = void 0;
|
|
28
|
+
exports.calculateCost = exports.MODEL_PRICING = exports.AISTUDIO_PREFIX = exports.BEDROCK_CLAUDE_HAIKU = exports.BEDROCK_CLAUDE_SONNET_4 = exports.BEDROCK_CLAUDE_SONNET_4_5 = exports.BEDROCK_CLAUDE_OPUS_4_1 = exports.BEDROCK_CLAUDE_OPUS_4_5 = exports.BEDROCK_CLAUDE_SONNET = exports.BEDROCK_CLAUDE_OPUS = void 0;
|
|
29
29
|
const sendNotification_1 = require("./sendNotification");
|
|
30
30
|
Object.defineProperty(exports, "sendNotification", { enumerable: true, get: function () { return sendNotification_1.sendNotification; } });
|
|
31
31
|
const sleep_1 = require("./sleep");
|
|
@@ -109,6 +109,12 @@ Object.defineProperty(exports, "describeTextFile", { enumerable: true, get: func
|
|
|
109
109
|
Object.defineProperty(exports, "analyzeFile", { enumerable: true, get: function () { return mediaProcessing_1.analyzeFile; } });
|
|
110
110
|
Object.defineProperty(exports, "processMediaFile", { enumerable: true, get: function () { return mediaProcessing_1.processMediaFile; } });
|
|
111
111
|
Object.defineProperty(exports, "reprocessRecentMedia", { enumerable: true, get: function () { return mediaProcessing_1.reprocessRecentMedia; } });
|
|
112
|
+
Object.defineProperty(exports, "reprocessMediaByIds", { enumerable: true, get: function () { return mediaProcessing_1.reprocessMediaByIds; } });
|
|
113
|
+
Object.defineProperty(exports, "corregirTranscripcion", { enumerable: true, get: function () { return mediaProcessing_1.corregirTranscripcion; } });
|
|
114
|
+
Object.defineProperty(exports, "GLOSARIO_RUNNING", { enumerable: true, get: function () { return mediaProcessing_1.GLOSARIO_RUNNING; } });
|
|
115
|
+
Object.defineProperty(exports, "MARCADOR_SIN_VOZ", { enumerable: true, get: function () { return mediaProcessing_1.MARCADOR_SIN_VOZ; } });
|
|
116
|
+
Object.defineProperty(exports, "MARCADOR_NO_TRANSCRITO", { enumerable: true, get: function () { return mediaProcessing_1.MARCADOR_NO_TRANSCRITO; } });
|
|
117
|
+
Object.defineProperty(exports, "MARCADORES_AUDIO", { enumerable: true, get: function () { return mediaProcessing_1.MARCADORES_AUDIO; } });
|
|
112
118
|
const prompt_1 = require("./prompt");
|
|
113
119
|
Object.defineProperty(exports, "generateObject", { enumerable: true, get: function () { return prompt_1.generateObject; } });
|
|
114
120
|
Object.defineProperty(exports, "generateText", { enumerable: true, get: function () { return prompt_1.generateText; } });
|
|
@@ -12,115 +12,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.
|
|
15
|
+
exports.MAX_CARACTERES_POR_SEGUNDO = exports.TRANSCRIPTION_MAX_SECONDS = exports.MARCADORES_AUDIO = exports.MARCADOR_ANTIGUO = exports.MARCADOR_NO_TRANSCRITO = exports.MARCADOR_SIN_VOZ = exports.convertToWav16k = exports.transcribeWav16k = exports.transcribeAudio = void 0;
|
|
16
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
|
-
const speech_1 = require("@google-cloud/speech");
|
|
19
18
|
const ffmpegUtils_1 = require("./ffmpegUtils");
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
// ✅
|
|
24
|
-
// cuando ffmpeg no ha podido convertir el archivo y hay que mandarlo tal cual.
|
|
25
|
-
const AUDIO_ENCODING_MAP = {
|
|
26
|
-
'audio/mpeg': 'MP3',
|
|
27
|
-
'audio/mp3': 'MP3',
|
|
28
|
-
'audio/wav': 'LINEAR16',
|
|
29
|
-
'audio/wave': 'LINEAR16',
|
|
30
|
-
'audio/x-wav': 'LINEAR16',
|
|
31
|
-
'audio/ogg': 'OGG_OPUS',
|
|
32
|
-
'audio/webm': 'WEBM_OPUS',
|
|
33
|
-
'audio/flac': 'FLAC',
|
|
34
|
-
'audio/x-flac': 'FLAC',
|
|
35
|
-
'audio/mp4': 'MP3',
|
|
36
|
-
'audio/m4a': 'MP3',
|
|
37
|
-
'audio/aac': 'MP3',
|
|
38
|
-
};
|
|
39
|
-
exports.AUDIO_ENCODING_MAP = AUDIO_ENCODING_MAP;
|
|
40
|
-
// ✅ `recognize` (síncrono) rechaza audio de más de 60s. Cuando se le declara LINEAR16 deduce la
|
|
41
|
-
// duración exacta de los bytes y lo aplica a rajatabla, así que por encima de esto no se le manda.
|
|
42
|
-
const SYNC_MAX_SECONDS = 55;
|
|
43
|
-
exports.SYNC_MAX_SECONDS = SYNC_MAX_SECONDS;
|
|
44
|
-
// ✅ Speech-to-Text admite 10 MB en línea; a 32 KB/s son ~290s. Gemini aguanta mucho más,
|
|
45
|
-
// pero recortamos para acotar el coste y la latencia de una nota de voz.
|
|
19
|
+
const azureSpeech_1 = require("./azureSpeech");
|
|
20
|
+
const azureTranscribe_1 = require("./azureTranscribe");
|
|
21
|
+
const correccionTranscripcion_1 = require("./correccionTranscripcion");
|
|
22
|
+
// ✅ Speech admite hasta 5 h por audio, pero se recorta para acotar coste y latencia de una nota de voz
|
|
46
23
|
const TRANSCRIPTION_MAX_SECONDS = 600;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
24
|
+
exports.TRANSCRIPTION_MAX_SECONDS = TRANSCRIPTION_MAX_SECONDS;
|
|
25
|
+
// ✅ Nadie habla tan rápido: por encima de esto el texto no ha salido del audio. El máximo real medido
|
|
26
|
+
// sobre 74 notas de voz es 19. Es la red final contra cualquier motor que se invente contenido.
|
|
27
|
+
const MAX_CARACTERES_POR_SEGUNDO = 25;
|
|
28
|
+
exports.MAX_CARACTERES_POR_SEGUNDO = MAX_CARACTERES_POR_SEGUNDO;
|
|
29
|
+
// ✅ Marcadores que se guardan en FILE TEXT cuando no hay transcripción. Nunca se guarda vacío ni NULL:
|
|
30
|
+
// el entrenador se salta la respuesta si un adjunto no tiene texto y no distingue «sin texto» de
|
|
31
|
+
// «aún sin procesar».
|
|
32
|
+
const MARCADOR_SIN_VOZ = '[Audio sin voz reconocible]';
|
|
33
|
+
exports.MARCADOR_SIN_VOZ = MARCADOR_SIN_VOZ;
|
|
34
|
+
const MARCADOR_NO_TRANSCRITO = '[Audio no transcrito]';
|
|
35
|
+
exports.MARCADOR_NO_TRANSCRITO = MARCADOR_NO_TRANSCRITO;
|
|
36
|
+
const MARCADOR_ANTIGUO = '[Audio sin contenido reconocible]';
|
|
37
|
+
exports.MARCADOR_ANTIGUO = MARCADOR_ANTIGUO;
|
|
38
|
+
const MARCADORES_AUDIO = [MARCADOR_SIN_VOZ, MARCADOR_NO_TRANSCRITO, MARCADOR_ANTIGUO];
|
|
39
|
+
exports.MARCADORES_AUDIO = MARCADORES_AUDIO;
|
|
50
40
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* @param content - Buffer del audio
|
|
54
|
-
* @param encoding - Encoding declarado (LINEAR16, MP3...)
|
|
55
|
-
* @returns Promise<string | null> - Transcripción, o null si no se reconoció nada
|
|
56
|
-
*/
|
|
57
|
-
const recognizeSpeech = (content, encoding) => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
|
-
var _a;
|
|
59
|
-
try {
|
|
60
|
-
const speechClient = new speech_1.SpeechClient();
|
|
61
|
-
// @ts-ignore - Tipos de Speech-to-Text son complejos
|
|
62
|
-
const [response] = yield speechClient.recognize({
|
|
63
|
-
audio: {
|
|
64
|
-
content: content.toString('base64'),
|
|
65
|
-
},
|
|
66
|
-
config: {
|
|
67
|
-
// @ts-ignore - El encoding se determina dinámicamente
|
|
68
|
-
encoding: encoding,
|
|
69
|
-
sampleRateHertz: ffmpegUtils_1.SPEECH_SAMPLE_RATE,
|
|
70
|
-
languageCode: 'es-ES', // Español de España
|
|
71
|
-
alternativeLanguageCodes: ['es-MX', 'es-AR', 'en-US'], // Alternativas: México, Argentina, Inglés
|
|
72
|
-
enableAutomaticPunctuation: true, // Puntuación automática
|
|
73
|
-
model: 'latest_long', // Modelo más reciente para audios largos
|
|
74
|
-
useEnhanced: true, // Modelo mejorado
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
const transcription = (_a = response.results) === null || _a === void 0 ? void 0 : _a.map((result) => { var _a, _b; return (_b = (_a = result.alternatives) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.transcript; }).filter(Boolean).join(' ').trim();
|
|
78
|
-
return transcription || null;
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
// eslint-disable-next-line no-console
|
|
82
|
-
console.error('[transcribeAudio] Error Speech-to-Text:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
/**
|
|
87
|
-
* Transcribe con un modelo multimodal de Google. Es la red que recoge lo que Speech-to-Text
|
|
88
|
-
* no reconoce y, sobre todo, los audios de más de 60s, que el modo síncrono no admite.
|
|
89
|
-
*
|
|
90
|
-
* @param wavBuffer - Buffer WAV mono 16 kHz
|
|
91
|
-
* @returns Promise<string | null> - Transcripción, o null si no hay voz
|
|
92
|
-
*/
|
|
93
|
-
const transcribeWithModel = (wavBuffer) => __awaiter(void 0, void 0, void 0, function* () {
|
|
94
|
-
try {
|
|
95
|
-
const model = (0, index_1.createGoogleModelFromString)(constants_1.GOOGLE_MODELS.FLASH);
|
|
96
|
-
// El primario es Vertex; si el proyecto no lo tiene disponible se pasa a AI Studio
|
|
97
|
-
model._fallbackModelNames = [`${constants_1.AISTUDIO_PREFIX}${constants_1.GOOGLE_MODELS.FLASH}`, 'gemini-2.5-flash', `${constants_1.AISTUDIO_PREFIX}gemini-2.5-flash`];
|
|
98
|
-
const { text } = yield (0, index_1.generateText)({
|
|
99
|
-
model,
|
|
100
|
-
// ⭐ Sin registro en LLM CACHE: el audio no aporta nada como clave y engorda la tabla
|
|
101
|
-
log: false,
|
|
102
|
-
messages: [
|
|
103
|
-
{
|
|
104
|
-
role: 'user',
|
|
105
|
-
content: [
|
|
106
|
-
{ type: 'file', data: wavBuffer, mediaType: 'audio/wav' },
|
|
107
|
-
{ type: 'text', text: TRANSCRIPTION_PROMPT },
|
|
108
|
-
],
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
});
|
|
112
|
-
const transcription = (text || '').trim();
|
|
113
|
-
return !transcription || transcription === 'SIN_VOZ' ? null : transcription;
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
// eslint-disable-next-line no-console
|
|
117
|
-
console.error('[transcribeAudio] Error transcribiendo con el modelo:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
exports.transcribeWithModel = transcribeWithModel;
|
|
122
|
-
/**
|
|
123
|
-
* Convierte cualquier audio al formato que espera Speech-to-Text (WAV PCM mono 16 kHz)
|
|
41
|
+
* Convierte cualquier audio a WAV PCM mono 16 kHz, recortado a 10 minutos
|
|
124
42
|
*
|
|
125
43
|
* @param fileBuffer - Buffer del audio original
|
|
126
44
|
* @param filename - Nombre original del archivo
|
|
@@ -145,72 +63,168 @@ const convertToWav16k = (fileBuffer, filename) => (0, ffmpegUtils_1.withTempDir)
|
|
|
145
63
|
}));
|
|
146
64
|
exports.convertToWav16k = convertToWav16k;
|
|
147
65
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* El audio se normaliza a WAV mono 16 kHz y luego se elige motor por duración:
|
|
151
|
-
* - hasta 55s → Speech-to-Text, que es barato y preciso.
|
|
152
|
-
* - más de 55s → modelo multimodal, porque `recognize` no admite audios más largos y
|
|
153
|
-
* `longRunningRecognize` exige subir el archivo a GCS antes.
|
|
154
|
-
* - si Speech-to-Text no reconoce nada, se reintenta con el modelo antes de darlo por perdido.
|
|
155
|
-
*
|
|
156
|
-
* ⚠️ El orden importa y no es intercambiable. Medido sobre notas de voz reales de producción
|
|
157
|
-
* que hoy acaban en '[Audio sin contenido reconocible]':
|
|
158
|
-
* - mandar el AAC crudo declarando 'MP3' (lo que se hacía): 0 de 14 recuperadas.
|
|
159
|
-
* - convertir a WAV y declarar LINEAR16: 13 de 14 (la que falla es de 81s).
|
|
160
|
-
* - modelo multimodal: 14 de 14.
|
|
161
|
-
* Y a la inversa, convertir a WAV SIN respetar el límite de 55s rompe los audios largos que hoy
|
|
162
|
-
* sí funcionan (4 de 4 medidos): con LINEAR16 la API deduce la duración exacta y los rechaza,
|
|
163
|
-
* mientras que con un códec comprimido declarado se saltaba esa comprobación.
|
|
66
|
+
* Mide duración y volumen medio del audio. Si ffmpeg falla se sigue con lo que hay: sin medida
|
|
67
|
+
* no se puede aplicar la guarda de silencio, pero sí transcribir.
|
|
164
68
|
*
|
|
165
|
-
* @param fileBuffer - Buffer del
|
|
166
|
-
* @param mimetype - Tipo MIME del archivo (ej: 'audio/aac', 'audio/x-m4a')
|
|
69
|
+
* @param fileBuffer - Buffer del audio
|
|
167
70
|
* @param filename - Nombre original del archivo
|
|
168
|
-
* @returns
|
|
71
|
+
* @returns Duración en segundos (0 si no se pudo medir) y volumen medio en dB (null si no se pudo)
|
|
72
|
+
*/
|
|
73
|
+
const medirAudio = (fileBuffer, filename) => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
try {
|
|
75
|
+
return yield (0, ffmpegUtils_1.withTempDir)((dir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
const inputPath = path_1.default.join(dir, `input${path_1.default.extname(filename || '') || '.m4a'}`);
|
|
77
|
+
fs_1.default.writeFileSync(inputPath, fileBuffer);
|
|
78
|
+
const [{ duration }, meanDb] = yield Promise.all([(0, ffmpegUtils_1.probeMedia)(inputPath), (0, ffmpegUtils_1.measureLoudness)(inputPath)]);
|
|
79
|
+
return { duration, meanDb };
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.error('[transcribeAudio] No se pudo medir el audio:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
85
|
+
return { duration: 0, meanDb: null };
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Pasa el audio por los motores en orden: Azure Speech y, si falla o no oye nada con señal,
|
|
90
|
+
* gpt-4o-mini-transcribe sobre WAV. Cada motor falla por su cuenta; nunca lanza.
|
|
169
91
|
*
|
|
170
|
-
* @
|
|
171
|
-
*
|
|
172
|
-
* const transcription = await transcribeAudio(audioBuffer, 'audio/aac', 'audio.aac');
|
|
173
|
-
* // Resultado: "Hola, quería comentarte que hoy me duele un poco la rodilla..."
|
|
174
|
-
* ```
|
|
92
|
+
* @param params - Audio original, WAV si ya existe, duración, locale y frases a reforzar
|
|
93
|
+
* @returns Texto (vacío si ningún motor oyó nada), motor que lo produjo y su confianza
|
|
175
94
|
*/
|
|
176
|
-
const
|
|
177
|
-
let
|
|
178
|
-
let duration;
|
|
95
|
+
const reconocer = (params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
let motor = 'ninguno';
|
|
179
97
|
try {
|
|
180
|
-
|
|
98
|
+
const { text, confidence } = yield (0, azureSpeech_1.transcribeWithAzureSpeech)(params.wav || params.original, {
|
|
99
|
+
locale: params.locale,
|
|
100
|
+
phrases: params.phrases,
|
|
101
|
+
mimetype: params.wav ? 'audio/wav' : params.mimetype,
|
|
102
|
+
});
|
|
103
|
+
motor = 'speech';
|
|
104
|
+
// ⭐ Un audio de menos de 1 s sin resultado no merece un segundo motor: la guarda física ya
|
|
105
|
+
// decidió que hay señal, pero no hay nada que un modelo pueda oír ahí
|
|
106
|
+
if (text || params.duration < 1)
|
|
107
|
+
return { text, motor, confidence };
|
|
181
108
|
}
|
|
182
109
|
catch (error) {
|
|
183
|
-
// ⭐ Sin ffmpeg no se puede elegir motor: se manda el original tal cual, como se hacía antes
|
|
184
110
|
// eslint-disable-next-line no-console
|
|
185
|
-
console.error('[transcribeAudio]
|
|
186
|
-
(0, index_2.err)(null, null, error, null);
|
|
187
|
-
const mime = (mimetype || '').toLowerCase().split(';')[0].trim();
|
|
188
|
-
return (yield recognizeSpeech(fileBuffer, AUDIO_ENCODING_MAP[mime] || 'MP3')) || '[Audio sin contenido reconocible]';
|
|
111
|
+
console.error('[transcribeAudio] Azure Speech falló, se pasa al respaldo:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
189
112
|
}
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
113
|
+
try {
|
|
114
|
+
const wav = params.wav || (yield convertToWav16k(params.original, params.filename)).wav;
|
|
115
|
+
const text = yield (0, azureTranscribe_1.transcribeWithAzureOpenAI)(wav, { language: params.locale.slice(0, 2) });
|
|
116
|
+
return { text, motor: 'openai', confidence: null };
|
|
194
117
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
: fromModel;
|
|
118
|
+
catch (error) {
|
|
119
|
+
// eslint-disable-next-line no-console
|
|
120
|
+
console.error('[transcribeAudio] El respaldo de Azure OpenAI también falló:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
121
|
+
return { text: '', motor, confidence: null };
|
|
200
122
|
}
|
|
201
|
-
|
|
123
|
+
});
|
|
124
|
+
/**
|
|
125
|
+
* Cadena completa sobre un audio ya medido: guarda de silencio → motores → guarda de densidad →
|
|
126
|
+
* corrección → una línea de log por audio (motor, duración, dB, confianza, caracteres, latencia).
|
|
127
|
+
*
|
|
128
|
+
* @param params - Audio original, WAV opcional, medidas y contexto
|
|
129
|
+
* @returns Transcripción o marcador
|
|
130
|
+
*/
|
|
131
|
+
const transcribirMedido = (params) => __awaiter(void 0, void 0, void 0, function* () {
|
|
132
|
+
var _a, _b;
|
|
133
|
+
const inicio = Date.now();
|
|
134
|
+
const { duration, meanDb } = params.medida;
|
|
135
|
+
const registrar = (datos) => {
|
|
136
|
+
var _a, _b;
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.log('[transcribeAudio]', JSON.stringify(Object.assign({ id: (_b = (_a = params.contexto) === null || _a === void 0 ? void 0 : _a.idMensaje) !== null && _b !== void 0 ? _b : null, dur: Number(duration.toFixed(1)), db: meanDb, ms: Date.now() - inicio }, datos)));
|
|
139
|
+
};
|
|
140
|
+
if ((0, ffmpegUtils_1.esAudioSinVoz)(duration, meanDb)) {
|
|
141
|
+
registrar({ motor: 'ninguno', resultado: 'sin voz' });
|
|
142
|
+
return MARCADOR_SIN_VOZ;
|
|
143
|
+
}
|
|
144
|
+
const locale = (0, azureSpeech_1.localeFromPreferredLanguage)((_a = params.contexto) === null || _a === void 0 ? void 0 : _a.idioma);
|
|
145
|
+
const phrases = [...(((_b = params.contexto) === null || _b === void 0 ? void 0 : _b.nombres) || []), ...correccionTranscripcion_1.GLOSARIO_RUNNING];
|
|
146
|
+
// ⭐ Solo se convierte a WAV si hay que recortar: Speech acepta el archivo original tal cual
|
|
147
|
+
let wav = params.wav || null;
|
|
148
|
+
const recortado = duration > TRANSCRIPTION_MAX_SECONDS;
|
|
149
|
+
if (recortado && !wav) {
|
|
150
|
+
try {
|
|
151
|
+
wav = (yield convertToWav16k(params.original, params.filename)).wav;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.error('[transcribeAudio] No se pudo recortar el audio, se manda entero:', (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const { text, motor, confidence } = yield reconocer({
|
|
159
|
+
original: params.original,
|
|
160
|
+
wav,
|
|
161
|
+
duration,
|
|
162
|
+
locale,
|
|
163
|
+
phrases,
|
|
164
|
+
mimetype: params.mimetype,
|
|
165
|
+
filename: params.filename,
|
|
166
|
+
});
|
|
167
|
+
if (!text) {
|
|
168
|
+
registrar({ motor, resultado: 'no transcrito' });
|
|
169
|
+
return MARCADOR_NO_TRANSCRITO;
|
|
170
|
+
}
|
|
171
|
+
if (duration > 0 && text.length / duration > MAX_CARACTERES_POR_SEGUNDO) {
|
|
172
|
+
registrar({ motor, resultado: 'densidad imposible', chars: text.length });
|
|
173
|
+
return MARCADOR_NO_TRANSCRITO;
|
|
174
|
+
}
|
|
175
|
+
const { texto, sustituciones } = yield (0, correccionTranscripcion_1.corregirTranscripcion)(text, params.contexto);
|
|
176
|
+
registrar({
|
|
177
|
+
motor,
|
|
178
|
+
conf: confidence,
|
|
179
|
+
chars: texto.length,
|
|
180
|
+
sustituciones: `${sustituciones.filter((sustitucion) => sustitucion.aceptada).length}/${sustituciones.length}`,
|
|
181
|
+
});
|
|
182
|
+
return recortado && wav ? `${texto} (Solo se transcribieron los primeros ${Math.round(TRANSCRIPTION_MAX_SECONDS / 60)} minutos.)` : texto;
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Transcribe una nota de voz.
|
|
186
|
+
*
|
|
187
|
+
* Cadena, toda en Azure: medir duración y volumen con ffmpeg → si no hay voz, marcador sin llamar
|
|
188
|
+
* a nadie → Azure AI Speech con el locale del cliente y una lista de frases (nombres + jerga) →
|
|
189
|
+
* si falla o no oye nada, gpt-4o-mini-transcribe sin prompt → si el texto es físicamente imposible
|
|
190
|
+
* (más de 25 caracteres por segundo), marcador → corrección de jerga y nombres con el modelo LITE
|
|
191
|
+
* detrás de un filtro determinista.
|
|
192
|
+
*
|
|
193
|
+
* ⚠️ Medido sobre 74 notas de voz reales (06-sep-2026): Google Speech-to-Text cambiaba a inglés al
|
|
194
|
+
* azar en 2 de 74 (la identificación multi-idioma) y dejaba vacíos 3 audios con voz; los modelos
|
|
195
|
+
* generativos (Gemini, gpt-audio, transcribe con prompt) escribían una nota entera ante clips en
|
|
196
|
+
* silencio. Azure Speech: 0 invenciones, 0 vacíos con voz, p50 1,2 s.
|
|
197
|
+
*
|
|
198
|
+
* @param fileBuffer - Buffer del archivo de audio (aac, m4a, mp3, wav, ogg…)
|
|
199
|
+
* @param mimetype - Tipo MIME del archivo (ej: 'audio/aac', 'audio/x-m4a')
|
|
200
|
+
* @param filename - Nombre original del archivo
|
|
201
|
+
* @param contexto - Idioma, nombres y últimos mensajes del cliente (opcional)
|
|
202
|
+
* @returns Promise<string> - Transcripción, o un marcador si no hay voz o no se pudo transcribir
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const transcription = await transcribeAudio(audioBuffer, 'audio/aac', 'audio.aac', { idioma: 'es', nombres: ['Ana', 'Rubén'] });
|
|
207
|
+
* // Resultado: "Hola Rubén, quería comentarte que hoy me duele un poco la rodilla..."
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
const transcribeAudio = (fileBuffer, mimetype, filename, contexto) => __awaiter(void 0, void 0, void 0, function* () {
|
|
211
|
+
const medida = yield medirAudio(fileBuffer, filename);
|
|
212
|
+
return transcribirMedido({ original: fileBuffer, medida, mimetype, filename, contexto });
|
|
202
213
|
});
|
|
203
214
|
exports.transcribeAudio = transcribeAudio;
|
|
204
215
|
/**
|
|
205
|
-
* Transcribe un WAV que ya viene en el
|
|
206
|
-
*
|
|
207
|
-
* que extrae la pista así y ya la recorta.
|
|
216
|
+
* Transcribe un WAV que ya viene en PCM 16 bits, mono, 16 kHz. Lo usa el análisis de vídeo, que
|
|
217
|
+
* extrae la pista así y ya la recorta.
|
|
208
218
|
*
|
|
209
|
-
* @param wavBuffer - Buffer WAV mono 16 kHz
|
|
210
|
-
* @
|
|
219
|
+
* @param wavBuffer - Buffer WAV mono 16 kHz
|
|
220
|
+
* @param contexto - Idioma, nombres y últimos mensajes del cliente (opcional)
|
|
221
|
+
* @returns Promise<string> - Transcripción, o un marcador
|
|
211
222
|
*/
|
|
212
|
-
const transcribeWav16k = (wavBuffer) => __awaiter(void 0, void 0, void 0, function* () {
|
|
213
|
-
const
|
|
214
|
-
|
|
223
|
+
const transcribeWav16k = (wavBuffer, contexto) => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
+
const medida = yield medirAudio(wavBuffer, 'audio.wav');
|
|
225
|
+
// ⭐ La duración de un WAV PCM se sabe por los bytes aunque ffprobe fallara
|
|
226
|
+
if (!medida.duration)
|
|
227
|
+
medida.duration = wavBuffer.length / (ffmpegUtils_1.SPEECH_SAMPLE_RATE * 2);
|
|
228
|
+
return transcribirMedido({ original: wavBuffer, wav: wavBuffer, medida, mimetype: 'audio/wav', filename: 'audio.wav', contexto });
|
|
215
229
|
});
|
|
216
230
|
exports.transcribeWav16k = transcribeWav16k;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ✅ Transcripción rápida de Azure AI Speech. Vive en el mismo recurso AIServices que Azure OpenAI,
|
|
3
|
+
// así que va con la misma clave (AZURE_API_KEY) y no hay que desplegar nada.
|
|
4
|
+
// Medido sobre 74 notas de voz reales (06-sep-2026): 0 invenciones, 0 vacíos con voz, p50 1,2 s.
|
|
5
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
6
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
7
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
8
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
9
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
10
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
11
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.SPEECH_API_VERSION = exports.localeFromPreferredLanguage = exports.transcribeWithAzureSpeech = void 0;
|
|
16
|
+
// ✅ Versión que admite `phraseList` (la 2024-11-15 no la tiene)
|
|
17
|
+
const SPEECH_API_VERSION = '2025-10-15';
|
|
18
|
+
exports.SPEECH_API_VERSION = SPEECH_API_VERSION;
|
|
19
|
+
// ✅ Techo generoso: admite audios de hasta 5 h, aunque aquí se recortan a 10 min antes
|
|
20
|
+
const SPEECH_TIMEOUT_MS = 120000;
|
|
21
|
+
// ✅ La lista de frases empuja el reconocimiento hacia esas palabras; con demasiadas deja de ayudar
|
|
22
|
+
const SPEECH_MAX_PHRASES = 80;
|
|
23
|
+
// ✅ Un solo locale por llamada. La identificación automática de idioma entre varios locales es
|
|
24
|
+
// justo lo que hacía que Google devolviera inglés al azar; aquí el idioma viene de la ficha del cliente.
|
|
25
|
+
const LOCALE_POR_IDIOMA = {
|
|
26
|
+
es: 'es-ES',
|
|
27
|
+
en: 'en-US',
|
|
28
|
+
ca: 'ca-ES',
|
|
29
|
+
pt: 'pt-PT',
|
|
30
|
+
fr: 'fr-FR',
|
|
31
|
+
it: 'it-IT',
|
|
32
|
+
de: 'de-DE',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Traduce el idioma preferido del cliente (`CLIENTE."PREFERRED LANGUAGE"`) al locale de Speech
|
|
36
|
+
*
|
|
37
|
+
* @param lang - Código de idioma ('es', 'en', 'ca'…), o vacío
|
|
38
|
+
* @returns Locale BCP-47; 'es-ES' si no se reconoce
|
|
39
|
+
*/
|
|
40
|
+
const localeFromPreferredLanguage = (lang) => LOCALE_POR_IDIOMA[(lang || '').toLowerCase().slice(0, 2)] || 'es-ES';
|
|
41
|
+
exports.localeFromPreferredLanguage = localeFromPreferredLanguage;
|
|
42
|
+
/**
|
|
43
|
+
* URL y clave de la transcripción rápida. Por defecto se derivan del recurso de Azure OpenAI
|
|
44
|
+
* (mismo dominio, mismo secreto); `AZURE_SPEECH_URL` y `AZURE_SPEECH_KEY` permiten separarlos.
|
|
45
|
+
*
|
|
46
|
+
* @returns URL completa del endpoint y clave
|
|
47
|
+
* @throws Error si no hay configuración suficiente
|
|
48
|
+
*/
|
|
49
|
+
const getSpeechConfig = () => {
|
|
50
|
+
const key = process.env.AZURE_SPEECH_KEY || process.env.AZURE_API_KEY;
|
|
51
|
+
if (!key)
|
|
52
|
+
throw new Error('Falta AZURE_SPEECH_KEY o AZURE_API_KEY');
|
|
53
|
+
if (process.env.AZURE_SPEECH_URL)
|
|
54
|
+
return { url: process.env.AZURE_SPEECH_URL, key };
|
|
55
|
+
if (!process.env.AZURE_BASE_URL)
|
|
56
|
+
throw new Error('Falta AZURE_SPEECH_URL o AZURE_BASE_URL');
|
|
57
|
+
const origin = new URL(process.env.AZURE_BASE_URL).origin;
|
|
58
|
+
return { url: `${origin}/speechtotext/transcriptions:transcribe?api-version=${SPEECH_API_VERSION}`, key };
|
|
59
|
+
};
|
|
60
|
+
const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
|
+
/**
|
|
62
|
+
* Transcribe un audio con la transcripción rápida de Azure AI Speech.
|
|
63
|
+
*
|
|
64
|
+
* Acepta el archivo tal cual (aac, m4a, mp3, wav, ogg…): Speech detecta el formato por contenido,
|
|
65
|
+
* así que no hace falta convertir a WAV. Sin filtro de palabrotas, porque «joder» forma parte de
|
|
66
|
+
* lo que el cliente dijo y el entrenador tiene que verlo.
|
|
67
|
+
*
|
|
68
|
+
* @param audio - Buffer del audio
|
|
69
|
+
* @param opts - locale del cliente, frases a reforzar (nombres, jerga) y MIME type si se conoce
|
|
70
|
+
* @returns Texto, confianza media de las frases y duración detectada
|
|
71
|
+
* @throws Error si la API responde con error (tras un reintento en 429/5xx)
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const { text } = await transcribeWithAzureSpeech(buffer, { locale: 'es-ES', phrases: ['Rubén', 'fartlek'] });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
const transcribeWithAzureSpeech = (audio, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
const { url, key } = getSpeechConfig();
|
|
80
|
+
const phrases = [...new Set((opts.phrases || []).map((phrase) => (phrase || '').trim()).filter(Boolean))].slice(0, SPEECH_MAX_PHRASES);
|
|
81
|
+
const definition = { locales: [opts.locale], profanityFilterMode: 'None' };
|
|
82
|
+
if (phrases.length)
|
|
83
|
+
definition.phraseList = { phrases };
|
|
84
|
+
const enviar = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
const form = new FormData();
|
|
86
|
+
form.append('audio', new Blob([audio], { type: opts.mimetype || 'application/octet-stream' }), 'audio');
|
|
87
|
+
form.append('definition', JSON.stringify(definition));
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), SPEECH_TIMEOUT_MS);
|
|
90
|
+
try {
|
|
91
|
+
return yield fetch(url, { method: 'POST', headers: { 'Ocp-Apim-Subscription-Key': key }, body: form, signal: controller.signal });
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
let response = yield enviar();
|
|
98
|
+
if (response.status === 429 || response.status >= 500) {
|
|
99
|
+
yield esperar(3000);
|
|
100
|
+
response = yield enviar();
|
|
101
|
+
}
|
|
102
|
+
const body = yield response.json().catch(() => ({}));
|
|
103
|
+
if (!response.ok)
|
|
104
|
+
throw new Error(`Azure Speech HTTP ${response.status}: ${JSON.stringify(body).slice(0, 200)}`);
|
|
105
|
+
const text = (body.combinedPhrases || [])
|
|
106
|
+
.map((phrase) => phrase === null || phrase === void 0 ? void 0 : phrase.text)
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join(' ')
|
|
109
|
+
.trim();
|
|
110
|
+
const confidences = (body.phrases || []).map((phrase) => phrase === null || phrase === void 0 ? void 0 : phrase.confidence).filter((value) => typeof value === 'number');
|
|
111
|
+
const confidence = confidences.length ? Number((confidences.reduce((sum, value) => sum + value, 0) / confidences.length).toFixed(3)) : null;
|
|
112
|
+
return { text, confidence, durationMs: typeof body.durationMilliseconds === 'number' ? body.durationMilliseconds : null };
|
|
113
|
+
});
|
|
114
|
+
exports.transcribeWithAzureSpeech = transcribeWithAzureSpeech;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ✅ Respaldo de transcripción con Azure OpenAI (gpt-4o-mini-transcribe). Solo entra cuando Azure
|
|
3
|
+
// Speech falla o devuelve vacío con señal. Va SIN `prompt` a propósito: con un prompt de vocabulario
|
|
4
|
+
// estos modelos se inventan una nota de voz entera ante un clip en silencio (medido 06-sep-2026).
|
|
5
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
6
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
7
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
8
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
9
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
10
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
11
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.TRANSCRIBE_API_VERSION = exports.getTranscribeConfig = exports.transcribeWithAzureOpenAI = void 0;
|
|
16
|
+
// ✅ La ruta v1 (/openai/v1/audio/transcriptions) devuelve DeploymentNotFound en este recurso;
|
|
17
|
+
// la de deployments/ sí responde
|
|
18
|
+
const TRANSCRIBE_API_VERSION = '2025-04-01-preview';
|
|
19
|
+
exports.TRANSCRIBE_API_VERSION = TRANSCRIBE_API_VERSION;
|
|
20
|
+
const TRANSCRIBE_TIMEOUT_MS = 120000;
|
|
21
|
+
const TRANSCRIBE_DEPLOYMENT_POR_DEFECTO = 'gpt-4o-mini-transcribe';
|
|
22
|
+
const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
/**
|
|
24
|
+
* URL y clave del despliegue de transcripción de Azure OpenAI
|
|
25
|
+
*
|
|
26
|
+
* @returns URL completa del endpoint y clave
|
|
27
|
+
* @throws Error si falta AZURE_BASE_URL o AZURE_API_KEY
|
|
28
|
+
*/
|
|
29
|
+
const getTranscribeConfig = () => {
|
|
30
|
+
const key = process.env.AZURE_API_KEY;
|
|
31
|
+
const base = (process.env.AZURE_BASE_URL || '').replace(/\/$/, '');
|
|
32
|
+
if (!key || !base)
|
|
33
|
+
throw new Error('Faltan AZURE_BASE_URL o AZURE_API_KEY');
|
|
34
|
+
const deployment = process.env.AZURE_TRANSCRIBE_DEPLOYMENT || TRANSCRIBE_DEPLOYMENT_POR_DEFECTO;
|
|
35
|
+
return { url: `${base}/deployments/${deployment}/audio/transcriptions?api-version=${TRANSCRIBE_API_VERSION}`, key, deployment };
|
|
36
|
+
};
|
|
37
|
+
exports.getTranscribeConfig = getTranscribeConfig;
|
|
38
|
+
/**
|
|
39
|
+
* Transcribe un WAV con el despliegue de transcripción de Azure OpenAI, sin prompt.
|
|
40
|
+
*
|
|
41
|
+
* @param wav - Buffer WAV (mono 16 kHz, como sale de `convertToWav16k`)
|
|
42
|
+
* @param opts - Idioma en código de dos letras ('es', 'en'…)
|
|
43
|
+
* @returns Texto transcrito; cadena vacía si el modelo no oyó nada (un "." suelto cuenta como vacío)
|
|
44
|
+
* @throws Error si la API responde con error (tras un reintento en 429/5xx)
|
|
45
|
+
*/
|
|
46
|
+
const transcribeWithAzureOpenAI = (wav, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
const { url, key } = getTranscribeConfig();
|
|
48
|
+
const enviar = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
const form = new FormData();
|
|
50
|
+
form.append('file', new Blob([wav], { type: 'audio/wav' }), 'audio.wav');
|
|
51
|
+
form.append('language', opts.language);
|
|
52
|
+
form.append('response_format', 'json');
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => controller.abort(), TRANSCRIBE_TIMEOUT_MS);
|
|
55
|
+
try {
|
|
56
|
+
return yield fetch(url, { method: 'POST', headers: { 'api-key': key }, body: form, signal: controller.signal });
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
let response = yield enviar();
|
|
63
|
+
if (response.status === 429 || response.status >= 500) {
|
|
64
|
+
yield esperar(3000);
|
|
65
|
+
response = yield enviar();
|
|
66
|
+
}
|
|
67
|
+
const body = yield response.json().catch(() => ({}));
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
throw new Error(`Azure OpenAI transcribe HTTP ${response.status}: ${JSON.stringify(body).slice(0, 200)}`);
|
|
70
|
+
const text = (body.text || '').trim();
|
|
71
|
+
return /^[.\s]*$/.test(text) ? '' : text;
|
|
72
|
+
});
|
|
73
|
+
exports.transcribeWithAzureOpenAI = transcribeWithAzureOpenAI;
|