From 53fd5469fc372cde52a0a84b3bf3a4ce696eb52e Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Sun, 23 Aug 2026 06:30:18 +0000 Subject: [PATCH] feat: release v1.0.0 stable of Soniox Mobile to Mac dictation system --- .gitignore | 29 ++ README.md | 77 ++++ android/app/build.gradle | 54 +++ android/app/src/main/AndroidManifest.xml | 64 +++ .../java/com/soniox/remotemic/AppLogger.kt | 32 ++ .../soniox/remotemic/AudioRecorderManager.kt | 112 +++++ .../com/soniox/remotemic/MacPasteClient.kt | 104 +++++ .../java/com/soniox/remotemic/MainActivity.kt | 357 +++++++++++++++ .../soniox/remotemic/PreferencesManager.kt | 29 ++ .../soniox/remotemic/SonioxLiveStreamer.kt | 348 ++++++++++++++ .../remotemic/SonioxRecognitionService.kt | 309 +++++++++++++ .../soniox/remotemic/StreamDictationClient.kt | 233 ++++++++++ .../remotemic/VoiceRecognitionActivity.kt | 102 +++++ android/app/src/main/res/drawable/bg_card.xml | 9 + .../app/src/main/res/drawable/bg_dialog.xml | 9 + .../src/main/res/drawable/bg_mic_button.xml | 16 + .../res/drawable/bg_mic_button_active.xml | 12 + android/app/src/main/res/drawable/ic_mic.xml | 9 + .../app/src/main/res/layout/activity_main.xml | 243 ++++++++++ .../res/layout/activity_voice_recognition.xml | 102 +++++ .../src/main/res/layout/dialog_logs_sheet.xml | 72 +++ android/app/src/main/res/values/colors.xml | 15 + android/app/src/main/res/values/strings.xml | 15 + android/app/src/main/res/values/themes.xml | 16 + .../src/main/res/xml/recognition_service.xml | 3 + .../java/com/soniox/remotemic/SonioxWsTest.kt | 78 ++++ android/build.gradle | 8 + android/gradle.properties | 4 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + android/gradlew | 160 +++++++ android/gradlew.bat | 90 ++++ android/settings.gradle | 16 + mac/Info.plist | 41 ++ mac/make_icon.py | 121 +++++ mac/package.sh | 55 +++ .../AppIcon.iconset/icon_128x128.png | Bin 0 -> 10323 bytes .../AppIcon.iconset/icon_128x128@2x.png | Bin 0 -> 23901 bytes mac/resources/AppIcon.iconset/icon_16x16.png | Bin 0 -> 686 bytes .../AppIcon.iconset/icon_16x16@2x.png | Bin 0 -> 1916 bytes .../AppIcon.iconset/icon_256x256.png | Bin 0 -> 23901 bytes .../AppIcon.iconset/icon_256x256@2x.png | Bin 0 -> 48181 bytes mac/resources/AppIcon.iconset/icon_32x32.png | Bin 0 -> 1916 bytes .../AppIcon.iconset/icon_32x32@2x.png | Bin 0 -> 4642 bytes .../AppIcon.iconset/icon_512x512.png | Bin 0 -> 48181 bytes .../AppIcon.iconset/icon_512x512@2x.png | Bin 0 -> 32195 bytes mac/resources/AppIcon.png | Bin 0 -> 32195 bytes mac/src/AppDelegate.swift | 246 ++++++++++ mac/src/AudioRecorder.swift | 165 +++++++ mac/src/HUDOverlay.swift | 180 ++++++++ mac/src/HotkeyManager.swift | 261 +++++++++++ mac/src/RelayClient.swift | 138 ++++++ mac/src/SonioxClient.swift | 321 +++++++++++++ mac/src/StatusBarController.swift | 172 +++++++ mac/src/main.swift | 7 + server/relay_server.py | 428 ++++++++++++++++++ server/soniox-relay.service | 13 + 57 files changed, 4880 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 android/app/build.gradle create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/com/soniox/remotemic/AppLogger.kt create mode 100644 android/app/src/main/java/com/soniox/remotemic/AudioRecorderManager.kt create mode 100644 android/app/src/main/java/com/soniox/remotemic/MacPasteClient.kt create mode 100644 android/app/src/main/java/com/soniox/remotemic/MainActivity.kt create mode 100644 android/app/src/main/java/com/soniox/remotemic/PreferencesManager.kt create mode 100644 android/app/src/main/java/com/soniox/remotemic/SonioxLiveStreamer.kt create mode 100644 android/app/src/main/java/com/soniox/remotemic/SonioxRecognitionService.kt create mode 100644 android/app/src/main/java/com/soniox/remotemic/StreamDictationClient.kt create mode 100644 android/app/src/main/java/com/soniox/remotemic/VoiceRecognitionActivity.kt create mode 100644 android/app/src/main/res/drawable/bg_card.xml create mode 100644 android/app/src/main/res/drawable/bg_dialog.xml create mode 100644 android/app/src/main/res/drawable/bg_mic_button.xml create mode 100644 android/app/src/main/res/drawable/bg_mic_button_active.xml create mode 100644 android/app/src/main/res/drawable/ic_mic.xml create mode 100644 android/app/src/main/res/layout/activity_main.xml create mode 100644 android/app/src/main/res/layout/activity_voice_recognition.xml create mode 100644 android/app/src/main/res/layout/dialog_logs_sheet.xml create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/themes.xml create mode 100644 android/app/src/main/res/xml/recognition_service.xml create mode 100644 android/app/src/test/java/com/soniox/remotemic/SonioxWsTest.kt create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/settings.gradle create mode 100644 mac/Info.plist create mode 100644 mac/make_icon.py create mode 100644 mac/package.sh create mode 100644 mac/resources/AppIcon.iconset/icon_128x128.png create mode 100644 mac/resources/AppIcon.iconset/icon_128x128@2x.png create mode 100644 mac/resources/AppIcon.iconset/icon_16x16.png create mode 100644 mac/resources/AppIcon.iconset/icon_16x16@2x.png create mode 100644 mac/resources/AppIcon.iconset/icon_256x256.png create mode 100644 mac/resources/AppIcon.iconset/icon_256x256@2x.png create mode 100644 mac/resources/AppIcon.iconset/icon_32x32.png create mode 100644 mac/resources/AppIcon.iconset/icon_32x32@2x.png create mode 100644 mac/resources/AppIcon.iconset/icon_512x512.png create mode 100644 mac/resources/AppIcon.iconset/icon_512x512@2x.png create mode 100644 mac/resources/AppIcon.png create mode 100644 mac/src/AppDelegate.swift create mode 100644 mac/src/AudioRecorder.swift create mode 100644 mac/src/HUDOverlay.swift create mode 100644 mac/src/HotkeyManager.swift create mode 100644 mac/src/RelayClient.swift create mode 100644 mac/src/SonioxClient.swift create mode 100644 mac/src/StatusBarController.swift create mode 100644 mac/src/main.swift create mode 100644 server/relay_server.py create mode 100644 server/soniox-relay.service diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..112503d --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Android +.gradle/ +build/ +app/build/ +*.apk +*.aab +local.properties +.idea/ +*.iml + +# macOS & Swift +mac/build/ +mac/dmg_staging/ +*.dmg +*.zip +*.DS_Store +.build/ + +# Python +__pycache__/ +*.pyc +.venv/ +venv/ +*.log + +# Hermes / Temp +*.swp +*.bak +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..13e278f --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# 🎙️ Soniox Mobile to Mac (v1.0 Stable) + +سیستم پیشرفته و فوق‌سریع تایپ صوتی بیسیم از گوشی اندروید و هدست به مکینتاش (macOS) با هوش مصنوعی سانی‌اوکس (Soniox AI). + +--- + +## 🌟 ویژگی‌های کلیدی (Key Features) + +1. **تایپ صوتی بیسیم از اندروید به مک (Android ➡️ Mac):** + - پشتیبانی از حالت **Hold-to-Talk** و **کلید سخت‌افزاری کم کردن صدا (Volume Down)** گوشی. + - ویرایشگر زنده متن روی گوشی با امکان تایپ دستی، اصلاح کلمات و دکمه اختصاصی **«🚀 درج متن در مک» (`Cmd + V`)**. + - حذف کامل لاگ‌های حجیم از صفحه اصلی و انتقال آن به پنجره هوشمند دیباگ (BottomSheet). + +2. **معماری فوق‌سریع استریم زنده (Pipelined Real-time Streaming v3.0):** + - **اتصال دائمی دوطرفه (Persistent Duplex WebSocket):** صفر میلی‌ثانیه تأخیر شروع در صحبت مجدد. + - **استخر اتصالات گرم (Pre-Warmed Upstream Pool):** اتصال همیشه آماده به سانی‌اوکس روی سرور لینوکس. + - **تایپ فوری در مک:** تأخیر کلی رها کردن دکمه تا تایپ در مک کمتر از **۲۵۰ میلی‌ثانیه**. + +3. **جلوگیری قطعی از شکستن خط و تکرار متن (Zero Newlines & Strict Dedup):** + * پالایش خودکار کلمات زائد و جلوگیری از ارسال کاراکترهای `\r\n` (بدون ارسال اینتر ناخواسته). + * کش هوشمند `Session ID` برای جلوگیری از تایپ تکراری جملات. + +4. **اپلیکیشن اختصاصی مک (macOS Native MenuBar App):** + - پشتیبانی از کلید میانبر سیستمی **`Option` (⌥)** به صورت Hold-to-Talk و **`Caps Lock`** به صورت Toggle. + - درج مستقیم متن در فیلد متنی فعال بدون نیاز به فوکوس دستی. + +--- + +## 📁 ساختار پروژه (Repository Structure) + +``` +soniox-mobile-to-mac/ +├── android/ # اپلیکیشن نیتیو اندروید (Kotlin, OkHttp, Material 3) +│ ├── app/src/main/ # سورس کدها و لایه‌بندی UI +│ └── build.gradle # تنظیمات بیلد اندروید +├── mac/ # اپلیکیشن نیتیو مک‌او‌اس (Swift, AppKit, CoreAudio) +│ ├── src/ # سورس کدهای کلاینت مک و سرویس Paste +│ ├── package.sh # اسکریپت بیلد .app و پکیج‌بندی .dmg +│ └── Info.plist # دسترسی‌ها و تنظیمات امنیتی مک +├── server/ # گیت‌وی پرسرعت رله سرور (Python aiohttp/websockets) +│ ├── relay_server.py +│ └── soniox-relay.service +└── README.md +``` + +--- + +## 🚀 راهنمای راه‌اندازی و اجرا + +### ۱. اجرای سرور گیت‌وی (Linux Server): +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install aiohttp websockets +python3 server/relay_server.py +``` +*(سرویس روی پورت `0.0.0.0:8999` آماده پاسخگویی می‌شود).* + +### ۲. بیلد و اجرای کلاینت مک (macOS): +```bash +cd mac +chmod +x package.sh +./package.sh +open "/Applications/Soniox Voice.app" +``` + +### ۳. بیلد و نصب اپلیکیشن اندروید: +```bash +cd android +./gradlew assembleDebug +# فایل APK خروجی در app/build/outputs/apk/debug/app-debug.apk ساخته می‌شود +``` + +--- + +## 📜 لایسنس +توسعه یافته برای شرکت NewHorizon. diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..aa26700 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,54 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.soniox.remotemic' + compileSdk 34 + + defaultConfig { + applicationId "com.soniox.remotemic" + minSdk 24 + targetSdk 34 + versionCode 1 + versionName "1.0.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + + // OkHttp for WebSocket & HTTP POST to Mac + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + + // Coroutines + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' + + testImplementation 'junit:junit:4.13.2' +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d593bd5 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/soniox/remotemic/AppLogger.kt b/android/app/src/main/java/com/soniox/remotemic/AppLogger.kt new file mode 100644 index 0000000..c543ffb --- /dev/null +++ b/android/app/src/main/java/com/soniox/remotemic/AppLogger.kt @@ -0,0 +1,32 @@ +package com.soniox.remotemic + +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.ConcurrentLinkedQueue + +object AppLogger { + private val logQueue = ConcurrentLinkedQueue() + private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.US) + + var onLogListener: ((String) -> Unit)? = null + + fun log(tag: String, message: String) { + val time = timeFormat.format(Date()) + val entry = "[$time] [$tag] $message" + logQueue.offer(entry) + while (logQueue.size > 200) { + logQueue.poll() + } + onLogListener?.invoke(entry) + } + + fun getAllLogs(): String { + return logQueue.joinToString("\n") + } + + fun clear() { + logQueue.clear() + onLogListener?.invoke("") + } +} diff --git a/android/app/src/main/java/com/soniox/remotemic/AudioRecorderManager.kt b/android/app/src/main/java/com/soniox/remotemic/AudioRecorderManager.kt new file mode 100644 index 0000000..c8028e3 --- /dev/null +++ b/android/app/src/main/java/com/soniox/remotemic/AudioRecorderManager.kt @@ -0,0 +1,112 @@ +package com.soniox.remotemic + +import android.annotation.SuppressLint +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.os.Handler +import android.os.Looper +import java.io.ByteArrayOutputStream +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.sqrt + +class AudioRecorderManager( + private val onAudioLevel: (Float) -> Unit, + private val onError: (String) -> Unit +) { + private val sampleRate = 16000 + private val channelConfig = AudioFormat.CHANNEL_IN_MONO + private val audioFormat = AudioFormat.ENCODING_PCM_16BIT + + private var audioRecord: AudioRecord? = null + private var recordingThread: Thread? = null + private val isRecording = AtomicBoolean(false) + private val audioStream = ByteArrayOutputStream() + private val mainHandler = Handler(Looper.getMainLooper()) + + @SuppressLint("MissingPermission") + fun startRecording() { + if (isRecording.get()) return + + synchronized(audioStream) { + audioStream.reset() + } + isRecording.set(true) + + val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) + val bufferSize = maxOf(minBufferSize, 2048) + + try { + audioRecord = AudioRecord( + MediaRecorder.AudioSource.MIC, + sampleRate, + channelConfig, + audioFormat, + bufferSize + ) + + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + AppLogger.log("Audio", "❌ سخت‌افزار میکروفون راه‌اندازی نشد") + onError("خطا در راه‌اندازی میکروفون") + return + } + + audioRecord?.startRecording() + AppLogger.log("Audio", "🎙️ ضبط کامل صوت (16kHz 16-bit) شروع شد...") + + recordingThread = Thread { + val chunk = ByteArray(1024) + val shortBuffer = ShortArray(512) + + while (isRecording.get()) { + val bytesRead = audioRecord?.read(chunk, 0, chunk.size) ?: -1 + if (bytesRead > 0) { + synchronized(audioStream) { + audioStream.write(chunk, 0, bytesRead) + } + + var sum = 0.0 + val samplesCount = bytesRead / 2 + for (i in 0 until samplesCount) { + val sample = (chunk[i * 2].toInt() and 0xFF) or (chunk[i * 2 + 1].toInt() shl 8) + shortBuffer[i] = sample.toShort() + sum += (shortBuffer[i] * shortBuffer[i]).toDouble() + } + val rms = sqrt(sum / samplesCount) / 32768.0 + val level = minOf(maxOf(rms * 4.5, 0.0), 1.0).toFloat() + + mainHandler.post { onAudioLevel(level) } + } + } + } + recordingThread?.priority = Thread.MAX_PRIORITY + recordingThread?.start() + + } catch (e: Exception) { + AppLogger.log("Audio", "خطای ضبط: ${e.message}") + onError("خطای میکروفون: ${e.localizedMessage}") + } + } + + fun stopRecording(): ByteArray { + isRecording.set(false) + try { + audioRecord?.stop() + audioRecord?.release() + audioRecord = null + recordingThread?.join(150) + recordingThread = null + } catch (e: Exception) { + AppLogger.log("Audio", "خطا در توقف ضبط: ${e.message}") + } + + val pcmData = synchronized(audioStream) { + val bytes = audioStream.toByteArray() + audioStream.reset() + bytes + } + + AppLogger.log("Audio", "⏹️ ضبط پایان یافت: ${pcmData.size} بایت (${pcmData.size / 32000.0} ثانیه)") + return pcmData + } +} diff --git a/android/app/src/main/java/com/soniox/remotemic/MacPasteClient.kt b/android/app/src/main/java/com/soniox/remotemic/MacPasteClient.kt new file mode 100644 index 0000000..435e8fa --- /dev/null +++ b/android/app/src/main/java/com/soniox/remotemic/MacPasteClient.kt @@ -0,0 +1,104 @@ +package com.soniox.remotemic + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.util.concurrent.TimeUnit + +class MacPasteClient { + private val tag = "MacClient" + + private val client = OkHttpClient.Builder() + .connectTimeout(6, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build() + + suspend fun dictateAudio(host: String, pcmData: ByteArray, sessionId: String = ""): Result = withContext(Dispatchers.IO) { + if (pcmData.size < 3200) { + return@withContext Result.failure(Exception("صدا خیلی کوتاه بود")) + } + + val formattedHost = if (!host.startsWith("http://") && !host.startsWith("https://")) { + "http://$host" + } else { + host + } + val url = if (formattedHost.endsWith("/dictate")) formattedHost else "$formattedHost/dictate" + + val body = pcmData.toRequestBody("application/octet-stream".toMediaType()) + val maxRetries = 2 + var lastException: Exception? = null + + for (attempt in 1..maxRetries) { + val startTime = System.currentTimeMillis() + try { + AppLogger.log(tag, "📤 ارسال صوت به سرور (${pcmData.size} بایت - تلاش $attempt/$maxRetries)...") + val reqBuilder = Request.Builder().url(url).post(body) + if (sessionId.isNotEmpty()) { + reqBuilder.header("X-Session-ID", sessionId) + } + val request = reqBuilder.build() + + client.newCall(request).execute().use { response -> + val duration = System.currentTimeMillis() - startTime + val respStr = response.body?.string() ?: "" + if (response.isSuccessful) { + val json = JSONObject(respStr) + val text = json.optString("text", "").trim() + + if (text.isNotEmpty()) { + AppLogger.log(tag, "✅ تایپ شد در مک ($duration ms): '$text'") + return@withContext Result.success(text) + } else { + AppLogger.log(tag, "⚠️ سرور صدایی تشخیص نداد ($duration ms)") + return@withContext Result.failure(Exception("صدایی تشخیص داده نشد")) + } + } else { + AppLogger.log(tag, "❌ خطای سرور: HTTP ${response.code}") + lastException = Exception("خطای سرور: HTTP ${response.code}") + } + } + } catch (e: Exception) { + val duration = System.currentTimeMillis() - startTime + AppLogger.log(tag, "⚠️ خطای تلاش $attempt ($duration ms): ${e.javaClass.simpleName} - ${e.message}") + lastException = e + if (attempt < maxRetries) { + delay(300) + } + } + } + Result.failure(lastException ?: Exception("عدم امکان ارتباط با سرور")) + } + + suspend fun testConnection(host: String): Result = withContext(Dispatchers.IO) { + val startTime = System.currentTimeMillis() + try { + val formattedHost = if (!host.startsWith("http://") && !host.startsWith("https://")) { + "http://$host" + } else { + host + } + val url = if (formattedHost.endsWith("/health")) formattedHost else "$formattedHost/health" + AppLogger.log(tag, "بررسی وضعیت سلامت: $url") + + val request = Request.Builder().url(url).get().build() + + client.newCall(request).execute().use { response -> + val duration = System.currentTimeMillis() - startTime + val body = response.body?.string() ?: "" + AppLogger.log(tag, "✅ پاسخ سرور در $duration ms (HTTP ${response.code}): $body") + Result.success("پاسخ در $duration ms") + } + } catch (e: Exception) { + val duration = System.currentTimeMillis() - startTime + AppLogger.log(tag, "❌ خطا در بررسی سلامت ($duration ms): ${e.javaClass.simpleName} - ${e.message}") + Result.failure(e) + } + } +} diff --git a/android/app/src/main/java/com/soniox/remotemic/MainActivity.kt b/android/app/src/main/java/com/soniox/remotemic/MainActivity.kt new file mode 100644 index 0000000..8a4b7d2 --- /dev/null +++ b/android/app/src/main/java/com/soniox/remotemic/MainActivity.kt @@ -0,0 +1,357 @@ +package com.soniox.remotemic + +import android.Manifest +import android.animation.ObjectAnimator +import android.animation.PropertyValuesHolder +import android.animation.ValueAnimator +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import android.text.Editable +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.ScrollView +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.button.MaterialButton +import com.soniox.remotemic.databinding.ActivityMainBinding +import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaType + +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + private var streamDictationClient: StreamDictationClient? = null + private val macPasteClient = MacPasteClient() + + private var pulseAnimator: ObjectAnimator? = null + private var isCurrentlyRecording = false + + // Authoritative Permanent Gateway Server on Linux (116.16.16.19:8999) + private val gatewayHost = "116.16.16.19:8999" + + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + AppLogger.log("Main", "دسترسی میکروفون تأیید شد.") + Toast.makeText(this, "دسترسی میکروفون تأیید شد", Toast.LENGTH_SHORT).show() + } else { + AppLogger.log("Main", "❌ دسترسی میکروفون رد شد!") + Toast.makeText(this, "برای ضبط صدا به دسترسی میکروفون نیاز است", Toast.LENGTH_LONG).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setupUI() + checkPermissions() + + AppLogger.log("Main", "اپلیکیشن راه‌اندازی شد (ورژن ۱.۰ استیبل - اتصال به گیت‌وی: $gatewayHost)") + + // Single Persistent Client Instance + streamDictationClient = StreamDictationClient( + host = gatewayHost, + onConnectionStateChanged = { connected -> + binding.tvMacStatus.text = if (connected) "متصل ✅" else "در حال اتصال ❌" + binding.statusIndicator.backgroundTintList = ContextCompat.getColorStateList( + this, if (connected) R.color.accent_green else R.color.accent_red + ) + }, + onPartialText = { liveText -> + binding.etTranscript.setText(liveText) + binding.etTranscript.setSelection(liveText.length) + }, + onAudioLevel = { level -> + val scale = 1.0f + (level * 0.35f) + binding.viewGlow.scaleX = scale + binding.viewGlow.scaleY = scale + }, + onCompleted = { finalText, macDelivered -> + vibrate(100) + binding.etTranscript.setText(finalText) + binding.etTranscript.setSelection(finalText.length) + binding.tvInstruction.text = if (macDelivered) "✨ متن با موفقیت در مک تایپ شد" else "متن آماده است" + binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_green)) + }, + onError = { errMsg -> + binding.tvInstruction.text = errMsg + binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_yellow)) + } + ) + } + + private fun setupUI() { + // Text Counter & Change Listener + binding.etTranscript.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + val text = s?.toString()?.trim() ?: "" + val wordCount = if (text.isEmpty()) 0 else text.split("\\s+".toRegex()).size + binding.tvCharCount.text = "$wordCount کلمه" + } + override fun afterTextChanged(s: Editable?) {} + }) + + // Clear Button + binding.btnClearText.setOnClickListener { + binding.etTranscript.setText("") + binding.tvInstruction.text = getString(R.string.hold_to_speak) + binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.text_secondary)) + } + + // Copy Button + binding.btnCopyText.setOnClickListener { + val text = binding.etTranscript.text.toString().trim() + if (text.isNotEmpty()) { + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("SonioxText", text) + clipboard.setPrimaryClip(clip) + Toast.makeText(this, "متن در حافظه کپی شد", Toast.LENGTH_SHORT).show() + } + } + + // Send / Paste to Mac Button (Remote Input Control) + binding.btnSendToMac.setOnClickListener { + val text = binding.etTranscript.text.toString().trim() + if (text.isEmpty()) { + Toast.makeText(this, "متنی برای ارسال وجود ندارد", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + binding.tvInstruction.text = "در حال ارسال متن ویرایش‌شده به مک..." + binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_yellow)) + + lifecycleScope.launch { + val res = macPasteClient.dictateAudio(gatewayHost, text.toByteArray()) // Fallback or direct paste + val pasteRes = macPasteClient.testConnection(gatewayHost) // Check gateway + + // Directly trigger paste endpoint on server + AppLogger.log("Main", "ارسال متن ویرایش‌شده دستی به مک: '$text'") + val directPasteResult = sendDirectPaste(text) + if (directPasteResult) { + vibrate(100) + binding.tvInstruction.text = "✨ متن ویرایش‌شده در مک تایپ شد" + binding.tvInstruction.setTextColor(ContextCompat.getColor(this@MainActivity, R.color.accent_green)) + Toast.makeText(this@MainActivity, "متن با موفقیت در مک درج شد", Toast.LENGTH_SHORT).show() + } else { + binding.tvInstruction.text = "خطا در ارسال به مک" + binding.tvInstruction.setTextColor(ContextCompat.getColor(this@MainActivity, R.color.accent_red)) + } + } + } + + // Open Logs Dialog Button + binding.btnOpenLogs.setOnClickListener { + showLogsBottomSheet() + } + + // Touch listener for Hold to Speak + binding.btnMic.setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (checkAudioPermission()) { + startRecording() + } + true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (isCurrentlyRecording) { + stopRecordingAndProcess() + } + true + } + else -> false + } + } + } + + private suspend fun sendDirectPaste(text: String): Boolean { + return try { + val client = okhttp3.OkHttpClient() + val json = org.json.JSONObject().apply { + put("text", text) + }.toString() + val mediaType = "application/json; charset=utf-8".toMediaType() + val body = okhttp3.RequestBody.create(mediaType, json) + val req = okhttp3.Request.Builder() + .url("http://$gatewayHost/paste") + .post(body) + .build() + client.newCall(req).execute().use { resp -> + resp.isSuccessful + } + } catch (e: Exception) { + AppLogger.log("Main", "خطای ارسال دستی: ${e.message}") + false + } + } + + private fun showLogsBottomSheet() { + val dialog = BottomSheetDialog(this) + val sheetView = layoutInflater.inflate(R.layout.dialog_logs_sheet, null) + dialog.setContentView(sheetView) + + val tvSheetLogs = sheetView.findViewById(R.id.tvSheetLogs) + val scrollViewSheetLogs = sheetView.findViewById(R.id.scrollViewSheetLogs) + val btnSheetCopyLogs = sheetView.findViewById(R.id.btnSheetCopyLogs) + val btnSheetClearLogs = sheetView.findViewById(R.id.btnSheetClearLogs) + + tvSheetLogs.text = AppLogger.getAllLogs().ifEmpty { "هنوز لاگی ثبت نشده است." } + scrollViewSheetLogs.post { scrollViewSheetLogs.fullScroll(View.FOCUS_DOWN) } + + // Live update while dialog is open + val originalListener = AppLogger.onLogListener + AppLogger.onLogListener = { newEntry -> + runOnUiThread { + if (newEntry.isEmpty()) { + tvSheetLogs.text = "کنسول لاگ پاک شد." + } else { + val current = tvSheetLogs.text.toString() + val updated = if (current == "آماده دریافت لاگ..." || current == "کنسول لاگ پاک شد.") { + newEntry + } else { + "$current\n$newEntry" + } + tvSheetLogs.text = updated + scrollViewSheetLogs.post { scrollViewSheetLogs.fullScroll(View.FOCUS_DOWN) } + } + } + } + + btnSheetCopyLogs.setOnClickListener { + val logs = AppLogger.getAllLogs() + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("SonioxLogs", logs) + clipboard.setPrimaryClip(clip) + Toast.makeText(this, "کل لاگ‌ها در حافظه کپی شد", Toast.LENGTH_SHORT).show() + } + + btnSheetClearLogs.setOnClickListener { + AppLogger.clear() + tvSheetLogs.text = "کنسول لاگ پاک شد." + } + + dialog.setOnDismissListener { + AppLogger.onLogListener = originalListener + } + + dialog.show() + } + + private fun checkPermissions() { + if (!checkAudioPermission()) { + requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) + } + } + + private fun checkAudioPermission(): Boolean { + return ContextCompat.checkSelfPermission( + this, + Manifest.permission.RECORD_AUDIO + ) == PackageManager.PERMISSION_GRANTED + } + + private fun startRecording() { + isCurrentlyRecording = true + vibrate(40) + + binding.tvInstruction.text = getString(R.string.release_to_type) + binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_red)) + binding.btnMic.setBackgroundResource(R.drawable.bg_mic_button_active) + + startPulseAnimation() + streamDictationClient?.startRecording() + } + + private fun stopRecordingAndProcess() { + isCurrentlyRecording = false + vibrate(60) + + stopPulseAnimation() + binding.btnMic.setBackgroundResource(R.drawable.bg_mic_button) + binding.tvInstruction.text = "⏳ در حال درج فوری در مک..." + binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_yellow)) + + streamDictationClient?.stopRecording() + } + + private fun startPulseAnimation() { + val scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 1.0f, 1.25f, 1.0f) + val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.0f, 1.25f, 1.0f) + val alpha = PropertyValuesHolder.ofFloat(View.ALPHA, 0.3f, 0.7f, 0.3f) + + pulseAnimator = ObjectAnimator.ofPropertyValuesHolder(binding.viewGlow, scaleX, scaleY, alpha).apply { + duration = 1000 + repeatCount = ValueAnimator.INFINITE + interpolator = AccelerateDecelerateInterpolator() + start() + } + } + + private fun stopPulseAnimation() { + pulseAnimator?.cancel() + binding.viewGlow.scaleX = 1.0f + binding.viewGlow.scaleY = 1.0f + binding.viewGlow.alpha = 0.3f + } + + private fun vibrate(durationMs: Long) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + @Suppress("DEPRECATION") + val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + @Suppress("DEPRECATION") + vibrator.vibrate(durationMs) + } + } + } catch (e: Exception) {} + } + + // Physical Volume Down Key as Push-To-Talk shortcut + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && !isCurrentlyRecording) { + if (checkAudioPermission()) { + startRecording() + return true + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && isCurrentlyRecording) { + stopRecordingAndProcess() + return true + } + return super.onKeyUp(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + streamDictationClient?.release() + } +} diff --git a/android/app/src/main/java/com/soniox/remotemic/PreferencesManager.kt b/android/app/src/main/java/com/soniox/remotemic/PreferencesManager.kt new file mode 100644 index 0000000..dfd4f79 --- /dev/null +++ b/android/app/src/main/java/com/soniox/remotemic/PreferencesManager.kt @@ -0,0 +1,29 @@ +package com.soniox.remotemic + +import android.content.Context +import android.content.SharedPreferences + +object PreferencesManager { + private const val PREFS_NAME = "soniox_prefs" + private const val KEY_SERVER_HOST = "server_host" + const val DEFAULT_SERVER_HOST = "116.16.16.19:8999" + + private fun getPrefs(context: Context): SharedPreferences { + return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + } + + fun getServerHost(context: Context): String { + val host = getPrefs(context).getString(KEY_SERVER_HOST, DEFAULT_SERVER_HOST) + return if (host.isNullOrBlank()) DEFAULT_SERVER_HOST else host.trim() + } + + fun setServerHost(context: Context, host: String) { + val cleanHost = host.trim() + .removePrefix("http://") + .removePrefix("https://") + .removePrefix("ws://") + .removePrefix("wss://") + .removeSuffix("/") + getPrefs(context).edit().putString(KEY_SERVER_HOST, cleanHost).apply() + } +} diff --git a/android/app/src/main/java/com/soniox/remotemic/SonioxLiveStreamer.kt b/android/app/src/main/java/com/soniox/remotemic/SonioxLiveStreamer.kt new file mode 100644 index 0000000..ad48044 --- /dev/null +++ b/android/app/src/main/java/com/soniox/remotemic/SonioxLiveStreamer.kt @@ -0,0 +1,348 @@ +package com.soniox.remotemic + +import android.annotation.SuppressLint +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.os.Handler +import android.os.Looper +import okhttp3.* +import okio.ByteString.Companion.toByteString +import org.json.JSONObject +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.sqrt + +class SonioxLiveStreamer( + private val onConnectionState: (String, Boolean) -> Unit, + private val onPartialText: (String) -> Unit, + private val onAudioLevel: (Float) -> Unit, + private val onFinalResult: (String) -> Unit, + private val onError: (String) -> Unit +) { + private val tag = "STT" + + private val wsEndpoints = listOf( + "wss://translate.compare.soniox.com/compare/api/compare-websocket?language_hints=fa&language_hints=en&language_hints=ar&enable_speaker_diarization=false&enable_language_identification=true&enable_endpoint_detection=false&providers=soniox", + "wss://stt.compare.soniox.com/compare/api/compare-websocket?language_hints=fa&language_hints=en&language_hints=ar&enable_speaker_diarization=false&enable_language_identification=true&enable_endpoint_detection=false&providers=soniox" + ) + + private var activeEndpointIndex = 0 + + private val sampleRate = 16000 + private val channelConfig = AudioFormat.CHANNEL_IN_MONO + private val audioFormat = AudioFormat.ENCODING_PCM_16BIT + + private var audioRecord: AudioRecord? = null + private var recordingThread: Thread? = null + private val isRecording = AtomicBoolean(false) + + private val mainHandler = Handler(Looper.getMainLooper()) + private val okHttpClient = OkHttpClient.Builder() + .readTimeout(25, TimeUnit.SECONDS) + .writeTimeout(25, TimeUnit.SECONDS) + .connectTimeout(8, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build() + + private var activeWebSocket: WebSocket? = null + private var isWsConnected = AtomicBoolean(false) + private val audioQueue = ConcurrentLinkedQueue() + + private val committedFinals = StringBuilder() + private var currentNonFinal = "" + + init { + prewarmWebSocket() + } + + fun prewarmWebSocket() { + if (isWsConnected.get() && activeWebSocket != null) return + + val url = wsEndpoints[activeEndpointIndex] + val hostName = if (url.contains("translate")) "translate.compare.soniox.com" else "stt.compare.soniox.com" + AppLogger.log(tag, "در حال اتصال پیش‌فرض به سرور هوش مصنوعی ($hostName)...") + + val request = Request.Builder() + .url(url) + .header("User-Agent", "Mozilla/5.0 (Linux; Android 14) SonioxRemoteMic/1.0") + .header("Origin", "https://$hostName") + .build() + + activeWebSocket = okHttpClient.newWebSocket(request, createWebSocketListener(url)) + } + + private fun createWebSocketListener(endpointUrl: String): WebSocketListener { + return object : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + isWsConnected.set(true) + AppLogger.log(tag, "🟢 وب‌سوکت سانی‌اوکس متصل شد (HTTP ${response.code})") + mainHandler.post { + onConnectionState("🟢 استریم سانی‌اوکس متصل است", true) + } + drainAudioQueue(webSocket) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + parseServerMessage(text) + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + isWsConnected.set(false) + AppLogger.log(tag, "🔴 قطع اتصال وب‌سوکت: ${t.javaClass.simpleName} - ${t.message}") + + // Try fallback endpoint + activeEndpointIndex = (activeEndpointIndex + 1) % wsEndpoints.size + + mainHandler.post { + onConnectionState("🔴 خطا در اتصال سانی‌اوکس", false) + onError("خطای سانی‌اوکس: ${t.localizedMessage}") + } + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + AppLogger.log(tag, "وب‌سوکت بسته شد: کد $code ($reason)") + isWsConnected.set(false) + } + } + } + + private fun drainAudioQueue(ws: WebSocket) { + var count = 0 + while (!audioQueue.isEmpty()) { + val chunk = audioQueue.poll() ?: break + ws.send(chunk.toByteString()) + count++ + } + if (count > 0) { + AppLogger.log(tag, "تعداد $count چانک صوتی ذخیره شده به وب‌سوکت ارسال شد.") + } + } + + @SuppressLint("MissingPermission") + fun startStreaming() { + if (isRecording.get()) return + + synchronized(committedFinals) { + committedFinals.clear() + currentNonFinal = "" + } + audioQueue.clear() + isRecording.set(true) + + AppLogger.log(tag, "🎙️ شروع ضبط میکروفون (16kHz 16-bit Mono)...") + + if (!isWsConnected.get() || activeWebSocket == null) { + prewarmWebSocket() + } + + val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) + val bufferSize = maxOf(minBufferSize, 2048) + + try { + audioRecord = AudioRecord( + MediaRecorder.AudioSource.MIC, + sampleRate, + channelConfig, + audioFormat, + bufferSize + ) + + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + AppLogger.log(tag, "❌ سخت‌افزار میکروفون مقداردهی نشد!") + onError("عدم امکان راه‌اندازی سخت‌افزار میکروفون") + return + } + + audioRecord?.startRecording() + AppLogger.log(tag, "ضبط صوت فعال شد. در حال استریم زنده...") + + recordingThread = Thread { + val chunk = ByteArray(1024) + val shortBuffer = ShortArray(512) + var totalBytesCaptured = 0 + + while (isRecording.get()) { + val bytesRead = audioRecord?.read(chunk, 0, chunk.size) ?: -1 + if (bytesRead > 0) { + totalBytesCaptured += bytesRead + val slice = if (bytesRead == chunk.size) chunk.clone() else chunk.copyOf(bytesRead) + + if (isWsConnected.get() && activeWebSocket != null) { + drainAudioQueue(activeWebSocket!!) + activeWebSocket?.send(slice.toByteString()) + } else { + audioQueue.offer(slice) + } + + var sum = 0.0 + val samplesCount = bytesRead / 2 + for (i in 0 until samplesCount) { + val sample = (slice[i * 2].toInt() and 0xFF) or (slice[i * 2 + 1].toInt() shl 8) + shortBuffer[i] = sample.toShort() + sum += (shortBuffer[i] * shortBuffer[i]).toDouble() + } + val rms = sqrt(sum / samplesCount) / 32768.0 + val level = minOf(maxOf(rms * 4.5, 0.0), 1.0).toFloat() + + mainHandler.post { onAudioLevel(level) } + } + } + AppLogger.log(tag, "مجموع صدای ضبط شده: $totalBytesCaptured بایت (${totalBytesCaptured / 32000.0} ثانیه)") + } + recordingThread?.priority = Thread.MAX_PRIORITY + recordingThread?.start() + + } catch (e: Exception) { + AppLogger.log(tag, "❌ خطای ضبط صوت: ${e.message}") + onError("خطای ضبط میکروفون: ${e.localizedMessage}") + } + } + + private fun parseServerMessage(jsonString: String) { + try { + val json = JSONObject(jsonString) + var gotFin = false + + if (json.optString("type") == "data" && json.has("parts")) { + val parts = json.getJSONArray("parts") + val newFinals = StringBuilder() + val nonFinalsBuilder = StringBuilder() + + for (i in 0 until parts.length()) { + val part = parts.getJSONObject(i) + val transStatus = part.optString("translation_status", "") + if (transStatus == "translation") continue + + val text = part.optString("text", "") + val isFinal = part.optBoolean("is_final", false) + + if (text.contains("")) { + gotFin = true + val clean = text.replace("", "") + if (clean.isNotEmpty()) newFinals.append(clean) + } else if (isFinal) { + if (text.isNotEmpty()) newFinals.append(text) + } else { + if (text.isNotEmpty()) nonFinalsBuilder.append(text) + } + } + + synchronized(committedFinals) { + if (newFinals.isNotEmpty()) { + committedFinals.append(newFinals.toString()) + } + currentNonFinal = nonFinalsBuilder.toString() + } + + val liveCombined = synchronized(committedFinals) { + committedFinals.toString() + currentNonFinal + } + + if (liveCombined.isNotEmpty()) { + AppLogger.log(tag, "⚡ دریافت توکن زنده: '$liveCombined'") + mainHandler.post { onPartialText(liveCombined) } + } + } + + val sessionEnded = json.optBoolean("session_ended", false) + val sessionDone = json.optString("type") == "session_done" + + if (gotFin || sessionEnded || sessionDone) { + AppLogger.log(tag, "سیگنال اتمام استریم دریافت شد.") + finalizeAndComplete() + } + } catch (e: Exception) { + AppLogger.log(tag, "خطای JSON سانی‌اوکس: ${e.message}") + } + } + + fun stopStreaming() { + if (!isRecording.get()) return + isRecording.set(false) + AppLogger.log(tag, "توقف ضبط و ارسال سیگنال Finalize...") + + try { + audioRecord?.stop() + audioRecord?.release() + audioRecord = null + recordingThread?.join(150) + recordingThread = null + } catch (e: Exception) { + AppLogger.log(tag, "خطا در بستن AudioRecord: ${e.message}") + } + + activeWebSocket?.send("{\"type\": \"finalize\"}") + + mainHandler.postDelayed({ + finalizeAndComplete() + }, 650) + } + + private fun finalizeAndComplete() { + val finalRaw = synchronized(committedFinals) { + val res = (committedFinals.toString() + currentNonFinal).trim() + committedFinals.clear() + currentNonFinal = "" + res + } + + val clean = sanitizeText(finalRaw) + AppLogger.log(tag, "✨ متن نهایی پردازش شده: '$clean'") + + try { + activeWebSocket?.close(1000, "Done") + activeWebSocket = null + isWsConnected.set(false) + } catch (e: Exception) {} + + mainHandler.postDelayed({ + prewarmWebSocket() + }, 200) + + mainHandler.post { + onFinalResult(clean) + } + } + + private fun sanitizeText(text: String): String { + val trimmed = text.trim() + if (trimmed.isEmpty()) return "" + + val words = trimmed.split("\\s+".toRegex()).filter { it.isNotEmpty() } + if (words.isEmpty()) return "" + + val faPattern = Regex("[\\u0600-\\u06FF\\u0750-\\u077F\\uFB50-\\uFDFF\\uFE70-\\uFEFF]") + val enPattern = Regex("[a-zA-Z]") + + var faCount = 0 + var enCount = 0 + for (w in words) { + if (faPattern.containsMatchIn(w)) faCount++ + if (enPattern.containsMatchIn(w)) enCount++ + } + + val total = faCount + enCount + if (total == 0) return trimmed + + val faRatio = faCount.toDouble() / total.toDouble() + val stopWords = setOf("sex", "from", "no", "oh", "you", "all", "know", "that", "god", "loves", "men", "for", "day", "night", "mankind", "wanted", "moments") + + val cleaned = mutableListOf() + if (faRatio >= 0.25) { + for (w in words) { + if (enPattern.containsMatchIn(w) && !faPattern.containsMatchIn(w)) { + val cleanW = w.lowercase().replace("[.,!?:;،؛؟\"'()\\[\\]{}«»–—-]".toRegex(), "") + if (stopWords.contains(cleanW)) continue + if (faRatio >= 0.70) continue + } + cleaned.add(w) + } + } else { + cleaned.addAll(words) + } + + return cleaned.joinToString(" ").trim() + } +} diff --git a/android/app/src/main/java/com/soniox/remotemic/SonioxRecognitionService.kt b/android/app/src/main/java/com/soniox/remotemic/SonioxRecognitionService.kt new file mode 100644 index 0000000..617abd7 --- /dev/null +++ b/android/app/src/main/java/com/soniox/remotemic/SonioxRecognitionService.kt @@ -0,0 +1,309 @@ +package com.soniox.remotemic + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.speech.RecognitionService +import android.speech.SpeechRecognizer +import android.util.Log +import androidx.core.content.ContextCompat +import okhttp3.* +import okio.ByteString.Companion.toByteString +import org.json.JSONObject +import java.util.UUID +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.log10 +import kotlin.math.sqrt + +class SonioxRecognitionService : RecognitionService() { + + private val tag = "SonioxRecService" + private val sampleRate = 16000 + private val channelConfig = AudioFormat.CHANNEL_IN_MONO + private val audioFormat = AudioFormat.ENCODING_PCM_16BIT + + private var currentCallback: Callback? = null + private var audioRecord: AudioRecord? = null + private var recordingThread: Thread? = null + private val isRecording = AtomicBoolean(false) + private val mainHandler = Handler(Looper.getMainLooper()) + + private var okHttpClient: OkHttpClient? = null + private var webSocket: WebSocket? = null + private val isConnected = AtomicBoolean(false) + private var currentSessionId: String = "" + private val isSessionActive = AtomicBoolean(false) + private var latestPartialText: String = "" + + override fun onCreate() { + super.onCreate() + Log.i(tag, "SonioxRecognitionService onCreate") + initWebSocketClient() + } + + private fun initWebSocketClient() { + if (okHttpClient != null) return + okHttpClient = OkHttpClient.Builder() + .connectTimeout(4, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.MILLISECONDS) + .writeTimeout(4, TimeUnit.SECONDS) + .pingInterval(10, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build() + connectWebSocket() + } + + @Synchronized + private fun connectWebSocket() { + if (isConnected.get() && webSocket != null) return + + val host = PreferencesManager.getServerHost(this) + val cleanHost = host.removePrefix("http://").removePrefix("https://").removePrefix("ws://").removePrefix("wss://") + val wsUrl = "ws://$cleanHost/ws/stream" + Log.d(tag, "Connecting persistent WebSocket to: $wsUrl") + + val req = Request.Builder().url(wsUrl).build() + webSocket = okHttpClient?.newWebSocket(req, object : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + Log.i(tag, "WebSocket connected successfully to $wsUrl") + isConnected.set(true) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + try { + val json = JSONObject(text) + val type = json.optString("type") + val sid = json.optString("session_id") + + if (sid.isNotEmpty() && sid != currentSessionId && isSessionActive.get()) { + return + } + + when (type) { + "live", "partial" -> { + val liveText = json.optString("text") + if (liveText.isNotEmpty()) { + latestPartialText = liveText + val bundle = Bundle().apply { + putStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION, arrayListOf(liveText)) + } + mainHandler.post { + try { + currentCallback?.partialResults(bundle) + } catch (e: Exception) { + Log.e(tag, "Error sending partial results", e) + } + } + } + } + "final" -> { + var finalText = json.optString("text") + if (finalText.isBlank() && latestPartialText.isNotBlank()) { + finalText = latestPartialText + } + isSessionActive.set(false) + val bundle = Bundle().apply { + putStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION, arrayListOf(finalText)) + putFloatArray(SpeechRecognizer.CONFIDENCE_SCORES, floatArrayOf(1.0f)) + } + mainHandler.post { + try { + currentCallback?.results(bundle) + } catch (e: Exception) { + Log.e(tag, "Error sending final results", e) + } + } + } + "error" -> { + val msg = json.optString("message", "Server error") + Log.e(tag, "WebSocket server error: $msg") + mainHandler.post { + try { + currentCallback?.error(SpeechRecognizer.ERROR_SERVER) + } catch (e: Exception) {} + } + } + } + } catch (e: Exception) { + Log.e(tag, "Error parsing WS message", e) + } + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Log.w(tag, "WebSocket failure: ${t.message}. Will reconnect on demand.") + isConnected.set(false) + this@SonioxRecognitionService.webSocket = null + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + Log.i(tag, "WebSocket closed ($code: $reason)") + isConnected.set(false) + this@SonioxRecognitionService.webSocket = null + } + }) + } + + override fun onStartListening(recognizerIntent: Intent?, listener: Callback?) { + Log.i(tag, "onStartListening invoked by system keyboard / caller") + currentCallback = listener + latestPartialText = "" + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + Log.e(tag, "Missing RECORD_AUDIO permission") + listener?.error(SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS) + return + } + + connectWebSocket() + + currentSessionId = "sess_${System.currentTimeMillis()}_${UUID.randomUUID().toString().take(6)}" + isSessionActive.set(true) + + val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) + val bufferSize = maxOf(minBufferSize, 2048) + + try { + // Attempt VOICE_RECOGNITION source (optimized by OS for speech) + var record: AudioRecord? = AudioRecord( + MediaRecorder.AudioSource.VOICE_RECOGNITION, + sampleRate, + channelConfig, + audioFormat, + bufferSize + ) + + if (record?.state != AudioRecord.STATE_INITIALIZED) { + record?.release() + // Fallback to standard MIC + record = AudioRecord( + MediaRecorder.AudioSource.MIC, + sampleRate, + channelConfig, + audioFormat, + bufferSize + ) + } + + if (record.state != AudioRecord.STATE_INITIALIZED) { + record.release() + Log.e(tag, "AudioRecord state is NOT initialized") + listener?.error(SpeechRecognizer.ERROR_AUDIO) + return + } + + audioRecord = record + + // Notify IME ready for speech + try { + listener?.readyForSpeech(Bundle()) + } catch (e: Exception) {} + + // Send start session frame to server + val startFrame = JSONObject().apply { + put("type", "start") + put("session_id", currentSessionId) + }.toString() + webSocket?.send(startFrame) + + audioRecord?.startRecording() + isRecording.set(true) + + try { + listener?.beginningOfSpeech() + } catch (e: Exception) {} + + recordingThread = Thread { + val chunk = ByteArray(2048) + val shortBuffer = ShortArray(1024) + + while (isRecording.get()) { + val bytesRead = audioRecord?.read(chunk, 0, chunk.size) ?: -1 + if (bytesRead > 0) { + val slice = if (bytesRead == chunk.size) chunk.clone() else chunk.copyOf(bytesRead) + webSocket?.send(slice.toByteString()) + + // RMS calculation for keyboard waveform animation + var sum = 0.0 + val samplesCount = bytesRead / 2 + for (i in 0 until samplesCount) { + val sample = (slice[i * 2].toInt() and 0xFF) or (slice[i * 2 + 1].toInt() shl 8) + shortBuffer[i] = sample.toShort() + sum += (shortBuffer[i] * shortBuffer[i]).toDouble() + } + val rms = sqrt(sum / samplesCount) / 32768.0 + val rmsDb = if (rms > 0.0001) (20 * log10(rms * 10)).toFloat().coerceIn(0f, 10f) else 0f + mainHandler.post { + try { + currentCallback?.rmsChanged(rmsDb) + } catch (e: Exception) {} + } + } + } + }.apply { + priority = Thread.MAX_PRIORITY + start() + } + + } catch (e: Exception) { + Log.e(tag, "Exception during onStartListening", e) + listener?.error(SpeechRecognizer.ERROR_AUDIO) + stopAudioInternal() + } + } + + override fun onStopListening(listener: Callback?) { + Log.i(tag, "onStopListening received") + try { + listener?.endOfSpeech() + } catch (e: Exception) {} + + stopAudioInternal() + + // Send STOP frame to finalize transcript + val stopFrame = JSONObject().apply { + put("type", "stop") + put("session_id", currentSessionId) + }.toString() + webSocket?.send(stopFrame) + } + + override fun onCancel(listener: Callback?) { + Log.i(tag, "onCancel received") + isSessionActive.set(false) + stopAudioInternal() + + val stopFrame = JSONObject().apply { + put("type", "stop") + put("session_id", currentSessionId) + }.toString() + webSocket?.send(stopFrame) + } + + private fun stopAudioInternal() { + isRecording.set(false) + try { + audioRecord?.stop() + audioRecord?.release() + audioRecord = null + recordingThread?.join(150) + recordingThread = null + } catch (e: Exception) { + Log.e(tag, "Error stopping AudioRecord", e) + } + } + + override fun onDestroy() { + super.onDestroy() + stopAudioInternal() + try { + webSocket?.close(1000, "Service destroyed") + } catch (e: Exception) {} + } +} diff --git a/android/app/src/main/java/com/soniox/remotemic/StreamDictationClient.kt b/android/app/src/main/java/com/soniox/remotemic/StreamDictationClient.kt new file mode 100644 index 0000000..27d5791 --- /dev/null +++ b/android/app/src/main/java/com/soniox/remotemic/StreamDictationClient.kt @@ -0,0 +1,233 @@ +package com.soniox.remotemic + +import android.annotation.SuppressLint +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.os.Handler +import android.os.Looper +import android.util.Log +import okhttp3.* +import okio.ByteString.Companion.toByteString +import org.json.JSONObject +import java.util.UUID +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.sqrt + +class StreamDictationClient( + private val host: String, + private val onConnectionStateChanged: (Boolean) -> Unit, + private val onPartialText: (String) -> Unit, + private val onAudioLevel: (Float) -> Unit, + private val onCompleted: (String, Boolean) -> Unit, + private val onError: (String) -> Unit +) { + private val tag = "StreamDictationClient" + private val sampleRate = 16000 + private val channelConfig = AudioFormat.CHANNEL_IN_MONO + private val audioFormat = AudioFormat.ENCODING_PCM_16BIT + + private var audioRecord: AudioRecord? = null + private var recordingThread: Thread? = null + private val isRecording = AtomicBoolean(false) + private val mainHandler = Handler(Looper.getMainLooper()) + + private val okHttpClient = OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.MILLISECONDS) // Keep-alive persistent WebSocket + .writeTimeout(5, TimeUnit.SECONDS) + .pingInterval(10, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build() + + private var webSocket: WebSocket? = null + private val isConnected = AtomicBoolean(false) + private var currentSessionId: String = "" + private var isSessionActive = AtomicBoolean(false) + + init { + connectWebSocket() + } + + @Synchronized + fun connectWebSocket() { + if (isConnected.get() && webSocket != null) return + + val cleanHost = host.removePrefix("http://").removePrefix("https://").removePrefix("ws://").removePrefix("wss://") + val wsUrl = "ws://$cleanHost/ws/stream" + AppLogger.log(tag, "اتصال به سوکت دائمی: $wsUrl") + + val req = Request.Builder().url(wsUrl).build() + webSocket = okHttpClient.newWebSocket(req, object : WebSocketListener() { + override fun onOpen(ws: WebSocket, response: Response) { + AppLogger.log(tag, "🟢 سوکت پرسرعت دائمی متصل شد") + isConnected.set(true) + mainHandler.post { onConnectionStateChanged(true) } + } + + override fun onMessage(ws: WebSocket, text: String) { + try { + val json = JSONObject(text) + val type = json.optString("type") + val sid = json.optString("session_id") + + if (sid.isNotEmpty() && sid != currentSessionId && isSessionActive.get()) { + return + } + + when (type) { + "live", "partial" -> { + val liveText = json.optString("text") + mainHandler.post { onPartialText(liveText) } + } + "final" -> { + val finalText = json.optString("text") + val macDelivered = json.optBoolean("mac_delivered", true) + isSessionActive.set(false) + AppLogger.log(tag, "⚡ متن نهایی دریافت شد: '$finalText'") + mainHandler.post { onCompleted(finalText, macDelivered) } + } + "error" -> { + val msg = json.optString("message", "خطای سرور") + AppLogger.log(tag, "❌ خطای سرور: $msg") + mainHandler.post { onError(msg) } + } + } + } catch (e: Exception) { + Log.e(tag, "Message parse error", e) + } + } + + override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) { + AppLogger.log(tag, "🔴 قطع اتصال سوکت: ${t.message}. تلاش مجدد در 2s...") + isConnected.set(false) + webSocket = null + mainHandler.post { onConnectionStateChanged(false) } + mainHandler.postDelayed({ connectWebSocket() }, 2000) + } + + override fun onClosed(ws: WebSocket, code: Int, reason: String) { + AppLogger.log(tag, "سوکت بسته شد ($code). تلاش مجدد...") + isConnected.set(false) + webSocket = null + mainHandler.post { onConnectionStateChanged(false) } + mainHandler.postDelayed({ connectWebSocket() }, 2000) + } + }) + } + + @SuppressLint("MissingPermission") + fun startRecording(): String { + if (isRecording.get()) return currentSessionId + + currentSessionId = "sess_${System.currentTimeMillis()}_${UUID.randomUUID().toString().take(6)}" + isRecording.set(true) + isSessionActive.set(true) + + if (!isConnected.get() || webSocket == null) { + connectWebSocket() + } + + // 1. Send START frame + val startFrame = JSONObject().apply { + put("type", "start") + put("session_id", currentSessionId) + }.toString() + webSocket?.send(startFrame) + AppLogger.log(tag, "🎙️ شروع ضبط و استریم (Session: $currentSessionId)...") + + // 2. Hardware recording setup (16kHz 16-bit Mono, 2048 bytes = 64ms) + val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) + val bufferSize = maxOf(minBufferSize, 2048) + + try { + audioRecord = AudioRecord( + MediaRecorder.AudioSource.MIC, + sampleRate, + channelConfig, + audioFormat, + bufferSize + ) + + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + AppLogger.log(tag, "❌ سخت‌افزار میکروفون راه‌اندازی نشد") + onError("خطا در راه‌اندازی سخت‌افزار میکروفون") + isRecording.set(false) + isSessionActive.set(false) + return currentSessionId + } + + audioRecord?.startRecording() + + recordingThread = Thread { + val chunk = ByteArray(2048) + val shortBuffer = ShortArray(1024) + + while (isRecording.get()) { + val bytesRead = audioRecord?.read(chunk, 0, chunk.size) ?: -1 + if (bytesRead > 0) { + val slice = if (bytesRead == chunk.size) chunk.clone() else chunk.copyOf(bytesRead) + + // Direct binary streaming over persistent WebSocket + webSocket?.send(slice.toByteString()) + + // RMS Audio Level calculation + var sum = 0.0 + val samplesCount = bytesRead / 2 + for (i in 0 until samplesCount) { + val sample = (slice[i * 2].toInt() and 0xFF) or (slice[i * 2 + 1].toInt() shl 8) + shortBuffer[i] = sample.toShort() + sum += (shortBuffer[i] * shortBuffer[i]).toDouble() + } + val rms = sqrt(sum / samplesCount) / 32768.0 + val level = minOf(maxOf(rms * 4.5, 0.0), 1.0).toFloat() + mainHandler.post { onAudioLevel(level) } + } + } + }.apply { + priority = Thread.MAX_PRIORITY + start() + } + + } catch (e: Exception) { + AppLogger.log(tag, "خطای ضبط: ${e.message}") + onError("خطای میکروفون: ${e.localizedMessage}") + isRecording.set(false) + isSessionActive.set(false) + } + + return currentSessionId + } + + fun stopRecording() { + if (!isRecording.get()) return + isRecording.set(false) + + try { + audioRecord?.stop() + audioRecord?.release() + audioRecord = null + recordingThread?.join(150) + recordingThread = null + } catch (e: Exception) { + Log.e(tag, "Error releasing audio hardware", e) + } + + // Send STOP frame over persistent WebSocket + val stopFrame = JSONObject().apply { + put("type", "stop") + put("session_id", currentSessionId) + }.toString() + webSocket?.send(stopFrame) + AppLogger.log(tag, "⏹️ پایان صحبت ($currentSessionId). انتظار برای دریافت متن...") + } + + fun release() { + isRecording.set(false) + try { + audioRecord?.release() + webSocket?.close(1000, "Client Shutdown") + } catch (e: Exception) {} + } +} diff --git a/android/app/src/main/java/com/soniox/remotemic/VoiceRecognitionActivity.kt b/android/app/src/main/java/com/soniox/remotemic/VoiceRecognitionActivity.kt new file mode 100644 index 0000000..cdaa198 --- /dev/null +++ b/android/app/src/main/java/com/soniox/remotemic/VoiceRecognitionActivity.kt @@ -0,0 +1,102 @@ +package com.soniox.remotemic + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.speech.RecognizerIntent +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import com.soniox.remotemic.databinding.ActivityVoiceRecognitionBinding + +class VoiceRecognitionActivity : AppCompatActivity() { + + private lateinit var binding: ActivityVoiceRecognitionBinding + private var streamDictationClient: StreamDictationClient? = null + private var lastTranscript: String = "" + + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + startListening() + } else { + Toast.makeText(this, "دسترسی میکروفون برای تایپ صوتی الزامی است", Toast.LENGTH_SHORT).show() + setResult(Activity.RESULT_CANCELED) + finish() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityVoiceRecognitionBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.btnDialogCancel.setOnClickListener { + streamDictationClient?.stopRecording() + setResult(Activity.RESULT_CANCELED) + finish() + } + + binding.btnDialogDone.setOnClickListener { + finishWithResult(lastTranscript) + } + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + startListening() + } else { + requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) + } + } + + private fun startListening() { + val host = PreferencesManager.getServerHost(this) + streamDictationClient = StreamDictationClient( + host = host, + onConnectionStateChanged = { connected -> + if (!connected) { + binding.tvDialogStatus.text = "در حال اتصال به سرور..." + } else { + binding.tvDialogStatus.text = "در حال گوش دادن..." + } + }, + onPartialText = { liveText -> + lastTranscript = liveText + binding.tvDialogTranscript.text = liveText + }, + onAudioLevel = { level -> + val scale = 1.0f + (level * 0.4f) + binding.dialogGlow.scaleX = scale + binding.dialogGlow.scaleY = scale + }, + onCompleted = { finalText, _ -> + lastTranscript = finalText + binding.tvDialogTranscript.text = finalText + finishWithResult(finalText) + }, + onError = { errMsg -> + binding.tvDialogStatus.text = "خطا: $errMsg" + } + ) + + streamDictationClient?.startRecording() + } + + private fun finishWithResult(text: String) { + streamDictationClient?.stopRecording() + val resultIntent = Intent().apply { + putStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS, arrayListOf(text)) + } + setResult(Activity.RESULT_OK, resultIntent) + finish() + } + + override fun onDestroy() { + super.onDestroy() + streamDictationClient?.release() + } +} diff --git a/android/app/src/main/res/drawable/bg_card.xml b/android/app/src/main/res/drawable/bg_card.xml new file mode 100644 index 0000000..4fa9446 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_card.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/bg_dialog.xml b/android/app/src/main/res/drawable/bg_dialog.xml new file mode 100644 index 0000000..bef9566 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_dialog.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/bg_mic_button.xml b/android/app/src/main/res/drawable/bg_mic_button.xml new file mode 100644 index 0000000..74e22f8 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_mic_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable/bg_mic_button_active.xml b/android/app/src/main/res/drawable/bg_mic_button_active.xml new file mode 100644 index 0000000..3c00ed3 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_mic_button_active.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_mic.xml b/android/app/src/main/res/drawable/ic_mic.xml new file mode 100644 index 0000000..44f6a80 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_mic.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..7cf5955 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_voice_recognition.xml b/android/app/src/main/res/layout/activity_voice_recognition.xml new file mode 100644 index 0000000..b0d3cee --- /dev/null +++ b/android/app/src/main/res/layout/activity_voice_recognition.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/dialog_logs_sheet.xml b/android/app/src/main/res/layout/dialog_logs_sheet.xml new file mode 100644 index 0000000..951959b --- /dev/null +++ b/android/app/src/main/res/layout/dialog_logs_sheet.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..d34b89d --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,15 @@ + + + #0F1115 + #181B22 + #282E3A + #3B82F6 + #EF4444 + #10B981 + #F59E0B + #F8FAFC + #94A3B8 + #64748B + #4DF43F5E + #263B82F6 + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..47231ea --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,15 @@ + + Soniox Voice + Soniox Voice Typing + نگه‌دارید و صحبت کنید + رها کنید تا تایپ شود + آماده برای ضبط صدا + 🎙️ در حال استریم زنده صدا... + ⏳ در حال دریافت متن... + ✨ متن آماده شد + تست اتصال + متن صحبت شما به‌صورت زنده در این قسمت نمایش داده می‌شود... + ⚙️ انتخاب Soniox به عنوان ورودی صوتی سیستم + آدرس سرور وب‌سوکت: + ذخیره آدرس + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..ce64efa --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/android/app/src/main/res/xml/recognition_service.xml b/android/app/src/main/res/xml/recognition_service.xml new file mode 100644 index 0000000..2021a3c --- /dev/null +++ b/android/app/src/main/res/xml/recognition_service.xml @@ -0,0 +1,3 @@ + + diff --git a/android/app/src/test/java/com/soniox/remotemic/SonioxWsTest.kt b/android/app/src/test/java/com/soniox/remotemic/SonioxWsTest.kt new file mode 100644 index 0000000..d708b21 --- /dev/null +++ b/android/app/src/test/java/com/soniox/remotemic/SonioxWsTest.kt @@ -0,0 +1,78 @@ +package com.soniox.remotemic + +import okhttp3.* +import okio.ByteString.Companion.toByteString +import org.junit.Test +import java.io.File +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class SonioxWsTest { + + @Test + fun testWebSocketStreaming() { + println("=== TESTING OKHTTP WEBSOCKET TO SONIOX ===") + val client = OkHttpClient.Builder() + .readTimeout(30, TimeUnit.SECONDS) + .connectTimeout(15, TimeUnit.SECONDS) + .build() + + val url = "wss://translate.compare.soniox.com/compare/api/compare-websocket?language_hints=fa&language_hints=en&enable_speaker_diarization=false&enable_language_identification=true&enable_endpoint_detection=false&providers=soniox" + val req = Request.Builder() + .url(url) + .header("User-Agent", "Mozilla/5.0 (Linux; Android 14) SonioxRemoteMic/1.0") + .header("Origin", "https://translate.compare.soniox.com") + .build() + + val latch = CountDownLatch(1) + var connected = false + + val webSocket = client.newWebSocket(req, object : WebSocketListener() { + override fun onOpen(ws: WebSocket, response: Response) { + println("OkHttp WebSocket onOpen! Response code: ${response.code}") + connected = true + } + + override fun onMessage(ws: WebSocket, text: String) { + println("OkHttp WebSocket onMessage: $text") + if (text.contains("") || text.contains("session_done")) { + latch.countDown() + } + } + + override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) { + println("OkHttp WebSocket onFailure: ${t.message}") + t.printStackTrace() + latch.countDown() + } + + override fun onClosed(ws: WebSocket, code: Int, reason: String) { + println("OkHttp WebSocket onClosed: $code, $reason") + latch.countDown() + } + }) + + Thread.sleep(1000) + + println("Sending audio chunks... Connected: $connected") + val pcmFile = File("/home/alialavi/projects/auto-dub-bot/workspace/tts_cache/d6fb879ba24b13b8cdb93e4465daea56.wav") + if (pcmFile.exists()) { + val p = ProcessBuilder("ffmpeg", "-loglevel", "error", "-i", pcmFile.absolutePath, "-f", "s16le", "-ac", "1", "-ar", "16000", "-").start() + val pcmBytes = p.inputStream.readBytes() + println("PCM Bytes: ${pcmBytes.size}") + + val chunkSize = 1024 + for (i in 0 until pcmBytes.size step chunkSize) { + val end = minOf(i + chunkSize, pcmBytes.size) + val chunk = pcmBytes.copyOfRange(i, end) + webSocket.send(chunk.toByteString()) + Thread.sleep(30) + } + println("Sent all audio chunks. Sending finalize...") + webSocket.send("{\"type\": \"finalize\"}") + } + + latch.await(10, TimeUnit.SECONDS) + println("Test finished.") + } +} diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..2f75d69 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'com.android.application' version '8.4.2' apply false + id 'org.jetbrains.kotlin.android' version '1.9.24' apply false +} + +tasks.register('clean', Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..97c802f --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -Djava.net.preferIPv6Addresses=true +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..efdcc4a --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..d57ca5c --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "SonioxRemoteMic" +include ':app' diff --git a/mac/Info.plist b/mac/Info.plist new file mode 100644 index 0000000..496a3f9 --- /dev/null +++ b/mac/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Soniox Voice + CFBundleExecutable + SonioxVoice + CFBundleIconFile + AppIcon + CFBundleIdentifier + com.soniox.voice + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + SonioxVoice + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 13.0 + LSUIElement + + NSHighResolutionCapable + + NSMicrophoneUsageDescription + Soniox Voice به دسترسی میکروفون جهت ضبط صدا و تبدیل آن به متن نیاز دارد. + NSAccessibilityUsageDescription + Soniox Voice به دسترسی Accessibility جهت درج خودکار متن در برنامه فعال نیاز دارد. + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/mac/make_icon.py b/mac/make_icon.py new file mode 100644 index 0000000..7b012d7 --- /dev/null +++ b/mac/make_icon.py @@ -0,0 +1,121 @@ +import sys +import os +import math +from PIL import Image, ImageDraw, ImageFilter + +def create_app_icon(output_dir): + os.makedirs(output_dir, exist_ok=True) + iconset_dir = os.path.join(output_dir, "AppIcon.iconset") + os.makedirs(iconset_dir, exist_ok=True) + + size = 1024 + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + # Background Squircle / Rounded rect with gradient + margin = 80 + rect = [margin, margin, size - margin, size - margin] + radius = 200 + + # Create base squircle mask + mask = Image.new("L", (size, size), 0) + mask_draw = ImageDraw.Draw(mask) + mask_draw.rounded_rectangle(rect, radius=radius, fill=255) + + # Render gradient + grad = Image.new("RGBA", (size, size)) + grad_draw = ImageDraw.Draw(grad) + + # Rich Purple / Blue / Cyber Teal gradient + for y in range(size): + ratio = y / size + r = int(79 + (124 - 79) * ratio) + g = int(70 + (58 - 70) * ratio) + b = int(229 + (237 - 229) * ratio) + grad_draw.line([(0, y), (size, y)], fill=(r, g, b, 255)) + + img.paste(grad, (0, 0), mask) + + # Draw Inner Glowing Waveform & Microphone + draw = ImageDraw.Draw(img) + + # Mic Capsule + center_x = size // 2 + center_y = size // 2 - 40 + mic_w = 120 + mic_h = 240 + + # Glow behind mic + glow = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + glow_draw = ImageDraw.Draw(glow) + glow_draw.rounded_rectangle([center_x - mic_w//2 - 20, center_y - mic_h//2 - 20, center_x + mic_w//2 + 20, center_y + mic_h//2 + 20], radius=80, fill=(255, 255, 255, 60)) + glow = glow.filter(ImageFilter.GaussianBlur(30)) + img.alpha_composite(glow) + + draw = ImageDraw.Draw(img) + + # Mic Body (Capsule) + draw.rounded_rectangle( + [center_x - mic_w//2, center_y - mic_h//2, center_x + mic_w//2, center_y + mic_h//2], + radius=60, + fill=(255, 255, 255, 250) + ) + + # Mic Arc / Cradle + cradle_w = 220 + cradle_h = 200 + arc_top = center_y - 20 + draw.arc( + [center_x - cradle_w//2, arc_top, center_x + cradle_w//2, arc_top + cradle_h], + start=0, + end=180, + fill=(255, 255, 255, 240), + width=24 + ) + + # Mic Stand / Stem + stem_top = arc_top + cradle_h + draw.line([(center_x, stem_top), (center_x, stem_top + 70)], fill=(255, 255, 255, 240), width=24) + # Mic Base + draw.line([(center_x - 80, stem_top + 70), (center_x + 80, stem_top + 70)], fill=(255, 255, 255, 240), width=24) + + # Sonic Sound Waves on sides + for side in [-1, 1]: + for i, r in enumerate([190, 260]): + wave_cx = center_x + side * 40 + wave_w = r * 2 + wave_h = r * 2 + start_ang = 300 if side == 1 else 120 + end_ang = 60 if side == 1 else 240 + draw.arc( + [wave_cx - wave_w//2, center_y - wave_h//2, wave_cx + wave_w//2, center_y + wave_h//2], + start=start_ang, + end=end_ang, + fill=(255, 255, 255, 180 - i * 60), + width=18 + ) + + # Save standard sizes + sizes = [ + (16, "icon_16x16.png"), + (32, "icon_16x16@2x.png"), + (32, "icon_32x32.png"), + (64, "icon_32x32@2x.png"), + (128, "icon_128x128.png"), + (256, "icon_128x128@2x.png"), + (256, "icon_256x256.png"), + (512, "icon_256x256@2x.png"), + (512, "icon_512x512.png"), + (1024, "icon_512x512@2x.png"), + ] + + for s, name in sizes: + resized = img.resize((s, s), Image.Resampling.LANCZOS) + resized.save(os.path.join(iconset_dir, name)) + + master_path = os.path.join(output_dir, "AppIcon.png") + img.save(master_path) + print("Iconset generated in", iconset_dir) + +if __name__ == "__main__": + create_app_icon("/tmp/soniox_build/resources") diff --git a/mac/package.sh b/mac/package.sh new file mode 100644 index 0000000..88c409c --- /dev/null +++ b/mac/package.sh @@ -0,0 +1,55 @@ +#!/bin/zsh +set -e + +PROJECT_DIR="/Users/alig/Develop/SonioxVoice" +BUILD_DIR="$PROJECT_DIR/build" +APP_NAME="Soniox Voice" +APP_BUNDLE="$BUILD_DIR/$APP_NAME.app" +DMG_NAME="SonioxVoice-v1.0.0.dmg" + +echo "🔨 Building $APP_NAME..." +mkdir -p "$BUILD_DIR" +rm -rf "$APP_BUNDLE" + +# 1. Compile Swift sources +swiftc -O -target arm64-apple-macosx13.0 \ + -framework Cocoa -framework AVFoundation -framework Carbon \ + "$PROJECT_DIR"/src/*.swift \ + -o "$BUILD_DIR/SonioxVoice" + +# 2. Assemble .app bundle +mkdir -p "$APP_BUNDLE/Contents/MacOS" +mkdir -p "$APP_BUNDLE/Contents/Resources" + +cp "$BUILD_DIR/SonioxVoice" "$APP_BUNDLE/Contents/MacOS/SonioxVoice" +cp "$PROJECT_DIR/Info.plist" "$APP_BUNDLE/Contents/Info.plist" +cp "$PROJECT_DIR/resources/AppIcon.icns" "$APP_BUNDLE/Contents/Resources/AppIcon.icns" + +# 3. Ad-hoc Codesign +codesign --force --deep --sign - "$APP_BUNDLE" + +echo "✅ App bundle assembled at $APP_BUNDLE" + +# 4. Create DMG Installer +echo "📦 Creating DMG Installer..." +DMG_STAGING="$BUILD_DIR/dmg_staging" +rm -rf "$DMG_STAGING" "$BUILD_DIR/$DMG_NAME" +mkdir -p "$DMG_STAGING" + +cp -R "$APP_BUNDLE" "$DMG_STAGING/" +ln -s /Applications "$DMG_STAGING/Applications" + +hdiutil create -volname "Soniox Voice" -srcfolder "$DMG_STAGING" -ov -format UDZO "$BUILD_DIR/$DMG_NAME" +echo "✅ DMG created at $BUILD_DIR/$DMG_NAME" + +# 5. Create ZIP Archive +cd "$BUILD_DIR" +zip -r -y "SonioxVoice-v1.0.0.zip" "$APP_NAME.app" + +# 6. Install to /Applications on Mac +echo "🚀 Installing to /Applications/$APP_NAME.app..." +rm -rf "/Applications/$APP_NAME.app" +cp -R "$APP_BUNDLE" "/Applications/$APP_NAME.app" + +echo "🎉 All Done Successfully!" +ls -lh "$BUILD_DIR" diff --git a/mac/resources/AppIcon.iconset/icon_128x128.png b/mac/resources/AppIcon.iconset/icon_128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..27dbadb83d2c4595751be2ff2d3f13659b05ad69 GIT binary patch literal 10323 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 literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.iconset/icon_128x128@2x.png b/mac/resources/AppIcon.iconset/icon_128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6d1fa4de82abcf5c9b2533d2544df8111364d286 GIT binary patch literal 23901 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 A<^TWy literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.iconset/icon_16x16.png b/mac/resources/AppIcon.iconset/icon_16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..aca1b51ce7e736fd8654d9a3d0e93baae2bd2f20 GIT binary patch literal 686 zcmV;f0#W^mP)QSl$pPwsb^E>nY zbMwU2?7ebfaI$=1vH9u7?a3p+Nx=Ed<=1Dy;!as?ZsgnnP=NL!96*L@g&5l=rvAc> z+ov82K&Ru%TG-s^7jKrH+eAN@+p^8bO=fPgE!#{#n0szhzxZo82C)?pkfCi-A)iD8p6#<#mOV%}BRYA&eEoJOF zT>K)~r7wfUuEU91i!yLfkNAgJ!9ZpJ2ml}k2F(}_!fd$4(e9IYY*<0Qz4}oUA@2S9 zd-3{NT%VjN`_%b&4M?iKhM4&K;uf7xjKtY#W zz+*>oR-stMHTM=LN3R5H51@$s_ps@=2n;FguSE6rnR_itI*?s;4`D7v>e zrA$y?`Rq`Eh{FTTrhn?h%&Z;@`k4!bKH73L`!?=GPX<~RhlsnrdxPBy17>d`U?9oG zTerJ6mkIzyNmC)|mf?&Q*9L}m_g@01VDE<%r(bUrC)bm80J)M?tz2w}u6BpYK>O6p zJlS=?$sO#-HC(eR7k1ANbo{*`erA^G^!LI<(1q$m@v@KmKVJXs(}zbX{y%a42Ub=^ UQrv{L_y7O^07*qoM6N<$g3Yu^>Hq)$ literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.iconset/icon_16x16@2x.png b/mac/resources/AppIcon.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a54235146e7193de256b72a41ac81a6a81cd6a01 GIT binary patch literal 1916 zcmV-?2ZQ*DP)hdxj*Y&)``6)Hk248v7xajA&3$n;5AiJMHQl|x^4Q_m!@w?r6_%M_oYpf z=Ao_n6e+116-DhsT7{G(B#r~+&nh%ciIs`~frc6z12*2DyZ6qW(}&$<*UOrps%JIQ z-I=-PJNL}_zL_B`VWSWp9|i+Jp>Uj*ns+lxr8WuxWV5jOJlWfPO(>TW^}hh#%&I=+ zh7W1!5CTX5;9DdA=~u3^L#*^$F-qjdmv3dvDrV^OKy=`b`}@weVzdNM9T*-y>o0dx zKLgT|8nlsq0><4$hcrp91gkkECXECdz?1N{InRXE-#aS|e~<%|I^S%L9+ z!xkK{#yUs$^UnSOx#~KIAp#}?YeK{(1d{RT0^NvFAZd_1vjg<(I1~e($C||xSLzR7d z%%I|!qe@6$-R{fJU%A)3@XEbr>qtVPN=RBo{Pyc+U|EvPx=q2QWA8=7L+hPM=Ga!# zU-#W60DwUtFi2oM>g>i$VcsTfRuc)7Rajr^%|SZ>eIn zUxKFQ`xKNHB$ZBcurMbcNj<=th_*;%^B4hGxFTL>Cj~)Kl8FQbt^V6A!D2pJ3)Y%U z9EozZ*m{vNRjINFU#k5~jUsbgvL=GkOtCeh7U@d60IEgU;CE?GI=eIK%KQxZnLf9u zSim6IkPwFir)dcshY*KA95NILih!OzF3wF;qAQ~=7v?0*($I^gSpWc5m^s4LX}fa8 zYCT<^*HkVV_x>TeGB>4Z$F5X3@Stb@ak$8}g^-Vr7P)`GH6!0lg>x4*tzL^c6NxHb zc6il&YHDuUeuilBhUuqi3G) zEuK7N+>=K_9^S3L*!OB>$b=GZ+UZS99*G}O912;YhX1x1sOmud2ez;kojCTk*|GCG z>Mw`iwAuZynYD=&$=K_GnmiJz+%tDoT%TmmiwpkLnOG&4k?jAyS=*7~*~h-8#wOm4 zx0ygCNgIJwcgCf404+*riC9&nFspix?u~XnuwEt|EinvHDor^1ajZ^^m7R|c1kULb zu}Y;0FhmYZJh*{QPV5V_3p2cuRUmAM7OQNV3;+!dU$1XrA{|J1-pPD&kg}{}luLxo zSvT7Jgdh4HETn*STfOLS?*{JCy`keLWA({F&H@b9fk2pAG1>;8NQ3~X^ZmLB5jd*S z-V*sf@aeHoe}2-aC=`MKjz^qh!uhB#9Vw5S`+~bpe&sSfWFj5(fAM+066Bc2-hM_iY?;Sem?w*t&>e|2E_sa#G}K2LVhvApoc#e`bsywl-yZ!1Q#5RuzEh2Fx5pp#Ay z%F)${Qe+?NdZN4VlRRp|e?5aj1d?aJGL>}G{x1OaeOGn(H*OzyYJxB>g@F975Kce) z>xsc@bu6H@;F|`X+c7iXT0d(=`b8q$&h;&gRIH%Rhf*1T@o>+-S}~T2m~y#4{NFF9 zqocyNaXD>z0m2w=XvWkXUeW9H1^|2rn@9WW`F{Z!^M5hZ?tFFt0000G literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.iconset/icon_256x256.png b/mac/resources/AppIcon.iconset/icon_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..6d1fa4de82abcf5c9b2533d2544df8111364d286 GIT binary patch literal 23901 zcmbqZ^;g{P)6Fin_+mwhEbi{^l;ZAA@ec(`@dArOf#P1=i@UoPFYXiy#f!VWeE)_w zza-}*Cz)sFX6CtfCPGc+9VQwH8UO&8@8xAQ008uQ2?8LI}Y?sOlB)$RWZDPg!>MluJX z^QF_^DulHYX{y zfFqd3b9O z(|@gsbm=yI@;B>pJVo!eu)-@j4f^hJpM1vI_XS1I7oHo#9bKQ>@b)>pJUl#zf*9ZJ z0;i*!^b(Df37Jv$dqGr>u0FIno_PH0>4Ogi(SybrC1+|oPIr}e?d!XyIh;Uf7sfkU$GdztKhi4=)VAoAZ=9~@cH}lw?iV`rz zYm3a@|GYzTq#O8aw!hXkFtc$}jp&*Xcp~qUH_X}qRTv|^LeyNmtucHPo7(}NCa*A4 z5@bzSJ{Oce*L31#+uJ7r25b4XvPx=R@oIl>tzVEg@Ir z+Yjpr-~#lmGu4)nQvE6GFDis7vTD&Fa(UNb5}Vb8splpy#x{;^sq!p5A3hcBHc8o; zqT{W(tC&Q*L_g7ALLBuv{ACl4zhh!?JS_R-og=K;-#fOlP_Xq#^uT@mZE6^9 zNZz4#J>WNygbDsVX>TVZewFxVaCz12HZT{Sr^7JN#-HSGsYB72d}v}pI!E4ZUnI9Y z-=EQ5Os=|H{YHlJ^RCf`)mC=(6Dw3jk1sNGt4g|I2 zC0m9h)8ijwKF_D80i$RikJT0`yQ|d!q)qXc@hW8=iwJRjxwX~ywi)U`!Kfd^IKSwL zmieugr1 zHe-k=A=Blrr+yZsgbB~ z4ccJ1`9I}1JkItF2T_&+gQ^gbpMo#`*8EykI_Tw|H?_=&w)vZxNiHe#>p2_;DGsV& z^+mdW=uYb84Rm#u=a%g*C#xEI`I-*a^h5P;;O7@;{pynxoPp*WT<_j~Yx6a~=tD8$ zUOas{79qRXnWJ2cOK$jx{HyS-`hgH&FFH7m2Lk0pf~q5bO=ievmU0V zvS^l~3EpjBl%^3|{=8gdnURr;VrlE#t;*p<{=xO)3sO?c2;W;Q4W{C}G+!K?HpwwG z4LxL3Ur~wnPdnX)dkEfvNa%V-bx;xi_4^=9=4YpQq4Cpvs}^c1?;!m&IR$UOp-15t z!9uSb;{eccgl;7wPIw5uZU&e3q$3WjY|8vJR_WIpqL>H+%vspI_-0K1cSj-=A&B!mo=U-t( zl?R5T`&yT%f+ft5FlN(`gS$Vpd{{5~wH(V1ji?zTr@DB3hAUP7Ef=Wp!e;gEv|kp% z3JD_|soIR3Mq-R^qG`OaWzeq!$H=k4N3RptPbq;gh$sDmmfly?KG62_9zv@6Pa{}Ry z`x}+iqihX7$As?@B5szkKn63`Fa9pjA;0~rwSZYt35IxFdF@BVjBx~CA)8tWa=lPf z-J{3Nch*LI&a5r33pi*4X=?-MZ;*x(SX8;Jroj%Pv%h9v*Ve^`NCytT;{gOLjNDwo zKYjpcNtU<8p}Md6S|&Pt(G(_OdkjaV=NUIJA9t$!NSgBl&FtBDO1Vx)55px-1Iu>t zpP0y$bHVM83Hgp zKHF`+#Q-?YswTh#K4ErXwOX}^cLj1vzs>}g6#;0>>G3)H8jGmo_8-O{sprR#Yn;@% zN)6L!DbL|Q0Pebg4+c$u+3^bE)(`A3OquGLl#$tI9frt`8^*=VDQvC)FjCyIGPdD$EUY$EEJS4-i>Ct{{T=S4FmS}?9 zJ+_l*1^HQodE!d3>arBtz@tf&g&Z>rw-u+ft-#>){=RI?R}FB=<&EYUiz;%sXwyuM=x%6j?AsMzVxRz z;G0GKQNh7BZmB+_0D^bB@saf&zA717^L70DcDL`r;l6!1r-3eogya8XR?ah_2sc`HKbC}h0);`t=VhNC-$0jjp?{BO)z26 zTYd1}{;|eDWdXQARevob*jN0*b2wh<`kQU@ z?O|kSxl->+gltp|w{J_#S$q`QCqwgPy%vPBQD1$LAl_W`WF%2zr}!?6xaQRbAt#E@ zK0VQH{NKn&-}=kCr=wfwKU&OcV5+C%?AQl6i ztR(L&FSOA$o4&jHZQU8E6c#2PwJD_XeR5=Fx}J;qci&Cncig!$?rM&8C^tuB;iGxw|3npWOvQ+|i9vp5`p*65f>eIqaC_j}mgH(Ym?`qGi0 zX*U|EgZ5#_YA7Csn_KMVIa#&qK3+h9$V<6^D3@5s^TvEKuyyz3cw(Jkv|CezJpl%U zrp*X9oq@#lV( zupiU?!DU|HzpV81=+?yh%wz2IR|LhSFWx|*v@qCp#bbHm@h5*B#o2u-hQw z==C}YWdF69F43osmt8VQ>U&7vF9qhq6OS^FrxSvT6whz*)4?e63`nRj_bcW6ee0rbBy01WQGftm8PpFSOZfo-|rMKXWvEthb}C@{QH_ znuu~Z5_07)^6=%tL`SEGAv#e4TLvwMzBlV-Du1>Xi9fm(K|+af*o@Rb$dT3L=xd~h z-+7^p^Y?l#$-K#&XvrHE=eC>WwUD-NrU0?cJ-ftyO;YSLeqWJ6h&{uYTX$uWjtp#qNN+hW=bq9+*}_zmAxxel?3V!=*7k z-WK#7bxRvf7D-LB8?q|il~{Bs{r>%j&r9(kh1Z4QwrPW5WIn=!%A95ZGr9<-W?-(E zpm%%I+2{iQI%>BDT7mETqUf<#bAy~7bgmKZ+T};{y2ObtS#BkkeE-djv!ecII-zOs zGtK)Jk`m(Gva&lG!wso`zmL6Ky!r4*zhEW)e4u!+e)Ht9W-+O5*B7WmK27l;)hFKM zw95Oh(B_ZpK+f~WxeztqfHiifemL~o#X`)qVO3kjbMd}+nkM;&++EXF0gqGF%2A`_ zN!Rp(l@mFcU@;YoedF68LeYU{G|`5Cf4g)|6ka|rJ*=s!|8u_lAgkq>wmEb!+7cEa z&p?-Z)-C!R($!jetTmIYzYzjg1I&zlXG&IV#^zss@utyF`BNbQuZ)lNXRYAFFLpX7 z)rE}AECfXd*&%~=`&Xv7NOk9^Syil$<`7D000l~iD!E*@f}hh8ZdSJ#6*cvZKv3kuT0zdyg!dE z=oNR=bQ;#tnZU~!hdZ%jD$n|IYBMwHtDEN`Tite=Vsc6Ip*1Xve9go`s|fJcCGB8J zUMmk+uPTH?$5ht4+r(l`T&|)qV^p%33|)!2o!Rbbb7Wx1!INDtr5-o_`p(HeK1|MR&Ks8|n(Sz1K1P8MwBE%R$RMrced)>sV?(07BRTS=t9Tz@b#2wp?glT1Zcn>Dx6#?C3@ZC>a zugGHaxuW$bx1+`evJf0r5%aB{t*zsFSoQeFPjS}#e=gnZA&Uh*Vwc> zlf&$FUjLytf8fu^kHKcwr0+FiTQ4=wY`Shc28&6CQoZ=A9D(z^(HZukgk>9Jzdcci zN=$E8ib-ZG0S5#l@3iK$JyN6l--vlkvaAzZPgWuz8T&UB8f-B!8^m3nNK(XHVHJ;Vu#Y}wH$Y;icFazY`noQ z!Z^V$T#hnAhllHDe4YrI_r)tB5*9@%x|Vb|vQ80675x13>DjQ4GGA=N19$AMT=HEu zjNkvyZgdRAm(;lfuj&F+YovI()nu}f&gW6qp{m;^{`Fdd)QAzEVbQdwwwrs2Uq&a> zGlw}g?3V{rp6eN4fK%SNrU6N+N)fI2U1W|{u|`Mhd|MlBH-@~X<;>xaXJ(2ex9O4M zO!ett1WZV7u1M-x@M+iO)1Z}8$FRvxTOi<^&FW4^IsVS;2fG&I=dLoxY}ET?de^!$^gf>%T_>V>A-2PG3_1 zD(A|XJUkVC)0gR+x7lBZ3adm?r4W;gcs9j8nf`;be=%ENRAtrB3;d05*CKMW)pR#(=;r25qO?q?-bUj90 z5$z;4)GTgf%cB8MBZj^L&zmf}wRJNFP%@XF@1z3$d305mmDY`u639J1%s+51x+3xV zE}D4lEWFJEnK2Lu+-jgv02`l-SJa#QiZ2%bD{lPrzG_HL_e-5(#;hnreIcu5_793b zdn*Vr7NU4}k%g8-E=za_MEWOuJU$)UR4R(w4Gdq1F6q)rz?8>m{2uy_23o#<&1lr>1(>maV*56lpY@kM zGas!`wKwAo3~K`Y%|Sq^LB=3i^_&F(WA$9P^$A6%*`xU6sJ4Wl4OK7nkVBF|rD ztV&bR&_irlValEvCN~rqlvys3mg7?(T=HS=XvN|Y-~ZT0O#P+c9Gd=o+VtP)J8|bG*DP2}-=ePU5F#Ks zcBhgs;csfOBfOqra@;CJer@t0-Py!@p3Lp)oxys$U&F^JdQoJ&nt$HqaE`s9LQBHO zlW7}4jA|r8E%I1q#7DHhj~``$cz_XLY)d+Il1daa^&&d{mU~5rfj)DZ zYSowD3F62u1Q&X)qfL)Ro;x?JiWKp@3VGh+9sPAXs?IOZ)b;InX0)Z6??<98h~Gd{ zk|ObXn*JoV`RpUS`?^b(_>DFu#&>VTibIimtn5x{*8na^*d?fz!98) zs}@3fa>SelEV?^jc;kM&v1ymv`fIR-MV>+oW5m*U`sZnV3*Xl$&MEAtTA`9%oBYe` zvc`V=x4WX^{E6c@b+Wuwt0cEje#jJkHZbmfD+upc6ksg$)OZT ziQ11i4+*{FHIolChbjfZtd>05m~rqEHA?V_3csc(%jBI3CQZt$N>d`v!xPh$70)uo ze~#VFMLX6>FQ6vV*6;QReX8tu+KQpe*35!|n?Fzt5!rE(YaTs3r34rz{#xKU5lF^) zf-Rl7d1!B0!zZ;=&#$7*m^$zm6il4_w`=X+Wc&WXd_IU_AbqTxta`%N<2=culf8y3 zoAW-Iz8m_A;LcGJ*Nfik8PJCy7D*z@L1Nb8_BB5V2Ui&+#Ii2V;nUzAnd!?X4?kE3 z1dhZ2p(KVs4xQB51O(+4DPYS7E{!Xu3nU4PT~w&6B5>q@RFSM-{uj&kr?4eKYa`Bp zKK19dvW6ihk2@vF;lN$vV2VB5c4mA#Xgx9)8Oc6!M82y0eqYSDivooI$hmtYs2Ed; zr_dH_dkT*YMGP9KR3~4AnHwFi%?uDnsTjof2J{i2l2wzWA~s~-Bh%26n9E%64#kM7 zb8+HIE-o#xe@ig07l~c(+!rT>rSkMBP|jb*%^tWptlsauaM2IplApY9aTn!_i)9mk z5Xfxb=k?z&u#@v4s*32kNw|9P+sZvI{#>7G`TlluZk~@?8U(Go-r9<#o^y5mJ7{u4 zN&`Z*qk3qgXzH*V^|tiR2Kyu0(wgb3W$UTkd}cxB*JFinD0{Mt9)7zUSI%_Bh`fP4 zicU!vneGdI75z{BHZlg{ZN+r8I1T$COneoMyK6k16C=)BF{E7UJtx3@@JHlNxInh2 z9GTE^qWAUBsOA^!3X3)SFkCy(G#rf$MhSCZeELb=evQJ1~aAvrCPTuXfK=>&OU`SA|n$tbs z2K^Jk6}z97#!2{EPbj}DHdn=M-CxbzU;@D5A}Vl1eXzxBvgSaGz< zUQYxi1)B!7B!vE8v5ZFOb02z3&DufK6Okd$T2^FiD zJ_Rv88}9Pk@=s0)g;Rc}&_Nu*S0@bfxp80;J+2J%ZVD!X^=UjlQ0&GST`nAfri07y z<-MTb0C3}f*A2KyFTqMQQ6jGtI_qL}`Z1sg6GaVx`cKjyBrn9cH`Q>-ViQyd+WX_f zyUYJe_k`BWnykuh)MEjw`_epB|KX4I}x&l zvwzmA>T+PM$zPYz5oDB#3h$cu-6obUTXpG`86e$m4T7(}-}O3)=s4Q3irJs-z^Ag9 z&oX~JTS(IcF-Y{f!hjFEq)tz7moeUuzdg8K=@TuZqwWTqQE3P~9ba1lCFM6w*`5Oa zXmLMQXQ?F~z*qV8u6HSKvrx?BQL%P2+M0}3xO~=_cou8Ej;nuGO*rN}T__Bbbz!Ki zMm1E&fk=3Dqg0X>PqckV)L=S2!IYM`+)Tle!;r~=i2P1X(7gAr%-AkIq)h}D6nyNB z*=n|Po?8nH@@Z{UFbm}1IVa|F>TEqGg3067V&o=GUn5bJ8@ev4*Y}Oz@YK~VSjKaZ znmp8IU7XMSqnQbPYXGWoWmkEmPyWn3rJ-f=^C`hV7f4Tf@0NcxU$>rQfZ2_;;X&Ts z=4$N|Nu)T9-ly*q7QW<4n|agOuZWp1Bj?p)fj1reFq~bdN%7DoKCW_`gi`QTYkClk zPU@!?1OEU*w&|a)^^KB^&+eJm9o5AfVaNI*00!Qv$nWkT$LGZq`diYEw{&QzQd$}^ zZ7qy#ExbD)C9oHoQ1tNhIkOikr21GF8pn`-BYow+-Cw^E-zO+4bsIQ)1D6;Z;f;Pc zITm>XNq%!fN|aEfzfwG&$Zl(uH~$XN-Vg*0Y6zg-H5dhLOW|Pym*Ou6-!YOAwURaD z`j2)ng>E57-Ybu@LWT9Lw0h}NZSE!8nd1B3Suu=B3s$&SM@KtYH|EA^A(1?g7L~!b zDhlTmzENLzag=aZYB+xw^j;|HtA_#V=K*0hMMkZ;h<>5nF-Sm58S%kF={YDLJs14hkUw~ae9bC_If+9AAaTr^$L9reIH3Qb8*R|Uu5o9m64>4s zy$m~go8*udplNK|+CfNFy-W0}LVwy_)wEbioTdZh@z&32CvIOvis+V52g(ObVj6CI zW*;_CC8boo;^Jc`xh6!qOk&c^YEDp!{2Oqnh$!Iz+vAk2tPBh%xta~~$k$f+sWypFk&uqj!1Z@T$#W59^>D{-#l1ih}a3U#rY)4_+Emy@0*(NaDZHu^u5OYR*HU`NY`G?GCpst+R^GcTMxnPYp zP=4y`@iV9eLD;|!qhJU)xw?-CwCcdhqHVo?*6Gq)4#ci}%?_ z!&@ap=<+cOipW2d>ZTMm6vdxA*UbdL>cIf1zk$Ys7)qt&qR_($@)4ul)E`0^#yA=4 z^y8l@p<-&I<9A+Aw`eZc>7{w0$)plb8Mq$vWW1`#cX3iU`YT8TcW)Q}4%RFVk09F5 zVwNZ)Ntn&k4XWnq7L%_IJl*5<=medB~-c4mkbZDIYYbN2y1aPDxVF|4oAxIF6`R|l& zFQ>Hkg`6_)KY}c7vvN4`1bw#b&WL~SIZU& zF(G)WmMkILmrJ~K43p!V5M`&Fe2k*5zTs>M7=}CjN8hP>9>gKFGVW_%BivG#G$?UT zkp5z}gHC!u=aA2{$)Ji)0(v>DYC#CPoH@9}n3a`>vq@^n>IuI+wjU$Eo^`McNUw4=0O&OvCfWu zAL5jZE}9EPq=AEG==$S>Rs;$9S+ej4mf#@{xO;bAsy9t$44=K$!oYN?$y;O@jAv66 z=LvxNQOj-??uwu_m}>-&0Ld1CA70~^#E%KD#wblfiL2xn9*iPW0Ul)I~BsLcitgzRt#!Q_NClv1ehNcmlYJN^&`#OynDUh_c;@-YGz;pkefKE=1P#;< zL4|wtzpbcPCM@~zmd_#gsU^f&4P$k7o?^Gjn z#2!*RDVt?2?*|>xkYVd`CwWd(_{6-w_i&77$9Yu-x4esv{Qzggw-7XjFf z*n3k&`8$&fiq(T$;3QfAEdHwv>Bo(;80j%HMd{3}0IO*MOy?eBFrQHXDjVHHKUvG`OZZb{KUd=1w|W1>-t z@n|F{mX;LxHF8~rAO87BG1@zXRQ;rth2Un^vdD!6WS1bg-GN zf85)boV5(+JEf_}T*Z`8fFzq;Xa!O1g3=A*B9b>Dy zH~A6#54^GAa$~Q~^;CM)!9`K1d(VMajVmBMid}O0D1AYx&%7jIUko9o5yWS>u$#=Oa)^hygC1Kd1Lh{%EO8LRg%2 z=a7WrQ5|MFqe8?GkFQxi)5PMZ3qRKmg4X3`Lz6}4%0^xP>8Q>1D$!tu@G?LBYEdCe zC}KOx&a;`|bFVF`ygF!rhV!`pr-KLTR}vnEA^qn6_GU$Y20q)RT4=VR6|qc$1cU3r z;tH@B<8;a8AsoQ3R^u3)0`*~?jTM(1R1|WS6~OjYPbgg)w{y7g)^;KyK|1mIt+24> z>E1R`J4BgZ?Cqle)Lb^oWflTrOcOR-@^ef|0S9X+-~leDIPPWU3`L@B6B;5|s2aoW zpZ-*``jKvq`~p5d+1gi>9Ah|_^6~x!bEKkL(hajhE?x>zj|G{s#+}LHkeMPNoB;)c zvuW;c0h~)ZDJMVU0pXQJwCJy(woo>4ez%vE)7*CTACO%7w2m|F3 zf?nyv%cu+$%uxu?y7Q2lXWlL>1{Lf#3B_*>gRZ&jg_@RUQ^BtzY?%_|K-hAIu)*TU zPBWI4q)aL>FXJ9H7mOf_Rp?VN-@Z48^eq14=;fqAp>TJtOni2{MhO85*N}sT=6noZ z(7D6haf%|LUu%_%!@faSdjQEx`%JIaglGcwSX*6*A9-r~ltH>d2AY6O0p||P5?x`T zh`;mi0;B1$g06=-veH}cRYYp+oZl}Of89*Jy4mjPbRDdbJ0yWx#jwiZWEpO_kVZ3I zG@61_O}Fi8IxTVtVx!Y33CNj9hOcNcwymJ>dnX%(_-ZLH5&(}QXRQMmX4ma6yp5}j z?ef5Q-@3~Qa$i;O0+L`9cmY0!#L?mpJeuZEzh}u0@GFv_aK!u$?V&_M7JLl|qKb&x zXT+~Ltpd;$if$Bx_T^Zy_!E&QP1En^ZgL=Um}&$x;X6f4I;FYzti>@yFK z*~)bkXC_7yw&G9i7+d@tuEpwBn*G;&LC@6bD+z^a7Nvv&Am;}xtK-!o4JmsqW(1DB zug=vr(wzxW3%U=CfKWZ6-MzV7My$~kC1JS)qVr zviS;Qh;Sj$+QXBA)|ccY(jw%L4tlv@aCmt5UYa~eMkdP4o)Oagj;UfMON<|%`bre( z(R-r%?H6yfM@&1YGy7192)KHek3;L`JG26=UDx~GrI{kANy?quzmBYo z!}8vZXfzKg;d=pjsXceC3ZE$a`ElDC(t3@i)AN$?fdHO|Z}qwrr-Q&L`l08TK(CQ% z&xwAIOb#rw!>Wu43*lH%!LI+ey3%GAevxyS)5&51Zc?!`3bTT$h}wU^?}tv@>EM61 znx)r#qI8SO-ow!WKhzb`fFA1{jn9p)7*@;a1;54Ks`Y$RHy#Z5gSt6qLZK$ifv1%5 z#qQ{#O2)VWNtl@L3wBLF8Vc9v+2q%slS9u9$4FsL-2_s>l#Yr)WoP@}M!9C@@ko4a zDnq_-1%{p;v!2wtUJ7;_W$RZpdP!?kM}1a_g~i^-Pk)oe)W_A~y;N+kT$Fs1CW|A) z&|0{GR%)r@Hz`dxr&|%h1Va+=yW_H5d}1xJd>WI{MBTN(ksxg;SKxOSh^=v+UlWDxlJi{}sG1E7K$M`~3WJE}1Gg;AwelOu~{ z3ED^@TxsTjD~Hx{HflQ6ge4gglYz3)%NvprqKVDHN+r7pDG;suJT3jpNREs?|S4v*1W@>4NDseSp%LHu>Dy7%7+5yKy55YU80j zxJHr6$tE^hlw=?cN1LB#>6(-d#+sZ&bgm{fObOAAbYpYE?kL2*y40gYw9RIS3*^62 ztHaA(!;+}-S*p4?k3R~Y^dUo|L9P!b)6BfeM?i-eX(bC8#ImiQsez@gb6e?iJoJ}Y-w6R$2w^`1c2$h3WRi8LV>DdES%ouHB0qYI$lt7 zm59|E?PzQ%^oAXzS42ZxL5bAk4eVudz>ipTjw>}+?qO^(O;GQY?NZN~u5N43>@*L~a20@R1F%=~ypjS{Db`(~XIYtOq!wd)NgwOwZrzKFSeSZnfVa zaCU?LV8vzxm^pCRN$m=0<<%+2{AeHra^{nfP$c@tMiQ$!e=8&Y4ioz>c&O)4Mz)|o z9e@2H7B^5u31ZF8JWvsRPh6(jJtlp|ih6C_-00-3TU(=ZF9#3d$x0SF&Yi#>#8OnjN1=q5xkIiv z;XuCRH@9}VPj*$q$ZR^2+I9nB$3CF8xMsDApWooZ;#leFks^U=cI7%j1msok7sU1o z9=+OVx>=Nvq~;}4bJ-He%u#>7F|$;9Su9x{&#>C>)*CP@=9oNZ(il-@&@x7L>=WNx zP}$Kl6<^*wLhc*ifCDG4WIFDs9urr4NJhk5;wwjx!(;+L5Q-JP5(H5HsE0?|=h!m? z=+6q7=OAVJg^qH$C|G8#gXH_A~?q>JJf!Ly<7#6&(GggU#%oGhJ%N%G( z8O;-7l#NJh{4Ty!;x$`{FmN5;lt40HOxlNf#;W=S$kCw2Rg18mm^Vwg`FRZ zLWanQ^fxZkG6Agao}z>ncXJkB266J-w>gP?-vri|1&^I;W?X61LlmQL2J5Fotan)R-i>qR25(Tgu8)21?}@Da;4 zNG**Ri0{_K{)-L3D0zB+0lvM0HCJDyUQ`jK|7$< z@+MaP2H%t#%Sb}|@PpYrTp1w(A)|kn9$I3hZYVf*|YYHHp6K>VcCXOkeev zwradzz+2%2f8HcAqWdpuZt^d(avWj5*|DNmoZU=p;0XTf;JO>QRH&!4{`c=V0d01H zEPNv(+QkJb#JX&L+lh$3MRsBOtRq1fH_n%DUIfK!9NVf3bD4!H>?Gc%x~JYz!OM5? zhh7F0M^~46Y$vCjFbv2VmXZ!^z*yLjUYp_>74b9d-JGU&`ZEkTBJj}Pv{?0g+>}Z7 zQKyJGji^6;IEvBlpNpv7+Neq2tn|2hp&7t2*-2Bg8Rt{S?fzf#d%GWE%DAZg&9hdD z@AqtH1RnAVRxW=I8oeu{q72FsUxVHK%RY*Fx+AP`Z{@7U?S|`MjM9}GWfd!hNpZ0v z;Q0b`ppc>G8|q9ch=eQUS(s#C>?oHt8QzWf#nbz=FFH68-H!4FG~#VE4(pe9#u0jf zYU|nVyf4gcA?2vnO8c(jv3^LKTNIny6c=$mwcNo+36e5U@-7#XEu>&BO6A+Px^0$? zAJt~w&#vG42N<}6h*(aK{!IKA?AYT9JA60V_KEelCz$P#?{ID|?%F^mSkWd0I`INsY~r zzxnc<+pE?Bv0@|$4k^b65zz@~QP;_3kmguk~B!#vL1rnJ;efmB|EAhhz8F5bC64*uoIR|ijnp_ojC~m4lnrG-YIs3d zbd@(Zx8sk>F-+)Cw%rR#?}o#PsYoInm!+8aOECZc;uXY`3j+%4-L7dP+-7Xf*w~%a ztU$#kDnH@)!H$Pdu-!#$Zx!*VQ`Pj3jFcd<^kq^k1g3#%mR@O(CWXmgv2v#!0ox}{ zP4e`FEU|iO3L%1k0_auPCV^NIS}jz2b+Fg7bs;>(GJ@;bW6O2d+o}}0Aczlu0{79= z$@l~`v{D47KvIrL>Ro@u12<~9>-yBfEj--(s-D&!n94ry?Ob7fn&Q_>M4E-zip9HZ^^L< zF_eYt&jmT!`x+Q9C8M*tyPI6{ev-{)P!Z2$4~-~{%+g8x>T!|TWCAAHzRP22b_2%+ zLB{WlR}xQ+$ZlU+O90#R%TyBHiK))F^TjL#YIji|41QCp&$Z2{5h7Mgx{;c@qDQ7{gue20usgj)aKjKz>O!R-aaTWtp+8B^@C zB=wW4krvUwx-NnNcL8YCug|^3FIsv|1B|rk@rmT+Cfcz!UsiG?{=~Ed1$0Vk_6RD< za<^3i!H64ma73mDb$#uKkSeBzgk|98h;z{vyOs;{;+XhFc0s_c$Cs`@)z@UbSX?~J zqD*+fz3m?TDO8C_God=<@nzX(-{>|iOLSg{Szenq0Ki#S+mFySnTgRIlzd7$>}lXm z`Yh*QXBrqVCKGym5jv;kKZHpZ26j$DBcTHB7aOiri{)zaMFSL?a9El_Q4(sO$hw&) z1(D&?e7oj&BKxe23mm2RpJx!Q*;>IG;54jg3SY0tnu9b_VsrnZ`%PS1f-jRCaAQN! z*8RwgXkO-X{7Aen>^x6y)yP2D$J$-MbyD723a-6(Q+7(i*Y!8z_-!J`u~bxEo6)iM zL@Mv5=u3ikkxTg)6Sf9pEYBvJX<~Nsk)+cPf}1qA5U}FZ(F)Hs@N{st+i$8(epKy0 z{6cpZl5M3?I~BhBXY9*nc)#jI)9FzU+}(SA=6 z=sgZSMkT2V{wY9JWsE(Cy&W1k@)1w*mSdD0o8_P6EC=D7Ugmq%oxXuW_<(d?{!LY( z;aQb8P^~x64{UTBM0ByoQ_Xj;E9`n<3rEHGw%9=lT+@tIJIuikjFCd#@VWn`a+ay; z#5Lc!rzS$H&^x(GdtdcMisik)KC2xeKoE*9O*{AsJpMW|SqE7AHky?-$F8ul(6RCL zfu(AUqXzj{;C}YuY>lwf80mZ3t`e!hlAV1*nrBoAhl&!8o0t7}nJq9(ErCc||DtOG zKmw}JMl9@g!%9YvFzoO;YsoRF#N|FAq{3n)E|+8g0s^;TvX`Sy?wynirH*z|il{o| zW0tDXj-9{%)a)j)hhHyJd7Oas5o!=O2mF0Ltstv@tqu2EjJ)uoupK+BHQ$ea3;l+6 z3!sLY<2>Kiy>WTcejEK-@*xn7un?9VR9kHgh!mnMNzb3ZZfsBY!YkVq=uJQW3kxKp zW=P3(`+5A@ua|;;%8b%nk8>YV=1V$P+>MniLKVG}n5Zv5{dP3}{-zSbaQP4;oH1|u zN&8;bR!<{PyShE5b*Ien-N~{Ns$J=bt}T`AwdE@U8m{Zn@z|NLIe}QZ^O)5H(Jv2& z=P)i(JvyVt%NCgn4E4GteZJA*D5via3>A$tSZ3Y_~4)Gl9N_e5?g0;fx`CMZ@ zPSju=PtQou)x2UtnQGgVN#+U9`{9llF8nosN*-ajo=fyOrW^^5{^y=c9xd+^>$?^RZXB=$c2sYsL75$nM_3f67eHyzgJ zN__b?8t;!J0qv=G0WK$f>WafCo6=970zkuv_zaQXu_+)@cI*lA>pqG@KU4fW&QzD* z89c=P^j>nGHazkd`yBR>YG#1|`_A^Y<$5+kKZe@Xm1{Xapj()&OKvfZ$XsZdVwB`p zV`-Doap`x_tv6y-`S}XK?8gfp@E&GWutQaSP|pb}B|$PNNiU zUSBvVq;r(R5gz_34X2nG*GLb>gI~SJiHG@(R;Gl5Xoc@K@pyK)o*9aBBmh-}*k()X z1|hh1{x{y$eL%-R=y_r@xV`nhGzlBc=V>eYam7i>k!Pw8Nl~PVLf2%mh?HcwCMU&nRZX7Cec*O|Ncf3i+acLXtYq9vzR{wF zz!U*06UrGN{O^L=Lpbd54}Y3;0jjRoKu%8k@sGBn{T8XphUp)Kcdm}J&m+0gYI`rWtvZ$SC)E zhW2x#tSlc9MdqyG=@BbI`?bwJ58kc!AmKL#IBY1t{ER(W2smKjQ#A=L6K#j9ma2-a zJH1tpWAwk0{xG<rq#+jWrTNnXL{@;v;)GNPJDN4kKi3)Nn} z{XOS4X>=NK>uO>We~t8YRFe?hO_UD28C?v)eyp5XuboJk1_-I{rh!Ckp}+M751Uod z$0+nl-0q%OQ5@_v3tu!JR_DjB zKg3reOJf9GFZ;BWQBe=QYJPN*)+$xT@87ZBX&aQ;ex zn@u^V73ALOZtRIzUvd8CHeI)R+9S7}OjK6i57kW&4 z0jP9V7zU#mALYHSD{fkKthrr~-Ydd^D8-WWBb{0T(7e`frUHA#y;`oEdpdvH%we^o zzfGVMVG8ff>w`BnBB*Z;GDiBcau9}F`H>iR5 z84N?qxJsZ}mEs@wD}lIGJbb^1!mbxDY0VV2(}cd_*+Y)?cE8QTPFyeE=*z}geMr*+ z`55dI)8VU0a1?UNomG0?{c}Bh#nHE&EWfS2=BHSgp(Rax2h+dboY!qiPL1j0J4L zLcCGdAM_>2KQasmhf6l3$q`UF5$(;@UTzgdmTj{D79kdfK){}Sk=~!jdWSKoBf;-j zbAZ!EKCDvETis!;D~wcF#*??^By<$iRfOyv1i5tsdUwP`H;-@HZyS zIqLgx&kFRys(7pYWevW|M0yw|43+(wb~W$Rw?L^*0*(;=TFV_*Vn{ED$1h+Ht;aJj z0p0a|z>mo%dKxHJKJ{hR90N7lU8JAiZ8@^MmaYv*FADGA;UFi`xC=c(Cj-pzr5&gH z{01-U@2gM1!-Z_iC2|&hP6F5|SWV~>W5C3>+OyWKp^J#L>%>|OyqH^^jX!{VQf2*0n`XS-l|N0;C*li=&ie{%(vL6SYZTeHtXB8H{RoIL`R zGrE;QF14B~etwtzjs~fi-sUnh?YFz2+4Fy2HNau+^UW<&+^=~AmMaRQbNkg;b`UE& z<4axYWs0G;dm-*5s{!`fxB>+G@EJ$zH?s1r7YPP!ELYRwd61>ZS40gBF~sF}G-O%p z9mPnPhxzF!v$}KnRBQM)(dJ?NR)MH;B1IAN)qkg*O^yURN+Yg1sF@85J;@1@C#L#+V zFjc?U_WTb6Y|Ql~O8{jQT(o!TJvYynw(v&upIkG)C%GNeizqxUa~%A-sC8`rI?8;JDdkP5rkVvZ#P z3=Hy}4~$3xuOBZi)}v|nS^FvouC?U)Fc1I(yTo`R+do+R6d}K-3iQxHRODtd(;_^e%n;M{&V~8(h(f2V(^4E4_JH;NB@Ws%x^-XjW#IU`%hRN;Bo6R=b|E4 zUfor1KXK5*;9_eAuwJQThv*uA`mbIQX@R<=EPF32TiX2bdvjUX>_mVf;u7tR+1#1n z9_c}nPT7C?aY?TJ=c33fWX5{seelU4yKWB&5V!dLI%{&EwU&OwqB@*Lr%zv`i~li> zJ8mfh;%lpXk9)d^@YiYu%BW5Yy!b8*B!`7*NL%5uxisv+T?MGu z*L-LR6eW5a{A}_X1r@&(nCch`Ls%5$hgNRmfmTJv{)r&V2Amk(@jcIYj50O!M8op9 zG?l1~0Z}nVkJVC0w#L-=#0grY*cN zkG%U?F&KD%;CZNlL2(2zQA2ocS^kMAK4^lb7>ksK8Nkdv;&(yL7C(%%$pM%kz2VLX zjiHfab$=wKiQWZ)+0yW8bKQ%i1tw+nPc9<5hYTyCR0q6&txXgum=UL`UyB~?mwlou zy;T0siHY))fJJy8(Nd)VqdQjKlafwSe;A%%q|+E6y0qvHrKBc620TlocfrKll;6@V=2J|t-9 zE3);q#WICnvOioPsw5g*;|JXd@ zuO5l=MJvh?1@s$|332Gj%)=b!dFmBByWWm}aCszl!0bjW;W?&BqOom&*akbYyL&h+ z{?qBrcgpM%@|~pm7Mbi}ZKj3HW-szmM*vf8Njt#~9Zl@5d>bkK%H2SQAfKxOc%X z+KAnTH(d5{Y1nQzu>;ya9sGgeL|q0_{(ozuiq7c@g35t~P4?5>5!t>6s19AN!VK-wz+k zCm-r9OY=AWlLU8l{g%WQW27Lz*)YX^IM&bq!QC{bbG}36()p*}257B9_Z2o+iM)^O zSf>WG8l~W_zduYpI+F7^k#7ePZygc2`~zXhBS)gZFXFek-*y;-{`^>!!c_m_( ztE#zh@-zmtVG!BI@$8IYnY4-Sqp7GL67#V2Q6cP?WH#k#cP~)dJEj0XslRHV`{m!C z-G#A$#)O=hmgErnjg=eoGWHj2(`AL=Kn1ksQHJ8m23SQ!ED5#sH60&o^`<4`TtC?e z`-@x5dRAW@rWT^f{IEI8H~=ambiY)Yd^%)}$c%vQ?`+Z8J3sK7rT03F|MNh(1N6`q zk17sla3=m^8)ypgDYmM?4jkmms{4{6+e_&c`}jtRs!inOij$j9d{KloL+dE`7`WMX zCal}FCR-`t?Z!)lwS=Q)-fU6aY`5HuWv9wGVQgBhT%F2sk=<0$9HnEJujb(6<^89V zZy&Kd=3&nHRT#VQYoO*3d>?WYu_u#?*kVq$-oZA-ieHAPx7$>7!=`xC zb>YDWux);)ZbwqqIh@~?KOFwv<7(lBXsLOOQ*dTsf|RXfjCh_VMKxBKtOqmn*e|{Q40+F(G~Z{_&elwALDvmKq7<4OK5y|7f+O zLB3x-#sU>4XTVr4WUHj|SaX}>vQVUqEfi_Kp{VuCo+u1Ro#tTfId-h@E5I9y8yf1;zSv%mez&`th$t&LEk z)MN6;@*$bC*9RaavP*Aq!Ccv!@Y|VL#An1sF<0$jVQi@=H@&zd*n0i-jg|2(@4TGQ zE}@nJm8#tpIu0v?5)lPv76$r>M@&)vrbL|9V177*>rq79->3S_NK{aolbxdX8(pNE zpJC^Vmww>T>2L`$Y4PVMGKuWxn-%{|*)jUJSB{SuJF`_vAjVoiAs$~C>UrQ%EFwVo zoRyk=WF{1)Gy6d4)#fnA(G?+C0^o*>Qx4^GH|{y#%zAw!@(mOGe=7N_iFdub|x?d9UGI#fI?v&o5$gn*n+jImB`v2i{Cp|#ROATT2|};Hyz4w$CYLNx*~6l4 z6QM};r#T{8E933Fwm{fO_Z@h?r}N^J{(Z+L&NU@QI4n>m-L(=mjb?BlW`BcOlqZe) zyHHh=U;h^lyCOew&J9fCkob%@&+l7*t3!vkU=GJG&K+lgMhHor@fJb59r~k(stfYP z(o172U6+U$VoHY>rTos@6dLwbg_bYk4(hJ?#QmaGAN*O4cw_`HjV31Jlo+llZT$B@ zHnmg&w9i3jcJl9yBwEh<&l>iQNC!>5@1k> zypoNd6OPH-4N5vfboE8-3m;ugJQ=ULe?+9o z=bJM4!4w%D#FyP7OT6ow=Ulo)@P*fj@*&_grDN%uOXu?RDn!6R<4WJjx5CU!u3L># zz$IQ%O#CxP;Bm!EukQ>y3e3Iu`1`kb-yX#L?H4c#ImpMEk_P|Gi(D6*5*^>e!mXmY zi~uNFG8sla_1LIW6uUiDx30}xUmXJdl%EK7W#3St^8A4LE$+e;-(YttPB;G-k>S*> zNDMdjHpY0i*7o3C(dYm-OR`T-pC1&s(Lof8gAIU575I1%$M&kD-dYGFe{?0eNfGuE zzVQMA`pq#!B8PL%BX}DjhG&*0GgB8Efybo-L4dG1;`BKbMdcHq&;3}F*9EQ13E+W> z3DjH(ahf;>22>E9gs(JVkC+G+5J_~sk?PzK>RBhyBV)B8u-YJiqw?#isVQmiMY2~` zIv*|z^^1rY=S5Rf=hl>m0NPZnFG7msiz%lLh_ghSiu&>4OWmiE*O{Q(cqBjp50kM)IktQ53P3DGZPQbbMV=C@#oNu6(L9VH&|;s00-jd`g=P zQ&W2YalirMdV|~H;Wh)N%g_=J;oi|#%JJN3u97HI`9QTn;>0phrST~|K8t13j~+xG z6rVDCkX_~3n^ei)0ZB=f6s4BYQSU!TtnFP)+nX3=VDhFX10UgD(gt3OQ4Bc?!C1;@E_|o=Z)`lE$!dE1=O*$SpQuXh>ho9*LqTfTTEs5-@ia z%)f{J4>5)(7*Ej9P6Vpk!KxE*Uj1a$J#+1bu>FuZB2XQ2RePzVriOsv7`wM?gu_PK zO=qPr(Kw@Xoi1!nXQPY4v61v0IMmeWC`(ImJ*J~i|9BKbok8O;Nbz(4JrORl={|io zXomo(1$ZyQ$c9C+m0jTiVz}Y|TuB=ZDdTP^$ue*)h^POn)5!2UrrH9Rp+=99{`KnZp75tLJD2kKR58%01aV|XyJ;g4liBA54K*@HhNWzv!*M;VB{=jx z&-lgcR*^5EF3uC_EUSj7Zx?UUZMUi?pSQl?7C+3?#f5XNV^USb8?H@`cu;9sP=XI6o2|cRSurB*_E04e`nz66rMnaL0+XHgq*oUh0S_ z_VDVxgj1^jIP_5qgpFBRL1Fv9tUN|bsYIWsQdR@$sKwx-P3%~2m?d=Bn`fQ9-3HSw zR{Hr|&_?$@Cd4Zoc>-*cjYMsi>uTeuY8)x49rghhbSxaxKD6z(BC&LXQ!Hl@zhALE z5+*y)|63TYR3z^{rs1GIP}?)vKVSHeyTo~r^D>NKe6W;ujv@CfaOLuFc*q9Z1gCW_ zfBPtI16(bLEg(QShyOjxxx2NE$|+UIIL4AP8B9n0LI`dN)D(9%+UcJA+=E_#kI@T4 z#8~3k86G`UijLtqR^)w|=Q(m-L}qU=j`CKZPO~?gO-kzSuZb{zL?(LEGa0_|?9H1_ zAcBG=}sdx*#$Y}I#Gf<*4NZ z*L&@s2NtiB*@g03&gzu&{>+;vl;HxR2rNJiYfM;}@$DJ!k(>t8oL-LR`1p+wpb7Ua z%idFUH9Clk9$;C$+hobx__+T5GBY7RdYT3PvCh$e%$mTkn2P?oztMBg+2LRwcrK=u zlY6!NL6FMe&E>R>(i ziGEyu1PDe!plKX+p3e`0VvHhV^Id8iDBR`Nxjtpo@FYDwA3 zgFXEaH$n+&#vI*f(E9$J&Uia*he^Se&&1`RxZXj>=WeGNuXwQNDPSS?_M}cl?m81sLu>23eEor}E-cM`59!#c A<^TWy literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.iconset/icon_256x256@2x.png b/mac/resources/AppIcon.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2fa55f033212714d97d9081cb93fbd870b6e7c7d GIT binary patch literal 48181 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 literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.iconset/icon_32x32.png b/mac/resources/AppIcon.iconset/icon_32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..a54235146e7193de256b72a41ac81a6a81cd6a01 GIT binary patch literal 1916 zcmV-?2ZQ*DP)hdxj*Y&)``6)Hk248v7xajA&3$n;5AiJMHQl|x^4Q_m!@w?r6_%M_oYpf z=Ao_n6e+116-DhsT7{G(B#r~+&nh%ciIs`~frc6z12*2DyZ6qW(}&$<*UOrps%JIQ z-I=-PJNL}_zL_B`VWSWp9|i+Jp>Uj*ns+lxr8WuxWV5jOJlWfPO(>TW^}hh#%&I=+ zh7W1!5CTX5;9DdA=~u3^L#*^$F-qjdmv3dvDrV^OKy=`b`}@weVzdNM9T*-y>o0dx zKLgT|8nlsq0><4$hcrp91gkkECXECdz?1N{InRXE-#aS|e~<%|I^S%L9+ z!xkK{#yUs$^UnSOx#~KIAp#}?YeK{(1d{RT0^NvFAZd_1vjg<(I1~e($C||xSLzR7d z%%I|!qe@6$-R{fJU%A)3@XEbr>qtVPN=RBo{Pyc+U|EvPx=q2QWA8=7L+hPM=Ga!# zU-#W60DwUtFi2oM>g>i$VcsTfRuc)7Rajr^%|SZ>eIn zUxKFQ`xKNHB$ZBcurMbcNj<=th_*;%^B4hGxFTL>Cj~)Kl8FQbt^V6A!D2pJ3)Y%U z9EozZ*m{vNRjINFU#k5~jUsbgvL=GkOtCeh7U@d60IEgU;CE?GI=eIK%KQxZnLf9u zSim6IkPwFir)dcshY*KA95NILih!OzF3wF;qAQ~=7v?0*($I^gSpWc5m^s4LX}fa8 zYCT<^*HkVV_x>TeGB>4Z$F5X3@Stb@ak$8}g^-Vr7P)`GH6!0lg>x4*tzL^c6NxHb zc6il&YHDuUeuilBhUuqi3G) zEuK7N+>=K_9^S3L*!OB>$b=GZ+UZS99*G}O912;YhX1x1sOmud2ez;kojCTk*|GCG z>Mw`iwAuZynYD=&$=K_GnmiJz+%tDoT%TmmiwpkLnOG&4k?jAyS=*7~*~h-8#wOm4 zx0ygCNgIJwcgCf404+*riC9&nFspix?u~XnuwEt|EinvHDor^1ajZ^^m7R|c1kULb zu}Y;0FhmYZJh*{QPV5V_3p2cuRUmAM7OQNV3;+!dU$1XrA{|J1-pPD&kg}{}luLxo zSvT7Jgdh4HETn*STfOLS?*{JCy`keLWA({F&H@b9fk2pAG1>;8NQ3~X^ZmLB5jd*S z-V*sf@aeHoe}2-aC=`MKjz^qh!uhB#9Vw5S`+~bpe&sSfWFj5(fAM+066Bc2-hM_iY?;Sem?w*t&>e|2E_sa#G}K2LVhvApoc#e`bsywl-yZ!1Q#5RuzEh2Fx5pp#Ay z%F)${Qe+?NdZN4VlRRp|e?5aj1d?aJGL>}G{x1OaeOGn(H*OzyYJxB>g@F975Kce) z>xsc@bu6H@;F|`X+c7iXT0d(=`b8q$&h;&gRIH%Rhf*1T@o>+-S}~T2m~y#4{NFF9 zqocyNaXD>z0m2w=XvWkXUeW9H1^|2rn@9WW`F{Z!^M5hZ?tFFt0000G literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.iconset/icon_32x32@2x.png b/mac/resources/AppIcon.iconset/icon_32x32@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bee1a9764bcf483564ec98404a5ca28ac19261ca GIT binary patch literal 4642 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 Y0RRMX8Ktp0b^rhX07*qoM6N<$g6y5pvj6}9 literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.iconset/icon_512x512.png b/mac/resources/AppIcon.iconset/icon_512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..2fa55f033212714d97d9081cb93fbd870b6e7c7d GIT binary patch literal 48181 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 literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.iconset/icon_512x512@2x.png b/mac/resources/AppIcon.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7c5030a74b681bba973e2017f1ef843dba9764ee GIT binary patch literal 32195 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 F`5(2+)$0HN literal 0 HcmV?d00001 diff --git a/mac/resources/AppIcon.png b/mac/resources/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..7c5030a74b681bba973e2017f1ef843dba9764ee GIT binary patch literal 32195 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 F`5(2+)$0HN literal 0 HcmV?d00001 diff --git a/mac/src/AppDelegate.swift b/mac/src/AppDelegate.swift new file mode 100644 index 0000000..2cfa54d --- /dev/null +++ b/mac/src/AppDelegate.swift @@ -0,0 +1,246 @@ +import Cocoa +import AVFoundation +import ApplicationServices +import Network + +public final class AppDelegate: NSObject, NSApplicationDelegate { + private var statusBarController: StatusBarController! + private var audioRecorder = AudioRecorder() + private var activeSession: SonioxLiveSession? + private var isBusyFinalizing = false + private var currentAudioLevel: Float = 0.0 + private var latestPartialText: String? = nil + + // Remote Android Phone Local Receiver (Port 8999) + private var remoteListener: NWListener? + + public func applicationDidFinishLaunching(_ notification: Notification) { + if UserDefaults.standard.object(forKey: "SonioxPlaySounds") == nil { + UserDefaults.standard.set(true, forKey: "SonioxPlaySounds") + } + + statusBarController = StatusBarController() + statusBarController.onToggleRecording = { [weak self] in + self?.toggleRecording() + } + + // Push-To-Talk / Toggle Hotkey Setup + HotkeyManager.shared.onHotkeyPressed = { [weak self] in + guard let self = self else { return } + if HotkeyManager.shared.currentMode == .pushToTalk { + if !self.audioRecorder.isRecording { + self.startRecording() + } + } else { + self.toggleRecording() + } + } + + HotkeyManager.shared.onHotkeyReleased = { [weak self] in + guard let self = self else { return } + if HotkeyManager.shared.currentMode == .pushToTalk { + if self.audioRecorder.isRecording { + self.stopRecordingAndTranscribe() + } + } + } + + audioRecorder.onAudioLevelUpdate = { [weak self] level in + guard let self = self else { return } + self.currentAudioLevel = level + if self.audioRecorder.isRecording { + HUDOverlayController.shared.show(state: .recording(level: level, liveText: self.latestPartialText)) + } + } + + audioRecorder.onAudioChunkAvailable = { [weak self] chunk in + self?.activeSession?.sendAudioChunk(chunk) + } + + SonioxSessionPool.shared.prewarmNextSession() + startRemotePasteServer() + + // Connect to Linux Persistent Gateway (116.16.16.19:8999/ws/mac) + RelayClient.shared.onPasteReceived = { [weak self] text in + guard let self = self else { return } + print("AppDelegate: 📥 Clean Remote Dictation Received: '\(text)'") + self.playSystemSound(name: "Tink") + self.pasteTextToFrontmostApp(text: text) + } + RelayClient.shared.start() + + checkInitialPermissions() + } + + private func startRemotePasteServer() { + do { + let port: NWEndpoint.Port = 8999 + let listener = try NWListener(using: .tcp, on: port) + + listener.newConnectionHandler = { [weak self] connection in + guard let self = self else { return } + connection.start(queue: .main) + self.handleRemoteConnection(connection) + } + + listener.start(queue: .main) + self.remoteListener = listener + print("RemotePasteServer: Listening on 0.0.0.0:8999") + } catch { + print("RemotePasteServer: Failed to bind port 8999:", error) + } + } + + private func handleRemoteConnection(_ connection: NWConnection) { + connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, _, _, _ in + guard let self = self, let data = data, let reqStr = String(data: data, encoding: .utf8) else { + connection.cancel() + return + } + + if reqStr.contains("GET /health") || reqStr.contains("GET /status") { + let response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}" + connection.send(content: response.data(using: .utf8), completion: .contentProcessed({ _ in + connection.cancel() + })) + return + } + + if reqStr.contains("POST /paste") { + if let bodyRange = reqStr.range(of: "\r\n\r\n") { + let bodyJsonStr = String(reqStr[bodyRange.upperBound...]) + if let bodyData = bodyJsonStr.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: bodyData) as? [String: Any], + let text = json["text"] as? String { + + DispatchQueue.main.async { + self.playSystemSound(name: "Tink") + self.pasteTextToFrontmostApp(text: text) + } + } + } + + let response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 19\r\n\r\n{\"status\":\"pasted\"}" + connection.send(content: response.data(using: .utf8), completion: .contentProcessed({ _ in + connection.cancel() + })) + return + } + + connection.cancel() + } + } + + private func checkInitialPermissions() { + audioRecorder.requestMicrophonePermission { granted in + if !granted { + print("Warning: Microphone permission not granted.") + } + } + } + + public func toggleRecording() { + if audioRecorder.isRecording { + stopRecordingAndTranscribe() + } else { + startRecording() + } + } + + public func startRecording() { + guard !audioRecorder.isRecording, !isBusyFinalizing else { return } + + latestPartialText = nil + playSystemSound(name: "Blow") + + let session = SonioxSessionPool.shared.acquireSession() + self.activeSession = session + + session.onPartialText = { [weak self] liveText in + guard let self = self, self.audioRecorder.isRecording else { return } + self.latestPartialText = liveText + HUDOverlayController.shared.show(state: .recording(level: self.currentAudioLevel, liveText: liveText)) + } + + session.onFinalResult = { [weak self] result in + guard let self = self else { return } + self.isBusyFinalizing = false + self.activeSession = nil + SonioxSessionPool.shared.prewarmNextSession() + + switch result { + case .success(let text): + self.playSystemSound(name: "Hero") + self.pasteTextToFrontmostApp(text: text) + case .failure(let error): + print("Soniox error:", error) + HUDOverlayController.shared.show(state: .error(message: error.localizedDescription)) + } + } + + do { + try audioRecorder.startRecording() + statusBarController.updateIcon(state: .recording) + statusBarController.buildMenu(isRecording: true) + HUDOverlayController.shared.show(state: .recording(level: 0.0, liveText: nil)) + } catch { + HUDOverlayController.shared.show(state: .error(message: error.localizedDescription)) + } + } + + public func stopRecordingAndTranscribe() { + guard audioRecorder.isRecording, !isBusyFinalizing else { return } + isBusyFinalizing = true + + _ = audioRecorder.stopRecording() + statusBarController.updateIcon(state: .transcribing) + statusBarController.buildMenu(isRecording: false) + HUDOverlayController.shared.show(state: .transcribing) + + guard let session = self.activeSession else { + self.isBusyFinalizing = false + HUDOverlayController.shared.hide(animated: true) + return + } + + session.finalizeStream() + } + + /// Inserts text into whatever text field/app is currently focused without adding any newlines or enters + public func pasteTextToFrontmostApp(text: String) { + // 1. Strict single-line flattening (replaces \r\n, \n, \r, \t with single spaces) + var cleaned = text.components(separatedBy: .newlines).joined(separator: " ") + cleaned = cleaned.replacingOccurrences(of: "\t", with: " ") + cleaned = cleaned.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) + cleaned = cleaned.trimmingCharacters(in: .whitespacesAndNewlines) + + guard !cleaned.isEmpty else { return } + + HUDOverlayController.shared.show(state: .success(text: cleaned)) + + // 2. Put clean single line on system clipboard + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(cleaned, forType: .string) + + // 3. Synthesize pure Cmd+V event (KeyCode 9 = 'v') without any Enter/Return + DispatchQueue.main.asyncAfter(deadline: .now() + 0.035) { + let src = CGEventSource(stateID: .hidSystemState) + let vKeyCode: CGKeyCode = 9 // ANSI 'v' + + let keyDown = CGEvent(keyboardEventSource: src, virtualKey: vKeyCode, keyDown: true) + keyDown?.flags = .maskCommand + + let keyUp = CGEvent(keyboardEventSource: src, virtualKey: vKeyCode, keyDown: false) + keyUp?.flags = [] + + keyDown?.post(tap: .cghidEventTap) + keyUp?.post(tap: .cghidEventTap) + } + } + + private func playSystemSound(name: String) { + guard UserDefaults.standard.bool(forKey: "SonioxPlaySounds") else { return } + NSSound(named: name)?.play() + } +} diff --git a/mac/src/AudioRecorder.swift b/mac/src/AudioRecorder.swift new file mode 100644 index 0000000..d712c65 --- /dev/null +++ b/mac/src/AudioRecorder.swift @@ -0,0 +1,165 @@ +import Foundation +import AVFoundation +import CoreMedia + +public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate { + private var captureSession: AVCaptureSession? + private var audioOutput: AVCaptureAudioDataOutput? + private var audioConverter: AVAudioConverter? + private let targetFormat: AVAudioFormat + + private var pcmBuffer = Data() + private let lock = NSLock() + private let captureQueue = DispatchQueue(label: "com.soniox.audiocapture", qos: .userInteractive) + + public private(set) var isRecording = false + public var onAudioLevelUpdate: ((Float) -> Void)? + public var onAudioChunkAvailable: ((Data) -> Void)? + + public override init() { + self.targetFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 16000, channels: 1, interleaved: false)! + super.init() + } + + public func requestMicrophonePermission(completion: @escaping (Bool) -> Void) { + switch AVCaptureDevice.authorizationStatus(for: .audio) { + case .authorized: + completion(true) + case .notDetermined: + AVCaptureDevice.requestAccess(for: .audio) { granted in + DispatchQueue.main.async { + completion(granted) + } + } + case .denied, .restricted: + completion(false) + @unknown default: + completion(false) + } + } + + public func startRecording() throws { + lock.lock() + defer { lock.unlock() } + + if isRecording { return } + pcmBuffer.removeAll() + + guard let device = AVCaptureDevice.default(for: .audio) else { + throw NSError(domain: "AudioRecorder", code: 1, userInfo: [NSLocalizedDescriptionKey: "میکروفونی یافت نشد"]) + } + + let session = AVCaptureSession() + let input = try AVCaptureDeviceInput(device: device) + + if session.canAddInput(input) { + session.addInput(input) + } + + let output = AVCaptureAudioDataOutput() + output.setSampleBufferDelegate(self, queue: captureQueue) + + if session.canAddOutput(output) { + session.addOutput(output) + } + + self.captureSession = session + self.audioOutput = output + + session.startRunning() + isRecording = true + print("AudioRecorder: Started recording with device:", device.localizedName) + } + + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + guard isRecording else { return } + + guard let formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer) else { return } + let srcFormat = AVAudioFormat(cmAudioFormatDescription: formatDesc) + + if audioConverter == nil || audioConverter?.inputFormat != srcFormat { + audioConverter = AVAudioConverter(from: srcFormat, to: targetFormat) + } + guard let converter = self.audioConverter else { return } + + guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { return } + let numSamples = CMSampleBufferGetNumSamples(sampleBuffer) + guard numSamples > 0 else { return } + + guard let srcBuffer = AVAudioPCMBuffer(pcmFormat: srcFormat, frameCapacity: AVAudioFrameCount(numSamples)) else { return } + srcBuffer.frameLength = AVAudioFrameCount(numSamples) + + var lengthAtOffset = 0 + var totalLength = 0 + var dataPointer: UnsafeMutablePointer? + + if CMBlockBufferGetDataPointer(blockBuffer, atOffset: 0, lengthAtOffsetOut: &lengthAtOffset, totalLengthOut: &totalLength, dataPointerOut: &dataPointer) == noErr, + let dataPtr = dataPointer { + if let floatData = srcBuffer.floatChannelData?[0] { + memcpy(floatData, dataPtr, min(totalLength, Int(srcBuffer.frameLength) * 4)) + } else if let int16Data = srcBuffer.int16ChannelData?[0] { + memcpy(int16Data, dataPtr, min(totalLength, Int(srcBuffer.frameLength) * 2)) + } + } + + let outCapacity = AVAudioFrameCount(Double(numSamples) * (16000.0 / srcFormat.sampleRate)) + 128 + guard let outBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: outCapacity) else { return } + + var error: NSError? + var haveData = true + let status = converter.convert(to: outBuffer, error: &error) { inNumPackets, outStatus in + if haveData { + haveData = false + outStatus.pointee = .haveData + return srcBuffer + } else { + outStatus.pointee = .noDataNow + return nil + } + } + + if status == .haveData || status == .inputRanDry { + let frameLen = Int(outBuffer.frameLength) + if frameLen > 0, let int16Ptr = outBuffer.int16ChannelData?[0] { + let bytesCount = frameLen * MemoryLayout.size + let data = Data(bytes: int16Ptr, count: bytesCount) + + lock.lock() + pcmBuffer.append(data) + lock.unlock() + + // Stream live audio chunk to WebSocket immediately + onAudioChunkAvailable?(data) + + // Calculate RMS level for HUD + var sumSquare: Float = 0 + for i in 0.. Data { + lock.lock() + defer { lock.unlock() } + + if !isRecording { return pcmBuffer } + isRecording = false + + captureSession?.stopRunning() + captureSession = nil + audioOutput = nil + audioConverter = nil + + print("AudioRecorder: Stopped. Total PCM captured: \(pcmBuffer.count) bytes (\(Double(pcmBuffer.count)/32000.0) seconds)") + return pcmBuffer + } +} diff --git a/mac/src/HUDOverlay.swift b/mac/src/HUDOverlay.swift new file mode 100644 index 0000000..71e79d6 --- /dev/null +++ b/mac/src/HUDOverlay.swift @@ -0,0 +1,180 @@ +import Cocoa + +public enum HUDState { + case hidden + case recording(level: Float, liveText: String? = nil) + case transcribing + case success(text: String) + case error(message: String) +} + +public final class HUDOverlayController { + public static let shared = HUDOverlayController() + + private var window: NSPanel? + private var visualEffectView: NSVisualEffectView? + private var iconImageView: NSImageView? + private var titleLabel: NSTextField? + private var subtitleLabel: NSTextField? + + private var hideTimer: Timer? + + private init() { + setupWindow() + } + + private func setupWindow() { + let width: CGFloat = 460 + let height: CGFloat = 80 + + let panel = NSPanel( + contentRect: NSRect(x: 0, y: 0, width: width, height: height), + styleMask: [.borderless, .nonactivatingPanel], + backing: .buffered, + defer: false + ) + + panel.level = .floating + panel.isOpaque = false + panel.backgroundColor = .clear + panel.hasShadow = true + panel.ignoresMouseEvents = true + panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] + + let visualEffect = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: width, height: height)) + visualEffect.material = .hudWindow + visualEffect.blendingMode = .behindWindow + visualEffect.state = .active + visualEffect.wantsLayer = true + visualEffect.layer?.cornerRadius = 24 + visualEffect.layer?.masksToBounds = true + visualEffect.layer?.borderWidth = 1.2 + visualEffect.layer?.borderColor = NSColor.white.withAlphaComponent(0.25).cgColor + + // Icon Image View + let iconView = NSImageView(frame: NSRect(x: 18, y: (height - 42) / 2, width: 42, height: 42)) + iconView.imageScaling = .scaleProportionallyUpOrDown + + // Title Label + let tLabel = NSTextField(frame: NSRect(x: 72, y: 40, width: width - 90, height: 24)) + tLabel.isBezeled = false + tLabel.drawsBackground = false + tLabel.isEditable = false + tLabel.isSelectable = false + tLabel.font = NSFont.systemFont(ofSize: 14, weight: .bold) + tLabel.textColor = .white + tLabel.alignment = .left + + // Subtitle / Preview Label + let sLabel = NSTextField(frame: NSRect(x: 72, y: 14, width: width - 90, height: 22)) + sLabel.isBezeled = false + sLabel.drawsBackground = false + sLabel.isEditable = false + sLabel.isSelectable = false + sLabel.font = NSFont.systemFont(ofSize: 13, weight: .medium) + sLabel.textColor = NSColor.white.withAlphaComponent(0.9) + sLabel.alignment = .left + + visualEffect.addSubview(iconView) + visualEffect.addSubview(tLabel) + visualEffect.addSubview(sLabel) + + panel.contentView = visualEffect + + self.window = panel + self.visualEffectView = visualEffect + self.iconImageView = iconView + self.titleLabel = tLabel + self.subtitleLabel = sLabel + } + + public func show(state: HUDState) { + hideTimer?.invalidate() + hideTimer = nil + + guard let panel = self.window else { return } + + // Position at bottom center of current active screen + if let screen = NSScreen.main { + let screenRect = screen.visibleFrame + let x = screenRect.origin.x + (screenRect.width - panel.frame.width) / 2 + let y = screenRect.origin.y + 60 + panel.setFrameOrigin(NSPoint(x: x, y: y)) + } + + switch state { + case .hidden: + hide(animated: true) + return + + case .recording(let level, let liveText): + let micImage = NSImage(systemSymbolName: "mic.fill", accessibilityDescription: "Recording") + let scale: CGFloat = 22.0 + CGFloat(level) * 6.0 + iconImageView?.image = micImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: scale, weight: .bold)) + iconImageView?.contentTintColor = NSColor.systemRed + titleLabel?.stringValue = "🎙️ در حال تبدیل زنده صدا..." + + if let live = liveText, !live.isEmpty { + let preview = live.count > 46 ? "..." + String(live.suffix(46)) : live + subtitleLabel?.stringValue = preview + subtitleLabel?.textColor = NSColor.systemGreen.withAlphaComponent(0.95) + } else { + let mode = HotkeyManager.shared.currentMode == .toggle ? "پایان: کلیک مجدد" : "رها کردن کلید ⌥ جهت درج متن" + subtitleLabel?.stringValue = mode + subtitleLabel?.textColor = NSColor.systemRed.withAlphaComponent(0.9) + } + + case .transcribing: + let waveImage = NSImage(systemSymbolName: "waveform.badge.magnifyingglass", accessibilityDescription: "Transcribing") + iconImageView?.image = waveImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 22, weight: .bold)) + iconImageView?.contentTintColor = NSColor.systemOrange + titleLabel?.stringValue = "⚡ درج آنی متن..." + subtitleLabel?.stringValue = "در حال تایپ در مکان‌نما" + subtitleLabel?.textColor = NSColor.systemOrange.withAlphaComponent(0.9) + + case .success(let text): + let checkImage = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: "Done") + iconImageView?.image = checkImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 22, weight: .bold)) + iconImageView?.contentTintColor = NSColor.systemGreen + titleLabel?.stringValue = "✨ متن درج شد" + let preview = text.count > 46 ? String(text.prefix(46)) + "..." : text + subtitleLabel?.stringValue = preview.isEmpty ? "کلیپ‌بورد به‌روز شد" : preview + subtitleLabel?.textColor = NSColor.white.withAlphaComponent(0.95) + + hideTimer = Timer.scheduledTimer(withTimeInterval: 1.2, repeats: false) { [weak self] _ in + self?.hide(animated: true) + } + + case .error(let msg): + let errImage = NSImage(systemSymbolName: "exclamationmark.triangle.fill", accessibilityDescription: "Error") + iconImageView?.image = errImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 22, weight: .bold)) + iconImageView?.contentTintColor = NSColor.systemYellow + titleLabel?.stringValue = "⚠️ خطا در تبدیل صوت" + subtitleLabel?.stringValue = msg + subtitleLabel?.textColor = NSColor.systemYellow.withAlphaComponent(0.9) + + hideTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in + self?.hide(animated: true) + } + } + + panel.alphaValue = 1.0 + panel.orderFrontRegardless() + } + + public func hide(animated: Bool) { + guard let panel = self.window, panel.isVisible else { return } + + if animated { + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.15 + panel.animator().alphaValue = 0.0 + }, completionHandler: { + panel.orderOut(nil) + }) + } else { + panel.alphaValue = 0.0 + panel.orderOut(nil) + } + } +} diff --git a/mac/src/HotkeyManager.swift b/mac/src/HotkeyManager.swift new file mode 100644 index 0000000..b5f68a7 --- /dev/null +++ b/mac/src/HotkeyManager.swift @@ -0,0 +1,261 @@ +import Cocoa +import Carbon +import ApplicationServices + +public enum DictationMode: String, CaseIterable { + case pushToTalk = "pushToTalk" // Hold to record, release to transcribe + case toggle = "toggle" // Press once to start, press again to stop + + public var localizedTitle: String { + switch self { + case .pushToTalk: + return "نگه‌داشتن برای صحبت (Hold to Talk)" + case .toggle: + return "فشردن برای شروع / توقف (Toggle)" + } + } +} + +public enum HotkeyPreset: String, CaseIterable { + case option = "Option (⌥ نگه‌داشتن)" + case capsLock = "Caps Lock" + case controlSpace = "Control + Space" + case optionSpace = "Option + Space" + case cmdShiftSpace = "Cmd + Shift + Space" + case f8 = "F8" + case f5 = "F5" + + public var keyCode: UInt32 { + switch self { + case .option: + return UInt32(kVK_Option) // 58 + case .capsLock: + return UInt32(kVK_CapsLock) // 57 + case .controlSpace, .optionSpace, .cmdShiftSpace: + return UInt32(kVK_Space) + case .f8: + return UInt32(kVK_F8) + case .f5: + return UInt32(kVK_F5) + } + } + + public var carbonModifiers: UInt32 { + switch self { + case .option, .capsLock: + return 0 + case .controlSpace: + return UInt32(controlKey) + case .optionSpace: + return UInt32(optionKey) + case .cmdShiftSpace: + return UInt32(cmdKey | shiftKey) + case .f8, .f5: + return 0 + } + } +} + +public final class HotkeyManager { + public static let shared = HotkeyManager() + + public var onHotkeyPressed: (() -> Void)? + public var onHotkeyReleased: (() -> Void)? + + private var hotKeyRef: EventHotKeyRef? + private var eventHandlerRef: EventHandlerRef? + private var eventTapPort: CFMachPort? + private var runLoopSource: CFRunLoopSource? + private var globalMonitor: Any? + + private var isOptionPhysicallyDown = false + private var isCapsLockPhysicallyDown = false + private var isKeyDown = false + + public var currentPreset: HotkeyPreset { + get { + let val = UserDefaults.standard.string(forKey: "SonioxHotkeyPreset") ?? HotkeyPreset.option.rawValue + return HotkeyPreset(rawValue: val) ?? .option + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "SonioxHotkeyPreset") + registerHotkeys() + } + } + + public var currentMode: DictationMode { + get { + let val = UserDefaults.standard.string(forKey: "SonioxDictationMode") ?? DictationMode.pushToTalk.rawValue + return DictationMode(rawValue: val) ?? .pushToTalk + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "SonioxDictationMode") + registerHotkeys() + } + } + + private init() {} + + public func registerHotkeys() { + unregisterHotkeys() + + let preset = currentPreset + print("Registering Hotkey for preset:", preset.rawValue, "mode:", currentMode.rawValue) + + // 1. Carbon HotKey for multi-key combos (Control+Space, etc.) + if preset != .option && preset != .capsLock { + var eventTypes = [ + EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)), + EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)) + ] + + let selfPtr = Unmanaged.passUnretained(self).toOpaque() + let handlerCallback: EventHandlerUPP = { (_, eventRef, userData) -> OSStatus in + guard let eventRef = eventRef, let userData = userData else { return noErr } + let manager = Unmanaged.fromOpaque(userData).takeUnretainedValue() + + let kind = GetEventKind(eventRef) + if kind == UInt32(kEventHotKeyPressed) { + DispatchQueue.main.async { + manager.onHotkeyPressed?() + } + } else if kind == UInt32(kEventHotKeyReleased) { + DispatchQueue.main.async { + manager.onHotkeyReleased?() + } + } + return noErr + } + + InstallEventHandler(GetApplicationEventTarget(), handlerCallback, 2, &eventTypes, selfPtr, &eventHandlerRef) + + let hotKeyID = EventHotKeyID(signature: OSType(0x534F4E58), id: 1) + RegisterEventHotKey( + preset.keyCode, + preset.carbonModifiers, + hotKeyID, + GetApplicationEventTarget(), + 0, + &hotKeyRef + ) + } + + // 2. Global Event Tap for single modifier keys (Option, CapsLock) + setupEventTap() + + // 3. Secondary NSEvent Global Monitor as backup + setupGlobalMonitor() + } + + private func setupEventTap() { + let mask = (1 << CGEventType.flagsChanged.rawValue) | (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue) + let selfPtr = Unmanaged.passUnretained(self).toOpaque() + + let callback: CGEventTapCallBack = { (proxy, type, event, refcon) -> Unmanaged? in + guard let refcon = refcon else { return Unmanaged.passRetained(event) } + let manager = Unmanaged.fromOpaque(refcon).takeUnretainedValue() + + let flags = event.flags.rawValue + let keyCode = event.getIntegerValueField(.keyboardEventKeycode) + + // 1. Option Key (Hold / Push-to-Talk) + if manager.currentPreset == .option { + let isAlt = (flags & CGEventFlags.maskAlternate.rawValue) != 0 + if isAlt != manager.isOptionPhysicallyDown { + manager.isOptionPhysicallyDown = isAlt + DispatchQueue.main.async { + if isAlt { + manager.onHotkeyPressed?() + } else { + if manager.currentMode == .pushToTalk { + manager.onHotkeyReleased?() + } + } + } + } + } + + // 2. CapsLock Key + else if manager.currentPreset == .capsLock { + if keyCode == 57 { + if !manager.isCapsLockPhysicallyDown { + manager.isCapsLockPhysicallyDown = true + DispatchQueue.main.async { + manager.onHotkeyPressed?() + } + } else { + manager.isCapsLockPhysicallyDown = false + if manager.currentMode == .pushToTalk { + DispatchQueue.main.async { + manager.onHotkeyReleased?() + } + } + } + return nil + } + } + + return Unmanaged.passRetained(event) + } + + if let tap = CGEvent.tapCreate( + tap: .cghidEventTap, + place: .headInsertEventTap, + options: .defaultTap, + eventsOfInterest: CGEventMask(mask), + callback: callback, + userInfo: selfPtr + ) { + self.eventTapPort = tap + let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) + self.runLoopSource = source + CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes) + CGEvent.tapEnable(tap: tap, enable: true) + print("CGEventTap created and enabled successfully.") + } else { + print("CGEventTap creation failed. Falling back to NSEvent global monitor.") + } + } + + private func setupGlobalMonitor() { + globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.flagsChanged, .keyDown, .keyUp]) { [weak self] event in + guard let self = self else { return } + + // Only use as fallback if event tap is inactive + if self.eventTapPort == nil { + if self.currentPreset == .option { + let isAlt = event.modifierFlags.contains(.option) + if isAlt != self.isOptionPhysicallyDown { + self.isOptionPhysicallyDown = isAlt + if isAlt { + self.onHotkeyPressed?() + } else if self.currentMode == .pushToTalk { + self.onHotkeyReleased?() + } + } + } + } + } + } + + public func unregisterHotkeys() { + if let ref = hotKeyRef { + UnregisterEventHotKey(ref) + hotKeyRef = nil + } + if let handler = eventHandlerRef { + RemoveEventHandler(handler) + eventHandlerRef = nil + } + if let tap = eventTapPort, let source = runLoopSource { + CGEvent.tapEnable(tap: tap, enable: false) + CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes) + self.eventTapPort = nil + self.runLoopSource = nil + } + if let mon = globalMonitor { + NSEvent.removeMonitor(mon) + self.globalMonitor = nil + } + } +} diff --git a/mac/src/RelayClient.swift b/mac/src/RelayClient.swift new file mode 100644 index 0000000..2c749be --- /dev/null +++ b/mac/src/RelayClient.swift @@ -0,0 +1,138 @@ +import Foundation +import Cocoa + +public final class RelayClient: NSObject, URLSessionWebSocketDelegate { + public static let shared = RelayClient() + + private let gatewayUrl = URL(string: "ws://116.16.16.19:8999/ws/mac")! + private var webSocketTask: URLSessionWebSocketTask? + private var urlSession: URLSession! + + private var isRunning = false + private var isConnected = false + private var reconnectTimer: Timer? + private var pingTimer: Timer? + + public var onPasteReceived: ((String) -> Void)? + + override private init() { + super.init() + let config = URLSessionConfiguration.default + config.waitsForConnectivity = true + config.timeoutIntervalForRequest = 30 + config.timeoutIntervalForResource = 300 + self.urlSession = URLSession(configuration: config, delegate: self, delegateQueue: .main) + } + + public func start() { + guard !isRunning else { return } + isRunning = true + print("RelayClient: Starting persistent gateway connection to \(gatewayUrl)...") + connect() + } + + public func stop() { + isRunning = false + reconnectTimer?.invalidate() + reconnectTimer = nil + pingTimer?.invalidate() + pingTimer = nil + webSocketTask?.cancel(with: .goingAway, reason: nil) + webSocketTask = nil + isConnected = false + } + + private func connect() { + guard isRunning else { return } + webSocketTask?.cancel() + + var request = URLRequest(url: gatewayUrl) + request.timeoutInterval = 10 + request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) SonioxVoice/1.0", forHTTPHeaderField: "User-Agent") + + webSocketTask = urlSession.webSocketTask(with: request) + webSocketTask?.resume() + + listenForMessages() + startPingTimer() + } + + private func listenForMessages() { + webSocketTask?.receive { [weak self] result in + guard let self = self, self.isRunning else { return } + + switch result { + case .success(let message): + self.isConnected = true + switch message { + case .string(let text): + self.handleMessageString(text) + case .data(let data): + if let text = String(data: data, encoding: .utf8) { + self.handleMessageString(text) + } + @unknown default: + break + } + // Continue listening + self.listenForMessages() + + case .failure(let error): + print("RelayClient: Connection error: \(error.localizedDescription)") + self.isConnected = false + self.scheduleReconnect() + } + } + } + + private func handleMessageString(_ text: String) { + guard let data = text.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return + } + + let action = json["action"] as? String + if action == "paste", let pasteText = json["text"] as? String { + let trimmed = pasteText.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { + print("RelayClient: ⚡ Received remote paste text from Gateway: '\(trimmed.prefix(30))...'") + DispatchQueue.main.async { + self.onPasteReceived?(trimmed) + } + } + } + } + + private func startPingTimer() { + pingTimer?.invalidate() + pingTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: true) { [weak self] _ in + guard let self = self, self.isRunning else { return } + self.webSocketTask?.send(.string("{\"type\":\"ping\"}")) { _ in } + } + } + + private func scheduleReconnect() { + guard isRunning else { return } + pingTimer?.invalidate() + pingTimer = nil + + if reconnectTimer == nil || !(reconnectTimer?.isValid ?? false) { + reconnectTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in + self?.reconnectTimer = nil + self?.connect() + } + } + } + + // URLSessionWebSocketDelegate + public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + print("RelayClient: 🟢 Persistent WebSocket Connected to Server Gateway!") + self.isConnected = true + } + + public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + print("RelayClient: 🔴 WebSocket Closed (code: \(closeCode.rawValue))") + self.isConnected = false + self.scheduleReconnect() + } +} diff --git a/mac/src/SonioxClient.swift b/mac/src/SonioxClient.swift new file mode 100644 index 0000000..f0451bf --- /dev/null +++ b/mac/src/SonioxClient.swift @@ -0,0 +1,321 @@ +import Foundation + +public final class SonioxLiveSession { + private let primaryWsBase = "wss://translate.compare.soniox.com/compare/api/compare-websocket" + private let fallbackWsBase = "wss://stt.compare.soniox.com/compare/api/compare-websocket" + private let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + private let origin = "https://translate.compare.soniox.com" + + private var webSocketTask: URLSessionWebSocketTask? + private var urlSession: URLSession? + private var isFinalizing = false + private var isClosed = false + private var isConnected = false + private let lock = NSLock() + + // Transcripts tracking + private var committedFinalTokens: [String] = [] + private var currentNonFinalTokens: [String] = [] + + public var onPartialText: ((String) -> Void)? + public var onFinalResult: ((Result) -> Void)? + public var onConnectionStateChanged: ((Bool) -> Void)? + + public var isReady: Bool { + lock.lock() + defer { lock.unlock() } + return isConnected && !isClosed && !isFinalizing + } + + public init(languageHints: [String] = ["fa", "en", "ar"]) { + let hints = languageHints.map { "language_hints=\($0)" }.joined(separator: "&") + let urlStr = "\(primaryWsBase)?\(hints)&enable_speaker_diarization=false&enable_language_identification=true&enable_endpoint_detection=false&providers=soniox" + guard let url = URL(string: urlStr) else { return } + + var request = URLRequest(url: url) + request.setValue(userAgent, forHTTPHeaderField: "User-Agent") + request.setValue(origin, forHTTPHeaderField: "Origin") + request.timeoutInterval = 20.0 + + let config = URLSessionConfiguration.default + config.waitsForConnectivity = true + config.requestCachePolicy = .reloadIgnoringLocalCacheData + + let session = URLSession(configuration: config) + self.urlSession = session + let task = session.webSocketTask(with: request) + self.webSocketTask = task + task.resume() + + // Fast ping to verify connection + task.sendPing { [weak self] error in + guard let self = self else { return } + self.lock.lock() + if error == nil && !self.isClosed { + self.isConnected = true + self.lock.unlock() + self.onConnectionStateChanged?(true) + print("SonioxLiveSession: WebSocket connected successfully.") + } else { + self.lock.unlock() + if let error = error { + print("SonioxLiveSession: Ping failed:", error) + } + } + } + + startReceiving() + } + + public func sendAudioChunk(_ data: Data) { + lock.lock() + defer { lock.unlock() } + guard !isFinalizing, !isClosed, let task = webSocketTask else { return } + + let message = URLSessionWebSocketTask.Message.data(data) + task.send(message) { error in + if let error = error { + print("Error streaming audio chunk:", error) + } + } + } + + public func finalizeStream() { + lock.lock() + guard !isFinalizing, !isClosed, let task = webSocketTask else { + lock.unlock() + return + } + isFinalizing = true + lock.unlock() + + print("SonioxLiveSession: Sending finalize packet...") + let finalizeMsg = URLSessionWebSocketTask.Message.string("{\"type\": \"finalize\"}") + task.send(finalizeMsg) { [weak self] error in + if let error = error { + print("Error sending finalize:", error) + self?.completeWithCurrentText() + } + } + + // Safety timeout fallback: finalize must complete within 800ms + DispatchQueue.global().asyncAfter(deadline: .now() + 0.80) { [weak self] in + self?.completeWithCurrentText() + } + } + + private func startReceiving() { + guard let task = webSocketTask else { return } + task.receive { [weak self] result in + guard let self = self else { return } + + self.lock.lock() + if self.isClosed { + self.lock.unlock() + return + } + self.lock.unlock() + + switch result { + case .success(let message): + var textReceived: String? + switch message { + case .string(let str): + textReceived = str + case .data(let data): + textReceived = String(data: data, encoding: .utf8) + @unknown default: + break + } + + if let text = textReceived, let jsonData = text.data(using: .utf8) { + self.parseMessage(jsonData) + } + self.startReceiving() + + case .failure(let error): + print("WebSocket receive status:", error) + self.completeWithCurrentText() + } + } + } + + private func parseMessage(_ data: Data) { + guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return } + + var gotFin = false + var newFinals: [String] = [] + var newNonFinals: [String] = [] + + if let type = json["type"] as? String, type == "data", + let parts = json["parts"] as? [[String: Any]] { + for part in parts { + let transStatus = part["translation_status"] as? String + if transStatus == "translation" { + continue + } + + let pText = part["text"] as? String ?? "" + let isFinal = part["is_final"] as? Bool ?? false + + if pText.contains("") { + gotFin = true + let clean = pText.replacingOccurrences(of: "", with: "") + if !clean.isEmpty { + newFinals.append(clean) + } + } else if isFinal { + if !pText.isEmpty { + newFinals.append(pText) + } + } else { + if !pText.isEmpty { + newNonFinals.append(pText) + } + } + } + + lock.lock() + if !newFinals.isEmpty { + committedFinalTokens.append(contentsOf: newFinals) + } + currentNonFinalTokens = newNonFinals + + let fullCommitted = committedFinalTokens.joined() + let fullNonFinal = currentNonFinalTokens.joined() + let combined = fullCommitted + fullNonFinal + lock.unlock() + + if !combined.isEmpty { + DispatchQueue.main.async { [weak self] in + self?.onPartialText?(combined) + } + } + } + + let sessionEnded = json["session_ended"] as? Bool ?? false + let sessionDone = (json["type"] as? String) == "session_done" + + if gotFin || sessionEnded || sessionDone { + completeWithCurrentText() + } + } + + public func completeWithCurrentText() { + lock.lock() + if isClosed { + lock.unlock() + return + } + isClosed = true + let fullCommitted = committedFinalTokens.joined() + let fullNonFinal = currentNonFinalTokens.joined() + let rawCombined = fullCommitted.isEmpty ? fullNonFinal : (fullCommitted + fullNonFinal) + let cleaned = sanitizeText(rawCombined) + + let cb = onFinalResult + webSocketTask?.cancel(with: .normalClosure, reason: nil) + webSocketTask = nil + urlSession = nil + lock.unlock() + + DispatchQueue.main.async { + cb?(.success(cleaned)) + } + } + + public func cancel() { + lock.lock() + isClosed = true + webSocketTask?.cancel(with: .normalClosure, reason: nil) + webSocketTask = nil + urlSession = nil + lock.unlock() + } + + private func sanitizeText(_ text: String) -> String { + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { return "" } + + let words = trimmed.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty } + if words.isEmpty { return "" } + + let faPattern = "[\\u0600-\\u06FF\\u0750-\\u077F\\uFB50-\\uFDFF\\uFE70-\\uFEFF]" + let enPattern = "[a-zA-Z]" + + func matches(_ pattern: String, in str: String) -> Bool { + return str.range(of: pattern, options: .regularExpression) != nil + } + + var faCount = 0 + var enCount = 0 + for w in words { + if matches(faPattern, in: w) { faCount += 1 } + if matches(enPattern, in: w) { enCount += 1 } + } + + let total = faCount + enCount + if total == 0 { return trimmed } + + let faRatio = Double(faCount) / Double(total) + var cleaned: [String] = [] + + let stopWords: Set = ["sex", "from", "no", "oh", "you", "all", "know", "that", "god", "loves", "men", "for", "day", "night", "mankind", "wanted", "moments"] + + if faRatio >= 0.25 { + for w in words { + if matches(enPattern, in: w) && !matches(faPattern, in: w) { + let cleanW = w.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: ".,!?:;،؛؟\"'()[]{}«»-–—")) + if stopWords.contains(cleanW) { continue } + if faRatio >= 0.70 { continue } + } + cleaned.append(w) + } + } else { + cleaned = words + } + + return cleaned.joined(separator: " ").trimmingCharacters(in: .whitespacesAndNewlines) + } +} + +/// Pre-warms and pools active WebSocket sessions for 0ms start latency +public final class SonioxSessionPool { + public static let shared = SonioxSessionPool() + + private var prewarmedSession: SonioxLiveSession? + private let lock = NSLock() + + private init() { + prewarmNextSession() + } + + public func prewarmNextSession() { + lock.lock() + defer { lock.unlock() } + + if let existing = prewarmedSession, existing.isReady { + return + } + + let session = SonioxLiveSession() + self.prewarmedSession = session + } + + public func acquireSession() -> SonioxLiveSession { + lock.lock() + let session = prewarmedSession + prewarmedSession = nil + lock.unlock() + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.prewarmNextSession() + } + + if let session = session, session.isReady { + return session + } + + return SonioxLiveSession() + } +} diff --git a/mac/src/StatusBarController.swift b/mac/src/StatusBarController.swift new file mode 100644 index 0000000..29457fc --- /dev/null +++ b/mac/src/StatusBarController.swift @@ -0,0 +1,172 @@ +import Cocoa + +public final class StatusBarController { + private var statusItem: NSStatusItem? + public var onToggleRecording: (() -> Void)? + + public init() { + setupStatusItem() + } + + private func setupStatusItem() { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + updateIcon(state: .idle) + buildMenu() + } + + public enum State { + case idle + case recording + case transcribing + } + + public func updateIcon(state: State) { + guard let button = statusItem?.button else { return } + + switch state { + case .idle: + if let image = NSImage(systemSymbolName: "mic", accessibilityDescription: "Soniox Voice") { + image.isTemplate = true + button.image = image + } + button.toolTip = "Soniox Voice (آماده)" + case .recording: + if let image = NSImage(systemSymbolName: "mic.fill", accessibilityDescription: "Recording") { + image.isTemplate = false + button.image = image + button.contentTintColor = NSColor.systemRed + } + button.toolTip = "در حال ضبط صدا..." + case .transcribing: + if let image = NSImage(systemSymbolName: "waveform", accessibilityDescription: "Transcribing") { + image.isTemplate = false + button.image = image + button.contentTintColor = NSColor.systemOrange + } + button.toolTip = "در حال تبدیل به متن..." + } + } + + public func buildMenu(isRecording: Bool = false) { + let menu = NSMenu() + menu.autoenablesItems = false + + // 1. Record Action + let recordTitle = isRecording ? "⏹️ توقف ضبط و درج متن" : "🎙️ شروع ضبط صدا (\(HotkeyManager.shared.currentPreset.rawValue))" + let recordItem = NSMenuItem(title: recordTitle, action: #selector(toggleRecordAction), keyEquivalent: "") + recordItem.target = self + menu.addItem(recordItem) + + menu.addItem(NSMenuItem.separator()) + + // 2. Mode Submenu + let modeMenu = NSMenu() + for mode in DictationMode.allCases { + let item = NSMenuItem(title: mode.localizedTitle, action: #selector(selectModeAction(_:)), keyEquivalent: "") + item.target = self + item.representedObject = mode + item.state = (HotkeyManager.shared.currentMode == mode) ? .on : .off + modeMenu.addItem(item) + } + let modeMenuItem = NSMenuItem(title: "⚙️ حالت کارکرد", action: nil, keyEquivalent: "") + modeMenuItem.submenu = modeMenu + menu.addItem(modeMenuItem) + + // 3. Hotkey Submenu + let hotkeyMenu = NSMenu() + for preset in HotkeyPreset.allCases { + let item = NSMenuItem(title: preset.rawValue, action: #selector(selectHotkeyAction(_:)), keyEquivalent: "") + item.target = self + item.representedObject = preset + item.state = (HotkeyManager.shared.currentPreset == preset) ? .on : .off + hotkeyMenu.addItem(item) + } + let hotkeyMenuItem = NSMenuItem(title: "⌨️ کلید میانبر (Hotkey)", action: nil, keyEquivalent: "") + hotkeyMenuItem.submenu = hotkeyMenu + menu.addItem(hotkeyMenuItem) + + menu.addItem(NSMenuItem.separator()) + + // 4. Sound Effects Toggle + let soundsEnabled = UserDefaults.standard.bool(forKey: "SonioxPlaySounds") + let soundItem = NSMenuItem(title: "🔊 پخش افکت صوتی", action: #selector(toggleSoundsAction(_:)), keyEquivalent: "") + soundItem.target = self + soundItem.state = soundsEnabled ? .on : .off + menu.addItem(soundItem) + + // 5. Accessibility Permission Check + let isAxTrusted = AXIsProcessTrusted() + let axTitle = isAxTrusted ? "✅ دسترسی Accessibility فعال است" : "🔑 اعطای دسترسی Accessibility..." + let axItem = NSMenuItem(title: axTitle, action: #selector(openAccessibilitySettings), keyEquivalent: "") + axItem.target = self + menu.addItem(axItem) + + // 6. Launch at Login + let launchLogin = UserDefaults.standard.bool(forKey: "SonioxLaunchAtLogin") + let launchItem = NSMenuItem(title: "🚀 اجرا هنگام بالا آمدن سیستم", action: #selector(toggleLaunchAtLoginAction(_:)), keyEquivalent: "") + launchItem.target = self + launchItem.state = launchLogin ? .on : .off + menu.addItem(launchItem) + + menu.addItem(NSMenuItem.separator()) + + // 7. About & Quit + let aboutItem = NSMenuItem(title: "ℹ️ درباره Soniox Voice", action: #selector(aboutAction), keyEquivalent: "") + aboutItem.target = self + menu.addItem(aboutItem) + + let quitItem = NSMenuItem(title: "❌ خروج", action: #selector(quitAction), keyEquivalent: "q") + quitItem.target = self + menu.addItem(quitItem) + + statusItem?.menu = menu + } + + @objc private func toggleRecordAction() { + onToggleRecording?() + } + + @objc private func selectModeAction(_ sender: NSMenuItem) { + if let mode = sender.representedObject as? DictationMode { + HotkeyManager.shared.currentMode = mode + buildMenu() + } + } + + @objc private func selectHotkeyAction(_ sender: NSMenuItem) { + if let preset = sender.representedObject as? HotkeyPreset { + HotkeyManager.shared.currentPreset = preset + buildMenu() + } + } + + @objc private func toggleSoundsAction(_ sender: NSMenuItem) { + let current = UserDefaults.standard.bool(forKey: "SonioxPlaySounds") + UserDefaults.standard.set(!current, forKey: "SonioxPlaySounds") + buildMenu() + } + + @objc private func toggleLaunchAtLoginAction(_ sender: NSMenuItem) { + let current = UserDefaults.standard.bool(forKey: "SonioxLaunchAtLogin") + UserDefaults.standard.set(!current, forKey: "SonioxLaunchAtLogin") + buildMenu() + } + + @objc private func openAccessibilitySettings() { + let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")! + NSWorkspace.shared.open(url) + } + + @objc private func aboutAction() { + let alert = NSAlert() + alert.messageText = "Soniox Voice v1.0" + alert.informativeText = "تبدیل بلادرنگ گفتار به متن فارسی و انگلیسی با موتور ابری فوق سریع Soniox.\n\nتوسعه یافته برای macOS." + alert.alertStyle = .informational + alert.addButton(withTitle: "باشه") + alert.runModal() + } + + @objc private func quitAction() { + NSApplication.shared.terminate(nil) + } +} diff --git a/mac/src/main.swift b/mac/src/main.swift new file mode 100644 index 0000000..467311d --- /dev/null +++ b/mac/src/main.swift @@ -0,0 +1,7 @@ +import Cocoa + +let app = NSApplication.shared +let delegate = AppDelegate() +app.delegate = delegate +app.setActivationPolicy(.accessory) +_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/server/relay_server.py b/server/relay_server.py new file mode 100644 index 0000000..4bd25f3 --- /dev/null +++ b/server/relay_server.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 +""" +Soniox Ultra-Low Latency Duplex Gateway (v3.0) +- Single persistent duplex WebSocket to Android. +- Pre-warmed upstream Soniox pool. +- Strict newline stripping & hallucination cleanup. +- Sub-50ms Cmd+V Mac paste dispatch. +""" + +import asyncio +import json +import logging +import re +import subprocess +import time +from collections import OrderedDict +import aiohttp +from aiohttp import web +import websockets +from websockets.protocol import State + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger("SonioxRelay") + +connected_mac_websockets = set() +recent_pasted_sessions = OrderedDict() # session_id -> (timestamp, text) + +MAC_DIRECT_HTTP_URL = "http://116.16.16.20:8999/paste" +SONIOX_WS_URL = ( + "wss://translate.compare.soniox.com/compare/api/compare-websocket" + "?language_hints=fa&language_hints=en&language_hints=ar" + "&enable_speaker_diarization=false&enable_language_identification=true" + "&enable_endpoint_detection=false&providers=soniox" +) +SONIOX_HEADERS = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + "Origin": "https://translate.compare.soniox.com", +} + +def is_ws_open(ws) -> bool: + if ws is None: + return False + if hasattr(ws, 'closed'): + return not ws.closed + if hasattr(ws, 'state'): + return ws.state == State.OPEN + return True + +class SonioxPool: + """Pre-warms upstream WebSockets to Soniox for 0ms speech start delay.""" + def __init__(self, size=3): + self._pool = asyncio.Queue(maxsize=size) + self._refilling = False + + async def get_session(self): + while not self._pool.empty(): + try: + ws = self._pool.get_nowait() + if is_ws_open(ws): + asyncio.create_task(self.refill()) + return ws + except asyncio.QueueEmpty: + break + + logger.info("Pool empty, connecting fresh Soniox session...") + asyncio.create_task(self.refill()) + return await self._create_ws() + + async def _create_ws(self): + try: + return await websockets.connect( + SONIOX_WS_URL, + additional_headers=SONIOX_HEADERS, + open_timeout=3.5, + ping_interval=20, + ) + except Exception as e: + logger.error("Failed to connect to upstream Soniox: %s", e) + return None + + async def refill(self): + if self._refilling or self._pool.full(): + return + self._refilling = True + try: + while not self._pool.full(): + ws = await self._create_ws() + if ws and is_ws_open(ws): + await self._pool.put(ws) + else: + break + finally: + self._refilling = False + +soniox_pool = SonioxPool() + +def sanitize_and_flatten_text(text: str) -> str: + """ + 1. Removes all line breaks (\\r, \\n) and collapses whitespace into single spaces. + 2. Strips English hallucination stop words during Persian speech. + 3. Guarantees zero trailing/leading enters or spaces. + """ + if not text: + return "" + + # Flatten all newlines, carriage returns, tabs into a single line + flattened = re.sub(r"[\r\n\t]+", " ", text) + flattened = re.sub(r"\s+", " ", flattened).strip() + + if not flattened: + return "" + + words = flattened.split() + fa_pattern = re.compile(r"[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]") + en_pattern = re.compile(r"[a-zA-Z]") + + fa_count = sum(1 for w in words if fa_pattern.search(w)) + en_count = sum(1 for w in words if en_pattern.search(w)) + total = fa_count + en_count + + if total == 0: + return flattened + + fa_ratio = fa_count / total + stop_words = {"sex", "from", "no", "oh", "you", "all", "know", "that", "god", "loves", "men", "for", "day", "night", "mankind", "wanted", "moments"} + + cleaned = [] + if fa_ratio >= 0.25: + for w in words: + if en_pattern.search(w) and not fa_pattern.search(w): + clean_w = re.sub(r"[.,!?:;،؛؟\"'()\[\]{}«»–—-]", "", w.lower()) + if clean_w in stop_words or fa_ratio >= 0.70: + continue + cleaned.append(w) + else: + cleaned = words + + result = " ".join(cleaned) + return re.sub(r"\s+", " ", result).strip() + +async def broadcast_paste_to_macs(text: str, session_id: str = "") -> bool: + """Pushes clean single-line paste payload to Mac in <50ms with strict deduplication.""" + clean_text = sanitize_and_flatten_text(text) + if not clean_text: + return False + + now = time.time() + if session_id: + if session_id in recent_pasted_sessions: + logger.info("🚫 Suppressed duplicate paste for session_id: %s", session_id) + return True + recent_pasted_sessions[session_id] = (now, clean_text) + while recent_pasted_sessions and (now - next(iter(recent_pasted_sessions.values()))[0] > 60): + recent_pasted_sessions.popitem(last=False) + else: + for prev_sid, (prev_time, prev_txt) in list(recent_pasted_sessions.items())[-5:]: + if prev_txt == clean_text and (now - prev_time) < 2.0: + logger.info("🚫 Suppressed rapid identical text paste: '%s'", clean_text[:25]) + return True + + delivered = False + payload = json.dumps({"action": "paste", "text": clean_text}, ensure_ascii=False) + + # 1. Primary: Direct Push via Active Persistent WebSocket (<15ms) + dead_sockets = set() + for ws in list(connected_mac_websockets): + try: + if is_ws_open(ws): + await ws.send_str(payload) + delivered = True + logger.info("⚡ Pushed paste via Mac Persistent WS: '%s'", clean_text[:30]) + else: + dead_sockets.add(ws) + except Exception: + dead_sockets.add(ws) + + for dead in dead_sockets: + connected_mac_websockets.discard(dead) + + if delivered: + return True + + # 2. Secondary: Direct LAN HTTP (/paste) (<35ms) + try: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=0.35)) as session: + async with session.post(MAC_DIRECT_HTTP_URL, json={"text": clean_text}) as resp: + if resp.status == 200: + logger.info("✅ Pasted to Mac via Direct LAN HTTP: '%s'", clean_text[:30]) + return True + except Exception: + pass + + # 3. Tertiary: SSH Tunnel (Port 2222) - FIXED: Zero-Newline printf '%s' pipe + try: + escaped_text = clean_text.replace("'", "'\\''") + remote_cmd = ( + f"printf '%s' '{escaped_text}' | pbcopy && " + f"/usr/bin/osascript -e 'tell application \"System Events\" to keystroke \"v\" using command down'" + ) + proc = await asyncio.create_subprocess_exec( + "ssh", "-p", "2222", "-o", "BatchMode=yes", "-o", "ConnectTimeout=2", "alig@127.0.0.1", + remote_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode == 0: + logger.info("✅ Pasted to Mac via SSH Tunnel (Zero-Newline printf): '%s'", clean_text[:30]) + return True + except Exception as e: + logger.warning("SSH tunnel fallback error: %s", e) + + return False + +async def handle_phone_stream_ws(request): + """ + ⚡ Single Persistent Duplex WebSocket Handler for Android. + Supports multiple sequential sessions without tearing down the connection. + """ + ws = web.WebSocketResponse(heartbeat=15.0) + await ws.prepare(request) + client_ip = request.remote + logger.info("📱 Android Client connected to Persistent Stream: %s", client_ip) + + active_soniox_ws = None + reader_task = None + stop_event = asyncio.Event() + current_session_id = "" + + full_final_tokens = [] + current_non_final = "" + + async def soniox_reader(soniox_ws, sid): + nonlocal current_non_final + try: + async for s_msg in soniox_ws: + if not is_ws_open(ws): + break + try: + data = json.loads(s_msg) + if data.get("type") == "data" and "parts" in data: + new_finals = [] + new_non_finals = [] + got_fin = False + for p in data["parts"]: + if p.get("translation_status") == "translation": + continue + txt = p.get("text", "") + is_final = p.get("is_final", False) + if "" in txt: + got_fin = True + clean = txt.replace("", "") + if clean: new_finals.append(clean) + elif is_final: + if txt: new_finals.append(txt) + else: + if txt: new_non_finals.append(txt) + + if new_finals: + full_final_tokens.extend(new_finals) + current_non_final = "".join(new_non_finals) + + live_text = sanitize_and_flatten_text("".join(full_final_tokens) + current_non_final) + if live_text and is_ws_open(ws): + await ws.send_str(json.dumps({ + "type": "live", + "session_id": sid, + "text": live_text + })) + + if got_fin or data.get("session_ended") or data.get("type") == "session_done": + stop_event.set() + break + except Exception as e: + logger.warning("Soniox parse error: %s", e) + except Exception as e: + logger.warning("Soniox reader error: %s", e) + finally: + stop_event.set() + + try: + async for msg in ws: + if msg.type == aiohttp.WSMsgType.BINARY: + # Live PCM audio chunk (2048 bytes / 64ms) + if active_soniox_ws and is_ws_open(active_soniox_ws): + await active_soniox_ws.send(msg.data) + + elif msg.type == aiohttp.WSMsgType.TEXT: + try: + data = json.loads(msg.data) + except Exception: + continue + + msg_type = data.get("type") or data.get("action") + sid = data.get("session_id", f"sess_{int(time.time()*1000)}") + + if msg_type == "start": + current_session_id = sid + full_final_tokens.clear() + current_non_final = "" + stop_event.clear() + + # Acquire pre-warmed Soniox session + active_soniox_ws = await soniox_pool.get_session() + if not active_soniox_ws or not is_ws_open(active_soniox_ws): + await ws.send_str(json.dumps({"type": "error", "message": "Upstream Soniox unavailable"})) + continue + + if reader_task and not reader_task.done(): + reader_task.cancel() + reader_task = asyncio.create_task(soniox_reader(active_soniox_ws, current_session_id)) + logger.info("🎙️ Started Live Session: %s", current_session_id) + + elif msg_type == "stop": + if active_soniox_ws and is_ws_open(active_soniox_ws): + await active_soniox_ws.send(json.dumps({"type": "finalize"})) + try: + await asyncio.wait_for(stop_event.wait(), timeout=0.55) + except asyncio.TimeoutError: + pass + + raw_final = "".join(full_final_tokens) + current_non_final + clean_final = sanitize_and_flatten_text(raw_final) + logger.info("⚡ Session %s final text: '%s'", sid, clean_final) + + # Instant <50ms broadcast to Mac + mac_ok = False + if clean_final: + mac_ok = await broadcast_paste_to_macs(clean_final, session_id=sid) + + if is_ws_open(ws): + await ws.send_str(json.dumps({ + "type": "final", + "session_id": sid, + "text": clean_final, + "mac_delivered": mac_ok + })) + + # Cleanup Soniox session for this turn + if reader_task and not reader_task.done(): + reader_task.cancel() + if active_soniox_ws: + try: + await active_soniox_ws.close() + except Exception: + pass + active_soniox_ws = None + + asyncio.create_task(soniox_pool.refill()) + + elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR): + break + + finally: + if reader_task and not reader_task.done(): + reader_task.cancel() + if active_soniox_ws: + try: + await active_soniox_ws.close() + except Exception: + pass + asyncio.create_task(soniox_pool.refill()) + logger.info("📱 Android Client disconnected: %s", client_ip) + + return ws + +async def handle_mac_ws(request): + """Persistent WebSocket for Mac Bridge.""" + ws = web.WebSocketResponse(heartbeat=10.0) + await ws.prepare(request) + client_ip = request.remote + logger.info("🖥️ Mac client connected to persistent WebSocket: %s", client_ip) + connected_mac_websockets.add(ws) + + try: + await ws.send_str(json.dumps({"type": "welcome", "message": "Connected to Soniox Ultra Gateway"})) + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + try: + data = json.loads(msg.data) + if data.get("type") == "ping": + await ws.send_str(json.dumps({"type": "pong"})) + except Exception: + pass + elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR): + break + finally: + connected_mac_websockets.discard(ws) + if is_ws_open(ws): + await ws.close() + logger.info("🖥️ Mac client disconnected: %s", client_ip) + + return ws + +async def handle_health(request): + return web.json_response({ + "status": "ok", + "service": "Soniox Pre-Warmed Duplex Gateway v3.0", + "connected_macs": len(connected_mac_websockets), + "recent_sessions": len(recent_pasted_sessions) + }) + +async def handle_paste(request): + try: + data = await request.json() + text = data.get("text", "") + session_id = data.get("session_id", "") + success = await broadcast_paste_to_macs(text, session_id=session_id) + return web.json_response({"status": "pasted" if success else "failed", "mac_delivered": success}) + except Exception as e: + return web.json_response({"error": str(e)}, status=400) + +async def start_background_tasks(app): + asyncio.create_task(soniox_pool.refill()) + +def create_app(): + app = web.Application(client_max_size=25 * 1024 * 1024) + app.on_startup.append(start_background_tasks) + app.router.add_get("/health", handle_health) + app.router.add_get("/status", handle_health) + app.router.add_post("/paste", handle_paste) + app.router.add_get("/ws/stream", handle_phone_stream_ws) + app.router.add_get("/ws/mac", handle_mac_ws) + return app + +if __name__ == "__main__": + app = create_app() + web.run_app(app, host="0.0.0.0", port=8999) diff --git a/server/soniox-relay.service b/server/soniox-relay.service new file mode 100644 index 0000000..f7bfab0 --- /dev/null +++ b/server/soniox-relay.service @@ -0,0 +1,13 @@ +[Unit] +Description=Soniox Remote Mic Relay Gateway +After=network.target + +[Service] +Type=simple +WorkingDirectory=/home/alialavi/projects/soniox-android-remote/server +ExecStart=/home/alialavi/.hermes/hermes-agent/venv/bin/python3 /home/alialavi/projects/soniox-android-remote/server/relay_server.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=default.target