Browse Source

fix(sync): append speech at cursor with trailing space, relax Starlink ping timeout, and avoid private IP loop on Starlink

main
Ali Alavi 3 hours ago
parent
commit
65ab8f68b6
  1. 8
      android/app/src/main/java/com/soniox/remotemic/MainActivity.kt
  2. 27
      android/app/src/main/java/com/soniox/remotemic/StreamDictationClient.kt
  3. 6
      mac/src/FocusedInputSync.swift
  4. 8
      server/relay_server.py

8
android/app/src/main/java/com/soniox/remotemic/MainActivity.kt

@ -89,7 +89,7 @@ class MainActivity : AppCompatActivity() {
setupUI()
checkPermissions()
AppLogger.log("Main", "اپلیکیشن همگام‌سازی صوتی سانی‌اوکس راه‌اندازی شد (نسخه بهینه‌شده فوق‌سریع v5.6)")
AppLogger.log("Main", "اپلیکیشن همگام‌سازی صوتی سانی‌اوکس راه‌اندازی شد (نسخه بهینه‌شده فوق‌سریع v5.7)")
// Initialize Collaborative WebSocket Client
streamDictationClient = StreamDictationClient(
@ -237,7 +237,7 @@ class MainActivity : AppCompatActivity() {
val formattedSpeech = buildString {
if (needsPreSpace) append(" ")
append(trimmedSpeech)
if (needsPostSpace) append(" ")
append(" ") // ALWAYS guarantee a trailing space after inserted speech
}
val mergedText = "$prefix$formattedSpeech$suffix"
@ -256,8 +256,8 @@ class MainActivity : AppCompatActivity() {
AppLogger.log("Main", "تزریق گفتار در نشانگر: '$trimmedSpeech' (موقعیت جدید: $newCursor)")
// Broadcast full mirrored text to Mac
streamDictationClient?.sendPhoneEdit(mergedText, newCursor)
// Send speech directly to Mac cursor (Cmd+V append, NO Cmd+A clobber!)
streamDictationClient?.sendSpeechInsert(formattedSpeech, newCursor)
}
private fun setupUI() {

27
android/app/src/main/java/com/soniox/remotemic/StreamDictationClient.kt

@ -44,17 +44,14 @@ class StreamDictationClient(
private val isRecording = AtomicBoolean(false)
private val mainHandler = Handler(Looper.getMainLooper())
private val candidateHosts: List<String> = listOf(
host,
if (host.contains("116.16.16.19")) "2.180.16.250:8999" else "116.16.16.19:8999"
).filter { it.isNotBlank() }.distinct()
private val candidateHosts: List<String> = listOf(host).filter { it.isNotBlank() }.distinct()
private var currentHostIndex = 0
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(3500, TimeUnit.MILLISECONDS)
.connectTimeout(5000, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS) // Persistent WebSocket
.writeTimeout(5000, TimeUnit.MILLISECONDS)
.pingInterval(4, TimeUnit.SECONDS) // 4s active heartbeat to keep NAT tables alive
.pingInterval(15, TimeUnit.SECONDS) // Relaxed 15s ping interval (prevents premature drops over Starlink/satellite jitter)
.retryOnConnectionFailure(true)
.build()
@ -163,6 +160,24 @@ class StreamDictationClient(
})
}
/**
* Sends speech chunk directly for pure cursor append on Mac (Cmd+V, NO Cmd+A)
*/
fun sendSpeechInsert(speechText: String, cursor: Int) {
if (!isConnected.get() || activeWebSocket == null) {
connectWebSocket()
}
val payload = JSONObject().apply {
put("type", "insert_speech")
put("action", "insert_speech")
put("source", "android")
put("text", speechText)
put("cursor", cursor)
put("timestamp", System.currentTimeMillis() / 1000.0)
}.toString()
activeWebSocket?.send(payload)
}
/**
* Broadcasts phone's live text to Mac for immediate real-time mirroring
*/

6
mac/src/FocusedInputSync.swift

@ -255,9 +255,11 @@ public final class FocusedInputSync {
}
}
// Normalize line endings and preserve intentional multiline text (\n)
// Normalize line endings and preserve intentional multiline text (\n) and trailing space for speech appends
var cleanText = text.replacingOccurrences(of: "\r\n", with: "\n").replacingOccurrences(of: "\r", with: "\n")
cleanText = cleanText.trimmingCharacters(in: .whitespacesAndNewlines)
if isFullReplace {
cleanText = cleanText.trimmingCharacters(in: .whitespacesAndNewlines)
}
let targetCursor = cursor ?? cleanText.count
lastObservedText = cleanText

8
server/relay_server.py

@ -105,9 +105,10 @@ class SonioxPool:
soniox_pool = SonioxPool()
def sanitize_text(text: str, allow_multiline: bool = True) -> str:
def sanitize_text(text: str, allow_multiline: bool = True, preserve_trailing_space: bool = False) -> str:
if not text:
return ""
has_trailing_space = text.endswith(" ")
if allow_multiline:
normalized = text.replace("\r\n", "\n").replace("\r", "\n")
lines = [re.sub(r"[ \t]+", " ", line) for line in normalized.split("\n")]
@ -120,6 +121,9 @@ def sanitize_text(text: str, allow_multiline: bool = True) -> str:
if re.fullmatch(r"[\s«»\.\,\،\؛\؟\!\?\:\;\-\–—\"\'\(\)\[\]\{\}]+", cleaned):
return ""
if (preserve_trailing_space or has_trailing_space) and not cleaned.endswith(" "):
cleaned += " "
return cleaned
def sanitize_and_flatten_text(text: str) -> str:
@ -258,7 +262,7 @@ async def handle_phone_stream_ws(request):
# ALL text / sync / insert operations must be broadcast to Mac!
if msg_type in ("sync_state", "insert_speech", "speech_insert", "phone_input_edit", "update_input", "paste"):
is_speech = msg_type in ("insert_speech", "speech_insert")
clean_text = sanitize_text(data.get("text", ""), allow_multiline=(not is_speech))
clean_text = sanitize_text(data.get("text", ""), allow_multiline=(not is_speech), preserve_trailing_space=is_speech)
if not clean_text and is_speech:
continue # Do not broadcast empty speech or lone quotes
data["text"] = clean_text

Loading…
Cancel
Save