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 { 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 { 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, ): Promise { 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, ); }