feat: add AI summary / transcript integration for recordings
Build & Push Docker Image / build (push) Successful in 4m17s
Build & Push Docker Image / build (release) Successful in 3m57s

Ported from greenlight-tm: when TRANSCRIPTION_API_URL and
TRANSCRIPTION_API_KEY are set in the .env and the per-room
"AI summary / transcript" setting is enabled, Redlight POSTs to
${TRANSCRIPTION_API_URL}/prompt (header X-Api-Key) as soon as the
"video" format of a recording becomes available.

- transcription.js: service with env gating, timeout and logging
- bbb.js: pass meta_bbb-recording-ready-url on meeting create when the
  room has the setting enabled
- recordings.js: unauthenticated /recording-ready callback that
  verifies BBB's signed_parameters JWT (HS256, shared secret), caches
  the recording and requests the transcription once per recording
  (deduped via transcript_requested_at)
- rooms: new recording_transcript column, accepted on create/update
- branding endpoint exposes transcriptionEnabled so the UI only shows
  the toggle when the server is configured
- RoomDetail: toggle under "Allow recording" (disabled when recording
  is off), de/en i18n
- chore: bump version to 2.4.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 11:56:45 +02:00
co-authored by Claude Opus 4.8
parent dffb68efe5
commit 51b42cfee5
13 changed files with 187 additions and 7 deletions
+7 -2
View File
@@ -193,6 +193,7 @@ router.post('/', authenticateToken, async (req, res) => {
record_meeting,
guest_access,
moderator_code,
recording_transcript,
} = req.body;
if (!name || name.trim().length === 0) {
@@ -227,8 +228,8 @@ router.post('/', authenticateToken, async (req, res) => {
const db = getDb();
const result = await db.run(`
INSERT INTO rooms (uid, name, user_id, welcome_message, max_participants, access_code, mute_on_join, require_approval, anyone_can_start, all_join_moderator, record_meeting, guest_access, moderator_code)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO rooms (uid, name, user_id, welcome_message, max_participants, access_code, mute_on_join, require_approval, anyone_can_start, all_join_moderator, record_meeting, guest_access, moderator_code, recording_transcript)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
uid,
name.trim(),
@@ -243,6 +244,7 @@ router.post('/', authenticateToken, async (req, res) => {
record_meeting !== false ? 1 : 0,
guest_access ? 1 : 0,
moderator_code || null,
recording_transcript ? 1 : 0,
]);
const room = await db.get('SELECT * FROM rooms WHERE id = ?', [result.lastInsertRowid]);
@@ -277,6 +279,7 @@ router.put('/:uid', authenticateToken, async (req, res) => {
moderator_code,
learning_analytics,
analytics_visibility,
recording_transcript,
} = req.body;
// M12: field length limits (same as create)
@@ -318,6 +321,7 @@ router.put('/:uid', authenticateToken, async (req, res) => {
moderator_code = ?,
learning_analytics = COALESCE(?, learning_analytics),
analytics_visibility = COALESCE(?, analytics_visibility),
recording_transcript = COALESCE(?, recording_transcript),
updated_at = CURRENT_TIMESTAMP
WHERE uid = ?
`, [
@@ -334,6 +338,7 @@ router.put('/:uid', authenticateToken, async (req, res) => {
moderator_code !== undefined ? (moderator_code || null) : room.moderator_code,
learning_analytics !== undefined ? (learning_analytics ? 1 : 0) : null,
analytics_visibility && ['owner', 'shared'].includes(analytics_visibility) ? analytics_visibility : null,
recording_transcript !== undefined ? (recording_transcript ? 1 : 0) : null,
req.params.uid,
]);