Initial marker-api benchmark setup: Containerfile, app.py, convert_both.py, compare_conversions.py
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Recursively convert supported documents to markdown via marker-api.
|
||||
.DESCRIPTION
|
||||
Scans a target folder for PDFs, DOCX, XLSX, PPTX, EPUB, images, and HTML.
|
||||
Sends each file to the marker-api /marker endpoint (multipart/form-data)
|
||||
and writes the resulting .md file beside the original.
|
||||
.EXAMPLE
|
||||
.\marker-convert.ps1 -TargetFolder .\documents
|
||||
.\marker-convert.ps1 .\documents -Force -MaxConcurrency 8
|
||||
.\marker-convert.ps1 .\documents -ApiUrl http://10.0.0.5:8000/marker
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0)]
|
||||
[string]$TargetFolder,
|
||||
|
||||
[Uri]$ApiUrl = "http://localhost:8000/marker",
|
||||
|
||||
[ValidateSet("markdown", "json", "html", "chunks")]
|
||||
[string]$OutputFormat = "markdown",
|
||||
|
||||
[switch]$Force,
|
||||
|
||||
[ValidateRange(1, 32)]
|
||||
[int]$MaxConcurrency = 4,
|
||||
|
||||
[ValidateRange(30, 1800)]
|
||||
[int]$Timeout = 300,
|
||||
|
||||
[string]$PageRange = "",
|
||||
|
||||
[switch]$ForceOcr,
|
||||
|
||||
[switch]$DisableImageExtraction,
|
||||
|
||||
[switch]$UseLlm,
|
||||
|
||||
[string]$LlmService = "marker.services.ollama.OllamaService",
|
||||
|
||||
[string]$Processors = "",
|
||||
|
||||
[string]$ConfigJson = "",
|
||||
|
||||
[string]$ConverterCls = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# ════════════ 1. Validate ════════════
|
||||
$TargetFolder = (Resolve-Path $TargetFolder).Path
|
||||
if (-not (Test-Path $TargetFolder)) {
|
||||
Write-Error "Target folder does not exist: $TargetFolder"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "====================================================" -ForegroundColor Cyan
|
||||
Write-Host " Force overwrite: $(if ($Force) { 'Yes' } else { 'No' })" -ForegroundColor White
|
||||
Write-Host " Force OCR : $(if ($ForceOcr) { 'Yes' } else { 'No' })" -ForegroundColor White
|
||||
Write-Host " Use LLM : $(if ($UseLlm) { 'Yes' } else { 'No' })" -ForegroundColor White
|
||||
Write-Host " LLM Service : $LlmService" -ForegroundColor White
|
||||
Write-Host " Disable ImgExt : $(if ($DisableImageExtraction) { 'Yes' } else { 'No' })" -ForegroundColor White
|
||||
if ($PageRange) { Write-Host " Page range : $PageRange" -ForegroundColor White }
|
||||
if ($Processors) { Write-Host " Processors : $Processors" -ForegroundColor White }
|
||||
if ($ConverterCls) { Write-Host " Converter : $ConverterCls" -ForegroundColor White }
|
||||
Write-Host "====================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# ════════════ 2. Discover files ════════════
|
||||
$supportedExtensions = @('.pdf', '.docx', '.xlsx', '.pptx', '.epub',
|
||||
'.png', '.jpg', '.jpeg', '.bmp', '.gif',
|
||||
'.tiff', '.tif', '.webp', '.heic', '.html', '.htm')
|
||||
|
||||
$files = Get-ChildItem -Path $TargetFolder -Recurse -File |
|
||||
Where-Object { $_.Extension -in $supportedExtensions } |
|
||||
Sort-Object FullName
|
||||
|
||||
if ($files.Count -eq 0) {
|
||||
Write-Host "No supported files found in $TargetFolder." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Found $($files.Count) document(s) to convert." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# ════════════ 3. Worker function ════════════
|
||||
# This script block runs inside Start-Job for each file.
|
||||
$ScriptBlock = {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Uri]$ApiUrl,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Ofmt,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[bool]$Force,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[int]$Timeout,
|
||||
|
||||
[string]$PageRange,
|
||||
|
||||
[bool]$ForceOcr,
|
||||
|
||||
[bool]$DisableImageExtraction,
|
||||
|
||||
[bool]$UseLlm,
|
||||
|
||||
[string]$LlmService,
|
||||
|
||||
[string]$Processors,
|
||||
|
||||
[string]$ConfigJson,
|
||||
|
||||
[string]$ConverterCls
|
||||
)
|
||||
|
||||
# Build output path (same directory, same base name with .md extension)
|
||||
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($FilePath)
|
||||
$dirPath = Split-Path $FilePath
|
||||
$mdPath = Join-Path $dirPath "$baseName.md"
|
||||
|
||||
$result = [ordered]@{}
|
||||
$result.File = (Split-Path $FilePath -Leaf)
|
||||
$result.SizeBytes = (Get-Item $FilePath).Length
|
||||
$result.Status = "pending"
|
||||
$result.ElapsedMs = 0
|
||||
$result.Error = ""
|
||||
$result.OutputFile = ""
|
||||
|
||||
# Skip if output already exists and -Force was not used
|
||||
if ((Test-Path $mdPath) -and ($Force -eq $false)) {
|
||||
$result.Status = "skipped"
|
||||
$result.OutputFile = $mdPath
|
||||
return $result
|
||||
}
|
||||
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
|
||||
try {
|
||||
$bin = [System.IO.File]::ReadAllBytes($FilePath)
|
||||
$name = [System.IO.Path]::GetFileName($FilePath)
|
||||
$boundary = "----marker-convert-$(Get-Random -Minimum 100000 -Maximum 999999)"
|
||||
$enc = [System.Text.Encoding]::UTF8
|
||||
|
||||
# ── Build multipart body ──
|
||||
$contentParts = [System.Collections.ArrayList]::new()
|
||||
|
||||
# Part 1: file
|
||||
$fileHeader = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"file`"; filename=`"$name`"`r`n" +
|
||||
"Content-Type: application/octet-stream`r`n`r`n"
|
||||
)
|
||||
$contentParts.Add($fileHeader) | Out-Null
|
||||
$contentParts.Add($bin) | Out-Null
|
||||
$contentParts.Add($enc.GetBytes("`r`n")) | Out-Null
|
||||
|
||||
# Part 2: output_format
|
||||
$fmtPart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"output_format`"`r`n`r`n" +
|
||||
"$Ofmt`r`n"
|
||||
)
|
||||
$contentParts.Add($fmtPart) | Out-Null
|
||||
|
||||
# Part 3: force_ocr
|
||||
$ocrPart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"force_ocr`"`r`n`r`n" +
|
||||
"$(if ($ForceOcr) { 'true' } else { 'false' })`r`n"
|
||||
)
|
||||
$contentParts.Add($ocrPart) | Out-Null
|
||||
|
||||
# Part 4: paginate_output (default false)
|
||||
$pagPart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"paginate_output`"`r`n`r`n" +
|
||||
"false`r`n"
|
||||
)
|
||||
$contentParts.Add($pagPart) | Out-Null
|
||||
|
||||
# Part 5: use_llm
|
||||
$llmPart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"use_llm`"`r`n`r`n" +
|
||||
"$(if ($UseLlm) { 'true' } else { 'false' })`r`n"
|
||||
)
|
||||
$contentParts.Add($llmPart) | Out-Null
|
||||
|
||||
# Part 6a: llm_service (only if UseLlm or custom service)
|
||||
if ($UseLlm -or $LlmService -ne "marker.services.ollama.OllamaService") {
|
||||
$svcPart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"llm_service`"`r`n`r`n" +
|
||||
"$LlmService`r`n"
|
||||
)
|
||||
$contentParts.Add($svcPart) | Out-Null
|
||||
}
|
||||
|
||||
# Part 6b: page_range
|
||||
if ($PageRange) {
|
||||
$prPart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"page_range`"`r`n`r`n" +
|
||||
"$PageRange`r`n"
|
||||
)
|
||||
$contentParts.Add($prPart) | Out-Null
|
||||
}
|
||||
|
||||
# Part 6c: disable_image_extraction
|
||||
if ($DisableImageExtraction) {
|
||||
$diePart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"disable_image_extraction`"`r`n`r`n" +
|
||||
"true`r`n"
|
||||
)
|
||||
$contentParts.Add($diePart) | Out-Null
|
||||
}
|
||||
|
||||
# Part 6d: processors
|
||||
if ($Processors) {
|
||||
$prcPart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"processors`"`r`n`r`n" +
|
||||
"$Processors`r`n"
|
||||
)
|
||||
$contentParts.Add($prcPart) | Out-Null
|
||||
}
|
||||
|
||||
# Part 6e: config_json
|
||||
if ($ConfigJson) {
|
||||
$cfgPart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"config_json`"`r`n`r`n" +
|
||||
"$ConfigJson`r`n"
|
||||
)
|
||||
$contentParts.Add($cfgPart) | Out-Null
|
||||
}
|
||||
|
||||
# Part 6f: converter_cls
|
||||
if ($ConverterCls) {
|
||||
$ccPart = $enc.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"converter_cls`"`r`n`r`n" +
|
||||
"$ConverterCls`r`n"
|
||||
)
|
||||
$contentParts.Add($ccPart) | Out-Null
|
||||
}
|
||||
|
||||
# Closing boundary
|
||||
$closing = $enc.GetBytes("--$boundary--`r`n")
|
||||
|
||||
# Calculate total content-length
|
||||
$contentLength = 0
|
||||
foreach ($p in $contentParts) {
|
||||
$contentLength += $p.Length
|
||||
}
|
||||
$contentLength += $closing.Length
|
||||
|
||||
# ── Create HTTP request ──
|
||||
$request = [System.Net.HttpWebRequest]::Create($ApiUrl)
|
||||
$request.Method = "POST"
|
||||
$request.ContentType = "multipart/form-data; boundary=$boundary"
|
||||
$request.ContentLength = $contentLength
|
||||
$request.Timeout = $Timeout * 1000
|
||||
$request.KeepAlive = $true
|
||||
$request.ProtocolVersion = [System.Net.HttpVersion]::Version11
|
||||
|
||||
$reqStream = $request.GetRequestStream()
|
||||
|
||||
# Write content parts
|
||||
foreach ($p in $contentParts) {
|
||||
$reqStream.Write($p, 0, $p.Length)
|
||||
}
|
||||
# Write closing boundary
|
||||
$reqStream.Write($closing, 0, $closing.Length)
|
||||
$reqStream.Flush()
|
||||
$reqStream.Close()
|
||||
|
||||
# ── Get response ──
|
||||
$response = $request.GetResponse()
|
||||
$resStream = $response.GetResponseStream()
|
||||
$reader = [System.IO.StreamReader]::new($resStream, $enc)
|
||||
$content = $reader.ReadToEnd()
|
||||
$reader.Close()
|
||||
$resStream.Close()
|
||||
$response.Close()
|
||||
|
||||
# ── Write output file ──
|
||||
[System.IO.File]::WriteAllText($mdPath, $content, $enc)
|
||||
|
||||
$sw.Stop()
|
||||
$result.Status = "success"
|
||||
$result.ElapsedMs = $sw.ElapsedMilliseconds
|
||||
$result.OutputFile = $mdPath
|
||||
$result.ResponseSize = $content.Length
|
||||
|
||||
} catch {
|
||||
$sw.Stop()
|
||||
$result.Status = "failed"
|
||||
$result.ElapsedMs = $sw.ElapsedMilliseconds
|
||||
$result.Error = $_.Exception.Message
|
||||
if ($_.Exception.Response) {
|
||||
$result.Error += "`n Status: $([int]$_.Exception.Response.StatusCode)"
|
||||
}
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# ════════════ 4. Dispatch ════════════
|
||||
Write-Host "Dispatching $($files.Count) conversion(s) with $($MaxConcurrency) worker(s)..." -ForegroundColor Yellow
|
||||
|
||||
$allResults = @()
|
||||
$runningJobs = @()
|
||||
|
||||
for ($i = 0; $i -lt $files.Count; $i++) {
|
||||
# If we've reached concurrency limit, wait for at least one job to complete
|
||||
while ($runningJobs.Count -ge $MaxConcurrency) {
|
||||
$completed = $runningJobs | Where-Object { $_.State -in @('Completed','Failed') }
|
||||
if ($completed) {
|
||||
foreach ($job in $completed) {
|
||||
try {
|
||||
$allResults += Receive-Job $job
|
||||
} catch {
|
||||
$r = [ordered]@{}
|
||||
$r.File = "unknown"
|
||||
$r.SizeBytes = 0
|
||||
$r.Status = "failed"
|
||||
$r.ElapsedMs = 0
|
||||
$r.Error = $_.Exception.Message
|
||||
$r.OutputFile = ""
|
||||
$allResults += $r
|
||||
}
|
||||
$runningJobs = @($runningJobs | Where-Object { $_.Id -ne $job.Id })
|
||||
Remove-Job $job 2>&1 | Out-Null
|
||||
}
|
||||
} else {
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
}
|
||||
|
||||
# Launch new job
|
||||
$f = $files[$i]
|
||||
$safeName = [System.Security.SecurityElement]::Escape($f.Name)
|
||||
$jobName = "mk_$safeName"
|
||||
|
||||
$job = Start-Job `
|
||||
-Name $jobName `
|
||||
-ScriptBlock $ScriptBlock `
|
||||
-ArgumentList @(
|
||||
$f.FullName
|
||||
$ApiUrl
|
||||
$OutputFormat
|
||||
[bool]$Force
|
||||
$Timeout
|
||||
$PageRange
|
||||
[bool]$ForceOcr
|
||||
[bool]$DisableImageExtraction
|
||||
[bool]$UseLlm
|
||||
$LlmService
|
||||
$Processors
|
||||
$ConfigJson
|
||||
$ConverterCls
|
||||
)
|
||||
|
||||
$runningJobs += $job
|
||||
Write-Host " [$($i + 1)/$($files.Count)] Queued: $($f.Name)" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Wait for remaining jobs
|
||||
while ($runningJobs.Count -gt 0) {
|
||||
$completed = $runningJobs | Where-Object { $_.State -in @('Completed','Failed') }
|
||||
if ($completed) {
|
||||
foreach ($job in $completed) {
|
||||
try {
|
||||
$allResults += Receive-Job $job
|
||||
} catch {
|
||||
$r = [ordered]@{}
|
||||
$r.File = "unknown"
|
||||
$r.SizeBytes = 0
|
||||
$r.Status = "failed"
|
||||
$r.ElapsedMs = 0
|
||||
$r.Error = $_.Exception.Message
|
||||
$r.OutputFile = ""
|
||||
$allResults += $r
|
||||
}
|
||||
$runningJobs = @($runningJobs | Where-Object { $_.Id -ne $job.Id })
|
||||
Remove-Job $job 2>&1 | Out-Null
|
||||
}
|
||||
} else {
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
}
|
||||
|
||||
# ════════════ 5. Report ════════════
|
||||
$ok = @($allResults | Where-Object { $_.Status -eq "success" })
|
||||
$fail = @($allResults | Where-Object { $_.Status -eq "failed" })
|
||||
$skip = @($allResults | Where-Object { $_.Status -eq "skipped" })
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "====== Conversion Results ======" -ForegroundColor Cyan
|
||||
Write-Host " Total files : $($files.Count)" -ForegroundColor White
|
||||
Write-Host " Successful : $($ok.Count) " -ForegroundColor Green
|
||||
Write-Host " Failed : $($fail.Count) " -ForegroundColor Red
|
||||
Write-Host " Skipped : $($skip.Count) " -ForegroundColor Yellow
|
||||
|
||||
if ($ok.Count -gt 0) {
|
||||
$avgMs = [Math]::Round(($ok | Measure-Object -Property ElapsedMs -Sum).Sum / $ok.Count)
|
||||
Write-Host " Avg time : $avgMs ms" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if ($fail.Count -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "Errors:" -ForegroundColor Red
|
||||
foreach ($r in $fail) {
|
||||
Write-Host " [FAIL] $($r.File)" -ForegroundColor DarkRed
|
||||
Write-Host " $($r.Error)" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
# Save CSV report
|
||||
$csvPath = Join-Path $TargetFolder "_marker_convert_results.csv"
|
||||
$resultsForCsv = @($allResults | ForEach-Object {
|
||||
[PSCustomObject]@{
|
||||
FileName = $_.File
|
||||
OutputFile = if ($_.OutputFile) { $_.OutputFile } else { "N/A" }
|
||||
Status = $_.Status
|
||||
SizeBytes = $_.SizeBytes
|
||||
ElapsedMs = $_.ElapsedMs
|
||||
Error = $_.Error
|
||||
}
|
||||
})
|
||||
$resultsForCsv | Sort-Object Status, File | Export-Csv $csvPath -NoTypeInformation -Encoding UTF8
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "CSV report: $csvPath" -ForegroundColor Magenta
|
||||
Write-Host "====================================================" -ForegroundColor Cyan
|
||||
Reference in New Issue
Block a user