0211cf33f3
Add handwriting OCR pipeline and update documentation - Introduce full OCR workflow: HWR extraction → PNG rendering → GLM-OCR → Ollama refinement - Add new modules for page rendering, GLM-OCR client, style refinement, and ZIP utilities - Update settings with drawj2d path and batch size controls - Document Phase 2 completion and known limitations in IMPLEMENTATION.md - Validate dependencies and handle cross-platform temp directory cleanup ```
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
import { runCommand } from "./process";
|
|
|
|
/**
|
|
* Extract a .rm file (which is a zip archive) to a temporary directory.
|
|
* Returns the path to the extraction directory.
|
|
*/
|
|
export async function extractRmFile(rmPath: string, outputDir: string): Promise<string> {
|
|
// .rm files are zip archives
|
|
const { code, stderr } = await runCommand("unzip", ["-o", rmPath, "-d", outputDir]);
|
|
if (code !== 0) {
|
|
throw new Error(`Failed to extract .rm file: ${stderr}`);
|
|
}
|
|
return outputDir;
|
|
}
|
|
|
|
/**
|
|
* List the contents of the extracted .rm archive.
|
|
*/
|
|
export async function listRmContents(extractedDir: string): Promise<string[]> {
|
|
const { stdout, code, stderr } = await runCommand("find", [extractedDir, "-type", "f"]);
|
|
if (code !== 0) {
|
|
throw new Error(`Failed to list .rm contents: ${stderr}`);
|
|
}
|
|
return stdout.trim().split("\n").filter(Boolean);
|
|
}
|
|
|
|
/**
|
|
* Check if a document is a notebook (handwritten) or a PDF/ebook.
|
|
* On reMarkable v3.x, notebooks have .rm page files and no content.pdf.
|
|
* Falls back to rendering a test page if ambiguous.
|
|
*/
|
|
export async function isNotebook(extractedDir: string): Promise<boolean> {
|
|
const contents = await listRmContents(extractedDir);
|
|
// Notebooks have .rm page files and no embedded PDF content
|
|
const hasRmPages = contents.some((f) => f.endsWith(".rm") && !f.endsWith(".zip"));
|
|
const hasContentPdf = contents.some((f) => f.endsWith("content.pdf") || f.endsWith(".pdf"));
|
|
|
|
// Clear case
|
|
if (hasRmPages && !hasContentPdf) return true;
|
|
if (!hasRmPages || hasContentPdf) return false;
|
|
|
|
// Ambiguous: try to render a page
|
|
const pageFiles = contents.filter((f) => f.endsWith(".rm") && !f.endsWith(".zip"));
|
|
if (pageFiles.length === 0) return false;
|
|
|
|
try {
|
|
// Try rendering the first page
|
|
const { code } = await runCommand("file", [pageFiles[0]]);
|
|
// If file command succeeds and identifies it as data, assume notebook
|
|
return code === 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the list of page .rm files from an extracted notebook.
|
|
* Returns basenames only.
|
|
* Recursively searches for .rm files (not just root).
|
|
*/
|
|
export async function getPageFiles(extractedDir: string): Promise<string[]> {
|
|
const contents = await listRmContents(extractedDir);
|
|
return contents.filter((f) => f.endsWith(".rm") && !f.endsWith(".zip")).sort();
|
|
}
|