embed meetus
This commit is contained in:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user