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}