Files
chatpapers/src/modules/lecture/infrastructure/storage/lectureStore.ts
T
2026-08-29 22:46:28 +08:00

148 lines
3.9 KiB
TypeScript

import type { BeatType, SummaryJson } from "../../domain/types";
import type { TextChunk } from "../pdf/chunker";
import { joinPath } from "../platform/paths";
import { ensureLectureDirs, lectureDataRoot } from "./paths";
export interface StoredBeat {
id: string;
orderIndex: number;
title?: string;
type: BeatType;
script: string;
refs: string[];
audioPath?: string;
ttsStatus: "pending" | "ready" | "failed";
}
export interface StoredSentence {
id: string;
orderIndex: number;
text: string;
translation: string;
page?: number;
searchText?: string;
audioPathEn?: string;
audioPathZh?: string;
ttsEnStatus: "pending" | "ready" | "failed" | "skipped";
ttsZhStatus: "pending" | "ready" | "failed";
}
export interface StoredParagraph {
id: string;
orderIndex: number;
originalText: string;
translation: string;
contextLink: string;
roleInPaper: string;
page?: number;
searchText?: string;
sentences?: StoredSentence[];
audioPathEn?: string;
audioPathZh?: string;
ttsEnStatus: "pending" | "ready" | "failed" | "skipped";
ttsZhStatus: "pending" | "ready" | "failed";
genStatus: "pending" | "ready" | "failed";
}
export interface LectureData {
paperId: string;
cacheKey: string;
/** 1 = summary, 2 = beats+TTS, 3 = paragraphs+note */
phase?: 1 | 2 | 3;
summary?: SummaryJson;
chunks?: TextChunk[];
beats?: StoredBeat[];
paragraphs?: StoredParagraph[];
noteItemId?: string;
extractChars?: number;
extractSource?: string;
updatedAt: number;
}
function lectureDataPath(cacheKey: string): string {
const safe = cacheKey.replace(/[^a-zA-Z0-9_|.-]/g, "_");
return joinPath(lectureDataRoot(), "data", `${safe}.json`);
}
export async function loadLectureData(
cacheKey: string,
): Promise<LectureData | undefined> {
await ensureLectureDirs();
const path = lectureDataPath(cacheKey);
try {
if (!(await IOUtils.exists(path))) return undefined;
const raw = await IOUtils.readUTF8(path);
return JSON.parse(raw) as LectureData;
} catch (e) {
ztoolkit.log("[ChatPapers:Lecture] loadLectureData failed", e);
return undefined;
}
}
export async function saveLectureData(data: LectureData): Promise<void> {
await ensureLectureDirs();
const dir = joinPath(lectureDataRoot(), "data");
await IOUtils.makeDirectory(dir, {
createAncestors: true,
ignoreExisting: true,
});
data.updatedAt = Date.now();
await IOUtils.writeUTF8(
lectureDataPath(data.cacheKey),
JSON.stringify(data, null, 2),
);
}
export async function mergeLectureData(
cacheKey: string,
paperId: string,
patch: Partial<LectureData>,
): Promise<LectureData> {
const existing =
(await loadLectureData(cacheKey)) ??
({
paperId,
cacheKey,
updatedAt: Date.now(),
} satisfies LectureData);
const merged: LectureData = {
...existing,
...patch,
paperId,
cacheKey,
updatedAt: Date.now(),
};
await saveLectureData(merged);
return merged;
}
export function isStep1Ready(data: LectureData | undefined): boolean {
if (!data?.beats?.length) return false;
return data.beats.some((b) => b.ttsStatus === "ready" && b.audioPath);
}
export function isStep2Ready(data: LectureData | undefined): boolean {
if (!data?.paragraphs?.length) return false;
return data.paragraphs.some(
(p) => p.genStatus === "ready" && p.ttsZhStatus === "ready" && p.audioPathZh,
);
}
export function isLectureComplete(data: LectureData | undefined): boolean {
return isStep1Ready(data) && isStep2Ready(data);
}
export function playableBeats(data: LectureData | undefined): StoredBeat[] {
if (!data?.beats) return [];
return data.beats.filter((b) => b.ttsStatus === "ready" && b.audioPath);
}
export function playableParagraphs(
data: LectureData | undefined,
): StoredParagraph[] {
if (!data?.paragraphs) return [];
return data.paragraphs.filter(
(p) => p.genStatus === "ready" && p.ttsZhStatus === "ready" && p.audioPathZh,
);
}