97 lines
2.7 KiB
Python
97 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
from typing import Any, TYPE_CHECKING
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
if TYPE_CHECKING:
|
|
from .client import UrbitClient
|
|
|
|
|
|
async def analyze_image(
|
|
client: UrbitClient,
|
|
image_url: str,
|
|
prompt: str = "Describe this image in detail.",
|
|
*,
|
|
vision_api_url: str | None = None,
|
|
vision_api_key: str | None = None,
|
|
) -> str | None:
|
|
try:
|
|
from .media import download_media
|
|
|
|
image_data, content_type = await download_media(client, image_url)
|
|
if not image_data:
|
|
logger.warning(f"[Urbit] Vision: failed to download image from {image_url}")
|
|
return None
|
|
|
|
logger.info(f"[Urbit] Vision: downloaded {len(image_data)} bytes ({content_type})")
|
|
return await _invoke_vision_pipeline(
|
|
image_data,
|
|
content_type,
|
|
prompt,
|
|
vision_api_url=vision_api_url,
|
|
vision_api_key=vision_api_key,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"[Urbit] Vision analysis failed: {e}")
|
|
return None
|
|
|
|
|
|
async def _invoke_vision_pipeline(
|
|
image_data: bytes,
|
|
content_type: str,
|
|
prompt: str,
|
|
*,
|
|
vision_api_url: str | None = None,
|
|
vision_api_key: str | None = None,
|
|
) -> str | None:
|
|
if not vision_api_url:
|
|
vision_api_url = "https://api.openai.com/v1/chat/completions"
|
|
|
|
base64_image = base64.b64encode(image_data).decode("utf-8")
|
|
data_url = f"data:{content_type};base64,{base64_image}"
|
|
|
|
payload = {
|
|
"model": "gpt-4o",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": prompt},
|
|
{"type": "image_url", "image_url": {"url": data_url}},
|
|
],
|
|
}
|
|
],
|
|
"max_tokens": 300,
|
|
}
|
|
|
|
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
if vision_api_key:
|
|
headers["Authorization"] = f"Bearer {vision_api_key}"
|
|
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as http:
|
|
r = await http.post(vision_api_url, json=payload, headers=headers)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
choice = data.get("choices", [{}])[0]
|
|
message = choice.get("message", {})
|
|
result = message.get("content", "")
|
|
if result:
|
|
logger.info(f"[Urbit] Vision: analysis complete ({len(result)} chars)")
|
|
return result or None
|
|
|
|
|
|
async def build_vision_context(
|
|
image_urls: list[str],
|
|
prompt: str = "What do you see in these images?",
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"images": [{"url": url} for url in image_urls],
|
|
"prompt": prompt,
|
|
"source": "urbit_vision",
|
|
}
|