from __future__ import annotations from typing import Any from yuxi.utils.logging_config import logger async def describe_image(image_data: bytes, vision_api_url: str = "", vision_api_key: str = "") -> str: """通过外部 Vision API 描述图片内容。 如果未配置 API URL,返回基本元数据描述。 """ if not vision_api_url: return f"Image attachment ({len(image_data)} bytes)" try: import base64 import aiohttp encoded = base64.b64encode(image_data).decode("utf-8") payload = { "model": "gpt-4o-mini", "messages": [ { "role": "user", "content": [ {"type": "text", "text": "Describe this image briefly in one sentence."}, { "type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{encoded}"}, }, ], } ], "max_tokens": 100, } headers = {"Authorization": f"Bearer {vision_api_key}"} async with aiohttp.ClientSession() as session: async with session.post( vision_api_url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=15) ) as resp: if resp.status == 200: data = await resp.json() return data["choices"][0]["message"]["content"] return f"Image attachment ({len(image_data)} bytes)" except Exception as e: logger.warning(f"[iMessage/AI] Vision API failed: {e}") return f"Image attachment ({len(image_data)} bytes)" async def transcribe_audio(audio_data: bytes, transcription_api_url: str = "", transcription_api_key: str = "") -> str: """通过外部 API 转录音频内容。""" if not transcription_api_url: return f"Audio attachment ({len(audio_data)} bytes)" try: import aiohttp from aiohttp import FormData form = FormData() form.add_field("file", audio_data, filename="audio.caf", content_type="audio/x-caf") form.add_field("model", "whisper-1") headers = {"Authorization": f"Bearer {transcription_api_key}"} async with aiohttp.ClientSession() as session: async with session.post( transcription_api_url, data=form, headers=headers, timeout=aiohttp.ClientTimeout(total=30) ) as resp: if resp.status == 200: data = await resp.json() return data.get("text", "") return f"Audio attachment ({len(audio_data)} bytes)" except Exception as e: logger.warning(f"[iMessage/AI] Transcription API failed: {e}") return f"Audio attachment ({len(audio_data)} bytes)" def is_vision_configured(config: dict[str, Any]) -> bool: return bool(config.get("vision_api_url") and config.get("vision_api_key")) def is_transcription_configured(config: dict[str, Any]) -> bool: return bool(config.get("transcription_api_url") and config.get("transcription_api_key"))