ForcePilot/backend/package/yuxi/channel/extensions/bluebubbles/tts.py

74 lines
2.1 KiB
Python
Raw Normal View History

import logging
import os
from pathlib import Path
from yuxi.channel.extensions.bluebubbles.client import BlueBubblesClient
from yuxi.channel.extensions.bluebubbles.outbound import send_attachment
logger = logging.getLogger(__name__)
SUPPORTED_AUDIO_FORMATS = frozenset({"caf", "mp3", "m4a", "wav", "aac"})
PREFERRED_FORMAT = "caf"
MAX_VOICE_MEMO_MB = 5.0
VOICE_MEMO_MIME = "audio/x-caf"
def _audio_file_ext(file_path: str) -> str:
return Path(file_path).suffix.lower().lstrip(".")
def _audio_file_size_mb(file_path: str) -> float:
try:
return os.path.getsize(file_path) / (1024 * 1024)
except OSError:
return 0.0
def validate_voice_input(file_path: str) -> None:
if not os.path.isfile(file_path):
raise FileNotFoundError(f"Voice memo file not found: {file_path}")
ext = _audio_file_ext(file_path)
if ext not in SUPPORTED_AUDIO_FORMATS:
raise ValueError(f"Unsupported audio format '.{ext}'. Supported: {', '.join(sorted(SUPPORTED_AUDIO_FORMATS))}")
size_mb = _audio_file_size_mb(file_path)
if size_mb > MAX_VOICE_MEMO_MB:
raise ValueError(f"Voice memo too large: {size_mb:.1f}MB (max {MAX_VOICE_MEMO_MB}MB)")
if ext != PREFERRED_FORMAT:
logger.warning(
"Voice memo format is '.%s', prefer '.%s' to avoid BlueBubbles Server CAF->MP3 race condition",
ext,
PREFERRED_FORMAT,
)
async def send_voice_memo(
client: BlueBubblesClient,
chat_guid: str,
audio_file_path: str,
*,
private_api_available: bool = False,
) -> dict:
validate_voice_input(audio_file_path)
ext = _audio_file_ext(audio_file_path)
mime_type = "audio/x-caf" if ext == "caf" else f"audio/{ext}"
logger.info(
"Sending iMessage voice memo: chat=%s format=%s size=%.1fMB",
chat_guid,
ext,
_audio_file_size_mb(audio_file_path),
)
return await send_attachment(
client,
chat_guid,
audio_file_path,
mime_type=mime_type,
as_voice=True,
private_api_available=private_api_available,
)