feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)
New features: - HLS video encoding/streaming (hls routes, hls-encoder) and a media transcripts pipeline (transcripts route + media-transcript queries) - AI assistant chat vs. run model: agent-course-generation worker/queue, run-monitor pane, chat-changes card, content ask-ai bar - Per-theme course-page previews across org landing-page themes (bold, classic, corporate, editorial, etc.) with a shared course token/component set - View-as-student mode and login-link / link-invite invite flows Refactor: - Extract packages/core consolidating config (env, storage, dashboard-url), services and utils previously duplicated under apps/api/src/config - Remove legacy agent service modules (chat-tools, chat-context, context-window, document, fetch-url, lesson-content) - Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding - Add format script to tenant-router so turbo run format covers it
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
'use strict';
|
||||
var __createBinding =
|
||||
(this && this.__createBinding) ||
|
||||
(Object.create
|
||||
? function (o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return m[k];
|
||||
}
|
||||
};
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}
|
||||
: function (o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
});
|
||||
var __exportStar =
|
||||
(this && this.__exportStar) ||
|
||||
function (m, exports) {
|
||||
for (var p in m)
|
||||
if (p !== 'default' && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
__exportStar(require('./send'), exports);
|
||||
@@ -1,217 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter =
|
||||
(this && this.__awaiter) ||
|
||||
function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) {
|
||||
return value instanceof P
|
||||
? value
|
||||
: new P(function (resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
}
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function rejected(value) {
|
||||
try {
|
||||
step(generator['throw'](value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function step(result) {
|
||||
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
||||
}
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator =
|
||||
(this && this.__generator) ||
|
||||
function (thisArg, body) {
|
||||
var _ = {
|
||||
label: 0,
|
||||
sent: function () {
|
||||
if (t[0] & 1) throw t[1];
|
||||
return t[1];
|
||||
},
|
||||
trys: [],
|
||||
ops: []
|
||||
},
|
||||
f,
|
||||
y,
|
||||
t,
|
||||
g = Object.create((typeof Iterator === 'function' ? Iterator : Object).prototype);
|
||||
return (
|
||||
(g.next = verb(0)),
|
||||
(g['throw'] = verb(1)),
|
||||
(g['return'] = verb(2)),
|
||||
typeof Symbol === 'function' &&
|
||||
(g[Symbol.iterator] = function () {
|
||||
return this;
|
||||
}),
|
||||
g
|
||||
);
|
||||
function verb(n) {
|
||||
return function (v) {
|
||||
return step([n, v]);
|
||||
};
|
||||
}
|
||||
function step(op) {
|
||||
if (f) throw new TypeError('Generator is already executing.');
|
||||
while ((g && ((g = 0), op[0] && (_ = 0)), _))
|
||||
try {
|
||||
if (
|
||||
((f = 1),
|
||||
y &&
|
||||
(t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) &&
|
||||
!(t = t.call(y, op[1])).done)
|
||||
)
|
||||
return t;
|
||||
if (((y = 0), t)) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
t = op;
|
||||
break;
|
||||
case 4:
|
||||
_.label++;
|
||||
return { value: op[1], done: false };
|
||||
case 5:
|
||||
_.label++;
|
||||
y = op[1];
|
||||
op = [0];
|
||||
continue;
|
||||
case 7:
|
||||
op = _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
default:
|
||||
if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) {
|
||||
_ = 0;
|
||||
continue;
|
||||
}
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
|
||||
_.label = op[1];
|
||||
break;
|
||||
}
|
||||
if (op[0] === 6 && _.label < t[1]) {
|
||||
_.label = t[1];
|
||||
t = op;
|
||||
break;
|
||||
}
|
||||
if (t && _.label < t[2]) {
|
||||
_.label = t[2];
|
||||
_.ops.push(op);
|
||||
break;
|
||||
}
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) {
|
||||
op = [6, e];
|
||||
y = 0;
|
||||
} finally {
|
||||
f = t = 0;
|
||||
}
|
||||
if (op[0] & 5) throw op[1];
|
||||
return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.processSendEmail = processSendEmail;
|
||||
var email_1 = require('@cio/email');
|
||||
var jobs_1 = require('@cio/jobs');
|
||||
var logger_1 = require('../../utils/logger');
|
||||
/**
|
||||
* Process an `emails:send` job. Dispatches based on payload `kind`, calls the
|
||||
* provider via `@cio/email`, and returns a provider message id (BullMQ stores
|
||||
* this as the job return value).
|
||||
*/
|
||||
function processSendEmail(rawPayload) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var payload, responses_1, result_1, responses, result;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
payload = jobs_1.ZSendEmailPayload.parse(rawPayload);
|
||||
if (!(payload.kind === 'template')) return [3 /*break*/, 2];
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, email_1.sendEmail)(payload.template, {
|
||||
to: payload.to,
|
||||
// Re-validated by `sendEmail` against the registered template schema; the
|
||||
// generic record was already validated in the API helper before enqueue.
|
||||
fields: payload.fields,
|
||||
from: payload.from,
|
||||
replyTo: payload.replyTo
|
||||
})
|
||||
];
|
||||
case 1:
|
||||
responses_1 = _a.sent();
|
||||
result_1 = extractProviderId(responses_1);
|
||||
logger_1.log.info('email-sent', {
|
||||
kind: 'template',
|
||||
template: payload.template,
|
||||
recipient: payload.to,
|
||||
providerId: result_1.providerId
|
||||
});
|
||||
return [2 /*return*/, result_1];
|
||||
case 2:
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, email_1.deliverEmail)([
|
||||
{
|
||||
to: payload.to,
|
||||
subject: payload.subject,
|
||||
content: payload.content,
|
||||
from: payload.from,
|
||||
replyTo: payload.replyTo
|
||||
}
|
||||
])
|
||||
];
|
||||
case 3:
|
||||
responses = _a.sent();
|
||||
result = extractProviderId(responses);
|
||||
logger_1.log.info('email-sent', { kind: 'raw', recipient: payload.to, providerId: result.providerId });
|
||||
return [2 /*return*/, result];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function extractProviderId(responses) {
|
||||
var _a, _b, _c, _d;
|
||||
var failed = responses.find(function (response) {
|
||||
return !response.success;
|
||||
});
|
||||
if (failed) {
|
||||
throw new Error(
|
||||
(_a = failed.error) !== null && _a !== void 0 ? _a : 'email provider returned an unsuccessful response'
|
||||
);
|
||||
}
|
||||
for (var _i = 0, responses_2 = responses; _i < responses_2.length; _i++) {
|
||||
var response = responses_2[_i];
|
||||
var details = response.details;
|
||||
var candidate =
|
||||
(_c =
|
||||
(_b = details === null || details === void 0 ? void 0 : details.messageId) !== null && _b !== void 0
|
||||
? _b
|
||||
: details === null || details === void 0
|
||||
? void 0
|
||||
: details.request_id) !== null && _c !== void 0
|
||||
? _c
|
||||
: (_d = details === null || details === void 0 ? void 0 : details.data) === null || _d === void 0
|
||||
? void 0
|
||||
: _d.request_id;
|
||||
if (candidate) {
|
||||
return { providerId: String(candidate) };
|
||||
}
|
||||
}
|
||||
return { providerId: '' };
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter =
|
||||
(this && this.__awaiter) ||
|
||||
function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) {
|
||||
return value instanceof P
|
||||
? value
|
||||
: new P(function (resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
}
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function rejected(value) {
|
||||
try {
|
||||
step(generator['throw'](value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function step(result) {
|
||||
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
||||
}
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator =
|
||||
(this && this.__generator) ||
|
||||
function (thisArg, body) {
|
||||
var _ = {
|
||||
label: 0,
|
||||
sent: function () {
|
||||
if (t[0] & 1) throw t[1];
|
||||
return t[1];
|
||||
},
|
||||
trys: [],
|
||||
ops: []
|
||||
},
|
||||
f,
|
||||
y,
|
||||
t,
|
||||
g = Object.create((typeof Iterator === 'function' ? Iterator : Object).prototype);
|
||||
return (
|
||||
(g.next = verb(0)),
|
||||
(g['throw'] = verb(1)),
|
||||
(g['return'] = verb(2)),
|
||||
typeof Symbol === 'function' &&
|
||||
(g[Symbol.iterator] = function () {
|
||||
return this;
|
||||
}),
|
||||
g
|
||||
);
|
||||
function verb(n) {
|
||||
return function (v) {
|
||||
return step([n, v]);
|
||||
};
|
||||
}
|
||||
function step(op) {
|
||||
if (f) throw new TypeError('Generator is already executing.');
|
||||
while ((g && ((g = 0), op[0] && (_ = 0)), _))
|
||||
try {
|
||||
if (
|
||||
((f = 1),
|
||||
y &&
|
||||
(t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) &&
|
||||
!(t = t.call(y, op[1])).done)
|
||||
)
|
||||
return t;
|
||||
if (((y = 0), t)) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
t = op;
|
||||
break;
|
||||
case 4:
|
||||
_.label++;
|
||||
return { value: op[1], done: false };
|
||||
case 5:
|
||||
_.label++;
|
||||
y = op[1];
|
||||
op = [0];
|
||||
continue;
|
||||
case 7:
|
||||
op = _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
default:
|
||||
if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) {
|
||||
_ = 0;
|
||||
continue;
|
||||
}
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
|
||||
_.label = op[1];
|
||||
break;
|
||||
}
|
||||
if (op[0] === 6 && _.label < t[1]) {
|
||||
_.label = t[1];
|
||||
t = op;
|
||||
break;
|
||||
}
|
||||
if (t && _.label < t[2]) {
|
||||
_.label = t[2];
|
||||
_.ops.push(op);
|
||||
break;
|
||||
}
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) {
|
||||
op = [6, e];
|
||||
y = 0;
|
||||
} finally {
|
||||
f = t = 0;
|
||||
}
|
||||
if (op[0] & 5) throw op[1];
|
||||
return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.processExtractAudio = processExtractAudio;
|
||||
var promises_1 = require('node:fs/promises');
|
||||
var node_path_1 = require('node:path');
|
||||
var node_os_1 = require('node:os');
|
||||
var queries_1 = require('@cio/db/queries');
|
||||
var media_1 = require('@cio/jobs/payloads/media');
|
||||
var storage_1 = require('../../utils/storage');
|
||||
var cancel_1 = require('../../utils/cancel');
|
||||
var ffmpeg_1 = require('../../utils/ffmpeg');
|
||||
var logger_1 = require('../../utils/logger');
|
||||
var STEP_KEY = 'extract-audio';
|
||||
var DOMAIN = 'media';
|
||||
function transcodeToMp3(inputPath, outputPath) {
|
||||
return (0, ffmpeg_1.ffmpegRun)([
|
||||
'-y',
|
||||
'-i',
|
||||
inputPath,
|
||||
'-vn',
|
||||
'-acodec',
|
||||
'libmp3lame',
|
||||
'-b:a',
|
||||
'32k',
|
||||
'-ac',
|
||||
'1',
|
||||
'-ar',
|
||||
'16000',
|
||||
'-f',
|
||||
'mp3',
|
||||
outputPath
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Extract a 16 kHz mono MP3 (32 kbps) from the source video for Whisper (25 MB
|
||||
* API limit). The transcribe step reads the object from the media bucket.
|
||||
*/
|
||||
function processExtractAudio(rawData) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var payload,
|
||||
mediaJobId,
|
||||
assetId,
|
||||
storageKey,
|
||||
existing,
|
||||
inputPath,
|
||||
outputPath,
|
||||
dir,
|
||||
audioKey,
|
||||
fileStat,
|
||||
result,
|
||||
error_1;
|
||||
var _a;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
payload = media_1.ZExtractAudioPayload.parse(rawData);
|
||||
((mediaJobId = payload.mediaJobId), (assetId = payload.assetId), (storageKey = payload.storageKey));
|
||||
return [4 /*yield*/, (0, queries_1.getJobStep)(DOMAIN, mediaJobId, STEP_KEY)];
|
||||
case 1:
|
||||
existing = _b.sent();
|
||||
if (
|
||||
(existing === null || existing === void 0 ? void 0 : existing.status) === 'completed' &&
|
||||
existing.result
|
||||
) {
|
||||
logger_1.log.info('extract-audio-skip', { mediaJobId: mediaJobId, reason: 'ledger-completed' });
|
||||
return [2 /*return*/, existing.result];
|
||||
}
|
||||
return [4 /*yield*/, (0, cancel_1.throwIfCancelRequested)(mediaJobId)];
|
||||
case 2:
|
||||
_b.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, { stage: 'extracting-audio', progressPercent: 70 })
|
||||
];
|
||||
case 3:
|
||||
_b.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'running',
|
||||
startedAt: new Date().toISOString(),
|
||||
attempt:
|
||||
((_a = existing === null || existing === void 0 ? void 0 : existing.attempt) !== null && _a !== void 0
|
||||
? _a
|
||||
: 0) + 1
|
||||
})
|
||||
];
|
||||
case 4:
|
||||
_b.sent();
|
||||
_b.label = 5;
|
||||
case 5:
|
||||
_b.trys.push([5, 14, 16, 19]);
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, storage_1.downloadObjectToTempFile)((0, storage_1.videosBucket)(), storageKey, 'audio-source.bin')
|
||||
];
|
||||
case 6:
|
||||
inputPath = _b.sent();
|
||||
dir = node_path_1.default.join((0, node_os_1.tmpdir)(), 'cio-jobs', 'audio');
|
||||
return [4 /*yield*/, (0, promises_1.mkdir)(dir, { recursive: true })];
|
||||
case 7:
|
||||
_b.sent();
|
||||
outputPath = node_path_1.default.join(dir, ''.concat(mediaJobId, '.mp3'));
|
||||
return [4 /*yield*/, (0, cancel_1.throwIfCancelRequested)(mediaJobId)];
|
||||
case 8:
|
||||
_b.sent();
|
||||
return [4 /*yield*/, transcodeToMp3(inputPath, outputPath)];
|
||||
case 9:
|
||||
_b.sent();
|
||||
audioKey = 'audio/'.concat(assetId, '/').concat(mediaJobId, '.mp3');
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, storage_1.uploadFileToBucket)(
|
||||
(0, storage_1.mediaBucket)(),
|
||||
audioKey,
|
||||
outputPath,
|
||||
'audio/mpeg',
|
||||
'private, max-age=0'
|
||||
)
|
||||
];
|
||||
case 10:
|
||||
_b.sent();
|
||||
return [4 /*yield*/, (0, promises_1.stat)(outputPath)];
|
||||
case 11:
|
||||
fileStat = _b.sent();
|
||||
result = { bucket: (0, storage_1.mediaBucket)(), key: audioKey, fileSizeBytes: fileStat.size };
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'completed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
result: result
|
||||
})
|
||||
];
|
||||
case 12:
|
||||
_b.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, { stage: 'audio-ready', progressPercent: 80 })
|
||||
];
|
||||
case 13:
|
||||
_b.sent();
|
||||
logger_1.log.info('extract-audio-done', { mediaJobId: mediaJobId, audioKey: audioKey });
|
||||
return [2 /*return*/, result];
|
||||
case 14:
|
||||
error_1 = _b.sent();
|
||||
logger_1.log.error('extract-audio-failed', {
|
||||
mediaJobId: mediaJobId,
|
||||
error: (0, cancel_1.errorMessage)(error_1)
|
||||
});
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'failed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
error: { code: 'EXTRACT_AUDIO_FAILED', message: (0, cancel_1.errorMessage)(error_1) }
|
||||
})
|
||||
];
|
||||
case 15:
|
||||
_b.sent();
|
||||
throw error_1;
|
||||
case 16:
|
||||
return [4 /*yield*/, (0, storage_1.safeUnlink)(inputPath)];
|
||||
case 17:
|
||||
_b.sent();
|
||||
return [4 /*yield*/, (0, storage_1.safeUnlink)(outputPath)];
|
||||
case 18:
|
||||
_b.sent();
|
||||
return [7 /*endfinally*/];
|
||||
case 19:
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter =
|
||||
(this && this.__awaiter) ||
|
||||
function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) {
|
||||
return value instanceof P
|
||||
? value
|
||||
: new P(function (resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
}
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function rejected(value) {
|
||||
try {
|
||||
step(generator['throw'](value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function step(result) {
|
||||
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
||||
}
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator =
|
||||
(this && this.__generator) ||
|
||||
function (thisArg, body) {
|
||||
var _ = {
|
||||
label: 0,
|
||||
sent: function () {
|
||||
if (t[0] & 1) throw t[1];
|
||||
return t[1];
|
||||
},
|
||||
trys: [],
|
||||
ops: []
|
||||
},
|
||||
f,
|
||||
y,
|
||||
t,
|
||||
g = Object.create((typeof Iterator === 'function' ? Iterator : Object).prototype);
|
||||
return (
|
||||
(g.next = verb(0)),
|
||||
(g['throw'] = verb(1)),
|
||||
(g['return'] = verb(2)),
|
||||
typeof Symbol === 'function' &&
|
||||
(g[Symbol.iterator] = function () {
|
||||
return this;
|
||||
}),
|
||||
g
|
||||
);
|
||||
function verb(n) {
|
||||
return function (v) {
|
||||
return step([n, v]);
|
||||
};
|
||||
}
|
||||
function step(op) {
|
||||
if (f) throw new TypeError('Generator is already executing.');
|
||||
while ((g && ((g = 0), op[0] && (_ = 0)), _))
|
||||
try {
|
||||
if (
|
||||
((f = 1),
|
||||
y &&
|
||||
(t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) &&
|
||||
!(t = t.call(y, op[1])).done)
|
||||
)
|
||||
return t;
|
||||
if (((y = 0), t)) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
t = op;
|
||||
break;
|
||||
case 4:
|
||||
_.label++;
|
||||
return { value: op[1], done: false };
|
||||
case 5:
|
||||
_.label++;
|
||||
y = op[1];
|
||||
op = [0];
|
||||
continue;
|
||||
case 7:
|
||||
op = _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
default:
|
||||
if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) {
|
||||
_ = 0;
|
||||
continue;
|
||||
}
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
|
||||
_.label = op[1];
|
||||
break;
|
||||
}
|
||||
if (op[0] === 6 && _.label < t[1]) {
|
||||
_.label = t[1];
|
||||
t = op;
|
||||
break;
|
||||
}
|
||||
if (t && _.label < t[2]) {
|
||||
_.label = t[2];
|
||||
_.ops.push(op);
|
||||
break;
|
||||
}
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) {
|
||||
op = [6, e];
|
||||
y = 0;
|
||||
} finally {
|
||||
f = t = 0;
|
||||
}
|
||||
if (op[0] & 5) throw op[1];
|
||||
return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.processGenerateThumbnail = processGenerateThumbnail;
|
||||
var promises_1 = require('node:fs/promises');
|
||||
var node_path_1 = require('node:path');
|
||||
var node_os_1 = require('node:os');
|
||||
var queries_1 = require('@cio/db/queries');
|
||||
var media_1 = require('@cio/jobs/payloads/media');
|
||||
var storage_1 = require('../../utils/storage');
|
||||
var cancel_1 = require('../../utils/cancel');
|
||||
var storage_2 = require('../../config/storage');
|
||||
var ffmpeg_1 = require('../../utils/ffmpeg');
|
||||
var logger_1 = require('../../utils/logger');
|
||||
var STEP_KEY = 'generate-thumbnail';
|
||||
var DOMAIN = 'media';
|
||||
function extractThumbnail(localPath, atSeconds, outputPath) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, ffmpeg_1.ffmpegRun)([
|
||||
'-y',
|
||||
'-ss',
|
||||
String(atSeconds),
|
||||
'-i',
|
||||
localPath,
|
||||
'-vframes',
|
||||
'1',
|
||||
'-vf',
|
||||
'scale=640:-1',
|
||||
outputPath
|
||||
])
|
||||
];
|
||||
case 1:
|
||||
_a.sent();
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Render a single JPEG frame to use as the asset thumbnail. Replaces the
|
||||
* browser-side thumbnail as the source of truth on the asset row.
|
||||
*/
|
||||
function processGenerateThumbnail(rawData) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var payload,
|
||||
mediaJobId,
|
||||
assetId,
|
||||
storageKey,
|
||||
actorContext,
|
||||
_a,
|
||||
atSeconds,
|
||||
existing,
|
||||
localPath,
|
||||
outputPath,
|
||||
dir,
|
||||
buffer,
|
||||
thumbKey,
|
||||
baseUrl,
|
||||
publicUrl,
|
||||
result,
|
||||
error_1;
|
||||
var _b;
|
||||
return __generator(this, function (_c) {
|
||||
switch (_c.label) {
|
||||
case 0:
|
||||
payload = media_1.ZGenerateThumbnailPayload.parse(rawData);
|
||||
((mediaJobId = payload.mediaJobId),
|
||||
(assetId = payload.assetId),
|
||||
(storageKey = payload.storageKey),
|
||||
(actorContext = payload.actorContext),
|
||||
(_a = payload.atSeconds),
|
||||
(atSeconds = _a === void 0 ? 1.0 : _a));
|
||||
return [4 /*yield*/, (0, queries_1.getJobStep)(DOMAIN, mediaJobId, STEP_KEY)];
|
||||
case 1:
|
||||
existing = _c.sent();
|
||||
if (
|
||||
(existing === null || existing === void 0 ? void 0 : existing.status) === 'completed' &&
|
||||
existing.result
|
||||
) {
|
||||
logger_1.log.info('generate-thumbnail-skip', { mediaJobId: mediaJobId, reason: 'ledger-completed' });
|
||||
return [2 /*return*/, existing.result];
|
||||
}
|
||||
return [4 /*yield*/, (0, cancel_1.throwIfCancelRequested)(mediaJobId)];
|
||||
case 2:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, { stage: 'thumbnailing', progressPercent: 35 })
|
||||
];
|
||||
case 3:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'running',
|
||||
startedAt: new Date().toISOString(),
|
||||
attempt:
|
||||
((_b = existing === null || existing === void 0 ? void 0 : existing.attempt) !== null && _b !== void 0
|
||||
? _b
|
||||
: 0) + 1
|
||||
})
|
||||
];
|
||||
case 4:
|
||||
_c.sent();
|
||||
_c.label = 5;
|
||||
case 5:
|
||||
_c.trys.push([5, 15, 17, 20]);
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, storage_1.downloadObjectToTempFile)((0, storage_1.videosBucket)(), storageKey, 'thumb-source.bin')
|
||||
];
|
||||
case 6:
|
||||
localPath = _c.sent();
|
||||
dir = node_path_1.default.join((0, node_os_1.tmpdir)(), 'cio-jobs', 'thumbs');
|
||||
return [4 /*yield*/, (0, promises_1.mkdir)(dir, { recursive: true })];
|
||||
case 7:
|
||||
_c.sent();
|
||||
outputPath = node_path_1.default.join(dir, ''.concat(mediaJobId, '.jpg'));
|
||||
return [4 /*yield*/, (0, cancel_1.throwIfCancelRequested)(mediaJobId)];
|
||||
case 8:
|
||||
_c.sent();
|
||||
return [4 /*yield*/, extractThumbnail(localPath, atSeconds, outputPath)];
|
||||
case 9:
|
||||
_c.sent();
|
||||
return [4 /*yield*/, (0, promises_1.readFile)(outputPath)];
|
||||
case 10:
|
||||
buffer = _c.sent();
|
||||
thumbKey = 'thumbnails/'.concat(assetId, '/').concat(Date.now(), '.jpg');
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, storage_1.uploadBufferToBucket)(
|
||||
(0, storage_1.mediaBucket)(),
|
||||
thumbKey,
|
||||
buffer,
|
||||
'image/jpeg',
|
||||
'public, max-age=31536000'
|
||||
)
|
||||
];
|
||||
case 11:
|
||||
_c.sent();
|
||||
baseUrl = (0, storage_2.getStorageConfig)().mediaPublicBaseUrl;
|
||||
publicUrl = baseUrl ? ''.concat(baseUrl.replace(/\/$/, ''), '/').concat(thumbKey) : null;
|
||||
result = { bucket: (0, storage_1.mediaBucket)(), key: thumbKey, publicUrl: publicUrl };
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateAsset)(assetId, actorContext.organizationId, {
|
||||
thumbnailUrl: publicUrl !== null && publicUrl !== void 0 ? publicUrl : undefined
|
||||
})
|
||||
];
|
||||
case 12:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'completed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
result: result
|
||||
})
|
||||
];
|
||||
case 13:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, { stage: 'thumbnailed', progressPercent: 60 })
|
||||
];
|
||||
case 14:
|
||||
_c.sent();
|
||||
logger_1.log.info('generate-thumbnail-done', { mediaJobId: mediaJobId, thumbKey: thumbKey });
|
||||
return [2 /*return*/, result];
|
||||
case 15:
|
||||
error_1 = _c.sent();
|
||||
logger_1.log.error('generate-thumbnail-failed', {
|
||||
mediaJobId: mediaJobId,
|
||||
error: (0, cancel_1.errorMessage)(error_1)
|
||||
});
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'failed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
error: { code: 'THUMBNAIL_FAILED', message: (0, cancel_1.errorMessage)(error_1) }
|
||||
})
|
||||
];
|
||||
case 16:
|
||||
_c.sent();
|
||||
throw error_1;
|
||||
case 17:
|
||||
return [4 /*yield*/, (0, storage_1.safeUnlink)(localPath)];
|
||||
case 18:
|
||||
_c.sent();
|
||||
return [4 /*yield*/, (0, storage_1.safeUnlink)(outputPath)];
|
||||
case 19:
|
||||
_c.sent();
|
||||
return [7 /*endfinally*/];
|
||||
case 20:
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -27,6 +27,13 @@ type Slot = (typeof SLOT_ORDER)[number];
|
||||
/** Fallback time offsets (seconds) tried when a slot's primary frame is black. */
|
||||
const FALLBACK_OFFSETS = [0, -1.5, 1.5];
|
||||
const DEFAULT_DURATION_SECONDS = 10;
|
||||
/** Hard ceiling for a stored thumbnail. Frames over this are re-encoded at lower quality. */
|
||||
const MAX_THUMBNAIL_BYTES = 500 * 1024;
|
||||
/**
|
||||
* mjpeg `-q:v` ladder (2 = best/largest, 31 = worst/smallest). We start at the
|
||||
* top and step down only when a frame exceeds MAX_THUMBNAIL_BYTES.
|
||||
*/
|
||||
const QSCALE_LADDER = [2, 5, 9, 15] as const;
|
||||
|
||||
interface CandidateUpload {
|
||||
slot: Slot;
|
||||
@@ -58,7 +65,12 @@ function targetForSlot(slot: Slot, duration: number): number {
|
||||
* keyframe and is the most common source of black thumbnails for codecs whose
|
||||
* first keyframe sits past the requested offset.
|
||||
*/
|
||||
async function extractFrame(localPath: string, atSeconds: number, outputPath: string): Promise<void> {
|
||||
async function extractFrame(
|
||||
localPath: string,
|
||||
atSeconds: number,
|
||||
outputPath: string,
|
||||
qscale: number = QSCALE_LADDER[0]
|
||||
): Promise<void> {
|
||||
await ffmpegRun([
|
||||
'-y',
|
||||
'-i',
|
||||
@@ -68,11 +80,38 @@ async function extractFrame(localPath: string, atSeconds: number, outputPath: st
|
||||
'-vframes',
|
||||
'1',
|
||||
'-vf',
|
||||
'scale=640:-1',
|
||||
"scale='min(1280,iw)':-2",
|
||||
'-q:v',
|
||||
String(qscale),
|
||||
outputPath
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-encode the frame at `atSeconds` with progressively lower quality until it
|
||||
* fits under MAX_THUMBNAIL_BYTES. Returns the smallest buffer produced if even
|
||||
* the worst rung overshoots. Assumes the frame was already extracted once at
|
||||
* QSCALE_LADDER[0].
|
||||
*/
|
||||
async function encodeUnderSizeCap(
|
||||
localPath: string,
|
||||
atSeconds: number,
|
||||
outputPath: string,
|
||||
buffer: Buffer
|
||||
): Promise<Buffer> {
|
||||
if (buffer.length <= MAX_THUMBNAIL_BYTES) return buffer;
|
||||
|
||||
let smallest = buffer;
|
||||
for (const qscale of QSCALE_LADDER.slice(1)) {
|
||||
await extractFrame(localPath, atSeconds, outputPath, qscale);
|
||||
const reencoded = await readFile(outputPath);
|
||||
if (reencoded.length < smallest.length) smallest = reencoded;
|
||||
if (reencoded.length <= MAX_THUMBNAIL_BYTES) return reencoded;
|
||||
}
|
||||
|
||||
return smallest;
|
||||
}
|
||||
|
||||
async function captureSlot(
|
||||
localPath: string,
|
||||
slot: Slot,
|
||||
@@ -86,7 +125,8 @@ async function captureSlot(
|
||||
await extractFrame(localPath, t, outputPath);
|
||||
const luma = await ffmpegProbeLuma(outputPath);
|
||||
if (luma >= BLACK_LUMA_THRESHOLD) {
|
||||
const buffer = await readFile(outputPath);
|
||||
const raw = await readFile(outputPath);
|
||||
const buffer = await encodeUnderSizeCap(localPath, t, outputPath, raw);
|
||||
return { buffer, meanLuma: luma, atSeconds: t };
|
||||
}
|
||||
log.warn('generate-thumbnail-black-frame', { slot, atSeconds: t, luma });
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
'use strict';
|
||||
var __createBinding =
|
||||
(this && this.__createBinding) ||
|
||||
(Object.create
|
||||
? function (o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return m[k];
|
||||
}
|
||||
};
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}
|
||||
: function (o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
});
|
||||
var __exportStar =
|
||||
(this && this.__exportStar) ||
|
||||
function (m, exports) {
|
||||
for (var p in m)
|
||||
if (p !== 'default' && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
__exportStar(require('./probe-metadata'), exports);
|
||||
__exportStar(require('./generate-thumbnail'), exports);
|
||||
__exportStar(require('./extract-audio'), exports);
|
||||
__exportStar(require('./transcribe-audio'), exports);
|
||||
@@ -1,311 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter =
|
||||
(this && this.__awaiter) ||
|
||||
function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) {
|
||||
return value instanceof P
|
||||
? value
|
||||
: new P(function (resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
}
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function rejected(value) {
|
||||
try {
|
||||
step(generator['throw'](value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function step(result) {
|
||||
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
||||
}
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator =
|
||||
(this && this.__generator) ||
|
||||
function (thisArg, body) {
|
||||
var _ = {
|
||||
label: 0,
|
||||
sent: function () {
|
||||
if (t[0] & 1) throw t[1];
|
||||
return t[1];
|
||||
},
|
||||
trys: [],
|
||||
ops: []
|
||||
},
|
||||
f,
|
||||
y,
|
||||
t,
|
||||
g = Object.create((typeof Iterator === 'function' ? Iterator : Object).prototype);
|
||||
return (
|
||||
(g.next = verb(0)),
|
||||
(g['throw'] = verb(1)),
|
||||
(g['return'] = verb(2)),
|
||||
typeof Symbol === 'function' &&
|
||||
(g[Symbol.iterator] = function () {
|
||||
return this;
|
||||
}),
|
||||
g
|
||||
);
|
||||
function verb(n) {
|
||||
return function (v) {
|
||||
return step([n, v]);
|
||||
};
|
||||
}
|
||||
function step(op) {
|
||||
if (f) throw new TypeError('Generator is already executing.');
|
||||
while ((g && ((g = 0), op[0] && (_ = 0)), _))
|
||||
try {
|
||||
if (
|
||||
((f = 1),
|
||||
y &&
|
||||
(t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) &&
|
||||
!(t = t.call(y, op[1])).done)
|
||||
)
|
||||
return t;
|
||||
if (((y = 0), t)) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
t = op;
|
||||
break;
|
||||
case 4:
|
||||
_.label++;
|
||||
return { value: op[1], done: false };
|
||||
case 5:
|
||||
_.label++;
|
||||
y = op[1];
|
||||
op = [0];
|
||||
continue;
|
||||
case 7:
|
||||
op = _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
default:
|
||||
if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) {
|
||||
_ = 0;
|
||||
continue;
|
||||
}
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
|
||||
_.label = op[1];
|
||||
break;
|
||||
}
|
||||
if (op[0] === 6 && _.label < t[1]) {
|
||||
_.label = t[1];
|
||||
t = op;
|
||||
break;
|
||||
}
|
||||
if (t && _.label < t[2]) {
|
||||
_.label = t[2];
|
||||
_.ops.push(op);
|
||||
break;
|
||||
}
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) {
|
||||
op = [6, e];
|
||||
y = 0;
|
||||
} finally {
|
||||
f = t = 0;
|
||||
}
|
||||
if (op[0] & 5) throw op[1];
|
||||
return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.processProbeMetadata = processProbeMetadata;
|
||||
var promises_1 = require('node:fs/promises');
|
||||
var queries_1 = require('@cio/db/queries');
|
||||
var media_1 = require('@cio/jobs/payloads/media');
|
||||
var storage_1 = require('../../utils/storage');
|
||||
var cancel_1 = require('../../utils/cancel');
|
||||
var ffmpeg_1 = require('../../utils/ffmpeg');
|
||||
var logger_1 = require('../../utils/logger');
|
||||
var STEP_KEY = 'probe-metadata';
|
||||
var DOMAIN = 'media';
|
||||
function parseProbe(metadata, fileSizeBytes) {
|
||||
var _a, _b;
|
||||
var videoStream = metadata.streams.find(function (stream) {
|
||||
return stream.codec_type === 'video';
|
||||
});
|
||||
var audioStream = metadata.streams.find(function (stream) {
|
||||
return stream.codec_type === 'audio';
|
||||
});
|
||||
var duration = Number(
|
||||
(_b =
|
||||
(_a = metadata.format.duration) !== null && _a !== void 0
|
||||
? _a
|
||||
: videoStream === null || videoStream === void 0
|
||||
? void 0
|
||||
: videoStream.duration) !== null && _b !== void 0
|
||||
? _b
|
||||
: 0
|
||||
);
|
||||
return {
|
||||
durationSeconds: Number.isFinite(duration) ? Math.max(0, Math.round(duration)) : 0,
|
||||
width: videoStream === null || videoStream === void 0 ? void 0 : videoStream.width,
|
||||
height: videoStream === null || videoStream === void 0 ? void 0 : videoStream.height,
|
||||
videoCodec: videoStream === null || videoStream === void 0 ? void 0 : videoStream.codec_name,
|
||||
audioCodec: audioStream === null || audioStream === void 0 ? void 0 : audioStream.codec_name,
|
||||
hasAudio: Boolean(audioStream),
|
||||
fileSizeBytes: fileSizeBytes
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Probe authoritative metadata (duration, codecs, dimensions) using ffprobe.
|
||||
* Replaces the browser-derived duration on the asset row.
|
||||
*/
|
||||
function processProbeMetadata(rawData) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var payload, mediaJobId, assetId, storageKey, actorContext, existing, localPath, fileStat, probe, result, error_1;
|
||||
var _a;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
payload = media_1.ZProbeMetadataPayload.parse(rawData);
|
||||
((mediaJobId = payload.mediaJobId),
|
||||
(assetId = payload.assetId),
|
||||
(storageKey = payload.storageKey),
|
||||
(actorContext = payload.actorContext));
|
||||
return [4 /*yield*/, (0, queries_1.getJobStep)(DOMAIN, mediaJobId, STEP_KEY)];
|
||||
case 1:
|
||||
existing = _b.sent();
|
||||
if (
|
||||
(existing === null || existing === void 0 ? void 0 : existing.status) === 'completed' &&
|
||||
existing.result
|
||||
) {
|
||||
logger_1.log.info('probe-metadata-skip', { mediaJobId: mediaJobId, reason: 'ledger-completed' });
|
||||
return [2 /*return*/, existing.result];
|
||||
}
|
||||
return [4 /*yield*/, (0, cancel_1.throwIfCancelRequested)(mediaJobId)];
|
||||
case 2:
|
||||
_b.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, { status: 'running', stage: 'probing', progressPercent: 5 })
|
||||
];
|
||||
case 3:
|
||||
_b.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'running',
|
||||
startedAt: new Date().toISOString(),
|
||||
attempt:
|
||||
((_a = existing === null || existing === void 0 ? void 0 : existing.attempt) !== null && _a !== void 0
|
||||
? _a
|
||||
: 0) + 1
|
||||
})
|
||||
];
|
||||
case 4:
|
||||
_b.sent();
|
||||
_b.label = 5;
|
||||
case 5:
|
||||
_b.trys.push([5, 13, 15, 17]);
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, storage_1.downloadObjectToTempFile)((0, storage_1.videosBucket)(), storageKey, 'source.bin')
|
||||
];
|
||||
case 6:
|
||||
localPath = _b.sent();
|
||||
return [4 /*yield*/, (0, promises_1.stat)(localPath)];
|
||||
case 7:
|
||||
fileStat = _b.sent();
|
||||
return [4 /*yield*/, (0, cancel_1.throwIfCancelRequested)(mediaJobId)];
|
||||
case 8:
|
||||
_b.sent();
|
||||
return [4 /*yield*/, (0, ffmpeg_1.ffprobeJson)(localPath)];
|
||||
case 9:
|
||||
probe = _b.sent();
|
||||
result = parseProbe(probe, fileStat.size);
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateAsset)(assetId, actorContext.organizationId, {
|
||||
durationSeconds: result.durationSeconds,
|
||||
aspectRatio: result.width && result.height ? ''.concat(result.width, ':').concat(result.height) : null,
|
||||
metadata: {
|
||||
probe: {
|
||||
durationSeconds: result.durationSeconds,
|
||||
width: result.width,
|
||||
height: result.height,
|
||||
videoCodec: result.videoCodec,
|
||||
audioCodec: result.audioCodec,
|
||||
hasAudio: result.hasAudio,
|
||||
fileSizeBytes: result.fileSizeBytes,
|
||||
probedAt: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
case 10:
|
||||
_b.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'completed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
result: result
|
||||
})
|
||||
];
|
||||
case 11:
|
||||
_b.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, {
|
||||
stage: 'probed',
|
||||
progressPercent: 25
|
||||
})
|
||||
];
|
||||
case 12:
|
||||
_b.sent();
|
||||
logger_1.log.info('probe-metadata-done', { mediaJobId: mediaJobId, durationSeconds: result.durationSeconds });
|
||||
return [2 /*return*/, result];
|
||||
case 13:
|
||||
error_1 = _b.sent();
|
||||
logger_1.log.error('probe-metadata-failed', {
|
||||
mediaJobId: mediaJobId,
|
||||
error: (0, cancel_1.errorMessage)(error_1)
|
||||
});
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'failed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
error: { code: 'PROBE_FAILED', message: (0, cancel_1.errorMessage)(error_1) }
|
||||
})
|
||||
];
|
||||
case 14:
|
||||
_b.sent();
|
||||
throw error_1;
|
||||
case 15:
|
||||
return [4 /*yield*/, (0, storage_1.safeUnlink)(localPath)];
|
||||
case 16:
|
||||
_b.sent();
|
||||
return [7 /*endfinally*/];
|
||||
case 17:
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,538 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter =
|
||||
(this && this.__awaiter) ||
|
||||
function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) {
|
||||
return value instanceof P
|
||||
? value
|
||||
: new P(function (resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
}
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function rejected(value) {
|
||||
try {
|
||||
step(generator['throw'](value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function step(result) {
|
||||
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
||||
}
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator =
|
||||
(this && this.__generator) ||
|
||||
function (thisArg, body) {
|
||||
var _ = {
|
||||
label: 0,
|
||||
sent: function () {
|
||||
if (t[0] & 1) throw t[1];
|
||||
return t[1];
|
||||
},
|
||||
trys: [],
|
||||
ops: []
|
||||
},
|
||||
f,
|
||||
y,
|
||||
t,
|
||||
g = Object.create((typeof Iterator === 'function' ? Iterator : Object).prototype);
|
||||
return (
|
||||
(g.next = verb(0)),
|
||||
(g['throw'] = verb(1)),
|
||||
(g['return'] = verb(2)),
|
||||
typeof Symbol === 'function' &&
|
||||
(g[Symbol.iterator] = function () {
|
||||
return this;
|
||||
}),
|
||||
g
|
||||
);
|
||||
function verb(n) {
|
||||
return function (v) {
|
||||
return step([n, v]);
|
||||
};
|
||||
}
|
||||
function step(op) {
|
||||
if (f) throw new TypeError('Generator is already executing.');
|
||||
while ((g && ((g = 0), op[0] && (_ = 0)), _))
|
||||
try {
|
||||
if (
|
||||
((f = 1),
|
||||
y &&
|
||||
(t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) &&
|
||||
!(t = t.call(y, op[1])).done)
|
||||
)
|
||||
return t;
|
||||
if (((y = 0), t)) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
t = op;
|
||||
break;
|
||||
case 4:
|
||||
_.label++;
|
||||
return { value: op[1], done: false };
|
||||
case 5:
|
||||
_.label++;
|
||||
y = op[1];
|
||||
op = [0];
|
||||
continue;
|
||||
case 7:
|
||||
op = _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
default:
|
||||
if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) {
|
||||
_ = 0;
|
||||
continue;
|
||||
}
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
|
||||
_.label = op[1];
|
||||
break;
|
||||
}
|
||||
if (op[0] === 6 && _.label < t[1]) {
|
||||
_.label = t[1];
|
||||
t = op;
|
||||
break;
|
||||
}
|
||||
if (t && _.label < t[2]) {
|
||||
_.label = t[2];
|
||||
_.ops.push(op);
|
||||
break;
|
||||
}
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop();
|
||||
continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) {
|
||||
op = [6, e];
|
||||
y = 0;
|
||||
} finally {
|
||||
f = t = 0;
|
||||
}
|
||||
if (op[0] & 5) throw op[1];
|
||||
return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
var __spreadArray =
|
||||
(this && this.__spreadArray) ||
|
||||
function (to, from, pack) {
|
||||
if (pack || arguments.length === 2)
|
||||
for (var i = 0, l = from.length, ar; i < l; i++) {
|
||||
if (ar || !(i in from)) {
|
||||
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
||||
ar[i] = from[i];
|
||||
}
|
||||
}
|
||||
return to.concat(ar || Array.prototype.slice.call(from));
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.processTranscribeAudio = processTranscribeAudio;
|
||||
var promises_1 = require('node:fs/promises');
|
||||
var queries_1 = require('@cio/db/queries');
|
||||
var media_1 = require('@cio/jobs/payloads/media');
|
||||
var env_1 = require('../../config/env');
|
||||
var openai_1 = require('../../services/transcription/openai');
|
||||
var vtt_1 = require('../../services/transcription/vtt');
|
||||
var storage_1 = require('../../utils/storage');
|
||||
var cancel_1 = require('../../utils/cancel');
|
||||
var logger_1 = require('../../utils/logger');
|
||||
var STEP_KEY = 'transcribe-audio';
|
||||
var DOMAIN = 'media';
|
||||
var EXTRACT_STEP_KEY = 'extract-audio';
|
||||
function transcriptionCostCents(durationSeconds) {
|
||||
return Math.ceil((durationSeconds / 60) * 0.6);
|
||||
}
|
||||
function mergeJobWarnings(mediaJobId, extra) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var job, prior;
|
||||
var _a;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
return [4 /*yield*/, (0, queries_1.getMediaJobById)(mediaJobId)];
|
||||
case 1:
|
||||
job = _b.sent();
|
||||
prior = (_a = job === null || job === void 0 ? void 0 : job.warnings) !== null && _a !== void 0 ? _a : [];
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, {
|
||||
warnings: __spreadArray(__spreadArray([], prior, true), extra, true)
|
||||
})
|
||||
];
|
||||
case 2:
|
||||
_b.sent();
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* OpenAI Whisper (`whisper-1`) with verbose segment timestamps; persists VTT +
|
||||
* `media_transcript` row. Skips cleanly when `OPENAI_API_KEY` is unset or when
|
||||
* extracted audio exceeds the 25 MB API limit.
|
||||
*/
|
||||
function processTranscribeAudio(rawData) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var payload,
|
||||
mediaJobId,
|
||||
assetId,
|
||||
actorContext,
|
||||
existing,
|
||||
result,
|
||||
extractStep,
|
||||
extractResult,
|
||||
message,
|
||||
localPath,
|
||||
localStat,
|
||||
whisper,
|
||||
normalizedSegments,
|
||||
vttBody,
|
||||
vttKey,
|
||||
bucket,
|
||||
transcriptCost,
|
||||
jobRow,
|
||||
priorCost,
|
||||
result,
|
||||
error_1,
|
||||
error_2;
|
||||
var _a, _b;
|
||||
return __generator(this, function (_c) {
|
||||
switch (_c.label) {
|
||||
case 0:
|
||||
payload = media_1.ZTranscribeAudioPayload.parse(rawData);
|
||||
((mediaJobId = payload.mediaJobId), (assetId = payload.assetId), (actorContext = payload.actorContext));
|
||||
return [4 /*yield*/, (0, queries_1.getJobStep)(DOMAIN, mediaJobId, STEP_KEY)];
|
||||
case 1:
|
||||
existing = _c.sent();
|
||||
if (
|
||||
(existing === null || existing === void 0 ? void 0 : existing.status) === 'completed' &&
|
||||
existing.result
|
||||
) {
|
||||
logger_1.log.info('transcribe-audio-skip', { mediaJobId: mediaJobId, reason: 'ledger-completed' });
|
||||
return [2 /*return*/, existing.result];
|
||||
}
|
||||
return [4 /*yield*/, (0, cancel_1.throwIfCancelRequested)(mediaJobId)];
|
||||
case 2:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, { stage: 'transcribing', progressPercent: 90 })
|
||||
];
|
||||
case 3:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'running',
|
||||
startedAt: new Date().toISOString(),
|
||||
attempt:
|
||||
((_a = existing === null || existing === void 0 ? void 0 : existing.attempt) !== null && _a !== void 0
|
||||
? _a
|
||||
: 0) + 1
|
||||
})
|
||||
];
|
||||
case 4:
|
||||
_c.sent();
|
||||
_c.label = 5;
|
||||
case 5:
|
||||
_c.trys.push([5, 35, , 37]);
|
||||
if (!!env_1.env.OPENAI_API_KEY) return [3 /*break*/, 9];
|
||||
result = { provider: 'skipped', text: '' };
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'completed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
result: result
|
||||
})
|
||||
];
|
||||
case 6:
|
||||
_c.sent();
|
||||
return [4 /*yield*/, mergeJobWarnings(mediaJobId, ['transcription-skipped-no-openai-key'])];
|
||||
case 7:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, {
|
||||
status: 'completed',
|
||||
stage: 'done',
|
||||
progressPercent: 100
|
||||
})
|
||||
];
|
||||
case 8:
|
||||
_c.sent();
|
||||
logger_1.log.info('transcribe-audio-skipped', { mediaJobId: mediaJobId, reason: 'no-openai-key' });
|
||||
return [2 /*return*/, result];
|
||||
case 9:
|
||||
return [4 /*yield*/, (0, queries_1.getJobStep)(DOMAIN, mediaJobId, EXTRACT_STEP_KEY)];
|
||||
case 10:
|
||||
extractStep = _c.sent();
|
||||
extractResult = extractStep === null || extractStep === void 0 ? void 0 : extractStep.result;
|
||||
if (
|
||||
!(
|
||||
!(extractResult === null || extractResult === void 0 ? void 0 : extractResult.bucket) ||
|
||||
!(extractResult === null || extractResult === void 0 ? void 0 : extractResult.key)
|
||||
)
|
||||
)
|
||||
return [3 /*break*/, 12];
|
||||
message = 'extract-audio step missing or incomplete';
|
||||
logger_1.log.error('transcribe-audio-failed', { mediaJobId: mediaJobId, error: message });
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'failed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
error: { code: 'EXTRACT_AUDIO_MISSING', message: message }
|
||||
})
|
||||
];
|
||||
case 11:
|
||||
_c.sent();
|
||||
throw new Error(message);
|
||||
case 12:
|
||||
localPath = void 0;
|
||||
_c.label = 13;
|
||||
case 13:
|
||||
_c.trys.push([13, 27, 32, 34]);
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, storage_1.downloadObjectToTempFile)(extractResult.bucket, extractResult.key, 'whisper-input.mp3')
|
||||
];
|
||||
case 14:
|
||||
localPath = _c.sent();
|
||||
return [4 /*yield*/, (0, promises_1.stat)(localPath)];
|
||||
case 15:
|
||||
localStat = _c.sent();
|
||||
if (!(localStat.size > openai_1.WHISPER_MAX_FILE_BYTES)) return [3 /*break*/, 19];
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'failed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
error: {
|
||||
code: 'AUDIO_TOO_LARGE',
|
||||
message: 'Audio '.concat(localStat.size, ' bytes exceeds ').concat(openai_1.WHISPER_MAX_FILE_BYTES)
|
||||
}
|
||||
})
|
||||
];
|
||||
case 16:
|
||||
_c.sent();
|
||||
return [4 /*yield*/, mergeJobWarnings(mediaJobId, ['transcription-skipped-audio-too-large'])];
|
||||
case 17:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, {
|
||||
status: 'completed',
|
||||
stage: 'done',
|
||||
progressPercent: 100
|
||||
})
|
||||
];
|
||||
case 18:
|
||||
_c.sent();
|
||||
logger_1.log.warn('transcribe-audio-skipped-audio-too-large', {
|
||||
mediaJobId: mediaJobId,
|
||||
fileSizeBytes: localStat.size
|
||||
});
|
||||
return [2 /*return*/, { provider: 'skipped', text: '' }];
|
||||
case 19:
|
||||
return [4 /*yield*/, (0, cancel_1.throwIfCancelRequested)(mediaJobId)];
|
||||
case 20:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, openai_1.transcribeAudioFile)({
|
||||
apiKey: env_1.env.OPENAI_API_KEY,
|
||||
localPath: localPath
|
||||
})
|
||||
];
|
||||
case 21:
|
||||
whisper = _c.sent();
|
||||
normalizedSegments = whisper.segments.map(function (segment) {
|
||||
return {
|
||||
start: segment.start,
|
||||
end: segment.end,
|
||||
text: segment.text
|
||||
};
|
||||
});
|
||||
vttBody = (0, vtt_1.segmentsToWebVtt)(normalizedSegments);
|
||||
vttKey = 'transcripts/'.concat(assetId, '/').concat(mediaJobId, '.vtt');
|
||||
bucket = (0, storage_1.mediaBucket)();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, storage_1.uploadBufferToBucket)(
|
||||
bucket,
|
||||
vttKey,
|
||||
Buffer.from(vttBody, 'utf8'),
|
||||
'text/vtt; charset=utf-8',
|
||||
'private, max-age=0'
|
||||
)
|
||||
];
|
||||
case 22:
|
||||
_c.sent();
|
||||
transcriptCost = transcriptionCostCents(whisper.durationSeconds);
|
||||
return [4 /*yield*/, (0, queries_1.getMediaJobById)(mediaJobId)];
|
||||
case 23:
|
||||
jobRow = _c.sent();
|
||||
priorCost =
|
||||
(_b = jobRow === null || jobRow === void 0 ? void 0 : jobRow.costCents) !== null && _b !== void 0 ? _b : 0;
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertMediaTranscript)({
|
||||
organizationId: actorContext.organizationId,
|
||||
assetId: assetId,
|
||||
mediaJobId: mediaJobId,
|
||||
language: whisper.language,
|
||||
provider: 'openai',
|
||||
model: 'whisper-1',
|
||||
text:
|
||||
whisper.fullText ||
|
||||
normalizedSegments
|
||||
.map(function (segment) {
|
||||
return segment.text;
|
||||
})
|
||||
.join(' ')
|
||||
.trim(),
|
||||
segments: normalizedSegments,
|
||||
vttStorageKey: vttKey,
|
||||
vttBucket: bucket,
|
||||
durationSeconds: Math.round(whisper.durationSeconds),
|
||||
costCents: transcriptCost
|
||||
})
|
||||
];
|
||||
case 24:
|
||||
_c.sent();
|
||||
result = {
|
||||
provider: 'openai',
|
||||
text: whisper.fullText,
|
||||
language: whisper.language,
|
||||
durationSeconds: whisper.durationSeconds,
|
||||
costCents: transcriptCost
|
||||
};
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'completed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
result: {
|
||||
language: whisper.language,
|
||||
segmentCount: normalizedSegments.length,
|
||||
vttKey: vttKey,
|
||||
costCents: transcriptCost
|
||||
},
|
||||
providerId: null
|
||||
})
|
||||
];
|
||||
case 25:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, {
|
||||
status: 'completed',
|
||||
stage: 'done',
|
||||
progressPercent: 100,
|
||||
costCents: priorCost + transcriptCost
|
||||
})
|
||||
];
|
||||
case 26:
|
||||
_c.sent();
|
||||
logger_1.log.info('transcribe-audio-done', {
|
||||
mediaJobId: mediaJobId,
|
||||
language: whisper.language,
|
||||
segmentCount: normalizedSegments.length
|
||||
});
|
||||
return [2 /*return*/, result];
|
||||
case 27:
|
||||
error_1 = _c.sent();
|
||||
if (!(error_1 instanceof openai_1.AudioFileTooLargeError)) return [3 /*break*/, 31];
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'failed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
error: { code: 'AUDIO_TOO_LARGE', message: error_1.message }
|
||||
})
|
||||
];
|
||||
case 28:
|
||||
_c.sent();
|
||||
return [4 /*yield*/, mergeJobWarnings(mediaJobId, ['transcription-skipped-audio-too-large'])];
|
||||
case 29:
|
||||
_c.sent();
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.updateMediaJob)(mediaJobId, {
|
||||
status: 'completed',
|
||||
stage: 'done',
|
||||
progressPercent: 100
|
||||
})
|
||||
];
|
||||
case 30:
|
||||
_c.sent();
|
||||
logger_1.log.warn('transcribe-audio-skipped-audio-too-large', {
|
||||
mediaJobId: mediaJobId,
|
||||
fileSizeBytes: error_1.fileSizeBytes
|
||||
});
|
||||
return [2 /*return*/, { provider: 'skipped', text: '' }];
|
||||
case 31:
|
||||
throw error_1;
|
||||
case 32:
|
||||
return [4 /*yield*/, (0, storage_1.safeUnlink)(localPath)];
|
||||
case 33:
|
||||
_c.sent();
|
||||
return [7 /*endfinally*/];
|
||||
case 34:
|
||||
return [3 /*break*/, 37];
|
||||
case 35:
|
||||
error_2 = _c.sent();
|
||||
logger_1.log.error('transcribe-audio-failed', {
|
||||
mediaJobId: mediaJobId,
|
||||
error: (0, cancel_1.errorMessage)(error_2)
|
||||
});
|
||||
return [
|
||||
4 /*yield*/,
|
||||
(0, queries_1.upsertJobStep)({
|
||||
domain: DOMAIN,
|
||||
runId: mediaJobId,
|
||||
stepKey: STEP_KEY,
|
||||
status: 'failed',
|
||||
finishedAt: new Date().toISOString(),
|
||||
error: { code: 'TRANSCRIBE_FAILED', message: (0, cancel_1.errorMessage)(error_2) }
|
||||
})
|
||||
];
|
||||
case 36:
|
||||
_c.sent();
|
||||
throw error_2;
|
||||
case 37:
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user