From c24df544b8a4b1aed1e640b74af1dfb4b52f38e9 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 10 Apr 2026 15:49:53 +0200 Subject: [PATCH] 1.0 --- .gitignore | 6 +- claude_pricing.json | 110 +++++++++++++++++++++++++++++++++++ demos/demo_colors.py | 19 ++++++ demos/demo_describe.py | 19 ++++++ demos/demo_movie.py | 19 ++++++ image_query.py | 129 ++++++++++++++++++++++++++++++++++++++--- 6 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 claude_pricing.json create mode 100644 demos/demo_colors.py create mode 100644 demos/demo_describe.py create mode 100644 demos/demo_movie.py diff --git a/.gitignore b/.gitignore index 21a1491..da146f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ __pycache__/ *.pyc *.pyo -response.txt -out.txt -Test_pic.jpg \ No newline at end of file +log.txt +output/* +pics/* \ No newline at end of file diff --git a/claude_pricing.json b/claude_pricing.json new file mode 100644 index 0000000..b8610cb --- /dev/null +++ b/claude_pricing.json @@ -0,0 +1,110 @@ +[ + { + "model": "Claude Opus 4.6", + "deprecated": false, + "base_input_tokens_per_mtok": 5.00, + "cache_writes_5m_per_mtok": 6.25, + "cache_writes_1h_per_mtok": 10.00, + "cache_hits_and_refreshes_per_mtok": 0.50, + "output_tokens_per_mtok": 25.00 + }, + { + "model": "Claude Opus 4.5", + "deprecated": false, + "base_input_tokens_per_mtok": 5.00, + "cache_writes_5m_per_mtok": 6.25, + "cache_writes_1h_per_mtok": 10.00, + "cache_hits_and_refreshes_per_mtok": 0.50, + "output_tokens_per_mtok": 25.00 + }, + { + "model": "Claude Opus 4.1", + "deprecated": false, + "base_input_tokens_per_mtok": 15.00, + "cache_writes_5m_per_mtok": 18.75, + "cache_writes_1h_per_mtok": 30.00, + "cache_hits_and_refreshes_per_mtok": 1.50, + "output_tokens_per_mtok": 75.00 + }, + { + "model": "Claude Opus 4", + "deprecated": false, + "base_input_tokens_per_mtok": 15.00, + "cache_writes_5m_per_mtok": 18.75, + "cache_writes_1h_per_mtok": 30.00, + "cache_hits_and_refreshes_per_mtok": 1.50, + "output_tokens_per_mtok": 75.00 + }, + { + "model": "Claude Sonnet 4.6", + "deprecated": false, + "base_input_tokens_per_mtok": 3.00, + "cache_writes_5m_per_mtok": 3.75, + "cache_writes_1h_per_mtok": 6.00, + "cache_hits_and_refreshes_per_mtok": 0.30, + "output_tokens_per_mtok": 15.00 + }, + { + "model": "Claude Sonnet 4.5", + "deprecated": false, + "base_input_tokens_per_mtok": 3.00, + "cache_writes_5m_per_mtok": 3.75, + "cache_writes_1h_per_mtok": 6.00, + "cache_hits_and_refreshes_per_mtok": 0.30, + "output_tokens_per_mtok": 15.00 + }, + { + "model": "Claude Sonnet 4", + "deprecated": false, + "base_input_tokens_per_mtok": 3.00, + "cache_writes_5m_per_mtok": 3.75, + "cache_writes_1h_per_mtok": 6.00, + "cache_hits_and_refreshes_per_mtok": 0.30, + "output_tokens_per_mtok": 15.00 + }, + { + "model": "Claude Sonnet 3.7", + "deprecated": true, + "base_input_tokens_per_mtok": 3.00, + "cache_writes_5m_per_mtok": 3.75, + "cache_writes_1h_per_mtok": 6.00, + "cache_hits_and_refreshes_per_mtok": 0.30, + "output_tokens_per_mtok": 15.00 + }, + { + "model": "Claude Haiku 4.5", + "deprecated": false, + "base_input_tokens_per_mtok": 1.00, + "cache_writes_5m_per_mtok": 1.25, + "cache_writes_1h_per_mtok": 2.00, + "cache_hits_and_refreshes_per_mtok": 0.10, + "output_tokens_per_mtok": 5.00 + }, + { + "model": "Claude Haiku 3.5", + "deprecated": false, + "base_input_tokens_per_mtok": 0.80, + "cache_writes_5m_per_mtok": 1.00, + "cache_writes_1h_per_mtok": 1.60, + "cache_hits_and_refreshes_per_mtok": 0.08, + "output_tokens_per_mtok": 4.00 + }, + { + "model": "Claude Opus 3", + "deprecated": true, + "base_input_tokens_per_mtok": 15.00, + "cache_writes_5m_per_mtok": 18.75, + "cache_writes_1h_per_mtok": 30.00, + "cache_hits_and_refreshes_per_mtok": 1.50, + "output_tokens_per_mtok": 75.00 + }, + { + "model": "Claude Haiku 3", + "deprecated": false, + "base_input_tokens_per_mtok": 0.25, + "cache_writes_5m_per_mtok": 0.30, + "cache_writes_1h_per_mtok": 0.50, + "cache_hits_and_refreshes_per_mtok": 0.03, + "output_tokens_per_mtok": 1.25 + } +] diff --git a/demos/demo_colors.py b/demos/demo_colors.py new file mode 100644 index 0000000..06555cb --- /dev/null +++ b/demos/demo_colors.py @@ -0,0 +1,19 @@ +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +from image_query import query_claude, resolve_output_path + +image = "Test_pic.jpg" # looked up in pics/ +prompt = "What colors dominate this image?" +output = "demo_colors.txt" # saved to output/ + +result = query_claude(image, prompt) + +resolved_output = resolve_output_path(output) +with open(resolved_output, "w", encoding="utf-8") as f: + f.write(result) + +print(result) +print(f"\nSaved to {resolved_output}") diff --git a/demos/demo_describe.py b/demos/demo_describe.py new file mode 100644 index 0000000..b769ad1 --- /dev/null +++ b/demos/demo_describe.py @@ -0,0 +1,19 @@ +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +from image_query import query_claude, resolve_output_path + +image = "Test_pic.jpg" # looked up in pics/ +prompt = "Describe this image in extensive detail detail." +output = "demo_describe.txt" # saved to output/ + +result = query_claude(image, prompt) + +resolved_output = resolve_output_path(output) +with open(resolved_output, "w", encoding="utf-8") as f: + f.write(result) + +print(result) +print(f"\nSaved to {resolved_output}") diff --git a/demos/demo_movie.py b/demos/demo_movie.py new file mode 100644 index 0000000..a9ffda1 --- /dev/null +++ b/demos/demo_movie.py @@ -0,0 +1,19 @@ +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +from image_query import query_claude, resolve_output_path + +image = "Test_pic.jpg" # looked up in pics/ +prompt = "If this image were a movie, what genre would it be and why?" +output = "demo_movie.txt" # saved to output/ + +result = query_claude(image, prompt) + +resolved_output = resolve_output_path(output) +with open(resolved_output, "w", encoding="utf-8") as f: + f.write(result) + +print(result) +print(f"\nSaved to {resolved_output}") diff --git a/image_query.py b/image_query.py index e029c54..9e3a1d0 100644 --- a/image_query.py +++ b/image_query.py @@ -1,11 +1,43 @@ import anthropic import base64 +import json import sys +from datetime import datetime from pathlib import Path from dotenv import load_dotenv load_dotenv() +# ── Settings ─────────────────────────────────────────────────────────────────── +MODEL = "claude-opus-4-6" +MAX_TOKENS = 4096 +TEMPERATURE = None # 0.0–1.0, None = model default (1.0) +TOP_P = None # 0.0–1.0, nucleus sampling, None = off +TOP_K = None # int, top-k sampling, None = off +STOP_SEQUENCES = None # e.g. ["END", "STOP"], None = off + +# Maps API model IDs → friendly names used in claude_pricing.json +MODEL_NAME_MAP = { + "claude-opus-4-6": "Claude Opus 4.6", + "claude-opus-4-5": "Claude Opus 4.5", + "claude-opus-4-1": "Claude Opus 4.1", + "claude-opus-4-0": "Claude Opus 4", + "claude-sonnet-4-6": "Claude Sonnet 4.6", + "claude-sonnet-4-5": "Claude Sonnet 4.5", + "claude-sonnet-4-0": "Claude Sonnet 4", + "claude-sonnet-3-7": "Claude Sonnet 3.7", + "claude-haiku-4-5": "Claude Haiku 4.5", + "claude-haiku-3-5": "Claude Haiku 3.5", + "claude-haiku-3": "Claude Haiku 3", + "claude-opus-3": "Claude Opus 3", +} + +# ── Paths ────────────────────────────────────────────────────────────────────── +LOG_FILE = "log.txt" +PRICING_FILE = Path("claude_pricing.json") +PICS_DIR = Path("pics") +OUTPUT_DIR = Path("output") + MEDIA_TYPES = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", @@ -15,6 +47,40 @@ MEDIA_TYPES = { } +# ── Pricing ──────────────────────────────────────────────────────────────────── +def load_pricing() -> dict: + with open(PRICING_FILE, encoding="utf-8") as f: + entries = json.load(f) + return {e["model"]: e for e in entries} + + +def estimate_cost(model_id: str, input_tokens: int, output_tokens: int) -> str: + pricing = load_pricing() + friendly_name = MODEL_NAME_MAP.get(model_id) + if not friendly_name or friendly_name not in pricing: + return "n/a (model not in pricing file)" + rates = pricing[friendly_name] + input_cost = (input_tokens / 1_000_000) * rates["base_input_tokens_per_mtok"] + output_cost = (output_tokens / 1_000_000) * rates["output_tokens_per_mtok"] + total = input_cost + output_cost + return f"${total:.6f} (in: ${input_cost:.6f} out: ${output_cost:.6f})" + + +# ── Path helpers ─────────────────────────────────────────────────────────────── +def resolve_image_path(image_path: str) -> Path: + p = Path(image_path) + if p.parent == Path(".") and not p.is_absolute(): + return PICS_DIR / p + return p + + +def resolve_output_path(output_file: str) -> Path: + p = Path(output_file) + if p.parent == Path(".") and not p.is_absolute(): + return OUTPUT_DIR / p + return p + + def get_media_type(image_path: str) -> str: ext = Path(image_path).suffix.lower() media_type = MEDIA_TYPES.get(ext) @@ -28,14 +94,53 @@ def load_image_b64(image_path: str) -> str: return base64.standard_b64encode(f.read()).decode("utf-8") -def query_claude(image_path: str, prompt: str) -> str: +# ── Logging ──────────────────────────────────────────────────────────────────── +def write_log(image_path: str, prompt: str, time_sent: datetime, time_received: datetime, usage, output_file: str): + duration = (time_received - time_sent).total_seconds() + cost_str = estimate_cost(MODEL, usage.input_tokens, usage.output_tokens) + active_settings = ( + f"temp={TEMPERATURE if TEMPERATURE is not None else 'default'} " + f"max_tokens={MAX_TOKENS} " + f"top_p={TOP_P if TOP_P is not None else 'off'} " + f"top_k={TOP_K if TOP_K is not None else 'off'} " + f"stop_seq={STOP_SEQUENCES if STOP_SEQUENCES is not None else 'off'}" + ) + entry = ( + f"[{time_sent.strftime('%Y-%m-%d %H:%M:%S')}]\n" + f" model: {MODEL}\n" + f" settings: {active_settings}\n" + f" image: {image_path}\n" + f" prompt: {prompt}\n" + f" output: {output_file}\n" + f" sent: {time_sent.strftime('%H:%M:%S.%f')[:-3]}\n" + f" received: {time_received.strftime('%H:%M:%S.%f')[:-3]}\n" + f" duration: {duration:.2f}s\n" + f" tokens in: {usage.input_tokens}\n" + f" tokens out: {usage.output_tokens}\n" + f" est. cost: {cost_str}\n" + ) + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(entry + "\n") + + +# ── Main query ───────────────────────────────────────────────────────────────── +def query_claude(image_path: str, prompt: str, output_file: str = "response.txt") -> str: client = anthropic.Anthropic() + image_path = str(resolve_image_path(image_path)) + output_file = str(resolve_output_path(output_file)) media_type = get_media_type(image_path) image_data = load_image_b64(image_path) + optional_params = {} + if TEMPERATURE is not None: optional_params["temperature"] = TEMPERATURE + if TOP_P is not None: optional_params["top_p"] = TOP_P + if TOP_K is not None: optional_params["top_k"] = TOP_K + if STOP_SEQUENCES is not None: optional_params["stop_sequences"] = STOP_SEQUENCES + + time_sent = datetime.now() response = client.messages.create( - model="claude-opus-4-6", - max_tokens=4096, + model=MODEL, + max_tokens=MAX_TOKENS, messages=[ { "role": "user", @@ -52,7 +157,11 @@ def query_claude(image_path: str, prompt: str) -> str: ], } ], + **optional_params, ) + time_received = datetime.now() + + write_log(image_path, prompt, time_sent, time_received, response.usage, output_file) return next(block.text for block in response.content if block.type == "text") @@ -66,20 +175,22 @@ def main(): prompt = sys.argv[2] output_file = sys.argv[3] if len(sys.argv) > 3 else "response.txt" - if not Path(image_path).exists(): - print(f"Error: image file '{image_path}' not found.") + resolved_image = resolve_image_path(image_path) + if not resolved_image.exists(): + print(f"Error: image file '{resolved_image}' not found.") sys.exit(1) - print(f"Querying Claude about '{image_path}'...") - response_text = query_claude(image_path, prompt) + print(f"Querying Claude about '{resolved_image}'...") + response_text = query_claude(image_path, prompt, output_file) + resolved_output = resolve_output_path(output_file) print("\n--- Response ---") print(response_text) - with open(output_file, "w", encoding="utf-8") as f: + with open(resolved_output, "w", encoding="utf-8") as f: f.write(response_text) - print(f"\nSaved to {output_file}") + print(f"\nSaved to {resolved_output}") if __name__ == "__main__":