File size: 7,348 Bytes
9834088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a7d12e
9834088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461c65a
9834088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461c65a
9834088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461c65a
9834088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461c65a
9834088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461c65a
9834088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
"""
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