Spaces:
Running on Zero
Running on Zero
| // 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: | |
| // <uses-permission android:name="android.permission.INTERNET" /> | |
| 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" مباشرة | |
| } | |
| ============================== */ | |