Compare commits

..

No commits in common. "master" and "2025-11-27.0" have entirely different histories.

7 changed files with 19 additions and 117 deletions

View File

@ -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()
// }
}

View File

@ -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,

View File

@ -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)
}
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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(

View File

@ -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)
}
}