a scrappy gimbal that insults you in shakespearean english
1package com.paytondeveloper.myrus_mobile 2 3import androidx.compose.animation.AnimatedVisibility 4import androidx.compose.foundation.Image 5import androidx.compose.foundation.layout.Box 6import androidx.compose.foundation.layout.Column 7import androidx.compose.foundation.layout.fillMaxSize 8import androidx.compose.foundation.layout.fillMaxWidth 9import androidx.compose.foundation.layout.offset 10import androidx.compose.foundation.layout.padding 11import androidx.compose.material.Button 12import androidx.compose.material.MaterialTheme 13import androidx.compose.material.Slider 14import androidx.compose.material.Text 15import androidx.compose.runtime.* 16import androidx.compose.ui.Alignment 17import androidx.compose.ui.Modifier 18import androidx.compose.ui.draw.drawWithCache 19import androidx.compose.ui.draw.scale 20import androidx.compose.ui.geometry.Offset 21import androidx.compose.ui.graphics.Color 22import androidx.compose.ui.graphics.RectangleShape 23import androidx.compose.ui.layout.onSizeChanged 24import androidx.compose.ui.platform.LocalDensity 25import androidx.compose.ui.platform.LocalViewConfiguration 26import androidx.compose.ui.text.font.FontWeight 27import androidx.compose.ui.unit.dp 28import androidx.graphics.shapes.RoundedPolygon 29import com.kashif.cameraK.builder.CameraControllerBuilder 30import com.kashif.cameraK.controller.CameraController 31import com.kashif.cameraK.enums.CameraLens 32import com.kashif.cameraK.enums.Directory 33import com.kashif.cameraK.enums.ImageFormat 34import com.kashif.cameraK.permissions.providePermissions 35import com.kashif.cameraK.result.ImageCaptureResult 36import com.kashif.cameraK.ui.CameraPreview 37import dev.shreyaspatil.ai.client.generativeai.GenerativeModel 38import dev.shreyaspatil.ai.client.generativeai.type.content 39import io.ktor.util.Identity.encode 40import kotlinx.coroutines.CoroutineScope 41import kotlinx.coroutines.Dispatchers 42import kotlinx.coroutines.IO 43import kotlinx.coroutines.delay 44import kotlinx.coroutines.launch 45import org.jetbrains.compose.resources.painterResource 46import org.jetbrains.compose.ui.tooling.preview.Preview 47 48import myrus_mobile.composeapp.generated.resources.Res 49import myrus_mobile.composeapp.generated.resources.compose_multiplatform 50import nl.marc_apps.tts.TextToSpeechEngine 51import nl.marc_apps.tts.rememberTextToSpeechOrNull 52 53expect fun analyzeImage(img: ByteArray, callback: (Rect, Size) -> Unit) 54 55data class Size(val width: Float, val height: Float) 56data class Rect(val top: Float, val left: Float, val bottom: Float, val right: Float) 57data class FaceData(val boundingBox: Rect) 58 59fun Rect.origin(): Size { 60 val midpointX = (this.left + this.right) / 2 61 val midpointY = (this.top + this.bottom) / 2 62 return Size(width = midpointX, height = midpointY) 63} 64 65expect suspend fun sayText(text: String) 66 67val genAI = GenerativeModel( 68 "gemini-2.0-flash", 69 apiKey = "AIzaSyCy56R6_T3Neu54W45MMSTGpXFEb92V2yI" 70) 71 72expect fun epochMillis(): Long 73 74enum class MovingDirection { 75 RIGHT, LEFT 76} 77 78@Composable 79@Preview 80fun App() { 81 MaterialTheme { 82 val permissions = providePermissions() 83 val camPermission = remember { mutableStateOf(permissions.hasCameraPermission()) } 84 if (!camPermission.value) { 85 permissions.RequestCameraPermission( { 86 camPermission.value = true 87 }, onDenied = { 88 camPermission.value = false 89 }) 90 } 91 92 93 94 if (camPermission.value) { 95 var camController by remember { mutableStateOf<CameraController?>(null) } 96 var camSize by remember { mutableStateOf<Size?>(null) } 97 var currentThingy by remember { mutableStateOf<Rect?>(Rect(0f,0f,0f,0f)) } 98 var delayMillis by remember { mutableStateOf(1000) } 99 var analyzing by remember { mutableStateOf(true) } 100 var moving by remember { mutableStateOf(false) } 101 val tts = rememberTextToSpeechOrNull(TextToSpeechEngine.Google) 102 val movingDirection = remember { mutableStateOf<MovingDirection?>(null) } 103 LaunchedEffect(Unit) { 104 //not proud of this. 105 suspend fun roast(image: ByteArray) { 106 var content = content { 107 image(image) 108 text("make a shakespearean insult for the person in the middle of the image. return only the insult. be specific to the person in the image") 109 } 110 val res = genAI.generateContent(content) 111 println("RES: ${res.text} TTS: ${tts}") 112// tts?.let { tts -> 113// tts.say(res.text ?: "uh oh its broken", true) 114// } 115 sayText(res.text ?: "uh oh its borken") 116 analyzing = true 117 } 118 suspend fun runloop() { 119 120 if (analyzing) { 121 122 val res = camController?.takePicture() 123 res?.let { 124 when (it) { 125 is ImageCaptureResult.Error -> { 126 println("error taking pic. skipping frame: ${it.exception}") 127 } 128 is ImageCaptureResult.Success -> { 129 analyzeImage(it.byteArray, { bounds, size -> 130// println("offset: ${it.top} ${it.left}") 131 val factorY = bounds.top / size.height 132 val factorX = bounds.left / size.width 133 134 val newY = factorY * camSize!!.height 135 val newX = factorX * camSize!!.width 136 137 currentThingy = bounds.copy(top = newY, left = newX) 138 analyzing = false 139 val leftCenter = bounds.right - bounds.left 140 println("BOUNDS: ${bounds.origin().width} SIZE: ${size.width}") 141 val midpointX = bounds.origin().width 142 if (midpointX < ((size.width / 2) - (size.width / 12))) { 143 //move left 144 println("move left") 145 movingDirection.value = MovingDirection.LEFT 146 analyzing = true 147 } else if (midpointX > ((size.width / 2) + (size.width / 12))) { 148 //move right 149 println("move right") 150 analyzing = true 151 movingDirection.value = MovingDirection.RIGHT 152 } else { 153 movingDirection.value = null 154 //centered 155 CoroutineScope(Dispatchers.IO).launch { 156 roast(it.byteArray) 157 } 158 } 159 160 }) 161 162 } 163 } 164 } 165 } 166 delay(delayMillis.toLong()) 167 runloop() 168 } 169 170 runloop() 171 } 172 Box(modifier = Modifier) { 173 val topPx = with(LocalDensity.current) { 174 currentThingy!!.top.toDp() 175 } 176 val leftPx = with(LocalDensity.current) { 177 currentThingy!!.left.toDp() 178 } 179 val camSizePx = with(LocalDensity.current) { 180 camSize?.width?.toDp() ?: 0.dp 181 } 182 println("offset (dp) ${topPx} ${leftPx}") 183 184 CameraPreview(modifier = Modifier.fillMaxSize().onSizeChanged { 185 camSize = Size( 186 width = it.width.toFloat(), 187 height = it.height.toFloat() 188 ) 189 println("camsize: ${camSize?.width}x${camSize?.height}") 190 }, { 191 setCameraLens(CameraLens.FRONT) 192 setImageFormat(ImageFormat.PNG) 193 setDirectory(Directory.PICTURES) 194 }, onCameraControllerReady = { 195 camController = it 196 if (getPlatform().name.contains("iOS")) { 197 camController!!.toggleCameraLens() 198 } 199 }) 200 Text("Face", modifier = Modifier.offset(x = leftPx, y = topPx)) 201// when (movingDirection) { 202// null -> {} 203// MovingDirection.RIGHT { 204// Text(">", fontWeight = FontWeight.Black, color = Color.White) 205// } 206// } 207 if (movingDirection.value != null) { 208 if (movingDirection.value == MovingDirection.RIGHT) { 209 Text(">", fontWeight = FontWeight.Black, color = Color.White, modifier = Modifier.padding(top = 128.dp, start = camSizePx - 64.dp).scale(20f)) 210 } else { 211 Text("<", fontWeight = FontWeight.Black, color = Color.White, modifier = Modifier.padding(top = 128.dp, start = 32.dp).scale(20f)) 212 } 213 } 214 } 215 Slider(modifier = Modifier.padding(top = 64.dp), value = delayMillis.toFloat(), onValueChange = { 216 delayMillis = it.toInt() 217 }, valueRange = 16.67f..5000f) 218 } else { 219 Text("no permissions!! can't do anything :(") 220 } 221 } 222}