1.0
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,6 +2,6 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
response.txt
|
log.txt
|
||||||
out.txt
|
output/*
|
||||||
Test_pic.jpg
|
pics/*
|
||||||
110
claude_pricing.json
Normal file
110
claude_pricing.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
19
demos/demo_colors.py
Normal file
19
demos/demo_colors.py
Normal file
@@ -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}")
|
||||||
19
demos/demo_describe.py
Normal file
19
demos/demo_describe.py
Normal file
@@ -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}")
|
||||||
19
demos/demo_movie.py
Normal file
19
demos/demo_movie.py
Normal file
@@ -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}")
|
||||||
129
image_query.py
129
image_query.py
@@ -1,11 +1,43 @@
|
|||||||
import anthropic
|
import anthropic
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
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 = {
|
MEDIA_TYPES = {
|
||||||
".jpg": "image/jpeg",
|
".jpg": "image/jpeg",
|
||||||
".jpeg": "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:
|
def get_media_type(image_path: str) -> str:
|
||||||
ext = Path(image_path).suffix.lower()
|
ext = Path(image_path).suffix.lower()
|
||||||
media_type = MEDIA_TYPES.get(ext)
|
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")
|
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()
|
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)
|
media_type = get_media_type(image_path)
|
||||||
image_data = load_image_b64(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(
|
response = client.messages.create(
|
||||||
model="claude-opus-4-6",
|
model=MODEL,
|
||||||
max_tokens=4096,
|
max_tokens=MAX_TOKENS,
|
||||||
messages=[
|
messages=[
|
||||||
{
|
{
|
||||||
"role": "user",
|
"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")
|
return next(block.text for block in response.content if block.type == "text")
|
||||||
|
|
||||||
@@ -66,20 +175,22 @@ def main():
|
|||||||
prompt = sys.argv[2]
|
prompt = sys.argv[2]
|
||||||
output_file = sys.argv[3] if len(sys.argv) > 3 else "response.txt"
|
output_file = sys.argv[3] if len(sys.argv) > 3 else "response.txt"
|
||||||
|
|
||||||
if not Path(image_path).exists():
|
resolved_image = resolve_image_path(image_path)
|
||||||
print(f"Error: image file '{image_path}' not found.")
|
if not resolved_image.exists():
|
||||||
|
print(f"Error: image file '{resolved_image}' not found.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"Querying Claude about '{image_path}'...")
|
print(f"Querying Claude about '{resolved_image}'...")
|
||||||
response_text = query_claude(image_path, prompt)
|
response_text = query_claude(image_path, prompt, output_file)
|
||||||
|
|
||||||
|
resolved_output = resolve_output_path(output_file)
|
||||||
print("\n--- Response ---")
|
print("\n--- Response ---")
|
||||||
print(response_text)
|
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)
|
f.write(response_text)
|
||||||
|
|
||||||
print(f"\nSaved to {output_file}")
|
print(f"\nSaved to {resolved_output}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user