embed meetus

This commit is contained in:
Mariano Gabriel
2026-07-05 20:52:37 -03:00
parent 8606520ef2
commit a92615d6bc
6 changed files with 217 additions and 15 deletions

View File

@@ -106,9 +106,91 @@ export function doocusApi(opts: Options): Plugin {
return true
}
// GET /api/meeting?path=<rel> meetus review data for a meeting
// GET /api/meeting/frame?path=<rel>&file=<f> stream a frame from its .meetus/
if (parts[0] === 'meeting' && parts[1] === 'frame') {
serveMeetingFrame(res, q.get('path') ?? '', q.get('file') ?? '')
return true
}
if (parts[0] === 'meeting') {
await getMeeting(res, q.get('path') ?? '')
return true
}
return false
}
/** The .meetus sidecar dir + the video stem for a meeting node's rel path. */
function meetusDirFor(rel: string): { dir: string; stem: string } {
return { dir: path.join(outputDir, rel + '.meetus'), stem: path.parse(rel).name }
}
async function getMeeting(res: ServerResponse, rel: string): Promise<void> {
const { dir, stem } = meetusDirFor(rel)
// Whisper segments (may be null-flagged in manifest but present on disk).
const segments: Array<{ start: number; end: number; text: string; speaker: string | null }> = []
const whisper = path.join(dir, `${stem}.json`)
if (fs.existsSync(whisper)) {
try {
const data = JSON.parse(await fsp.readFile(whisper, 'utf-8'))
for (const s of (Array.isArray(data) ? data : data.segments ?? [])) {
segments.push({
start: Number(s.start ?? 0), end: Number(s.end ?? s.start ?? 0),
text: String(s.text ?? '').trim(), speaker: s.speaker ?? null,
})
}
} catch { /* none */ }
}
// Frame → time from enhanced.txt ([MM:SS] before each `Frame: frames/…`).
const enhanced = path.join(dir, `${stem}_enhanced.txt`)
const times = new Map<string, number>()
let enhancedAvailable = false
if (fs.existsSync(enhanced)) {
try {
enhancedAvailable = true
let last: number | null = null
for (const line of (await fsp.readFile(enhanced, 'utf-8')).split('\n')) {
const ts = line.match(/^\[(\d+):(\d+)\]/)
if (ts) last = Number(ts[1]) * 60 + Number(ts[2])
const fm = line.match(/Frame:\s*\S*?([^/\\\s]+\.jpg)/i)
if (fm && last != null) times.set(fm[1], last)
}
} catch { /* none */ }
}
const frames: Array<{ file: string; url: string; time: number | null }> = []
try {
for (const file of (await fsp.readdir(path.join(dir, 'frames'))).filter((f) => f.toLowerCase().endsWith('.jpg')).sort()) {
const fny = file.match(/_(\d+(?:\.\d+)?)s\.jpg$/i)
frames.push({
file,
url: `/api/meeting/frame?path=${encodeURIComponent(rel)}&file=${encodeURIComponent(file)}`,
time: fny ? Number(fny[1]) : times.get(file) ?? null,
})
}
} catch { /* no frames */ }
sendJson(res, 200, {
path: rel,
hasOutput: fs.existsSync(dir),
segments,
frames,
enhancedAvailable,
videoUrl: `/api/original?path=${encodeURIComponent(rel)}`,
})
}
function serveMeetingFrame(res: ServerResponse, rel: string, file: string): void {
const { dir } = meetusDirFor(rel)
const safe = path.basename(file)
const full = path.join(dir, 'frames', safe)
if (safe !== file || !fs.existsSync(full)) { sendJson(res, 404, { error: 'frame not found' }); return }
res.setHeader('Content-Type', 'image/jpeg')
res.setHeader('Cache-Control', 'no-cache')
fs.createReadStream(full).pipe(res)
}
async function getDetail(res: ServerResponse, rel: string): Promise<void> {
const index = readIndex()
if (!index) { sendJson(res, 404, { error: 'no index' }); return }