I found a very interesting and useful open-source project BableDOC.
Install BabelDOC
First install uv:
pip install uvThen we can install BabelDOC:
uv tool install --python 3.12 BabelDOCConfigure DeepSeek API
On MacOS:
echo 'export DEEPSEEK_API_KEY="My DeepSeek API Key"' >> ~/.zshrc
echo 'export DEEPSEEK_BASE_URL="https://api.deepseek.com/v1"' >> ~/.zshrc
echo 'export DEEPSEEK_MODEL="deepseek-chat"' >> ~/.zshrc
source ~/.zshrcOn Windows:
setx DEEPSEEK_API_KEY "My DeepSeek API Key"
setx DEEPSEEK_BASE_URL "https://api.deepseek.com"
setx DEEPSEEK_MODEL "deepseek-chat"Restart Powershell to take effect.
Create a Command Line Function to Make Translation Easie
On MacOS:
Paste following function in to ~/.zshrc:
bd() {
# -----------------------
# Argument Parsing
# -----------------------
FILE=""
OUTDIR=""
LANG_IN="en"
LANG_OUT="zh"
FULL_MODE=0 # 0 = bilingual only, 1 = mono + bilingual
PROVIDER="deepseek" # default
# Optional performance knobs (apply to BOTH providers)
QPS=""
POOL=""
TERM_POOL=""
while [[ $# -gt 0 ]]; do
case "$1" in
--out)
OUTDIR="$2"
shift 2
;;
--lang-in)
LANG_IN="$2"
shift 2
;;
--lang-out)
LANG_OUT="$2"
shift 2
;;
--full)
FULL_MODE=1
shift
;;
--provider)
PROVIDER="$2"
shift 2
;;
--doubao)
PROVIDER="doubao"
shift
;;
--deepseek)
PROVIDER="deepseek"
shift
;;
# NEW: apply to both providers if provided
--qps)
QPS="$2"
shift 2
;;
--pool)
POOL="$2"
shift 2
;;
--term-pool)
TERM_POOL="$2"
shift 2
;;
-*)
echo "Unknown option: $1"
echo "Usage: bd <pdf_path> [--provider deepseek|doubao] [--deepseek|--doubao] [--full] [--out dir] [--lang-in en] [--lang-out zh] [--qps N] [--pool N] [--term-pool N]"
return 1
;;
*)
FILE="$1"
shift
;;
esac
done
if [ -z "$FILE" ]; then
echo "Usage: bd <pdf_path> [--provider deepseek|doubao] [--deepseek|--doubao] [--full] [--out dir] [--lang-in en] [--lang-out zh] [--qps N] [--pool N] [--term-pool N]"
return 1
fi
if [ -z "$OUTDIR" ]; then
OUTDIR="$(dirname "$FILE")/translated"
fi
mkdir -p "$OUTDIR"
# -----------------------
# Provider config
# -----------------------
PROVIDER_LC="$(printf '%s' "$PROVIDER" | tr '[:upper:]' '[:lower:]')"
OPENAI_MODEL=""
OPENAI_BASE_URL=""
OPENAI_API_KEY=""
if [ "$PROVIDER_LC" = "doubao" ]; then
OPENAI_MODEL="$DOUBAO_MODEL"
OPENAI_BASE_URL="$DOUBAO_BASE_URL"
OPENAI_API_KEY="$DOUBAO_API_KEY"
else
PROVIDER_LC="deepseek"
OPENAI_MODEL="$DEEPSEEK_MODEL"
OPENAI_BASE_URL="$DEEPSEEK_BASE_URL"
OPENAI_API_KEY="$DEEPSEEK_API_KEY"
fi
# Validate required envs
if [ -z "$OPENAI_MODEL" ] || [ -z "$OPENAI_BASE_URL" ] || [ -z "$OPENAI_API_KEY" ]; then
echo "Missing environment variables for provider: $PROVIDER_LC"
if [ "$PROVIDER_LC" = "doubao" ]; then
echo "Please set: DOUBAO_MODEL, DOUBAO_BASE_URL, DOUBAO_API_KEY"
else
echo "Please set: DEEPSEEK_MODEL, DEEPSEEK_BASE_URL, DEEPSEEK_API_KEY"
fi
return 1
fi
echo "=========================="
echo "Provider : $PROVIDER_LC"
echo "Input file : $FILE"
echo "Output dir : $OUTDIR"
echo "Language : $LANG_IN -> $LANG_OUT"
if [ $FULL_MODE -eq 1 ]; then
echo "Output mode : bilingual + monolingual"
else
echo "Output mode : bilingual only"
fi
echo "Model : $OPENAI_MODEL"
echo "Base URL : $OPENAI_BASE_URL"
if [ -n "$QPS" ] || [ -n "$POOL" ] || [ -n "$TERM_POOL" ]; then
echo "Perf knobs : qps=${QPS:-default} pool=${POOL:-default} term_pool=${TERM_POOL:-default}"
else
echo "Perf knobs : (default; same for both providers)"
fi
echo "=========================="
# -----------------------
# Build BabelDOC arguments
# -----------------------
EXTRA_ARGS=()
if [ $FULL_MODE -eq 0 ]; then
EXTRA_ARGS+=(--no-mono) # only dual-language PDF
fi
# Apply throttling args ONLY if user explicitly provided them (same behavior for doubao/deepseek)
if [ -n "$QPS" ]; then
EXTRA_ARGS+=(--qps "$QPS")
fi
if [ -n "$POOL" ]; then
EXTRA_ARGS+=(--pool-max-workers "$POOL")
fi
if [ -n "$TERM_POOL" ]; then
EXTRA_ARGS+=(--term-pool-max-workers "$TERM_POOL")
fi
# -----------------------
# Run BabelDOC with live output
# -----------------------
echo ""
echo "▶ Running BabelDOC (live output)..."
LOG_FILE="$(mktemp -t babeldoc_run.XXXXXX.log)"
cleanup() { :; } # keep log by default
trap cleanup EXIT INT TERM
babeldoc \
--openai \
--openai-model "$OPENAI_MODEL" \
--openai-base-url "$OPENAI_BASE_URL" \
--openai-api-key "$OPENAI_API_KEY" \
--files "$FILE" \
--lang-in "$LANG_IN" \
--lang-out "$LANG_OUT" \
--output "$OUTDIR" \
"${EXTRA_ARGS[@]}" \
2>&1 | tee "$LOG_FILE"
BABELDOC_RC=${PIPESTATUS[0]}
if (( BABELDOC_RC != 0 )); then
echo ""
echo "⚠️ BabelDOC exited with error code: $BABELDOC_RC"
echo " Log saved at: $LOG_FILE"
return $BABELDOC_RC
fi
echo ""
echo "=========================="
echo " Translation completed. Checking token usage & balance..."
echo "=========================="
echo "Log file: $LOG_FILE"
# -----------------------
# Parse token usage from BabelDOC log
# -----------------------
echo ""
echo "Parsing token usage..."
TOKENS_LINE=$(grep -E "Total tokens" "$LOG_FILE" | tail -n 1)
if [ -n "$TOKENS_LINE" ]; then
TOKENS=$(printf '%s\n' "$TOKENS_LINE" | sed 's/.*[=:][ ]*\([0-9][0-9]*\).*/\1/')
if [ -n "$TOKENS" ]; then
echo "Total tokens: $TOKENS"
else
echo "Found 'Total tokens' but failed to parse:"
echo " $TOKENS_LINE"
fi
else
echo "No 'Total tokens' line found."
grep -E "Prompt tokens|Completion tokens|Total tokens" "$LOG_FILE" | tail -n 5
fi
# -----------------------
# Fetch balance (DeepSeek only by default)
# -----------------------
echo ""
if [ "$PROVIDER_LC" = "deepseek" ]; then
echo "Fetching DeepSeek balance..."
BALANCE_JSON=$(curl -s -H "Authorization: Bearer $DEEPSEEK_API_KEY" https://api.deepseek.com/user/balance)
if [[ "$BALANCE_JSON" == *"balance_infos"* ]]; then
TOTAL_BALANCE=$(printf '%s\n' "$BALANCE_JSON" | sed -n 's/.*"total_balance":"\([^"]*\)".*/\1/p' | head -n 1)
CURRENCY=$(printf '%s\n' "$BALANCE_JSON" | sed -n 's/.*"currency":"\([^"]*\)".*/\1/p' | head -n 1)
if [ -n "$TOTAL_BALANCE" ]; then
echo "DeepSeek balance: $TOTAL_BALANCE $CURRENCY"
else
echo "Balance JSON returned but parsing failed:"
echo " $BALANCE_JSON"
fi
elif [ -n "$BALANCE_JSON" ]; then
echo "Unexpected DeepSeek balance response:"
echo " $BALANCE_JSON"
else
echo "Failed to retrieve DeepSeek balance (empty response)."
fi
else
echo "Skipping balance check for Doubao (Ark)."
fi
echo ""
echo "Done! Translated files saved to: $OUTDIR"
}
Later I found the function was so large and make itself hard to maintain, so I put the function at folder ~/.config/bd, and add source ~/.config/bd/bd.zsh in ~/.zshrc.
mkdir -p ~/.config/bd
vim ~/.config/bd/bd.zshOn Windows:
Create bd.ps:
notepad $HOME\bd.ps1Write:
param(
[Parameter(Mandatory=$true)]
[string]$File,
[string]$Out = "",
[string]$LangIn = "en",
[string]$LangOut = "zh",
[switch]$Full
)
# ---------------------
# Determine output directory
# ---------------------
if ($Out -eq "") {
$Out = Join-Path (Split-Path $File -Parent) "translated"
}
mkdir $Out -Force | Out-Null
Write-Host "=========================="
Write-Host "Input file: $File"
Write-Host "Output directory: $Out"
Write-Host "Language: $LangIn → $LangOut"
if ($Full) {
Write-Host "Output mode: bilingual + monolingual"
} else {
Write-Host "Output mode: bilingual only"
}
Write-Host "=========================="
# ---------------------
# Build BabelDOC args
# ---------------------
$ExtraArgs = @()
if (-not $Full) {
$ExtraArgs += "--no-mono"
}
# ---------------------
# Run BabelDOC with live output, also save to log
# ---------------------
Write-Host ""
Write-Host "Running BabelDOC..."
$cmd = $babeldocCmd + @(
"--openai",
"--openai-model", $env:DEEPSEEK_MODEL,
"--openai-base-url", $env:DEEPSEEK_BASE_URL,
"--openai-api-key", $env:DEEPSEEK_API_KEY,
"--files", $File,
"--lang-in", $LangIn,
"--lang-out", $LangOut,
"--output", $Out
) + $ExtraArgs
$logFile = Join-Path $Out "babeldoc_run.log"
# live output + log to file
& $cmd[0] $cmd[1..($cmd.Count-1)] 2>&1 | Tee-Object -FilePath $logFile
Write-Host "`n=========================="
Write-Host " Translation completed. Checking token usage & balance..."
Write-Host "=========================="
# ---------------------
# Parse token usage
# ---------------------
Write-Host "`n📘 Parsing token usage..."
$TokenLine = $Output | Select-String -Pattern "Total tokens"
if ($TokenLine) {
$Token = ($TokenLine -replace ".*:\s*", "")
Write-Host "🔢 Token usage: $Token"
} else {
Write-Host "⚠️ No token info found."
}
# ---------------------
# Fetch DeepSeek balance
# ---------------------
Write-Host "`n📡 Fetching DeepSeek API balance..."
$BalanceJson = Invoke-RestMethod -Headers @{Authorization = "Bearer $env:DEEPSEEK_API_KEY"} `
-Uri "https://api.deepseek.com/user/balance" `
-Method GET `
-ErrorAction SilentlyContinue
if ($BalanceJson) {
$Balance = $BalanceJson.balance_infos[0].total_balance
$Currency = $BalanceJson.balance_infos[0].currency
Write-Host "💰 DeepSeek Balance: $Balance $Currency"
} else {
Write-Host "⚠️ Failed to fetch balance."
}
Write-Host "`n🎉 Done! Files saved to $Out"
Allow powershell to run the .ps1 script:
Set-ExecutionPolicy -Scope CurrentUser RemoteSignedUse bd anywhere:
notepad $PROFILEAdd bd to profile:
function bd {
& "$HOME\bd.ps1" @args
}Then:
. $PROFILE
Usage
Simplest (default: en → zh, default output directory)
bd paper.pdfSpecify output directory
bd paper.pdf --out ./translatedBoth Mono and Dual language
bd paper.pdf --fullSpecify languages
bd paper.pdf --lang-in en --lang-out jaUse DOUBAO API:
bd paper.pdf --doubao