85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
|
|
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"))
|