"""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()