// TtsClientHF.kt — عميل أندرويد لـ Hugging Face Space (Gradio API) // يستبدل TtsClient.kt السابق // // build.gradle: // implementation("com.squareup.okhttp3:okhttp:4.12.0") // implementation("org.json:json:20240303") // أو استخدم kotlinx-serialization // AndroidManifest.xml: // package com.example.ttsapp import android.content.Context import android.media.MediaPlayer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONArray import org.json.JSONObject import java.io.File import java.util.concurrent.TimeUnit class TtsClientHF( private val context: Context, // مثال: https://ahmed-tts-app.hf.space (بدون / في النهاية) private val spaceUrl: String = "https://USERNAME-SPACENAME.hf.space", // فقط إذا كان الـ Space خاصاً (Private): توكن من إعدادات حسابك في HF private val hfToken: String? = null, ) { private val client = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(180, TimeUnit.SECONDS) // أول طلب بعد خمول يوقظ الـ Space .build() private var player: MediaPlayer? = null suspend fun speak( text: String, language: String = "ar", exaggeration: Double = 0.5, cfgWeight: Double = 0.5, ) { val file = synthesize(text, language, exaggeration, cfgWeight) withContext(Dispatchers.Main) { play(file) } } /** * Gradio API يعمل على خطوتين: * 1) POST /gradio_api/call/tts → يرجع event_id * 2) GET /gradio_api/call/tts/{event_id} → بث SSE، آخر سطر data يحوي رابط الملف * 3) تنزيل ملف الصوت من الرابط */ suspend fun synthesize( text: String, language: String, exaggeration: Double, cfgWeight: Double, ): File = withContext(Dispatchers.IO) { // ---- الخطوة 1: إرسال الطلب ---- val payload = JSONObject().put( "data", JSONArray().put(text).put(language) .put(exaggeration).put(cfgWeight) .put(JSONObject.NULL) // voice_ref: نستخدم الصوت الافتراضي ) val callReq = authed( Request.Builder() .url("$spaceUrl/gradio_api/call/tts") .post(payload.toString().toRequestBody("application/json".toMediaType())) ).build() val eventId = client.newCall(callReq).execute().use { resp -> check(resp.isSuccessful) { "TTS call failed: HTTP ${resp.code}" } JSONObject(resp.body!!.string()).getString("event_id") } // ---- الخطوة 2: قراءة النتيجة من بث SSE ---- val resultReq = authed( Request.Builder().url("$spaceUrl/gradio_api/call/tts/$eventId").get() ).build() var audioUrl: String? = null client.newCall(resultReq).execute().use { resp -> check(resp.isSuccessful) { "TTS result failed: HTTP ${resp.code}" } val reader = resp.body!!.byteStream().bufferedReader() var event = "" reader.forEachLine { line -> when { line.startsWith("event:") -> event = line.removePrefix("event:").trim() line.startsWith("data:") && event == "complete" -> { val data = JSONArray(line.removePrefix("data:").trim()) audioUrl = data.getJSONObject(0).getString("url") } line.startsWith("data:") && event == "error" -> throw Exception("TTS server error: ${line.removePrefix("data:").trim()}") } } } val url = audioUrl ?: throw Exception("No audio returned") // ---- الخطوة 3: تنزيل الصوت ---- val dlReq = authed(Request.Builder().url(url).get()).build() client.newCall(dlReq).execute().use { resp -> check(resp.isSuccessful) { "Audio download failed: HTTP ${resp.code}" } val out = File(context.cacheDir, "speech_${System.currentTimeMillis()}.wav") out.outputStream().use { resp.body!!.byteStream().copyTo(it) } out } } private fun authed(b: Request.Builder): Request.Builder = if (hfToken != null) b.header("Authorization", "Bearer $hfToken") else b private fun play(file: File) { player?.release() player = MediaPlayer().apply { setDataSource(file.absolutePath) setOnCompletionListener { file.delete() } prepare(); start() } } fun stop() { player?.release(); player = null } } /* ========= الاستخدام ========= val tts = TtsClientHF(context, spaceUrl = "https://ahmed-tts.hf.space") lifecycleScope.launch { tts.speak("أهلاً وسهلاً بكم", language = "العربية (ar)") // أو "ar" مباشرة } ============================== */