Local AI with Ollama and Nginx: A Guide to Automated Validation
Translated from the Spanish original. Read in Spanish

Project Purpose
The goal of this project is to run an AI process that analyses bingo cards and determines whether the numbers provided make a bingo. We do this using local LLMs with Ollama for AI inference and Nginx as a security layer to handle SSL and authentication. Although this tutorial focuses on analysing bingo cards, the same idea can be applied to many other tasks that need AI-driven validation.
This tutorial covers setting up Ollama on a Linux system, including configuring Nginx as a reverse proxy with SSL.
This is only an example for testing purposes and is not intended for production environments.
For more details about Ollama, visit the official repository: Ollama GitHub.
Install Ollama
To install Ollama, run the following commands:
curl -fsSL https://ollama.com/install.sh | sh
Download Models
Get the models you need:
ollama pull llama3.2-vision
ollama pull phi4
Install Nginx
Install Nginx on your server:
sudo apt update
sudo apt install nginx -y
Get SSL Certificates
You need an SSL certificate for secure communication. You can use Let’s Encrypt or another certificate authority, but the process isn’t covered in this tutorial.
Configure Nginx as a Reverse Proxy with SSL and Authentication
Edit the Nginx configuration:
sudo vim /etc/nginx/nginx.conf
Add or modify the following content:
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 3600;
client_max_body_size 5G;
client_body_timeout 3600;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
server {
listen 11435 ssl;
server_name yourdomain.com;
ssl_certificate /path/to/your/cert.pem;
ssl_certificate_key /path/to/your/key.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 60m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://localhost:11434;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
For details on setting up authentication in Nginx, see the official documentation: Configuring HTTP Basic Authentication.
Restart Nginx to apply the changes:
sudo systemctl restart nginx
The Code
First, we need to import the required libraries and define some constants.
import base64
import requests
import json
OLLAMA_SERVER_URL = "https://ollama.zerogap.com:11435"
MODEL_NAME = "llama3.2-vision"
IMAGE_PATH = "bingo.jpg"
Function to Read and Encode the Image
This function reads the image of the bingo card and encodes it in base64 so it can be sent in the POST request.
def encode_image(image_path):
with open(image_path, 'rb') as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
Function to Send a POST Request
This function sends a POST request to the server with the encoded image and the AI model.
Seed: keeps responses consistent by fixing the model’s randomness.
Temperature: controls how creative the responses are; low values give predictable outputs, while high values allow more variability.
def send_post_request(model, prompt, images=None, stream=False, response_format=None, system_prompt=None):
body = {
"model": model,
"prompt": prompt,
"system": system_prompt,
"images": images if images else [],
"stream": stream,
"format": response_format if response_format else {},
"options": {
"seed": 46,
"temperature": 0.1
}
}
headers = {
"Content-Type": "application/json",
}
try:
response = requests.post(f"{OLLAMA_SERVER_URL}/api/generate", json=body, headers=headers)
response.raise_for_status() # Raise an HTTPError on bad response
return response.json()
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
return None
Defining the Expected Response Format
Here we define the format we expect the AI to respond with. This helps us structure the data properly. Using structured output with language models (LLMs) improves accuracy, makes integration with external systems easier, reduces ambiguity and streamlines data analysis. The result is greater reliability, interoperability and scalability in automated workflows.
response_format = {
"type": "object",
"properties": {
"found_numbers": {
"type": "array",
"items": {"type": "number"}
}
},
"required": ["found_numbers"]
}
validation_response_format = {
"type": "object",
"properties": {
"is_valid": {"type": "boolean"}
},
"required": ["is_valid"]
}
Preparing the Image and the Initial Request
We read and encode the image of the bingo card, then prepare the prompt to identify the numbers in the image.
encoded_image = encode_image(IMAGE_PATH)
vision_identifier_prompt = "Identify and return all numbers present in the image. The output should be a structured JSON object with a key 'found_numbers' containing a list of all detected numbers."
system_prompt="Your task is to analyze the image provided, following specific instructions from the user."
vision_results = send_post_request(MODEL_NAME, vision_identifier_prompt, images=[encoded_image],response_format=response_format, system_prompt=system_prompt)
Analysing the Results and Validating
We analyse the results from the AI to get the list of numbers found, and then check whether a bingo can be called with a list of selected numbers.
if vision_results:
message_content = vision_results.get('response')
vision_data = json.loads(message_content)
found_numbers = vision_data.get('found_numbers', [])
validator_prompt = (
f"""CARD: {found_numbers}, SELECTED: [32,29,17,75]"""
)
system_prompt = "Your task is to determine if all numbers in the 'SELECTED' list are present in the 'CARD' list. Return only a JSON object with the key 'is_valid' and the boolean value true if all numbers are found, or false otherwise. Do not provide any explanation or code. Example: Input: { \"CARD\": [3, 9, 10, 14, 17, 20, 24, 25, 29, 32, 34, 37, 42, 47, 48, 50, 55, 57, 62, 68, 69, 72, 70, 75], \"SELECTED\": [302, 29, 17, 9] } Output: { \"is_valid\": false }"
MODEL_NAME = "phi4"
validation_results = send_post_request(MODEL_NAME, validator_prompt, response_format=validation_response_format, system_prompt=system_prompt)
if validation_results:
validation_message_content = validation_results.get('response')
validation_data = json.loads(validation_message_content)
validated_numbers = validation_data.get('is_valid', [])
Complete code
import base64
import requests
import json
# Define constants for server URL and model
OLLAMA_SERVER_URL = "https://ollama.zerogap.com:11435"
MODEL_NAME = "llama3.2-vision"
IMAGE_PATH = "bingo.jpg"
# Function to read and encode the image
def encode_image(image_path):
with open(image_path, 'rb') as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
# Function to send a POST request
def send_post_request(model, prompt, images=None, stream=False, response_format=None, system_prompt=None):
body = {
"model": model,
"prompt": prompt,
"system": system_prompt,
"images": images if images else [],
"stream": stream,
"format": response_format if response_format else {},
"options": {
"seed": 46,
"temperature": 0.1
}
}
headers = {
"Content-Type": "application/json",
}
try:
response = requests.post(f"{OLLAMA_SERVER_URL}/api/generate", json=body, headers=headers)
response.raise_for_status() # Raise an HTTPError on bad response
return response.json()
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
return None
# Define the expected response format for a list of numbers
response_format = {
"type": "object",
"properties": {
"found_numbers": {
"type": "array",
"items": {"type": "number"}
}
},
"required": ["found_numbers"]
}
# Define the expected response format for validations (boolean)
validation_response_format = {
"type": "object",
"properties": {
"is_valid": {"type": "boolean"}
},
"required": ["is_valid"]
}
# Prepare the image
encoded_image = encode_image(IMAGE_PATH)
# Improved vision identifier prompt
vision_identifier_prompt = "Identify and return all numbers present in the image. The output should be a structured JSON object with a key 'found_numbers' containing a list of all detected numbers."
system_prompt="Your task is to analyze the image provided."
vision_results = send_post_request(MODEL_NAME, vision_identifier_prompt, images=[encoded_image],response_format=response_format, system_prompt=system_prompt)
if vision_results:
# Parse the structured output from the vision results
message_content = vision_results.get('response')
vision_data = json.loads(message_content)
found_numbers = vision_data.get('found_numbers', [])
# Validate the results
validator_prompt = (
f"""CARD: {found_numbers}, SELECTED: [32,29,17,75]"""
)
system_prompt = "Your task is to determine if all numbers in the 'SELECTED' list are present in the 'CARD' list. Return only a JSON object with the key 'is_valid' and the boolean value true if all numbers are found, or false otherwise. Do not provide any explanation or code. Example: Input: { \"CARD\": [3, 9, 10, 14, 17, 20, 24, 25, 29, 32, 34, 37, 42, 47, 48, 50, 55, 57, 62, 68, 69, 72, 70, 75], \"SELECTED\": [302, 29, 17, 9] } Output: { \"is_valid\": false }"
MODEL_NAME = "phi4"
validation_results = send_post_request(MODEL_NAME, validator_prompt, response_format=validation_response_format, system_prompt=system_prompt)
if validation_results:
# Parse the structured output from the validation results
validation_message_content = validation_results.get('response')
validation_data = json.loads(validation_message_content)
validated_numbers = validation_data.get('is_valid', [])
# Print the final analysis
print("--- Final Analysis ---")
if validated_numbers:
print("BINGO!!!")
else:
print("No BINGO :( !!!")
This process lets you run a secure Ollama server on Linux with SSL and authentication while performing number recognition and validation with AI models.
Note: This example is intended only for testing and development. It is not recommended for production environments.
