GenAI / backend /sunbird_client.py
Hildah-N's picture
Update backend/sunbird_client.py
6a7d12e verified
Raw
History Blame Contribute Delete
7.35 kB
"""
Thin wrapper around Sunbird AI API endpoints (robust version).
"""
import os
import requests
from typing import Optional
SUNBIRD_API_BASE = "https://api.sunbird.ai"
# TTS speaker IDs per language
TTS_SPEAKER_IDS = {
"Acholi": 241,
"Ateso": 242,
"Runyankole": 243,
"Lugbara": 245,
"Luganda": 248,
}
STT_LANGUAGE_CODES = {
"Acholi": "ach",
"Ateso": "teo",
"English": "eng",
"Luganda": "lug",
"Lugbara": "lgg",
"Runyankole": "nyn",
}
LANGUAGE_NAMES = {
"Acholi": "Acholi",
"Ateso": "Ateso",
"Runyankole": "Runyankole",
"Lugbara": "Lugbara",
"Luganda": "Luganda",
}
# Languages supported by the summarisation endpoint
SUMMARISE_SUPPORTED_LANGUAGES = {"eng", "lug"}
# -----------------------------
# Helpers
# -----------------------------
def _get_headers(content_type: Optional[str] = None) -> dict:
token = os.environ.get("SUNBIRD_API_TOKEN", "").strip()
if not token:
raise ValueError("SUNBIRD_API_TOKEN is not set in environment variables.")
headers = {"Authorization": f"Bearer {token}"}
if content_type:
headers["Content-Type"] = content_type
return headers
def _extract(data: dict, *keys):
"""
Robust extractor for Sunbird API inconsistencies.
Never assumes presence of 'output'.
"""
if not isinstance(data, dict):
raise Exception(f"Invalid API response: {data}")
# ----------------------------
# Case 1: output wrapper exists
# ----------------------------
if isinstance(data.get("output"), dict):
inner = data["output"]
for k in keys:
if k in inner:
return inner[k]
# fallback: sometimes content is nested differently
if "content" in inner:
return inner["content"]
if "text" in inner:
return inner["text"]
# ----------------------------
# Case 2: flat response
# ----------------------------
for k in keys:
if k in data:
return data[k]
# ----------------------------
# Case 3: known Sunbird variations
# ----------------------------
if "content" in data:
return data["content"]
if "text" in data:
return data["text"]
if "summary" in data:
return data["summary"]
if "summarized_text" in data:
return data["summarized_text"]
if "audio_transcription" in data:
return data["audio_transcription"]
# last resort debugging
raise Exception(f"Unexpected API response format: {data}")
# -----------------------------
# LANGUAGE DETECTION
# -----------------------------
def detect_text_language(text: str) -> str:
"""
Returns the detected language code (e.g. 'eng', 'lug') for a text string.
Uses /tasks/language_id endpoint.
"""
url = f"{SUNBIRD_API_BASE}/tasks/language_id"
headers = _get_headers("application/json")
payload = {"text": text}
response = requests.post(url, json=payload, headers=headers, timeout=(30, 60))
response.raise_for_status()
data = response.json()
return _extract(data, "language")
def detect_audio_language(audio_path: str) -> str:
"""
Returns the detected language code (e.g. 'ach', 'lug') for an audio file.
Uses /tasks/auto_detect_audio_language endpoint.
"""
url = f"{SUNBIRD_API_BASE}/tasks/auto_detect_audio_language"
headers = _get_headers()
with open(audio_path, "rb") as f:
files = {"audio": f}
response = requests.post(url, files=files, headers=headers, timeout=(40, 300))
response.raise_for_status()
data = response.json()
return _extract(data, "language")
# -----------------------------
# STT
# -----------------------------
def transcribe_audio(audio_path: str) -> str:
url = f"{SUNBIRD_API_BASE}/tasks/stt"
headers = _get_headers()
import mimetypes
ext = os.path.splitext(audio_path)[-1].lower() or ".wav"
mime = mimetypes.types_map.get(ext, "audio/wav")
filename = f"audio{ext}"
with open(audio_path, "rb") as f:
files = {"audio": (filename, f, mime)}
response = requests.post(url, files=files, headers=headers, timeout=(40, 300))
response.raise_for_status()
data = response.json()
return _extract(data, "audio_transcription", "text")
# -----------------------------
# SUMMARISATION
# -----------------------------
def summarise_text(text: str, language_code: str = "eng") -> str:
if language_code not in SUMMARISE_SUPPORTED_LANGUAGES:
raise ValueError(
f"Summarisation only supports English (eng) and Luganda (lug), "
f"but received '{language_code}'. Please provide input text in English or Luganda."
)
url = f"{SUNBIRD_API_BASE}/tasks/summarise"
headers = _get_headers("application/json")
payload = {"text": text}
response = requests.post(url, json=payload, headers=headers, timeout=(40, 300))
response.raise_for_status()
data = response.json()
return _extract(data, "summary", "summarized_text")
# -----------------------------
# TRANSLATION
# -----------------------------
def translate_text(text: str, target_language: str) -> str:
url = f"{SUNBIRD_API_BASE}/tasks/sunflower_inference"
headers = _get_headers("application/json")
lang_name = LANGUAGE_NAMES.get(target_language, target_language)
payload = {
"messages": [
{
"role": "system",
"content": (
f"You are a professional translator for Ugandan languages. "
f"Translate into {lang_name}. Return ONLY translation."
),
},
{
"role": "user",
"content": f"Translate into {lang_name}:\n\n{text}",
},
]
}
response = requests.post(url, json=payload, headers=headers, timeout=(40, 300))
response.raise_for_status()
data = response.json()
return _extract(data, "content", "text")
# -----------------------------
# TTS
# -----------------------------
def synthesise_speech(text: str, language: str) -> bytes:
speaker_id = TTS_SPEAKER_IDS.get(language)
if speaker_id is None:
raise ValueError(f"No TTS speaker available for language: {language}")
url = f"{SUNBIRD_API_BASE}/tasks/tts"
headers = _get_headers("application/json")
payload = {
"text": text,
"speaker_id": speaker_id
}
response = requests.post(url, json=payload, headers=headers, timeout=(40, 120))
response.raise_for_status()
data = response.json()
audio_url = _extract(data, "audio_url")
try:
audio_response = requests.get(audio_url, timeout=(10, 120))
audio_response.raise_for_status()
except requests.exceptions.ConnectionError:
raise RuntimeError(
"The speech audio was generated but could not be downloaded. "
"Please check your internet connection and try again."
)
except requests.exceptions.Timeout:
raise RuntimeError(
"The audio download timed out. Please try again."
)
except requests.exceptions.HTTPError:
raise RuntimeError(
"The speech audio could not be retrieved from the server. Please try again."
)
return audio_response.content