هذا المستودع يوفر مكتبة whisper.cpp مجمعة كملف AAR، جاهزة للدمج المباشر في تطبيقات أندرويد المكتوبة بلغة Kotlin. تم بناء هذه المكتبة استنادًا إلى الكود المصدري الرسمي لـ whisper.cpp ومثال الأندرويد المرفق به.
| الملف / المجلد | الوصف |
|---|---|
whisper-cpp-android.aar |
ملف مكتبة الأندرويد المجمعة (Android Archive) الذي يحتوي على واجهة Kotlin/Java وجميع المكتبات الأصلية (.so). |
libs/arm64-v8a/libwhisper.so |
ملف المكتبة الأصلية المنفصل لبنية arm64-v8a (للتصحيح المتقدم). |
lib/ |
الكود المصدري الكامل لوحدة مكتبة الأندرويد لمن يرغب في البناء من المصدر. |
README.md |
هذا الملف. |
لدمج المكتبة في مشروعك، اتبع الخطوات التالية:
- أنشئ مجلدًا باسم
libsفي المجلد الرئيسي لوحدة التطبيق الخاصة بك (app/libs). - انسخ ملف
whisper-cpp-android.aarإلى هذا المجلد.
في ملف build.gradle.kts الخاص بوحدة التطبيق (app/build.gradle.kts):
// في بداية الملف، أضف المستودع المحلي
repositories {
flatDir {
dirs("libs")
}
}
// في قسم dependencies، أضف الاعتمادية
dependencies {
// ...
implementation(name = "whisper-cpp-android", ext = "aar")
// ...
}ملاحظة هامة: هذه المكتبة تدعم الآن توقيت الكلمات الدقيق (Word-Level Timestamps) لإنشاء ملفات SRT.
لضمان عمل استدعاءات JNI بشكل صحيح وتجنب خطأ UnsatisfiedLinkError، يجب أن تتطابق أسماء الحزم والفئات مع ما تم استخدامه في بناء المكتبة الأصلية.
لضمان عمل استدعاءات JNI بشكل صحيح وتجنب خطأ UnsatisfiedLinkError، يجب أن تتطابق أسماء الحزم والفئات مع ما تم استخدامه في بناء المكتبة الأصلية.
اسم الحزمة المطلوب: com.whispercpp.whisper
أ. إنشاء ملف WhisperLib.kt
أنشئ ملفًا باسم WhisperLib.kt في المسار app/src/main/java/com/whispercpp/whisper/ وانسخ الكود التالي. هذه الفئة هي نقطة الاتصال المباشرة مع كود C++:
package com.whispercpp.whisper
import android.util.Log
class WhisperLib {
companion object {
init {
// تحميل المكتبة الأصلية عند أول استخدام للفئة
System.loadLibrary("whisper")
}
// ---------------------------------------------------------------------
// دوال JNI الخارجية (External JNI Functions)
// ---------------------------------------------------------------------
@JvmStatic
external fun getSystemInfo(): String
@JvmStatic
external fun initContext(modelPath: String): Long
@JvmStatic
external fun freeContext(contextPtr: Long)
@JvmStatic
external fun fullTranscribe(contextPtr: Long, numThreads: Int, audioData: FloatArray)
@JvmStatic
external fun getTextSegmentCount(contextPtr: Long): Int
@JvmStatic
external fun getTextSegment(contextPtr: Long, index: Int): String
// دوال توقيت الكلمات (Word-Level Timestamps)
external fun getTextSegmentTokensCount(contextPtr: Long, index: Int): Int
external fun getTokenText(contextPtr: Long, segmentIndex: Int, tokenIndex: Int): String
external fun getTokenT0(contextPtr: Long, segmentIndex: Int, tokenIndex: Int): Long
external fun getTokenT1(contextPtr: Long, segmentIndex: Int, tokenIndex: Int): Long
}
}ب. إنشاء ملف WhisperContext.kt (الغلاف)
أنشئ ملفًا باسم WhisperContext.kt في نفس المسار. هذه الفئة توفر واجهة نظيفة وموجهة للكائنات (Object-Oriented) للاستخدام في تطبيقك:
package com.whispercpp.whisper
class WhisperContext private constructor(private var ptr: Long) {
companion object {
fun getSystemInfo(): String {
return WhisperLib.getSystemInfo()
}
fun createContext(filePath: String): WhisperContext {
val ptr = WhisperLib.initContext(filePath)
if (ptr == 0L) {
throw RuntimeException("فشل في إنشاء سياق Whisper. تأكد من صحة مسار النموذج.")
}
return WhisperContext(ptr)
}
fun transcribeData(audioData: FloatArray, numThreads: Int = 4): String {
require(ptr != 0L) { "سياق Whisper غير مهيأ أو تم تحريره." }
WhisperLib.fullTranscribe(ptr, numThreads, audioData)
val textCount = WhisperLib.getTextSegmentCount(ptr)
return buildString {
for (i in 0 until textCount) {
append(WhisperLib.getTextSegment(ptr, i))
}
}
}
fun release() {
if (ptr != 0L) {
WhisperLib.freeContext(ptr)
ptr = 0
}
}
/**
* فئة بيانات لتمثيل كلمة واحدة مع توقيتاتها الدقيقة.
* @param text نص الكلمة.
* @param startTimeMs وقت البداية بالمللي ثانية.
* @param endTimeMs وقت النهاية بالمللي ثانية.
*/
data class Word(
val text: String,
val startTimeMs: Long,
val endTimeMs: Long
)
/**
* استخراج توقيتات الكلمات الدقيقة من آخر عملية نسخ.
* @return قائمة بكائنات Word.
*/
fun getWordTimestamps(): List<Word> {
require(ptr != 0L) { "سياق Whisper غير مهيأ أو تم تحريره." }
val words = mutableListOf<Word>()
val segmentCount = WhisperLib.getTextSegmentCount(ptr)
for (i in 0 until segmentCount) {
val tokenCount = WhisperLib.getTextSegmentTokensCount(ptr, i)
for (j in 0 until tokenCount) {
val text = WhisperLib.getTokenText(ptr, i, j).trim()
val t0 = WhisperLib.getTokenT0(ptr, i, j)
val t1 = WhisperLib.getTokenT1(ptr, i, j)
if (text.isNotEmpty()) {
// يتم ضرب التوقيت في 10 للتحويل من centiseconds إلى milliseconds
words.add(Word(text, t0 * 10, t1 * 10))
}
}
}
return words
}
}يوضح هذا القسم كيفية استخدام المكتبة لنسخ ملف صوتي يختاره المستخدم.
- تنزيل النموذج: قم بتنزيل نموذج Whisper الذي تفضله (مثل
ggml-tiny.bin) من صفحة نماذج Whisper.cpp الرسمية. - إضافة إلى المشروع: ضع ملف النموذج الذي قمت بتنزيله داخل مجلد
app/src/main/assets.
استخدم CoroutineScope لتشغيل عملية النسخ الصوتي في خلفية التطبيق لتجنب تجميد واجهة المستخدم.
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.whispercpp.whisper.WhisperContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
class MainActivity : AppCompatActivity() {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private lateinit var modelPath: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// اختبار سريع لـ JNI
Log.d("Whisper", "System Info: ${WhisperContext.getSystemInfo()}")
// 1. نسخ ملف النموذج من Assets إلى مسار يمكن الوصول إليه
modelPath = copyAssetToCache("ggml-tiny.bin")
// 2. بدء عملية النسخ الصوتي (يجب استبدال هذا بآلية اختيار ملف صوتي)
val audioFilePath = "path/to/user/selected/audio.wav"
coroutineScope.launch {
transcribeAudio(audioFilePath)
}
}
// ---------------------------------------------------------------------
// الدوال المساعدة
// ---------------------------------------------------------------------
/**
* ينسخ ملف النموذج من مجلد assets إلى مجلد cache الخاص بالتطبيق.
*/
private fun copyAssetToCache(assetName: String): String {
val cacheFile = File(cacheDir, assetName)
if (!cacheFile.exists()) {
try {
assets.open(assetName).use { inputStream: InputStream ->
FileOutputStream(cacheFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
} catch (e: Exception) {
Log.e("Whisper", "فشل في نسخ النموذج من Assets.", e)
throw e
}
}
return cacheFile.absolutePath
}
/**
* يحمل ملف صوتي ويحوله إلى FloatArray.
* ⚠️ يجب أن يكون الصوت بصيغة 16kHz Mono FloatArray.
*/
private fun loadAudioFile(filePath: String): FloatArray {
// يجب على المطور تنفيذ هذا الجزء باستخدام مكتبة خارجية لقراءة ملف WAV/MP3
// وتحويله إلى الصيغة المطلوبة (16kHz, Mono, FloatArray).
Log.w("Whisper", "⚠️ يجب تنفيذ دالة loadAudioFile لتحويل ملف الصوت إلى 16kHz Mono FloatArray.")
return FloatArray(0)
}
/**
* دالة النسخ الصوتي الرئيسية التي تعمل في خلفية التطبيق.
*/
private suspend fun transcribeAudio(audioFilePath: String) {
var context: WhisperContext? = null
try {
// 1. تحميل النموذج وتهيئة السياق
context = WhisperContext.createContext(modelPath)
Log.i("Whisper", "تم تهيئة سياق Whisper بنجاح.")
< // 2. تحميل بيانات الصوت
val audioData = loadAudioFile(audioFilePath)
if (audioData.isEmpty()) {
Log.e("Whisper", "بيانات الصوت فارغة. لا يمكن إجراء النسخ.")
return
}
// 3. بدء عملية النسخ
val transcription = context.transcribeData(audioData)
// 4. عرض النتيجة (نص عادي)
Log.i("Whisper", "نتيجة النسخ: $transcription")
// 5. استخراج توقيتات الكلمات الدقيقة
val wordTimestamps = context.getWordTimestamps()
Log.i("Whisper", "عدد الكلمات مع التوقيت: ${wordTimestamps.size}")
// 6. مثال على إنشاء ملف SRT (دالة مساعدة)
val srtContent = generateSrt(wordTimestamps)
Log.i("Whisper", "محتوى SRT جاهز.")
} catch (e: Exception) {
Log.e("Whisper", "حدث خطأ أثناء عملية النسخ.", e)
} finally {
// 7. تحرير الموارد
context?.release()
}
}
/**
* دالة مساعدة لتحويل قائمة الكلمات ذات التوقيت إلى تنسيق SRT.
* يتم تجميع الكلمات في جملة واحدة لكل سطر SRT.
*/
private fun generateSrt(words: List<WhisperContext.Word>): String {
val srtBuilder = StringBuilder()
var srtIndex = 1
var currentLine = ""
var startTime = 0L
var endTime = 0L
val maxWordsPerLine = 8 // لتسهيل القراءة
for ((index, word) in words.withIndex()) {
if (currentLine.isEmpty()) {
startTime = word.startTimeMs
}
currentLine += "${word.text} "
endTime = word.endTimeMs
if ((index + 1) % maxWordsPerLine == 0 || index == words.size - 1) {
val start = formatTime(startTime)
val end = formatTime(endTime)
srtBuilder.append("$srtIndex\n")
srtBuilder.append("$start --> $end\n")
srtBuilder.append("${currentLine.trim()}\n\n")
currentLine = ""
srtIndex++
}
}
return srtBuilder.toString()
}
/**
* دالة مساعدة لتنسيق الوقت إلى تنسيق SRT (HH:MM:SS,mmm).
*/
private fun formatTime(ms: Long): String {
val hours = ms / 3600000
val minutes = (ms % 3600000) / 60000
val seconds = (ms % 60000) / 1000
val milliseconds = ms % 1000
return String.format("%02d:%02d:%02d,%03d", hours, minutes, seconds, milliseconds)
}
}context?.release()
}
}
>>>>>>> 9cb6a49 (Feature: Add AudioUtils for direct WAV file transcription and update documentation.)
}إذا كنت مطورًا وترغب في بناء ملف AAR بنفسك أو تعديل الكود المصدري لـ whisper.cpp، يمكنك استخدام الكود المصدري الموجود في مجلد lib.
- Android Studio مع تثبيت Android NDK (الإصدار 25.2.9519653).
- تنزيل شفرة مصدر
whisper.cppووضعها في مجلد بجوار مجلدlib(لأن ملفCMakeLists.txtيعتمد على المسار النسبي).
- افتح مشروع الأندرويد الخاص بك في Android Studio.
- استورد المجلد
libكوحدة (Module) جديدة (File -> New -> Import Module). - قم بتعديل ملف
settings.gradleلإضافة الوحدة:include ':app', ':lib'
- قم بمزامنة المشروع (Sync Project with Gradle Files).
- يمكنك الآن بناء الوحدة
libمباشرة من Android Studio أو باستخدام سطر الأوامر:سيتم إنشاء ملف./gradlew :lib:assembleRelease
AARفي المسارlib/build/outputs/aar/lib-release.aar.
- الصيغة المطلوبة: تتوقع مكتبة
whisper.cppبيانات صوتية خام (Raw Audio Data) في صيغة FloatArray، بمعدل أخذ عينات (Sample Rate) يبلغ 16000 هرتز (16kHz)، و أحادية القناة (Mono). * تنفيذloadAudioFile: يجب عليك تنفيذ الدالةloadAudioFileبنفسك باستخدام مكتبة خارجية أو كود مخصص لقراءة ملف WAV أو MP3 الذي يختاره المستخدم وتحويله إلى الصيغة المطلوبة (16kHz Mono FloatArray).