Instructions to use yuuko-eth/LocateAnything-3B-GGUF with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use yuuko-eth/LocateAnything-3B-GGUF with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="yuuko-eth/LocateAnything-3B-GGUF", filename="LocateAnything-3B-BF16.gguf", )
llm.create_chat_completion( messages = [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] ) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use yuuko-eth/LocateAnything-3B-GGUF with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M # Run inference directly in the terminal: llama cli -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M # Run inference directly in the terminal: llama cli -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M # Run inference directly in the terminal: ./llama-cli -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama-cli -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
Use Docker
docker model run hf.co/yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
- LM Studio
- Jan
- vLLM
How to use yuuko-eth/LocateAnything-3B-GGUF with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "yuuko-eth/LocateAnything-3B-GGUF" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "yuuko-eth/LocateAnything-3B-GGUF", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
- Ollama
How to use yuuko-eth/LocateAnything-3B-GGUF with Ollama:
ollama run hf.co/yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
- Unsloth Studio
How to use yuuko-eth/LocateAnything-3B-GGUF with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for yuuko-eth/LocateAnything-3B-GGUF to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for yuuko-eth/LocateAnything-3B-GGUF to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for yuuko-eth/LocateAnything-3B-GGUF to start chatting
- Pi
How to use yuuko-eth/LocateAnything-3B-GGUF with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent new
How to use yuuko-eth/LocateAnything-3B-GGUF with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
Run Hermes
hermes
- Atomic Chat new
- OpenClaw new
How to use yuuko-eth/LocateAnything-3B-GGUF with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
- Docker Model Runner
How to use yuuko-eth/LocateAnything-3B-GGUF with Docker Model Runner:
docker model run hf.co/yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
- Lemonade
How to use yuuko-eth/LocateAnything-3B-GGUF with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull yuuko-eth/LocateAnything-3B-GGUF:Q4_K_M
Run and chat with the model
lemonade run user.LocateAnything-3B-GGUF-Q4_K_M
List all available models
lemonade list
Working code with bounding boxes visualization
Enjoy:
import tkinter as tk
from tkinter import filedialog, messagebox
import requests
import re
import cv2
import numpy as np
from PIL import Image, ImageTk
import base64
import io
# --- CONFIGURATION ---
VERSION = "1.7.1"
SERVER_URL = "http://127.0.0.1:8080/v1/chat/completions"
class App:
def __init__(self, root):
self.root = root
self.root.title(f"LocateAnything Visualizer (v{VERSION})")
# State
self.current_cv_image = None
# UI
tk.Button(root, text="1. Load Image", command=self.load_image).pack(pady=5)
tk.Label(root, text="2. Prompt:").pack()
self.prompt_entry = tk.Entry(root, width=60)
self.prompt_entry.pack(pady=5)
tk.Button(root, text="3. Ask", command=self.process_query).pack(pady=5)
self.canvas = tk.Canvas(root, width=800, height=600)
self.canvas.pack()
print(f"Visualizer v{VERSION} | author: Manamama.")
print(f"See https://huggingface.co/yuuko-eth/LocateAnything-3B-GGUF/discussions/3 and https://huggingface.co/yuuko-eth/LocateAnything-3B-GGUF/ for the context of what it does. ")
print(f"Load an image first.")
def load_image(self):
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.jpeg *.png")])
if not file_path: return
self.current_cv_image = cv2.imread(file_path)
if self.current_cv_image is None:
messagebox.showerror("Error", "Could not load image.")
return
img_rgb = cv2.cvtColor(self.current_cv_image, cv2.COLOR_BGR2RGB)
self.display_image(img_rgb)
print("Image loaded.")
def process_query(self):
if self.current_cv_image is None:
messagebox.showwarning("Error", "Please load an image first.")
return
text = self.prompt_entry.get()
if not text:
messagebox.showwarning("Input Error", "Please enter a prompt.")
return
# Prepare API call
_, encoded = cv2.imencode('.jpg', self.current_cv_image)
img_str = base64.b64encode(encoded).decode()
payload = {"messages": [{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_str}"}},
{"type": "text", "text": text}
]}]}
try:
response = requests.post(SERVER_URL, json=payload).json()
model_text = response['choices'][0]['message']['content']
print(f"Query: {text}")
print(f"Model Output: {model_text}")
except Exception as e:
messagebox.showerror("API Error", str(e))
return
# Parse & Draw
h, w, _ = self.current_cv_image.shape
img_rgb = cv2.cvtColor(self.current_cv_image.copy(), cv2.COLOR_BGR2RGB)
parts = re.split(r"<ref>(.*?)</ref>", model_text)
found_any = False
for i in range(1, len(parts), 2):
label = parts[i]
box_content = parts[i+1]
# Find all <box>...</box> chunks
box_chunks = re.findall(r"<box>(.*?)</box>", box_content)
for box_str in box_chunks:
# Extract all numbers
nums = [int(n) for n in re.findall(r"<(\d+)>", box_str)]
if len(nums) == 4:
found_any = True
# --- MAPPING ---
# Model output: xmin, ymin, xmax, ymax (Empirically verified)
x_min, y_min, x_max, y_max = [int(n/1000*w) if i % 2 == 0 else int(n/1000*h) for i, n in enumerate(nums)]
# Correction: Actually x and y are mixed. Based on previous: xmin=val2, ymin=val1?
# Let's use the successful mapping: x_min = val1/1000*w, y_min = val2/1000*h...
x_min = int(nums[0]/1000*w)
y_min = int(nums[1]/1000*h)
x_max = int(nums[2]/1000*w)
y_max = int(nums[3]/1000*h)
cv2.rectangle(img_rgb, (x_min, y_min), (x_max, y_max), (255, 0, 0), 3)
label_y = y_min - 10 if y_min > 30 else y_min + 20
cv2.putText(img_rgb, label, (x_min + 5, label_y), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)
elif len(nums) == 2:
found_any = True
# Point visualization: Centered circle
cx, cy = int(nums[0]/1000*w), int(nums[1]/1000*h)
cv2.circle(img_rgb, (cx, cy), 10, (0, 0, 255), -1)
cv2.putText(img_rgb, label, (cx + 15, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
if not found_any:
messagebox.showinfo("Result", "No boxes or points detected.")
self.display_image(img_rgb)
def display_image(self, img_rgb):
img_pil = Image.fromarray(img_rgb)
img_pil.thumbnail((800, 600))
self.img_tk = ImageTk.PhotoImage(img_pil)
self.canvas.config(width=img_pil.width, height=img_pil.height)
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.img_tk)
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
root.mainloop()
Needs a working server:
LD_LIBRARY_PATH=$PWD/build/bin:$LD_LIBRARY_PATH ~/Downloads/GitHub/llama-locate-anything/build/bin/llama-server --mmproj ~/AI_models/LocateAnything/3B/mmproj-LocateAnything-3B-BF16.gguf -m ~/AI_models/LocateAnything/3B/LocateAnything-3B-Q6_K.gguf --special
sic, this LD_LIBRARY_PATH=$PWD/build/bin:$LD_LIBRARY_PATH is a must if you have also standard llama.cpp .
Result:
Query: Locate all objects that are toys
Model Output: <ref>toys</ref><box><0><453><71><550></box><box><20><80><87><160></box><box><102><18><170><136></box><|im_end|>
and:
Note: it does not (yet) handle "Query: Point to" queries, as:Model Output: <ref>old lady</ref><box><220><495></box><|im_end|> is a point, not rectangle.
Update: also fixed this by now in code above.
Update: or one can adapt https://github.com/NVlabs/Eagle/blob/3af39904391929b34073192c90a53f3aa69a324e/Embodied/eaglevl/train/fastseek/draw_marker.py#L4 , e.g. into:
import tkinter as tk
from tkinter import filedialog, messagebox
import requests
import re
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageTk
import base64
import io
# --- NVIDIA DRAWING PRIMITIVES ---
def scale_bbox(bbox, width, height):
# bbox in normalized (0-1000) [xmin, ymin, xmax, ymax]
return (np.array(bbox) / 1000) * np.array([width, height, width, height])
def draw_thick_bbox(draw, image, bbox, color, stroke=20):
bbox_scaled = scale_bbox(bbox, image.width, image.height)
extend = stroke * 7 / 8
# PIL rectangle expects (xmin, ymin, xmax, ymax)
bbox_out = [bbox_scaled[0] - extend, bbox_scaled[1] - extend, bbox_scaled[2] + extend, bbox_scaled[3] + extend]
draw.rectangle(tuple(map(int, bbox_out)), outline=color, width=stroke)
# --- APP CONFIGURATION ---
VERSION = "2.0-NVIDIA-Style"
SERVER_URL = "http://127.0.0.1:8080/v1/chat/completions"
class App:
def __init__(self, root):
self.root = root
self.root.title(f"LocateAnything Visualizer (v{VERSION})")
self.current_pil_image = None
tk.Button(root, text="1. Load Image", command=self.load_image).pack(pady=5)
tk.Label(root, text="2. Prompt:").pack()
self.prompt_entry = tk.Entry(root, width=60)
self.prompt_entry.pack(pady=5)
tk.Button(root, text="3. Ask", command=self.process_query).pack(pady=5)
self.canvas = tk.Canvas(root, width=800, height=600)
self.canvas.pack()
def load_image(self):
file_path = filedialog.askopenfilename()
if not file_path: return
self.current_pil_image = Image.open(file_path).convert("RGB")
self.display_image(self.current_pil_image)
def process_query(self):
if self.current_pil_image is None:
messagebox.showwarning("Error", "Load image first.")
return
text = self.prompt_entry.get()
buffered = io.BytesIO()
self.current_pil_image.save(buffered, format="JPEG")
img_str = base64.b64encode(buffered.getvalue()).decode()
payload = {"messages": [{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_str}"}},
{"type": "text", "text": text}
]}]}
try:
response = requests.post(SERVER_URL, json=payload).json()
model_text = response['choices'][0]['message']['content']
except Exception as e:
messagebox.showerror("API", str(e))
return
img_work = self.current_pil_image.copy()
draw = ImageDraw.Draw(img_work)
parts = re.split(r"<ref>(.*?)</ref>", model_text)
print(f"Query: {text}. Result: {model_text}")
for i in range(1, len(parts), 2):
label = parts[i]
for box_str in re.findall(r"<box>(.*?)</box>", parts[i+1]):
nums = [int(n) for n in re.findall(r"<(\d+)>", box_str)]
if len(nums) == 4:
# Model: xmin, ymin, xmax, ymax
draw_thick_bbox(draw, img_work, nums, "red", stroke=5)
draw.text((nums[0]/1000*img_work.width, nums[1]/1000*img_work.height), label, fill="red")
self.display_image(img_work)
def display_image(self, pil_img):
img_tk = ImageTk.PhotoImage(pil_img.copy().resize((800, 600)))
self.canvas.create_image(0, 0, anchor=tk.NW, image=img_tk)
self.canvas.image = img_tk
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
root.mainloop()
shows same thing. (Note: am too lazy to add the pointer version).
Oh. One can also tweak it to work on the sample movie Wukong.mp4:
via (key trick below) this:
def main():
parser = argparse.ArgumentParser(description=f"AI Video Grounder v{VERSION} - SceneDetect + LocateAnything")
parser.add_argument("video", help="Path to input video file")
parser.add_argument("-p", "--prompt", required=True, help="Grounding prompt (e.g., 'Locate the workers')")
parser.add_argument("-t", "--threshold", type=int, default=27, help="Scene detection threshold (default: 27)")
parser.add_argument("-n", "--num-images", type=int, default=3, help="Images per scene (default: 3)")
parser.add_argument("--url", default=DEFAULT_SERVER_URL, help="llama-server API URL")
args = parser.parse_args()
# 1. Create Output Directory
basename = os.path.basename(args.video)
name_noext = os.path.splitext(basename)[0]
output_dir = f"{name_noext}_ai_storyboard"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
print(f"--- Starting Pipeline v{VERSION} ---")
print(f"Video: {args.video}")
print(f"Output: {output_dir}/")
# 2. Step 1: Algorithmic Scene Slicing (Your Bash Logic)
print("\n[Step 1/2] Running Scene Detection (PySceneDetect)...")
scenedetect_cmd = [
"scenedetect", "-i", args.video,
"detect-content", "-t", str(args.threshold),
"save-images", "-n", str(args.num_images), "-o", output_dir
]
(the whole code would be too long to paste here. )
#!/usr/bin/env python3
"""VLM VNC Controller β use LocalAnything-3b to point at UI elements via VNC."""
import sys
import re
import shlex
import base64
import subprocess
import tempfile
import requests
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QCheckBox, QTextEdit, QPushButton, QLabel, QDialog,
QFormLayout, QLineEdit, QSpinBox, QGroupBox,
)
from PySide6.QtCore import QThread, Signal, Qt, QSettings
from PySide6.QtGui import QPixmap, QKeySequence, QShortcut, QTextCursor
# ββ Point / box parsing βββββββββββββββββββββββββββββββββββββββββββββ
def parse_points(answer: str, image_width: int, image_height: int) -> list[dict]:
"""
Parse <ref>label</ref><box>...</box> blocks from LocateAnything output.
Handles both point (<x><y>) and bbox (<x1><y1><x2><y2>) formats.
Coordinates are on a 0β1000 scale; rescaled to pixel dimensions.
For bboxes the centre point is returned.
"""
points = []
parts = re.split(r"<ref>(.*?)</ref>", answer)
for i in range(1, len(parts), 2):
label = parts[i]
box_content = parts[i + 1]
box_chunks = re.findall(r"<box>(.*?)</box>", box_content)
for box_str in box_chunks:
nums = [int(n) for n in re.findall(r"<(\d+)>", box_str)]
if len(nums) == 2:
cx = int(nums[0] / 1000 * image_width)
cy = int(nums[1] / 1000 * image_height)
points.append({"label": label, "x": cx, "y": cy})
elif len(nums) == 4:
x_min = int(nums[0] / 1000 * image_width)
y_min = int(nums[1] / 1000 * image_height)
x_max = int(nums[2] / 1000 * image_width)
y_max = int(nums[3] / 1000 * image_height)
cx = (x_min + x_max) // 2
cy = (y_min + y_max) // 2
points.append({
"label": label,
"x": cx, "y": cy,
"x_min": x_min, "y_min": y_min,
"x_max": x_max, "y_max": y_max,
})
return points
# ββ Clickable screenshot label βββββββββββββββββββββββββββββββββββββββ
class ScreenshotLabel(QLabel):
doubleClicked = Signal()
def mouseDoubleClickEvent(self, event):
self.doubleClicked.emit()
# ββ VLM worker (raw requests, no openai lib) ββββββββββββββββββββββββ
class VLMWorker(QThread):
result_received = Signal(str)
error_occurred = Signal(str)
def __init__(self, server_url: str, messages: list, model: str):
super().__init__()
self.server_url = server_url
self.messages = messages
self.model = model
def run(self):
payload = {
"model": self.model,
"messages": self.messages,
}
try:
resp = requests.post(self.server_url, json=payload, timeout=500)
resp.raise_for_status()
data = resp.json()
answer = data["choices"][0]["message"]["content"]
self.result_received.emit(answer)
except Exception as e:
self.error_occurred.emit(str(e))
# ββ Settings dialog ββββββββββββββββββββββββββββββββββββββββββββββββββ
class SettingsDialog(QDialog):
def __init__(self, settings: QSettings, parent=None):
super().__init__(parent)
self.settings = settings
self.setWindowTitle("Settings")
self.setMinimumWidth(560)
layout = QVBoxLayout(self)
# ββ API ββ
api_group = QGroupBox("Server (OpenAIβstyle /v1/chat/completions)")
api_form = QFormLayout()
self.server_url = QLineEdit()
self.model = QLineEdit()
api_form.addRow("Server URL:", self.server_url)
api_form.addRow("Model:", self.model)
api_group.setLayout(api_form)
layout.addWidget(api_group)
# ββ VNC ββ
vnc_group = QGroupBox("VNC Connection")
vnc_form = QFormLayout()
self.vnc_host = QLineEdit()
self.vnc_port = QSpinBox()
self.vnc_port.setRange(1, 65535)
self.vnc_port.setValue(5900)
self.vnc_password = QLineEdit()
self.vnc_password.setEchoMode(QLineEdit.Password)
vnc_form.addRow("Host:", self.vnc_host)
vnc_form.addRow("Port:", self.vnc_port)
vnc_form.addRow("Password:", self.vnc_password)
vnc_group.setLayout(vnc_form)
layout.addWidget(vnc_group)
# ββ Buttons ββ
btn_row = QHBoxLayout()
save_btn = QPushButton("Save")
save_btn.clicked.connect(self._save)
cancel_btn = QPushButton("Cancel")
cancel_btn.clicked.connect(self.reject)
btn_row.addWidget(save_btn)
btn_row.addWidget(cancel_btn)
layout.addLayout(btn_row)
self._load()
def _load(self):
s = self.settings
self.server_url.setText(
s.value("api/server_url", "http://127.0.0.1:8080/v1/chat/completions")
)
self.model.setText(s.value("api/model", "LocalAnything-3b"))
self.vnc_host.setText(s.value("vnc/host", "localhost"))
self.vnc_port.setValue(int(s.value("vnc/port", 5900)))
self.vnc_password.setText(s.value("vnc/password", ""))
def _save(self):
s = self.settings
s.setValue("api/server_url", self.server_url.text())
s.setValue("api/model", self.model.text())
s.setValue("vnc/host", self.vnc_host.text())
s.setValue("vnc/port", self.vnc_port.value())
s.setValue("vnc/password", self.vnc_password.text())
self.accept()
# ββ Main window ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("LocalAnythingβ3b β VNC Pointer")
self.setMinimumSize(780, 800)
self.settings = QSettings("VLMVNC", "Controller")
self.vlm_worker: VLMWorker | None = None
self.last_screenshot_path: str | None = None
self.screenshot_size: tuple[int, int] = (0, 0)
self.last_b64_jpeg: str | None = None
self._build_ui()
# ββββββββββββββββββββββ UI construction ββββββββββββββββββββββββββ
def _build_ui(self):
central = QWidget()
self.setCentralWidget(central)
root = QVBoxLayout(central)
# top bar
top = QHBoxLayout()
self.chk_screenshot = QCheckBox("Attach screenshot")
self.chk_screenshot.setChecked(True)
self.btn_screenshot = QPushButton("π· Screenshot")
self.btn_screenshot.clicked.connect(self._take_screenshot)
self.btn_settings = QPushButton("β Settings")
self.btn_settings.clicked.connect(self._open_settings)
top.addWidget(self.chk_screenshot)
top.addWidget(self.btn_screenshot)
top.addStretch()
top.addWidget(self.btn_settings)
root.addLayout(top)
# screenshot preview
self.lbl_screenshot = ScreenshotLabel(
"Doubleβclick or press π· to capture"
)
self.lbl_screenshot.setMinimumHeight(180)
self.lbl_screenshot.setMaximumHeight(280)
self.lbl_screenshot.setAlignment(Qt.AlignCenter)
self.lbl_screenshot.setStyleSheet(
"border:1px solid #666; background:#1e1e1e; color:#777; font-size:13px;"
)
self.lbl_screenshot.doubleClicked.connect(self._take_screenshot)
root.addWidget(self.lbl_screenshot)
# prompt
root.addWidget(QLabel("Point to (phrase):"))
self.txt_prompt = QTextEdit()
self.txt_prompt.setMaximumHeight(72)
self.txt_prompt.setPlaceholderText(
'e.g. "Apple logo", "Start button", "Close icon"'
)
root.addWidget(self.txt_prompt)
# action buttons
btns = QHBoxLayout()
self.btn_send = QPushButton("π― Locate with VLM")
self.btn_send.clicked.connect(self._send_to_vlm)
self.btn_exec = QPushButton("βΆ Execute Command")
self.btn_exec.setEnabled(False)
self.btn_exec.clicked.connect(self._execute_command)
btns.addWidget(self.btn_send)
btns.addWidget(self.btn_exec)
root.addLayout(btns)
# log area (readβonly)
root.addWidget(QLabel("Log:"))
self.txt_log = QTextEdit()
self.txt_log.setReadOnly(True)
self.txt_log.setMaximumHeight(160)
self.txt_log.setStyleSheet(
"font-family:Consolas,Monaco,monospace; font-size:12px; color:#aaa;"
)
root.addWidget(self.txt_log)
# editable command area
root.addWidget(QLabel("VNC Command (edit before executing):"))
self.txt_response = QTextEdit()
self.txt_response.setStyleSheet(
"font-family:Consolas,Monaco,monospace; font-size:13px;"
)
root.addWidget(self.txt_response)
# shortcut Ctrl+Return β locate
QShortcut(QKeySequence("Ctrl+Return"), self, self._send_to_vlm)
# ββββββββββββββββββββββ helpers ββββββββββββββββββββββββββββββββββ
def _vnc_prefix(self) -> list[str]:
host = self.settings.value("vnc/host", "localhost")
port = self.settings.value("vnc/port", 5900)
pw = self.settings.value("vnc/password", "")
cmd = ["vncdo", "-s", f"{host}::{port}"]
if pw:
cmd += ["-p", pw]
return cmd
# ββββββββββββββββββββββ screenshot βββββββββββββββββββββββββββββββ
def _take_screenshot(self) -> str | None:
tmp = tempfile.mktemp(suffix=".png")
cmd = self._vnc_prefix() + ["capture", tmp]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if r.returncode != 0:
self._log(f"β Screenshot failed: {r.stderr.strip()}")
return None
except FileNotFoundError:
self._log("β vncdotool not found. pip install vncdotool")
return None
except subprocess.TimeoutExpired:
self._log("β Screenshot timed out")
return None
pix = QPixmap(tmp)
if pix.isNull():
self._log("β Could not load screenshot image")
return None
self.last_screenshot_path = tmp
self.screenshot_size = (pix.width(), pix.height())
# Preβencode as JPEG for the VLM (matching the working reference)
self._encode_screenshot(tmp)
self._log(f"π· Screenshot captured: {pix.width()}Γ{pix.height()}")
self._show_screenshot(pix)
return tmp
def _encode_screenshot(self, path: str):
"""Read image, encode to JPEG base64 β same pipeline as the
working tkinter reference (cv2.imencode β base64)."""
try:
import cv2
img = cv2.imread(path)
if img is not None:
_, encoded = cv2.imencode(".jpg", img)
self.last_b64_jpeg = base64.b64encode(encoded).decode()
return
except ImportError:
pass
# Fallback without cv2: use PIL
try:
from PIL import Image as PILImage
img = PILImage.open(path)
buf = io.BytesIO()
img.save(buf, format="JPEG")
self.last_b64_jpeg = base64.b64encode(buf.getvalue()).decode()
return
except ImportError:
pass
# Last resort: raw PNG bytes
with open(path, "rb") as f:
self.last_b64_jpeg = base64.b64encode(f.read()).decode()
def _show_screenshot(self, pix: QPixmap):
scaled = pix.scaled(
self.lbl_screenshot.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
self.lbl_screenshot.setPixmap(scaled)
def resizeEvent(self, event):
super().resizeEvent(event)
if self.last_screenshot_path:
pix = QPixmap(self.last_screenshot_path)
if not pix.isNull():
self._show_screenshot(pix)
# ββββββββββββββββββββββ VLM call βββββββββββββββββββββββββββββββββ
def _send_to_vlm(self):
phrase = self.txt_prompt.toPlainText().strip()
if not phrase:
return
self.btn_send.setEnabled(False)
self.btn_exec.setEnabled(False)
self.txt_response.clear()
# Build content: image_url FIRST, then text β matching working reference
user_content: list[dict] = []
if self.chk_screenshot.isChecked():
if not self.last_screenshot_path:
self._take_screenshot()
if self.last_b64_jpeg:
user_content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{self.last_b64_jpeg}"
},
})
else:
self._log("β No screenshot β model needs an image")
user_content.append({
"type": "text",
"text": f"Point to: {phrase}."
})
# No system prompt β only user message
messages = [{"role": "user", "content": user_content}]
server_url = self.settings.value(
"api/server_url",
"http://127.0.0.1:8080/v1/chat/completions",
)
model = self.settings.value("api/model", "LocalAnything-3b")
self._log(f"β POST {server_url} model={model}")
self._log(f" Prompt: Point to: {phrase}.")
self.vlm_worker = VLMWorker(server_url, messages, model)
self.vlm_worker.result_received.connect(self._on_result)
self.vlm_worker.error_occurred.connect(self._on_vlm_error)
self.vlm_worker.start()
def _on_result(self, answer: str):
self._log(f"β Raw model output: {answer}")
img_w, img_h = self.screenshot_size
if img_w == 0 or img_h == 0:
self._log("β Unknown screenshot dimensions, assuming 1920Γ1080")
img_w, img_h = 1920, 1080
points = parse_points(answer, img_w, img_h)
if points:
lines = []
for i, pt in enumerate(points):
x, y = pt["x"], pt["y"]
label = pt.get("label", f"point{i}")
line = f"move {x} {y}"
lines.append(line)
if "x_min" in pt:
self._log(
f" [{label}] bbox({pt['x_min']},{pt['y_min']})-"
f"({pt['x_max']},{pt['y_max']}) β centre ({x}, {y})"
)
else:
self._log(f" [{label}] point ({x}, {y})")
cmd_text = "\n".join(lines)
else:
self._log("β No <box> coordinates found in model output")
cmd_text = f"# No coordinates found\n# Raw: {answer}"
self.txt_response.setPlainText(cmd_text)
self.btn_send.setEnabled(True)
self.btn_exec.setEnabled(True)
def _on_vlm_error(self, err: str):
self.txt_response.setPlainText(f"# Error: {err}")
self._log(f"β Error: {err}")
self.btn_send.setEnabled(True)
# ββββββββββββββββββββββ execute vncdotool ββββββββββββββββββββββββ
def _execute_command(self):
text = self.txt_response.toPlainText()
lines = [
l.strip()
for l in text.strip().splitlines()
if l.strip() and not l.strip().startswith("#")
]
if not lines:
self._log("Nothing to execute")
return
self._log("\nββ Executing ββ")
for line in lines:
try:
parts = shlex.split(line)
except ValueError as e:
self._log(f" β {line} (parse error: {e})")
continue
cmd = self._vnc_prefix() + parts
try:
r = subprocess.run(
cmd, capture_output=True, text=True, timeout=15
)
if r.returncode == 0:
self._log(f" β {line}")
else:
self._log(f" β {line} β {r.stderr.strip()}")
except subprocess.TimeoutExpired:
self._log(f" β {line} β timed out")
except Exception as e:
self._log(f" β {line} β {e}")
self._log("ββ Done ββ\n")
# ββββββββββββββββββββββ settings βββββββββββββββββββββββββββββββββ
def _open_settings(self):
dlg = SettingsDialog(self.settings, self)
if dlg.exec() == QDialog.Accepted:
pass # URL read fresh from QSettings each call
# ββββββββββββββββββββββ logging ββββββββββββββββββββββββββββββββββ
def _log(self, msg: str):
self.txt_log.append(msg)
cur = self.txt_log.textCursor()
cur.movePosition(QTextCursor.MoveOperation.End)
self.txt_log.setTextCursor(cur)
# ββ Entry point ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec())
Hm. Works kind of, after installing pip install vncdotool service_identity, but what do we need it for?
@Manamama i think @barinov274 is demonstrating how the concept can transfer to controlling a computer over VNC (Remote Desktop)
Pretty lovely idea!
Ok, after sleeping over it and consulting with Grok xAI, below, I get it now:
Yeah, the Tkinter visualizer is great for testing and seeing what the model can do, but I initially had the same reaction β βwhy not just click things myself?β
The real value (and what the VNC controller code demonstrates) is closing the loop for full automation.
What the VNC code actually does:
It turns LocateAnything into a vision-based GUI agent:
- Takes a screenshot of the remote/local desktop via VNC.
- Sends the screenshot + a natural language instruction to the model (e.g. βclick the Login buttonβ, βtype into the search fieldβ, βfind and double-click the Excel iconβ).
- The model returns precise coordinates (
<ref>...</ref><box>...</box>or point format). - The script uses
vncdotoolto automatically move the mouse and click / type / drag at those exact coordinates.
No brittle pixel matching, no reliance on accessibility APIs, no manual scripting of every UI change β it works on any GUI.
Practical uses:
- Robotic Process Automation (RPA): Automate repetitive desktop tasks (filling forms, downloading reports, navigating internal tools).
- Headless / remote agents: Control VMs, servers, or cloud desktops without a human watching.
- GUI testing: Reliably locate and interact with elements even if the interface changes (theme, resolution, layout).
- Full AI agents: Combine with a reasoning LLM for multi-step tasks (βlog into the portal, find the latest invoice, download it, then email itβ).
- Robotics / embodied AI: Similar grounding on camera feeds.
- Batch processing or accessibility tools.
In short: the visualizer is for humans exploring/debugging. The VNC part shows how to make the model act on what it sees β a step toward open, local βcomputer useβ agents like the ones big companies are demoing.
The author shared a working controller but didnβt explain the βwhyβ, which made it confusing at first. Once you see it as the bridge from βunderstand screenshotβ to βcontrol computerβ, it clicks.
So like mine https://huggingface.co/yuuko-eth/LocateAnything-3B-GGUF/discussions/3#6a25810e84627862175c0d29 plus VNC


