refactor: update MinerU to use tar.gz and mineru CLI
- Change archive format from zip to tar.gz - Use 'mineru' CLI command instead of 'magic-pdf' - Install mineru[all] via pipx in Dockerfile - Remove zip dependency from Dockerfile - Update normalizeFiletype to output tar.gz extension - Update all tests for tar.gz output format - Simplify README MinerU section
This commit is contained in:
parent
e5ca364563
commit
f7ebc084ea
5 changed files with 93 additions and 58 deletions
|
|
@ -139,10 +139,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
pipx \
|
pipx \
|
||||||
# === 系統工具 ===
|
# === 系統工具 ===
|
||||||
locales \
|
locales \
|
||||||
zip \
|
|
||||||
# === 清理 ===
|
# === 清理 ===
|
||||||
&& pipx install "markitdown[all]" \
|
&& pipx install "markitdown[all]" \
|
||||||
&& pipx install magic-pdf \
|
&& pipx install "mineru[all]" \
|
||||||
# 清理 apt cache
|
# 清理 apt cache
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
|
|
||||||
|
|
@ -173,12 +173,12 @@ docker compose up -d
|
||||||
|
|
||||||
## MinerU
|
## MinerU
|
||||||
|
|
||||||
ConvertX 內建 MinerU 轉換引擎,可將文件轉換為 Markdown。
|
ConvertX 內建文件轉換引擎。
|
||||||
|
|
||||||
- md-t
|
- md-t
|
||||||
- md-i
|
- md-i
|
||||||
|
|
||||||
輸出格式為 ZIP。
|
輸出格式:tar.gz
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,32 +14,32 @@ export const properties = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to create a ZIP archive from a directory
|
* Helper function to create a tar.gz archive from a directory
|
||||||
*/
|
*/
|
||||||
function createZipArchive(
|
function createTarGzArchive(
|
||||||
sourceDir: string,
|
sourceDir: string,
|
||||||
outputZip: string,
|
outputTarGz: string,
|
||||||
execFile: ExecFileFn,
|
execFile: ExecFileFn,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Use zip command to create archive
|
// Use tar command to create gzipped archive
|
||||||
|
// tar -czf <output.tar.gz> -C <sourceDir> .
|
||||||
execFile(
|
execFile(
|
||||||
"zip",
|
"tar",
|
||||||
["-r", outputZip, "."],
|
["-czf", outputTarGz, "-C", sourceDir, "."],
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`zip error: ${error}`);
|
reject(`tar error: ${error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
console.log(`zip stdout: ${stdout}`);
|
console.log(`tar stdout: ${stdout}`);
|
||||||
}
|
}
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
console.error(`zip stderr: ${stderr}`);
|
console.error(`tar stderr: ${stderr}`);
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
{ cwd: sourceDir },
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +104,7 @@ export async function convert(
|
||||||
args.push("--table-mode", "markdown");
|
args.push("--table-mode", "markdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
execFile("magic-pdf", args, async (error, stdout, stderr) => {
|
execFile("mineru", args, async (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`mineru error: ${error}`);
|
reject(`mineru error: ${error}`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -122,30 +122,30 @@ export async function convert(
|
||||||
// MinerU outputs to a subdirectory, find the actual output
|
// MinerU outputs to a subdirectory, find the actual output
|
||||||
const mineruActualOutput = join(mineruOutputDir, "auto");
|
const mineruActualOutput = join(mineruOutputDir, "auto");
|
||||||
|
|
||||||
// Create ZIP archive from the output directory
|
// Create tar.gz archive from the output directory
|
||||||
const zipPath = targetPath.endsWith(".zip")
|
const tarGzPath = targetPath.endsWith(".tar.gz")
|
||||||
? targetPath
|
? targetPath
|
||||||
: `${targetPath}.zip`;
|
: `${targetPath}.tar.gz`;
|
||||||
|
|
||||||
// Ensure the parent directory exists
|
// Ensure the parent directory exists
|
||||||
const zipDir = dirname(zipPath);
|
const tarGzDir = dirname(tarGzPath);
|
||||||
if (!existsSync(zipDir)) {
|
if (!existsSync(tarGzDir)) {
|
||||||
mkdirSync(zipDir, { recursive: true });
|
mkdirSync(tarGzDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the actual MinerU output directory for zipping
|
// Use the actual MinerU output directory for archiving
|
||||||
const outputToZip = existsSync(mineruActualOutput)
|
const outputToArchive = existsSync(mineruActualOutput)
|
||||||
? mineruActualOutput
|
? mineruActualOutput
|
||||||
: mineruOutputDir;
|
: mineruOutputDir;
|
||||||
|
|
||||||
await createZipArchive(outputToZip, zipPath, execFile);
|
await createTarGzArchive(outputToArchive, tarGzPath, execFile);
|
||||||
|
|
||||||
// Clean up the temporary directory
|
// Clean up the temporary directory
|
||||||
removeDir(mineruOutputDir);
|
removeDir(mineruOutputDir);
|
||||||
|
|
||||||
resolve("Done");
|
resolve("Done");
|
||||||
} catch (zipError) {
|
} catch (tarError) {
|
||||||
reject(`Failed to create ZIP archive: ${zipError}`);
|
reject(`Failed to create tar.gz archive: ${tarError}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,10 @@ export const normalizeOutputFiletype = (filetype: string): string => {
|
||||||
case "markdown_mmd":
|
case "markdown_mmd":
|
||||||
case "markdown":
|
case "markdown":
|
||||||
return "md";
|
return "md";
|
||||||
// MinerU output formats - output as ZIP
|
// MinerU output formats - output as tar.gz
|
||||||
case "md-t":
|
case "md-t":
|
||||||
case "md-i":
|
case "md-i":
|
||||||
return "zip";
|
return "tar.gz";
|
||||||
default:
|
default:
|
||||||
return lowercaseFiletype;
|
return lowercaseFiletype;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test";
|
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
||||||
import { convert, properties } from "../../src/converters/mineru";
|
import { convert, properties } from "../../src/converters/mineru";
|
||||||
import type { ExecFileException } from "node:child_process";
|
import type { ExecFileException } from "node:child_process";
|
||||||
import { ExecFileFn } from "../../src/converters/types";
|
import { ExecFileFn } from "../../src/converters/types";
|
||||||
|
|
@ -44,8 +44,8 @@ describe("MinerU converter md-t mode", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should call magic-pdf with markdown table mode for md-t", async () => {
|
test("should call mineru with markdown table mode for md-t", async () => {
|
||||||
let magicPdfArgs: string[] = [];
|
let mineruArgs: string[] = [];
|
||||||
|
|
||||||
const mockExecFile: ExecFileFn = (
|
const mockExecFile: ExecFileFn = (
|
||||||
cmd: string,
|
cmd: string,
|
||||||
|
|
@ -53,8 +53,8 @@ describe("MinerU converter md-t mode", () => {
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
options?: any,
|
options?: any,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "magic-pdf") {
|
if (cmd === "mineru") {
|
||||||
magicPdfArgs = args;
|
mineruArgs = args;
|
||||||
// Simulate MinerU creating output
|
// Simulate MinerU creating output
|
||||||
const outputDir = args[3]; // -o argument
|
const outputDir = args[3]; // -o argument
|
||||||
if (outputDir && !existsSync(outputDir)) {
|
if (outputDir && !existsSync(outputDir)) {
|
||||||
|
|
@ -66,17 +66,17 @@ describe("MinerU converter md-t mode", () => {
|
||||||
}
|
}
|
||||||
writeFileSync(join(autoDir, "output.md"), "# Test\n\n| Col1 | Col2 |\n|---|---|\n| A | B |");
|
writeFileSync(join(autoDir, "output.md"), "# Test\n\n| Col1 | Col2 |\n|---|---|\n| A | B |");
|
||||||
callback(null, "MinerU conversion complete", "");
|
callback(null, "MinerU conversion complete", "");
|
||||||
} else if (cmd === "zip") {
|
} else if (cmd === "tar") {
|
||||||
// Simulate zip creation
|
// Simulate tar.gz creation
|
||||||
callback(null, "Archive created", "");
|
callback(null, "Archive created", "");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const targetPath = join(testDir, "output.zip");
|
const targetPath = join(testDir, "output.tar.gz");
|
||||||
await convert("test.pdf", "pdf", "md-t", targetPath, undefined, mockExecFile);
|
await convert("test.pdf", "pdf", "md-t", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
expect(magicPdfArgs).toContain("--table-mode");
|
expect(mineruArgs).toContain("--table-mode");
|
||||||
expect(magicPdfArgs).toContain("markdown");
|
expect(mineruArgs).toContain("markdown");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -95,7 +95,7 @@ describe("MinerU converter md-i mode", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should call magic-pdf with image table mode for md-i", async () => {
|
test("should call mineru with image table mode for md-i", async () => {
|
||||||
let capturedArgs: string[] = [];
|
let capturedArgs: string[] = [];
|
||||||
|
|
||||||
const mockExecFile: ExecFileFn = (
|
const mockExecFile: ExecFileFn = (
|
||||||
|
|
@ -104,7 +104,7 @@ describe("MinerU converter md-i mode", () => {
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
options?: any,
|
options?: any,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "magic-pdf") {
|
if (cmd === "mineru") {
|
||||||
capturedArgs = args;
|
capturedArgs = args;
|
||||||
// Simulate MinerU creating output
|
// Simulate MinerU creating output
|
||||||
const outputDir = args[3]; // -o argument
|
const outputDir = args[3]; // -o argument
|
||||||
|
|
@ -117,13 +117,13 @@ describe("MinerU converter md-i mode", () => {
|
||||||
}
|
}
|
||||||
writeFileSync(join(autoDir, "output.md"), "# Test\n\n");
|
writeFileSync(join(autoDir, "output.md"), "# Test\n\n");
|
||||||
callback(null, "MinerU conversion complete", "");
|
callback(null, "MinerU conversion complete", "");
|
||||||
} else if (cmd === "zip") {
|
} else if (cmd === "tar") {
|
||||||
// Simulate zip creation
|
// Simulate tar.gz creation
|
||||||
callback(null, "Archive created", "");
|
callback(null, "Archive created", "");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const targetPath = join(testDir, "output.zip");
|
const targetPath = join(testDir, "output.tar.gz");
|
||||||
await convert("test.pdf", "pdf", "md-i", targetPath, undefined, mockExecFile);
|
await convert("test.pdf", "pdf", "md-i", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
expect(capturedArgs).toContain("--table-mode");
|
expect(capturedArgs).toContain("--table-mode");
|
||||||
|
|
@ -131,8 +131,8 @@ describe("MinerU converter md-i mode", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("MinerU converter ZIP output", () => {
|
describe("MinerU converter tar.gz output", () => {
|
||||||
const testDir = "./test-output-mineru-zip";
|
const testDir = "./test-output-mineru-targz";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
if (!existsSync(testDir)) {
|
if (!existsSync(testDir)) {
|
||||||
|
|
@ -146,9 +146,9 @@ describe("MinerU converter ZIP output", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should create ZIP archive from output", async () => {
|
test("should create tar.gz archive from output", async () => {
|
||||||
let zipCalled = false;
|
let tarCalled = false;
|
||||||
let zipArgs: string[] = [];
|
let tarArgs: string[] = [];
|
||||||
|
|
||||||
const mockExecFile: ExecFileFn = (
|
const mockExecFile: ExecFileFn = (
|
||||||
cmd: string,
|
cmd: string,
|
||||||
|
|
@ -156,7 +156,7 @@ describe("MinerU converter ZIP output", () => {
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
options?: any,
|
options?: any,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "magic-pdf") {
|
if (cmd === "mineru") {
|
||||||
// Simulate MinerU creating output
|
// Simulate MinerU creating output
|
||||||
const outputDir = args[3]; // -o argument
|
const outputDir = args[3]; // -o argument
|
||||||
if (outputDir && !existsSync(outputDir)) {
|
if (outputDir && !existsSync(outputDir)) {
|
||||||
|
|
@ -170,35 +170,71 @@ describe("MinerU converter ZIP output", () => {
|
||||||
mkdirSync(join(autoDir, "images"), { recursive: true });
|
mkdirSync(join(autoDir, "images"), { recursive: true });
|
||||||
writeFileSync(join(autoDir, "images", "img1.png"), "fake image data");
|
writeFileSync(join(autoDir, "images", "img1.png"), "fake image data");
|
||||||
callback(null, "MinerU conversion complete", "");
|
callback(null, "MinerU conversion complete", "");
|
||||||
} else if (cmd === "zip") {
|
} else if (cmd === "tar") {
|
||||||
zipCalled = true;
|
tarCalled = true;
|
||||||
zipArgs = args;
|
tarArgs = args;
|
||||||
callback(null, "Archive created", "");
|
callback(null, "Archive created", "");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const targetPath = join(testDir, "output.zip");
|
const targetPath = join(testDir, "output.tar.gz");
|
||||||
const result = await convert("test.pdf", "pdf", "md-t", targetPath, undefined, mockExecFile);
|
const result = await convert("test.pdf", "pdf", "md-t", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
expect(result).toBe("Done");
|
expect(result).toBe("Done");
|
||||||
expect(zipCalled).toBe(true);
|
expect(tarCalled).toBe(true);
|
||||||
expect(zipArgs).toContain("-r");
|
expect(tarArgs).toContain("-czf");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should reject on magic-pdf error", async () => {
|
test("should reject on mineru error", async () => {
|
||||||
const mockExecFile: ExecFileFn = (
|
const mockExecFile: ExecFileFn = (
|
||||||
cmd: string,
|
cmd: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
options?: any,
|
options?: any,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "magic-pdf") {
|
if (cmd === "mineru") {
|
||||||
callback(new Error("MinerU failed") as ExecFileException, "", "Error processing file");
|
callback(new Error("MinerU failed") as ExecFileException, "", "Error processing file");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const targetPath = join(testDir, "output.zip");
|
const targetPath = join(testDir, "output.tar.gz");
|
||||||
expect(convert("test.pdf", "pdf", "md-t", targetPath, undefined, mockExecFile))
|
expect(convert("test.pdf", "pdf", "md-t", targetPath, undefined, mockExecFile))
|
||||||
.rejects.toMatch(/mineru error/);
|
.rejects.toMatch(/mineru error/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should use correct tar arguments for compression", async () => {
|
||||||
|
let tarArgs: string[] = [];
|
||||||
|
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
options?: any,
|
||||||
|
) => {
|
||||||
|
if (cmd === "mineru") {
|
||||||
|
const outputDir = args[3];
|
||||||
|
if (outputDir && !existsSync(outputDir)) {
|
||||||
|
mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const autoDir = join(outputDir, "auto");
|
||||||
|
if (!existsSync(autoDir)) {
|
||||||
|
mkdirSync(autoDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(join(autoDir, "output.md"), "# Test");
|
||||||
|
callback(null, "Done", "");
|
||||||
|
} else if (cmd === "tar") {
|
||||||
|
tarArgs = args;
|
||||||
|
callback(null, "Archive created", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "test_MINERU_md-t.tar.gz");
|
||||||
|
await convert("test.pdf", "pdf", "md-t", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
|
// Verify tar is called with correct compression flags
|
||||||
|
expect(tarArgs[0]).toBe("-czf");
|
||||||
|
expect(tarArgs[1]).toContain(".tar.gz");
|
||||||
|
expect(tarArgs[2]).toBe("-C");
|
||||||
|
expect(tarArgs[4]).toBe(".");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue