OCR
GLM-OCR receives an image and returns recognized text. Unlike classical OCR, an LLM-based OCR model can often preserve document structure more naturally, while the complete process remains local.
Linux · Ollama · OpenCode · GLM-OCR · Tesseract · yt-dlp · MCP
A local LLM stack turns an ordinary Linux shell into a compact environment for OCR, programming and text processing without sending every document to an online service. Ollama provides model management and a local inference API, while OpenCode can use these models as an interactive programming agent. The workflows below focus on three practical tasks: extracting text from document images, programming with local models, and summarizing large texts such as cleaned YouTube transcripts. The same environment can also be extended with remote MCP servers for structured access to external tools and databases.
The common component is Ollama. It stores and runs local models and exposes an HTTP API
on localhost. A model can be used directly from the shell with ollama run,
called programmatically through the API, or exposed to a client such as OpenCode through
its OpenAI-compatible endpoint.
GLM-OCR receives an image and returns recognized text. Unlike classical OCR, an LLM-based OCR model can often preserve document structure more naturally, while the complete process remains local.
OpenCode adds a terminal user interface, project context, file editing and shell tools around a local Ollama model. This is more useful for software work than a plain chat prompt.
Shell tools prepare the source material first. The cleaned text is then combined with a prompt
and streamed through standard input to ollama run.
Minimal Linux setup for local inference and terminal-based programming.
The current official Linux installer can be invoked directly from the shell:
curl -fsSL https://ollama.com/install.sh | sh
Download the models required for the workflows, for example:
ollama pull glm-ocr
ollama pull qwen3:8b
ollama pull qwen2.5-coder:7b
ollama pull qwen3.5:4b
ollama pull granite4.1:3b
When Ollama is not already running as a service, start the local server:
ollama serve
In another terminal, start an interactive model session:
ollama run qwen3:8b
The local API used by the OCR script is available below
http://127.0.0.1:11434/api. OpenCode uses the OpenAI-compatible endpoint
http://127.0.0.1:11434/v1.
The current OpenCode installation script is:
curl -fsSL https://opencode.ai/install | bash
An alternative installation through Node.js is:
npm install -g opencode-ai
Start it inside the project directory whose files should become available to the coding agent:
cd ~/my-project
opencode
The provider configuration shown later maps OpenCode to the local Ollama endpoint and enumerates the available models.
Local OCR is especially attractive for receipts, invoices and tax documents because the original image does not have to be uploaded to an online OCR service.
There are three practical routes. A smartphone scanner can capture, deskew and crop the document close to the source. Classical Tesseract is fast and deterministic. GLM-OCR uses a vision-capable local model and can be preferable when the visual structure of the document matters. A useful workflow is therefore to capture a clean image first, attempt local GLM-OCR, and retain Tesseract as a robust fallback.
myollamaocr
The script does not pass a potentially huge Base64 image as a shell argument. It writes the encoded image
to a temporary file and lets jq --rawfile read it into the JSON request. This avoids shell
argument-size limits. The request explicitly caps both output size and total HTTP runtime to prevent runaway
generations.
MODEL="glm-ocr"
OLLAMA_URL="http://127.0.0.1:11434/api/generate"
MAX_TOKENS=4096
MAX_TIME=120
base64 -w 0 "$INPUT" > "$IMAGE64"
jq -n \
--rawfile image "$IMAGE64" \
--arg model "$MODEL" \
--argjson max_tokens "$MAX_TOKENS" \
'{
model: $model,
prompt: "Text Recognition:",
images: [$image],
stream: false,
options: {
num_predict: $max_tokens,
temperature: 0.0,
top_k: 1,
top_p: 0.00001,
repeat_penalty: 1.1
}
}' > "$REQUEST"
curl \
--silent \
--show-error \
--max-time "$MAX_TIME" \
--output "$RESPONSE" \
--write-out '%{http_code}' \
--header 'Content-Type: application/json' \
--data-binary "@$REQUEST" \
"$OLLAMA_URL"
jq -er '.response' "$RESPONSE" > "$RESULT"
mv -- "$RESULT" "$OUTPUT"
The model prompt is intentionally minimal: Text Recognition:. The image is sent to Ollama's
/api/generate endpoint, generation is non-streaming, and conservative sampling settings are used
because OCR should reproduce rather than invent text. The final .response field is extracted with
jq and written to a text file with the same basename as the image.
myocrbatch
The wrapper has two deliberately simple modes. By default it delegates to myollamaocrbatch,
whose intended behavior is “GLM-OCR first, Tesseract as fallback”. With -t, Ollama is bypassed
completely and every JPG/PNG file is processed by Tesseract only.
# Default mode: delegate to the GLM-OCR batch wrapper
if [ "$TESSERACT_ONLY" -eq 0 ]; then
exec myollamaocrbatch
fi
# -t mode: Tesseract only
for file in "${files[@]}"; do
target="${file%.*}_tess.txt"
tesseract "$file" stdout -l deu --psm 4 --oem 1 \
-c preserve_interword_spaces=1 > "$target"
done
tesseract "$file" stdout -l deu --psm 4 --oem 1 \
-c preserve_interword_spaces=1 > ${file%.*}_tess.txt
-l deuSelects the German language data used by Tesseract.
--psm 4Uses a page-segmentation mode suitable for text arranged in columns or varying regions, a useful baseline for receipts.
preserve_interword_spaces=1 asks Tesseract to retain inter-word spacing rather than normalize it aggressively.
Local models are attractive for privacy, independence and predictable operation, but the available RAM, VRAM and CPU/GPU performance limit the model size that can be run comfortably. Cloud inference removes this hardware restriction: OpenCode still runs locally as the programming client, while the selected LLM runs on a remote provider.
Ollama complements its local runtime with cloud-hosted models. Depending on the account and service level, cloud use can be available within a free allowance, through a subscription, or through additional usage credits. The subscription model is convenient for regular use; credits are useful when additional capacity is needed without changing the local workflow.
The important distinction is that ollama run model:cloud looks similar to running a local model,
but inference takes place in the cloud. Local Ollama remains useful as the command-line/runtime integration
layer.
OpenRouter provides one API and one account for models from many different providers. Some models or routes can be used free of charge, while paid models consume prepaid credits according to actual usage.
The main advantage of the credit model is flexibility: there is no need to maintain a separate subscription with every model provider. A single credit balance and API key can be used to switch between inexpensive models for routine work and stronger models for difficult tasks. This is particularly convenient for experimenting with OpenCode because the client and project workflow remain unchanged when the model changes.
/connect, select Ollama Cloud, and enter the Ollama Cloud API key.
ollama pull gpt-oss:20b-cloud
For a cloud model this step prepares the local Ollama-side model information; the actual model inference
remains remote rather than requiring the complete model weights to run on the local GPU or CPU.
/models in OpenCode and select the desired Ollama Cloud model.
/connect in OpenCode and select OpenRouter./models. Many OpenRouter models are already known to OpenCode and can be selected immediately.
Free variants can be used for experiments; paid variants draw from the OpenRouter credit balance.
If only selected OpenRouter models should appear in the OpenCode configuration, they can be declared explicitly
in opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"openrouter": {
"models": {
"z-ai/glm-5.2": {},
"z-ai/glm-5.2:free": {}
}
}
}
}
The exact model IDs must match the IDs exposed by OpenRouter. Authentication does not need to be stored in this
JSON file when it has already been added with /connect.
| Mode | Where inference runs | Payment concept | Main advantage |
|---|---|---|---|
| Local Ollama | local computer | no per-request cloud cost | privacy and independence |
| Ollama Cloud | Ollama cloud | free allowance / subscription / credits | cloud models with an Ollama-oriented workflow |
| OpenRouter | selected remote provider | free models or usage credits | many model providers behind one API key and credit balance |
From plain model prompting to a project-aware terminal coding agent.
A coding model can be queried directly with Ollama, for example:
ollama run qwen2.5-coder:7b "Explain this Bash function and simplify it."
For actual software work, OpenCode provides the more useful abstraction. It can operate inside a project, inspect files, apply edits and invoke shell commands. The supplied configuration exposes several Ollama models to OpenCode through the local OpenAI-compatible API.
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama (local)",
"options": {
"baseURL": "http://127.0.0.1:11434/v1"
},
"models": {
"qwen3:8b": { "name": "Qwen 3 8B" },
"qwen2.5-coder:7b": { "name": "Qwen 2.5-coder 7B" },
"qwen3.5:4b": { "name": "Qwen 3.5 4B" },
"glm-ocr:latest": {
"name": "GLM-OCR",
"modalities": {
"input": ["text", "image"],
"output": ["text"]
}
},
"granite4.1:3b": { "name": "Granite 4.1 3B" }
}
}
}
}
Use shell tools for acquisition and cleanup, then let the LLM perform the semantic compression.
YouTube automatically generated subtitles are a convenient source for lectures, talks and long technical videos,
but raw VTT files contain timestamps, metadata, markup, blank lines and frequently duplicated auto-caption lines.
The supplied myyoutubesubs script separates acquisition from cleanup.
# Download automatically generated subtitles
yt-dlp --write-auto-subs --skip-download "$URL"
# Keep only VTT files created by this invocation
mapfile -t VTTFILES < <(
find . -maxdepth 1 -type f -name '*.vtt' -newer "$MARKER" -printf '%T@ %p\n' |
sort -n |
cut -d' ' -f2-
)
# Remove VTT metadata/timestamps/empty lines, strip HTML-like tags,
# de-duplicate consecutive identical subtitle lines, join to flowing text
# and wrap at the configured line length.
awk '
/^WEBVTT/ { next }
/^Kind:/ { next }
/^Language:/ { next }
/^NOTE/ { next }
/-->/ { next }
/^[[:space:]]*$/ { next }
{
gsub(/<[^>]*>/, "")
sub(/^[[:space:]]+/, "")
sub(/[[:space:]]+$/, "")
if (length($0) && $0 != previous) {
print
previous = $0
}
}
' "$dst_vtt" |
tr '\n' ' ' |
sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//' |
fold -s -w "$LINELENGTH" > "$dst_txt"
For a single video the script produces ausgabe.vtt and cleaned ausgabe.txt.
For playlists it generates numbered pairs such as ausgabe_001.vtt and
ausgabe_001.txt. Option -a additionally downloads the best available audio stream,
while -l controls the output line width.
catA particularly shell-friendly pattern is to keep the instruction in one file and the transcript in another, concatenate both, and pass standard input to the model:
cat summary_prompt.txt ausgabe.txt |
ollama run qwen3:8b > condensed.txt
A prompt file could contain only the instruction, for example:
Summarize the following transcript in concise English.
Preserve the essential technical arguments, commands and conclusions.
Do not invent information that is not present in the source.
--- TRANSCRIPT ---
The same principle works without a separate prompt file. A shell group can prepend the instruction and then stream the source text:
{
printf '%s\n\n' \
'Summarize the following text concisely while preserving technical details:'
cat ausgabe.txt
} | ollama run qwen3:8b > condensed.txt
Local models and MCP solve different problems: Ollama supplies local inference; MCP supplies external tools and structured data sources. The supplied configurations demonstrate how command-line clients can combine these layers.
config.toml
Codex stores MCP configuration in config.toml. The supplied example registers the remote
example endpoint, obtains its Authorization header from an environment variable, and allows the two
database tools without repeated approval.
[mcp_servers.example]
url = "https://www.drbreinlinger.de/mcp/example"
[mcp_servers.example.env_http_headers]
Authorization = "example_MCP_AUTHORIZATION"
[mcp_servers.example.tools.get_database_semantics]
approval_mode = "approve"
[mcp_servers.example.tools.query_sql]
approval_mode = "approve"
The value example_MCP_AUTHORIZATION is the name of the environment variable whose value becomes
the HTTP Authorization header.
opencode.jsonThe supplied OpenCode configuration combines two roles in one file: a local Ollama provider and a remote MCP server. The local provider points to port 11434; the MCP server points to the HTTPS endpoint.
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama (local)",
"options": {
"baseURL": "http://127.0.0.1:11434/v1"
},
"models": {
"qwen3:8b": {
"name": "Qwen 3 8B"
},
"qwen2.5-coder:7b": {
"name": "Qwen 2.5-coder 7B"
},
"qwen3.5:4b": {
"name": "Qwen 3.5 4B"
},
"glm-ocr:latest": {
"name": "GLM-OCR",
"modalities": {
"input": [
"text",
"image"
],
"output": [
"text"
]
}
},
"granite4.1:3b": {
"name": "Granite 4.1 3B"
}
}
}
},
"mcp": {
"example": {
"type": "remote",
"url": "https://www.drbreinlinger.de/mcp/example",
"oauth": false,
"headers": {
"Authorization": "Bearer {env:EXAMPLE_MCP_TOKEN}"
},
"enabled": true
},
"model": "granite4.1:3b"
}
The token is referenced as {env:EXAMPLE_MCP_TOKEN}, so the secret remains outside the configuration.
.mcp.json
Claude Code uses a project-scoped .mcp.json. The supplied file registers the same remote HTTP MCP
endpoint and obtains the Bearer token from EXAMPLE_MCP_TOKEN.
{
"mcpServers": {
"example": {
"type": "http",
"url": "https://www.drbreinlinger.de/mcp/example",
"headers": {
"Authorization": "Bearer ${EXAMPLE_MCP_TOKEN}"
}
}
}
}
export EXAMPLE_MCP_TOKEN='...'
export example_MCP_AUTHORIZATION='Bearer ...'
The two configuration formats expect slightly different values: the OpenCode and Claude examples construct the
Bearer prefix inside the configuration, whereas the supplied Codex TOML maps the environment variable
directly to the complete Authorization header.
| Task | Primary tool | Input | Output | Why local? |
|---|---|---|---|---|
| Document OCR | GLM-OCR via Ollama | JPG / PNG | TXT | privacy + structural interpretation |
| OCR fallback | Tesseract | JPG / PNG | TXT | fast + deterministic + offline |
| Programming | OpenCode + Ollama | project files + prompt | analysis / edits / commands | project data remains local |
| Transcript cleanup | yt-dlp + awk/sed/fold | YouTube URL / VTT | clean TXT | minimal reproducible shell pipeline |
| Summarization | ollama run <model> | prompt + clean text via stdin | condensed TXT | large source text need not be uploaded |
docs.ollama.comopencode.ai/docsollama.com / docs.ollama.comopenrouter.aidevelopers.openai.com/codex/mcpdocs.anthropic.com/en/docs/claude-code/mcp