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:
Rotimi Best
2026-06-07 18:43:05 +01:00
committed by GitHub
parent a2e70779a0
commit d5b07d356e
496 changed files with 19050 additions and 8312 deletions
-214
View File
@@ -1,214 +0,0 @@
'use strict';
var __extends =
(this && this.__extends) ||
(function () {
var extendStatics = function (d, b) {
extendStatics =
Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array &&
function (d, b) {
d.__proto__ = b;
}) ||
function (d, b) {
for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p];
};
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== 'function' && b !== null)
throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null');
extendStatics(d, b);
function __() {
this.constructor = d;
}
d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __());
};
})();
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.JobCanceledError = void 0;
exports.isMediaJobCancelRequested = isMediaJobCancelRequested;
exports.throwIfCancelRequested = throwIfCancelRequested;
exports.errorMessage = errorMessage;
var jobs_1 = require('@cio/db/queries/jobs');
var logger_1 = require('./logger');
/**
* Lightweight cooperative cancellation. Workers check `isCancelRequested` at
* step boundaries and inside long ffmpeg/fetch loops. The plan calls for
* `cancelRequestedAt` on each run table, with the worker abandoning further
* work the moment it observes a request.
*/
var JobCanceledError = /** @class */ (function (_super) {
__extends(JobCanceledError, _super);
function JobCanceledError(mediaJobId) {
var _this = _super.call(this, 'Media job '.concat(mediaJobId, ' canceled by user request')) || this;
_this.mediaJobId = mediaJobId;
_this.name = 'JobCanceledError';
return _this;
}
return JobCanceledError;
})(Error);
exports.JobCanceledError = JobCanceledError;
function isMediaJobCancelRequested(mediaJobId) {
return __awaiter(this, void 0, void 0, function () {
var job, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, (0, jobs_1.getMediaJobById)(mediaJobId)];
case 1:
job = _a.sent();
return [2 /*return*/, Boolean(job === null || job === void 0 ? void 0 : job.cancelRequestedAt)];
case 2:
error_1 = _a.sent();
logger_1.log.warn('cancel-check-failed', { mediaJobId: mediaJobId, error: errorMessage(error_1) });
return [2 /*return*/, false];
case 3:
return [2 /*return*/];
}
});
});
}
function throwIfCancelRequested(mediaJobId) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [4 /*yield*/, isMediaJobCancelRequested(mediaJobId)];
case 1:
if (_a.sent()) {
throw new JobCanceledError(mediaJobId);
}
return [2 /*return*/];
}
});
});
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
-250
View File
@@ -1,250 +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 _a, _b;
Object.defineProperty(exports, '__esModule', { value: true });
exports.FFPROBE_BIN = exports.FFMPEG_BIN = void 0;
exports.ffprobeJson = ffprobeJson;
exports.ffmpegRun = ffmpegRun;
exports.warnIfFfmpegMissing = warnIfFfmpegMissing;
var node_child_process_1 = require('node:child_process');
var node_util_1 = require('node:util');
var logger_1 = require('./logger');
var execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
/**
* Resolved at module load. Set `FFMPEG_PATH` / `FFPROBE_PATH` to point at
* non-default binary locations (e.g. when ffmpeg lives outside PATH for the
* worker process). When unset we fall through to `ffmpeg` / `ffprobe` on
* PATH, which is what most installs provide.
*/
exports.FFMPEG_BIN = (_a = process.env.FFMPEG_PATH) !== null && _a !== void 0 ? _a : 'ffmpeg';
exports.FFPROBE_BIN = (_b = process.env.FFPROBE_PATH) !== null && _b !== void 0 ? _b : 'ffprobe';
/** Cap subprocess stdout/stderr so a runaway log can't blow up the worker. */
var MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
/**
* Run `ffprobe -print_format json -show_format -show_streams <file>` and
* return the parsed JSON. Mirrors the shape we used to get from
* `fluent-ffmpeg.ffprobe()` but without the deprecated wrapper.
*/
function ffprobeJson(localPath) {
return __awaiter(this, void 0, void 0, function () {
var args, stdout;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
args = ['-v', 'error', '-print_format', 'json', '-show_format', '-show_streams', localPath];
return [4 /*yield*/, execFileAsync(exports.FFPROBE_BIN, args, { maxBuffer: MAX_OUTPUT_BYTES })];
case 1:
stdout = _a.sent().stdout;
return [2 /*return*/, JSON.parse(stdout)];
}
});
});
}
/**
* Spawn ffmpeg with the supplied argv. Captures stderr and surfaces a useful
* tail in the error message when ffmpeg exits non-zero, since ffmpeg writes
* progress and diagnostics to stderr by default.
*/
function ffmpegRun(args) {
return __awaiter(this, void 0, void 0, function () {
var error_1, stderr, tail;
var _a, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
_c.trys.push([0, 2, , 3]);
return [4 /*yield*/, execFileAsync(exports.FFMPEG_BIN, args, { maxBuffer: MAX_OUTPUT_BYTES })];
case 1:
_c.sent();
return [3 /*break*/, 3];
case 2:
error_1 = _c.sent();
stderr =
(_b =
(_a = error_1 === null || error_1 === void 0 ? void 0 : error_1.stderr) === null || _a === void 0
? void 0
: _a.toString('utf8')) !== null && _b !== void 0
? _b
: '';
tail = stderr.split('\n').filter(Boolean).slice(-5).join(' | ');
throw new Error('ffmpeg exited with error'.concat(tail ? ': '.concat(tail) : ''), { cause: error_1 });
case 3:
return [2 /*return*/];
}
});
});
}
/**
* Probe for the binaries at worker startup. Logs a warning when missing so
* media jobs surface a clearer failure than a raw ENOENT from the first
* spawn. Non-media workers (emails, maintenance) keep booting either way.
*/
function warnIfFfmpegMissing() {
return __awaiter(this, void 0, void 0, function () {
var checks, missing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [4 /*yield*/, Promise.all([isExecutable(exports.FFMPEG_BIN), isExecutable(exports.FFPROBE_BIN)])];
case 1:
checks = _a.sent();
missing = [];
if (!checks[0]) missing.push(exports.FFMPEG_BIN);
if (!checks[1]) missing.push(exports.FFPROBE_BIN);
if (missing.length === 0) {
logger_1.log.info('ffmpeg-binaries-resolved', { ffmpeg: exports.FFMPEG_BIN, ffprobe: exports.FFPROBE_BIN });
return [2 /*return*/];
}
logger_1.log.warn('ffmpeg-binaries-missing', {
missing: missing,
hint: 'Install ffmpeg (macOS: brew install ffmpeg, Debian/Ubuntu: apt install ffmpeg) or set FFMPEG_PATH / FFPROBE_PATH. Media jobs will fail until this is resolved; other workers are unaffected.'
});
return [2 /*return*/];
}
});
});
}
function isExecutable(bin) {
return __awaiter(this, void 0, void 0, function () {
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_b.trys.push([0, 2, , 3]);
return [4 /*yield*/, execFileAsync(bin, ['-version'], { maxBuffer: MAX_OUTPUT_BYTES })];
case 1:
_b.sent();
return [2 /*return*/, true];
case 2:
_a = _b.sent();
return [2 /*return*/, false];
case 3:
return [2 /*return*/];
}
});
});
}
+29
View File
@@ -62,6 +62,35 @@ export async function ffmpegRun(args: string[]): Promise<void> {
}
}
/**
* Compute the mean luminance (`YAVG`) of an image or single video frame using
* ffmpeg's `signalstats` filter. Returned on the original 0-255 scale; a fully
* black frame is ~0, fully white ~255. Throws if ffmpeg fails or the value is
* not present in stderr.
*/
export async function ffmpegProbeLuma(filePath: string): Promise<number> {
const args = ['-hide_banner', '-nostats', '-i', filePath, '-vf', 'signalstats', '-f', 'null', '-'];
let stderr = '';
try {
const result = await execFileAsync(FFMPEG_BIN, args, { maxBuffer: MAX_OUTPUT_BYTES });
const raw = result.stderr as string | Buffer | undefined;
stderr = typeof raw === 'string' ? raw : (raw?.toString('utf8') ?? '');
} catch (error) {
const raw = (error as { stderr?: string | Buffer })?.stderr;
stderr = typeof raw === 'string' ? raw : (raw?.toString('utf8') ?? '');
if (!stderr) {
throw error as Error;
}
}
const match = stderr.match(/YAVG:([0-9]+(?:\.[0-9]+)?)/);
if (!match) {
throw new Error('ffmpegProbeLuma: signalstats YAVG not found in ffmpeg output');
}
return Number.parseFloat(match[1]);
}
/**
* Probe for the binaries at worker startup. Logs a warning when missing so
* media jobs surface a clearer failure than a raw ENOENT from the first
-39
View File
@@ -1,39 +0,0 @@
'use strict';
var __assign =
(this && this.__assign) ||
function () {
__assign =
Object.assign ||
function (t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, '__esModule', { value: true });
exports.log = void 0;
function emit(level, message, fields) {
if (fields === void 0) {
fields = {};
}
var payload = __assign({ ts: new Date().toISOString(), level: level, msg: message }, fields);
var stream = level === 'error' || level === 'warn' ? console.error : console.log;
stream(JSON.stringify(payload));
}
exports.log = {
debug: function (msg, fields) {
return emit('debug', msg, fields);
},
info: function (msg, fields) {
return emit('info', msg, fields);
},
warn: function (msg, fields) {
return emit('warn', msg, fields);
},
error: function (msg, fields) {
return emit('error', msg, fields);
}
};
-266
View File
@@ -1,266 +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.downloadObjectToTempFile = downloadObjectToTempFile;
exports.uploadFileToBucket = uploadFileToBucket;
exports.uploadBufferToBucket = uploadBufferToBucket;
exports.safeUnlink = safeUnlink;
exports.videosBucket = videosBucket;
exports.mediaBucket = mediaBucket;
var client_s3_1 = require('@aws-sdk/client-s3');
var node_fs_1 = require('node:fs');
var promises_1 = require('node:fs/promises');
var node_os_1 = require('node:os');
var node_path_1 = require('node:path');
var promises_2 = require('node:stream/promises');
var storage_1 = require('../config/storage');
/**
* Streams an S3 object to a temp file on disk and returns the absolute path.
* Workers prefer disk over buffers so ffmpeg can seek without holding the
* whole video in RAM.
*/
function downloadObjectToTempFile(bucket, key, fileNameHint) {
return __awaiter(this, void 0, void 0, function () {
var dir, safeName, localPath, response, body;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
dir = node_path_1.default.join((0, node_os_1.tmpdir)(), 'cio-jobs');
return [4 /*yield*/, (0, promises_1.mkdir)(dir, { recursive: true })];
case 1:
_b.sent();
safeName =
(_a =
fileNameHint === null || fileNameHint === void 0
? void 0
: fileNameHint.replace(/[^a-zA-Z0-9._-]/g, '_')) !== null && _a !== void 0
? _a
: 'media';
localPath = node_path_1.default.join(
dir,
''.concat(Date.now(), '_').concat(process.pid, '_').concat(safeName)
);
return [
4 /*yield*/,
(0, storage_1.getS3Client)().send(new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key }))
];
case 2:
response = _b.sent();
if (!response.Body) {
throw new Error('S3 object '.concat(bucket, '/').concat(key, ' returned no body'));
}
body = response.Body;
return [4 /*yield*/, (0, promises_2.pipeline)(body, (0, node_fs_1.createWriteStream)(localPath))];
case 3:
_b.sent();
return [2 /*return*/, localPath];
}
});
});
}
function uploadFileToBucket(bucket, key, localPath, contentType, cacheControl) {
return __awaiter(this, void 0, void 0, function () {
var buffer;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [4 /*yield*/, (0, promises_1.readFile)(localPath)];
case 1:
buffer = _a.sent();
return [
4 /*yield*/,
(0, storage_1.getS3Client)().send(
new client_s3_1.PutObjectCommand({
Bucket: bucket,
Key: key,
Body: buffer,
ContentType: contentType,
CacheControl: cacheControl
})
)
];
case 2:
_a.sent();
return [2 /*return*/];
}
});
});
}
function uploadBufferToBucket(bucket, key, buffer, contentType, cacheControl) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [
4 /*yield*/,
(0, storage_1.getS3Client)().send(
new client_s3_1.PutObjectCommand({
Bucket: bucket,
Key: key,
Body: buffer,
ContentType: contentType,
CacheControl: cacheControl
})
)
];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
}
function safeUnlink(filePath) {
return __awaiter(this, void 0, void 0, function () {
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
if (!filePath) return [2 /*return*/];
_b.label = 1;
case 1:
_b.trys.push([1, 3, , 4]);
return [4 /*yield*/, (0, promises_1.unlink)(filePath)];
case 2:
_b.sent();
return [3 /*break*/, 4];
case 3:
_a = _b.sent();
return [3 /*break*/, 4];
case 4:
return [2 /*return*/];
}
});
});
}
function videosBucket() {
return (0, storage_1.getStorageConfig)().bucketVideos;
}
function mediaBucket() {
return (0, storage_1.getStorageConfig)().bucketMedia;
}