mirror of
https://github.com/PenumbraOS/mabl.git
synced 2026-02-04 02:08:57 -06:00
Compare commits
No commits in common. "master" and "2025-11-27.0" have entirely different histories.
master
...
2025-11-27
@ -65,28 +65,15 @@ open class PlatformInputHandler(
|
||||
object : ITouchpadGestureDelegate {
|
||||
override fun onGesture(gesture: TouchpadGesture) {
|
||||
// TODO: Build proper API for Input Handler to perform standardized triggers
|
||||
if (gesture.kind != TouchpadGestureKind.HOLD_END &&
|
||||
gesture.kind != TouchpadGestureKind.FINGER_DOWN &&
|
||||
gesture.kind != TouchpadGestureKind.GESTURE_CANCEL) {
|
||||
// Any gesture that isn't a release (or intermediate finger down/cancel) should halt talking
|
||||
interactionFlowManager.cancelTalking()
|
||||
if (gesture.kind != TouchpadGestureKind.HOLD_END) {
|
||||
// Any gesture that isn't a release should halt talking
|
||||
interactionFlowManager.finishListening()
|
||||
}
|
||||
|
||||
when (gesture.kind) {
|
||||
TouchpadGestureKind.FINGER_DOWN -> {
|
||||
// Immediately start listening, even if we abort later
|
||||
interactionFlowManager.startListening()
|
||||
}
|
||||
|
||||
TouchpadGestureKind.GESTURE_CANCEL -> {
|
||||
interactionFlowManager.finishListening(abort = true)
|
||||
}
|
||||
|
||||
TouchpadGestureKind.DOUBLE_TAP -> {
|
||||
// TODO: Fix double tap with two fingers
|
||||
// if (gesture.fingerCount == 2) {
|
||||
// Cancel listening if it is ongoing
|
||||
interactionFlowManager.finishListening(abort = true)
|
||||
interactionFlowManager.takePicture()
|
||||
// }
|
||||
}
|
||||
|
||||
@ -7,8 +7,6 @@ interface ITouchpadGestureDelegate {
|
||||
data class TouchpadGesture(val kind: TouchpadGestureKind, val duration: Long, val fingerCount: Int)
|
||||
|
||||
enum class TouchpadGestureKind {
|
||||
FINGER_DOWN,
|
||||
GESTURE_CANCEL,
|
||||
SINGLE_TAP,
|
||||
DOUBLE_TAP,
|
||||
HOLD_START,
|
||||
|
||||
@ -73,14 +73,6 @@ class TouchpadGestureManager(
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
activePointers.add(event.getPointerId(0))
|
||||
|
||||
sendEventIfAllowed(event, updateLastEventTime = false) {
|
||||
TouchpadGesture(
|
||||
TouchpadGestureKind.FINGER_DOWN,
|
||||
0,
|
||||
activePointers.size
|
||||
)
|
||||
}
|
||||
|
||||
if (activePointers.size == 1) {
|
||||
holdStartTime = event.eventTime
|
||||
singleFingerHoldHandler = Handler(Looper.getMainLooper())
|
||||
@ -99,26 +91,14 @@ class TouchpadGestureManager(
|
||||
activePointers.remove(event.getPointerId(0))
|
||||
|
||||
// Cancel any pending single finger hold
|
||||
val wasPendingHold = singleFingerHoldHandler != null
|
||||
singleFingerHoldHandler?.removeCallbacksAndMessages(null)
|
||||
singleFingerHoldHandler = null
|
||||
|
||||
val duration = event.eventTime - holdStartTime
|
||||
|
||||
// Handle hold end
|
||||
if (isHolding) {
|
||||
val duration = event.eventTime - holdStartTime
|
||||
delegate.onGesture(TouchpadGesture(TouchpadGestureKind.HOLD_END, duration, 1))
|
||||
isHolding = false
|
||||
} else if (wasPendingHold && activePointers.isEmpty()) {
|
||||
// Finger was lifted before any gesture started
|
||||
// Only send if we didn't just send a recognized gesture
|
||||
sendEventIfAllowed(event, updateLastEventTime = false) {
|
||||
TouchpadGesture(
|
||||
TouchpadGestureKind.GESTURE_CANCEL,
|
||||
duration,
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A non-primary touch has changed
|
||||
@ -169,18 +149,12 @@ class TouchpadGestureManager(
|
||||
/**
|
||||
* Send TouchpadGesture if allowed based on time since last event. Specifically to prevent sending gesture start events too close together
|
||||
*/
|
||||
private fun sendEventIfAllowed(
|
||||
event: MotionEvent,
|
||||
updateLastEventTime: Boolean = true,
|
||||
lambda: () -> TouchpadGesture,
|
||||
) {
|
||||
private fun sendEventIfAllowed(event: MotionEvent, lambda: () -> TouchpadGesture) {
|
||||
if (event.eventTime < lastEventTime + MIN_GESTURE_SEPARATION_MS) {
|
||||
return
|
||||
}
|
||||
|
||||
if (updateLastEventTime) {
|
||||
lastEventTime = event.eventTime
|
||||
}
|
||||
lastEventTime = event.eventTime
|
||||
delegate.onGesture(lambda())
|
||||
}
|
||||
|
||||
@ -189,15 +163,8 @@ class TouchpadGestureManager(
|
||||
|
||||
if (isHolding) {
|
||||
delegate.onGesture(TouchpadGesture(TouchpadGestureKind.HOLD_END, duration, 2))
|
||||
isHolding = false
|
||||
} else if (duration < 200) {
|
||||
delegate.onGesture(TouchpadGesture(TouchpadGestureKind.SINGLE_TAP, duration, 2))
|
||||
} else {
|
||||
// Finger was lifted before any gesture completed
|
||||
// Only send if we didn't just send a recognized gesture
|
||||
sendEventIfAllowed(event, updateLastEventTime = false) {
|
||||
TouchpadGesture(TouchpadGestureKind.GESTURE_CANCEL, duration, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,8 +6,7 @@ import com.penumbraos.mabl.types.Error
|
||||
interface IInteractionFlowManager {
|
||||
fun startListening(requestImage: Boolean = false)
|
||||
fun startConversationFromInput(userInput: String)
|
||||
fun finishListening(abort: Boolean = false)
|
||||
fun cancelTalking()
|
||||
fun finishListening()
|
||||
fun isFlowActive(): Boolean
|
||||
fun getCurrentFlowState(): InteractionFlowState
|
||||
|
||||
|
||||
@ -44,8 +44,6 @@ class InteractionFlowManager
|
||||
private var stateCallback: InteractionStateCallback? = null
|
||||
private var contentCallback: InteractionContentCallback? = null
|
||||
|
||||
private var didAbort: Boolean = false
|
||||
|
||||
private var cameraService: CameraService? = null
|
||||
private var isCameraServiceBound = false
|
||||
|
||||
@ -66,30 +64,17 @@ class InteractionFlowManager
|
||||
|
||||
private val sttCallback = object : ISttCallback.Stub() {
|
||||
override fun onPartialTranscription(partialText: String) {
|
||||
if (didAbort) {
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "STT partial transcription: $partialText")
|
||||
contentCallback?.onPartialTranscription(partialText)
|
||||
}
|
||||
|
||||
override fun onFinalTranscription(finalText: String) {
|
||||
if (didAbort) {
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "STT final transcription: $finalText")
|
||||
setState(InteractionFlowState.PROCESSING)
|
||||
contentCallback?.onFinalTranscription(finalText)
|
||||
|
||||
if (finalText.trim().isEmpty()) {
|
||||
Log.d(TAG, "STT transcription was empty, skipping")
|
||||
setState(InteractionFlowState.IDLE)
|
||||
stateCallback?.onError(Error.SttError("Empty transcription"))
|
||||
} else {
|
||||
Log.d(TAG, "STT final transcription: $finalText")
|
||||
setState(InteractionFlowState.PROCESSING)
|
||||
contentCallback?.onFinalTranscription(finalText)
|
||||
|
||||
// Start conversation with the transcribed text
|
||||
startConversationFromInput(finalText)
|
||||
}
|
||||
// Start conversation with the transcribed text
|
||||
startConversationFromInput(finalText)
|
||||
}
|
||||
|
||||
override fun onError(errorMessage: String) {
|
||||
@ -110,22 +95,16 @@ class InteractionFlowManager
|
||||
}
|
||||
|
||||
override fun startListening(requestImage: Boolean) {
|
||||
currentModality =
|
||||
if (requestImage) InteractionFlowModality.Vision else InteractionFlowModality.Speech
|
||||
|
||||
if (currentState == InteractionFlowState.LISTENING) {
|
||||
Log.d(TAG, "Already listening. Continuing")
|
||||
return
|
||||
} else if (currentState != InteractionFlowState.IDLE) {
|
||||
if (currentState != InteractionFlowState.IDLE) {
|
||||
Log.w(TAG, "Cannot start listening, current state: $currentState")
|
||||
return
|
||||
}
|
||||
|
||||
didAbort = false
|
||||
|
||||
try {
|
||||
allControllers.stt.startListening()
|
||||
setState(InteractionFlowState.LISTENING)
|
||||
currentModality =
|
||||
if (requestImage) InteractionFlowModality.Vision else InteractionFlowModality.Speech
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start listening: ${e.message}")
|
||||
stateCallback?.onError(Error.SttError("Failed to start listening: ${e.message}"))
|
||||
@ -192,11 +171,10 @@ class InteractionFlowManager
|
||||
}
|
||||
}
|
||||
|
||||
override fun finishListening(abort: Boolean) {
|
||||
override fun finishListening() {
|
||||
Log.d(TAG, "Stopping listening, state: $currentState")
|
||||
setState(InteractionFlowState.CANCELLING)
|
||||
|
||||
didAbort = abort
|
||||
allControllers.stt.cancelListening()
|
||||
allControllers.tts.service?.stopSpeaking()
|
||||
|
||||
@ -204,10 +182,6 @@ class InteractionFlowManager
|
||||
stateCallback?.onUserFinished()
|
||||
}
|
||||
|
||||
override fun cancelTalking() {
|
||||
allControllers.tts.service?.stopSpeaking()
|
||||
}
|
||||
|
||||
override fun isFlowActive(): Boolean {
|
||||
return currentState != InteractionFlowState.IDLE
|
||||
}
|
||||
|
||||
@ -18,8 +18,6 @@ import io.ktor.client.request.setBody
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.content.TextContent
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.isSuccess
|
||||
import io.ktor.util.toMap
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
@ -34,8 +32,6 @@ class KtorHttpClient : HttpClient {
|
||||
constructor(coroutineScope: CoroutineScope, penumbraClient: PenumbraClient) {
|
||||
this.coroutineScope = coroutineScope
|
||||
this.ktorClient = io.ktor.client.HttpClient {
|
||||
// Otherwise ktor strips ContentType
|
||||
useDefaultTransformers = false
|
||||
install(HttpClientPlugin) {
|
||||
this.penumbraClient = penumbraClient
|
||||
}
|
||||
@ -98,20 +94,7 @@ class KtorHttpClient : HttpClient {
|
||||
for ((key, values) in langChainRequest.headers()) {
|
||||
builder.headers.appendAll(key, values)
|
||||
}
|
||||
val contentTypeString = langChainRequest.headers()["ContentType"]?.first() ?: ""
|
||||
|
||||
val contentType = try {
|
||||
ContentType.parse(contentTypeString)
|
||||
} catch (_: Exception) {
|
||||
ContentType.Application.Json
|
||||
}
|
||||
|
||||
builder.setBody(
|
||||
TextContent(
|
||||
langChainRequest.body(),
|
||||
contentType
|
||||
)
|
||||
)
|
||||
builder.setBody(langChainRequest.body())
|
||||
}
|
||||
|
||||
private suspend fun buildResponse(
|
||||
|
||||
@ -39,14 +39,8 @@ class DemoSttService : MablService("DemoSttService") {
|
||||
client.stt.initialize(object : SttRecognitionListener() {
|
||||
override fun onError(error: Int) {
|
||||
try {
|
||||
// RecognitionError.ERROR_NO_MATCH
|
||||
if (error == 7) {
|
||||
Log.d("DemoSttService", "No speech recognized")
|
||||
currentCallback?.onFinalTranscription("")
|
||||
} else {
|
||||
currentCallback?.onError("Recognition error: $error")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
currentCallback?.onError("Recognition error: $error")
|
||||
} catch (e: RemoteException) {
|
||||
Log.e("DemoSttService", "Callback error", e)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user