feat(ai): read the importer's deck and transcript cache; studio DraftWriter fits DraftSink as is
Retrieval now reads <dataDir>/cache/decks/<courseId>/<deckId>/text.json and
<dataDir>/cache/transcripts/<courseId>/<lessonId>.json (skipped when the lesson
has a transcript file). DraftSink returns { location?, branch?, commit? }, so
the studio's createDraftWriter(ctx) needs no adapter; generateDraft computes
paths itself and reports the draft branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
08d3e3f642
commit
6fb0019cd9
6 files changed
+156
-12
No files matched your search
@@ -128,6 +128,18 @@ function lessonTitleOf(course: LoadedCourse, lessonId: string | undefined): stri
|
||||
return lessonId ? course.lessonIndex.get(lessonId)?.lesson.title : undefined;
|
||||
}
|
||||
|
||||
async function readJson<T>(path: string): Promise<T | null> {
|
||||
try {
|
||||
const f = Bun.file(path);
|
||||
if (!(await f.exists()) || f.size > MAX_FILE * 25) return null;
|
||||
return (await f.json()) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const clock = (s: number) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
|
||||
|
||||
export async function buildCourseIndex(course: LoadedCourse, dataDir: string): Promise<CourseIndex> {
|
||||
const bm = new Bm25<Chunk>();
|
||||
const seen = new Set<string>();
|
||||
@@ -179,8 +191,50 @@ export async function buildCourseIndex(course: LoadedCourse, dataDir: string): P
|
||||
});
|
||||
}
|
||||
|
||||
// Derived text from the importer and transcription jobs: <dataDir>/cache/<courseId>/**.
|
||||
const decks = new Map((course.manifest?.decks ?? []).map((d) => [d.id, d]));
|
||||
|
||||
// The importer's deck cache: <dataDir>/cache/decks/<courseId>/<deckId>/text.json = [{ page, text }].
|
||||
for (const deck of decks.values()) {
|
||||
const pages = await readJson<{ page?: number; text?: string }[]>(join(dataDir, "cache", "decks", course.id, deck.id, "text.json"));
|
||||
if (!Array.isArray(pages)) continue;
|
||||
const lessonId = course.lessonIndex.has(deck.attachTo) ? deck.attachTo : undefined;
|
||||
for (const p of pages) {
|
||||
const text = typeof p?.text === "string" ? p.text.trim() : "";
|
||||
if (!text) continue;
|
||||
for (const part of splitLong(text)) {
|
||||
add({ path: posix(deck.file), kind: "deck", text: part, heading: p.page ? `Slide ${p.page}` : deck.title, lessonId, lessonTitle: lessonTitleOf(course, lessonId) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The importer's transcription cache: <dataDir>/cache/transcripts/<courseId>/<lessonId>.json =
|
||||
// { lessonId, text, segments: [{ start, end, text }] }. Skipped when the lesson has a transcript file in the
|
||||
// course, which the transcription job writes alongside and which is indexed above.
|
||||
const trDir = join(dataDir, "cache", "transcripts", course.id);
|
||||
for (const name of await readdir(trDir).catch(() => [] as string[])) {
|
||||
if (extname(name) !== ".json") continue;
|
||||
const lessonId = name.slice(0, -5);
|
||||
const entry = course.lessonIndex.get(lessonId);
|
||||
if (!entry || entry.lesson.transcript) continue;
|
||||
const tr = await readJson<{ text?: string; segments?: { start?: number; text?: string }[] }>(join(trDir, name));
|
||||
if (!tr) continue;
|
||||
const segs = Array.isArray(tr.segments) ? tr.segments.filter((s) => typeof s?.text === "string" && s.text.trim()) : [];
|
||||
const blocks: { at: number; text: string }[] = [];
|
||||
if (segs.length) {
|
||||
for (const s of segs) {
|
||||
const last = blocks[blocks.length - 1];
|
||||
if (last && last.text.length + s.text!.length < MAX_CHUNK) last.text += ` ${s.text!.trim()}`;
|
||||
else blocks.push({ at: Number(s.start) || 0, text: s.text!.trim() });
|
||||
}
|
||||
} else if (typeof tr.text === "string" && tr.text.trim()) {
|
||||
blocks.push(...splitLong(tr.text.trim()).map((t) => ({ at: -1, text: t })));
|
||||
}
|
||||
for (const b of blocks) {
|
||||
add({ path: entry.lesson.file ? posix(entry.lesson.file) : `transcripts/${lessonId}`, kind: "transcript", text: b.text, heading: b.at >= 0 ? `at ${clock(b.at)}` : undefined, lessonId, lessonTitle: entry.lesson.title });
|
||||
}
|
||||
}
|
||||
|
||||
// Older layout: plain text under <dataDir>/cache/<courseId>/**.
|
||||
const cacheRoot = join(dataDir, "cache", course.id);
|
||||
await walk(cacheRoot, async (abs, rel) => {
|
||||
if (!CACHE_TEXT.has(extname(rel).toLowerCase())) return true;
|
||||
|
||||
Reference in new issue
Block a user