95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
"""Thin wrapper around the OpenAI API for generating alt text."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import requests
|
|
|
|
|
|
class AltTextGenerationError(RuntimeError):
|
|
"""Raised when the OpenAI API cannot generate alt text."""
|
|
|
|
|
|
class AltTextGenerator:
|
|
"""Request alt text from a GPT-4o compatible OpenAI endpoint."""
|
|
|
|
def __init__(self, api_key: str, model: str = "gpt-4o-mini") -> None:
|
|
if not api_key:
|
|
raise ValueError("OPENAI_API_KEY is required")
|
|
self.model = model
|
|
self.session = requests.Session()
|
|
self.session.headers.update(
|
|
{
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
)
|
|
self.endpoint = "https://api.openai.com/v1/chat/completions"
|
|
|
|
def generate_alt_text(
|
|
self,
|
|
image_source: str,
|
|
notes: Optional[str] = None,
|
|
captured_at: Optional[str] = None,
|
|
location: Optional[str] = None,
|
|
coordinates: Optional[str] = None,
|
|
) -> str:
|
|
if not image_source:
|
|
raise AltTextGenerationError("Image URL required for alt text generation")
|
|
|
|
prompt_lines = [
|
|
"You write vivid but concise Mastodon alt text.",
|
|
"Keep it under 400 characters and mention key visual details, colours, "
|
|
"actions, and text. No need to mention the mood unless you think it is "
|
|
"super relevant.",
|
|
"Avoid speculation beyond what is visible. Use UK English spelling.",
|
|
]
|
|
if notes:
|
|
prompt_lines.append(f"Creator notes: {notes.strip()}")
|
|
if captured_at:
|
|
prompt_lines.append(f"Captured: {captured_at}")
|
|
if location:
|
|
prompt_lines.append(f"Location: {location}")
|
|
if coordinates:
|
|
prompt_lines.append(f"Coordinates: {coordinates}")
|
|
text_prompt = "\n".join(prompt_lines)
|
|
|
|
content: List[Dict[str, Any]] = [
|
|
{"type": "text", "text": text_prompt},
|
|
{"type": "image_url", "image_url": {"url": image_source}},
|
|
]
|
|
|
|
payload = {
|
|
"model": self.model,
|
|
"temperature": 0.2,
|
|
"max_tokens": 300,
|
|
"messages": [
|
|
{
|
|
"role": "system",
|
|
"content": "You help write accessible alt text for social media posts.",
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": content,
|
|
},
|
|
],
|
|
}
|
|
try:
|
|
response = self.session.post(self.endpoint, json=payload, timeout=30)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
except requests.RequestException as exc: # pragma: no cover
|
|
raise AltTextGenerationError(str(exc)) from exc
|
|
|
|
choices = data.get("choices") or []
|
|
if not choices:
|
|
raise AltTextGenerationError("OpenAI response did not include choices")
|
|
|
|
message = choices[0].get("message", {})
|
|
content_text = message.get("content")
|
|
if not content_text:
|
|
raise AltTextGenerationError("OpenAI response missing content")
|
|
|
|
return content_text.strip()
|