← Back Image Loading...

Deploy de Modelos de Hugging Face en Android: App de Análisis de Sentimientos con Jetpack Compose

By Denis Sanchez Leyva

5/23/2025


Buy Now on Amazon

1. Configuración Inicial

1.1 Dependencias Clave

En tu build.gradle:

 

dependencies {  
    implementation("org.tensorflow:tensorflow-lite:2.16.1")  
    implementation("androidx.compose.material3:material3:1.2.1")  
    implementation("com.google.code.gson:gson:2.10.1")  
    // Lista completa en el repositorio  
}


1.2 Preparación del Modelo

Pasos esenciales:

Convertir el modelo a TFLite (optimizado para móviles):


# app.py  
converter = tf.lite.TFLiteConverter.from_saved_model("sentiment_tf")  
converter.optimizations = [tf.lite.Optimize.DEFAULT]  
tflite_model = converter.convert()  

2. Reducir el vocabulario (¡ahorra hasta un 60% de espacio!):


# create_vocab.py  
common_words = ["feliz", "triste", "good", "bad", ...]  # Palabras clave  
small_vocab = {word: vocab[word] for word in common_words}  

2. Arquitectura del Proyecto

2.1 Estructura de Archivos


📂 app  
├─ 📂 assets  
│  ├─ sentiment.tflite    # Modelo convertido  
│  └─ vocab.json         # Vocabulario optimizado  
├─ 📂 modelo  
│  └─ SimpleTokenizer.kt # Tokenizador personalizado  
└─ 📂 ui  
   └─ MainActivity.kt     # Lógica principal

2.2 Tokenizador Eficiente

Clave para procesar texto en dispositivos móviles:


class SimpleTokenizer(context: Context) {  
    private val vocab: Map = // Carga desde JSON  
    
    fun tokenize(text: String): Pair {  
        // Lógica optimizada para móviles  
    }  
} 

3. Implementación con Jetpack Compose

3.1 Interfaz de Usuario


@Composable  
fun HomeScreen(tflite: Interpreter, tokenizer: SimpleTokenizer) {  
    var textInput by remember { mutableStateOf("") }  
    
    Column {  
        OutlinedTextField(  
            value = textInput,  
            onValueChange = { textInput = it },  
            label = { Text("¿Cómo te sientes hoy?") }  
        )  
        
        Button(onClick = { analyzeText(textInput) }) {  
            Text("Analizar")  
        }  
    }  
} 

3.2 Animaciones Profesionales



@Composable  
fun SplashScreen() {  
    val alpha by animateFloatAsState(targetValue = 1f)  
    Box(modifier = Modifier.alpha(alpha)) {  
        Icon(Icons.Default.Mood, "Splash")  
    }  
} 

4. Gestión del Modelo

4.1 Carga Segura del Modelo



private fun loadModelFile(context: Context): MappedByteBuffer {  
    val fileDescriptor = context.assets.openFd("sentiment.tflite")  
    return FileInputStream(fileDescriptor.fileDescriptor).channel  
        .map(FileChannel.MapMode.READ_ONLY, fileDescriptor.startOffset, fileDescriptor.declaredLength)  
}  
 

4.2 Inferencia en Tiempo Real



fun analyzeSentiment(text: String): SentimentResult {  
    val (inputIds, attentionMask) = tokenizer.tokenize(text)  
    val output = Array(1) { FloatArray(5) }  
    tflite.runForMultipleInputsOutputs(  
        arrayOf(inputIdsArray, attentionMaskArray),  
        mapOf(0 to output)  
    )  
    // Procesamiento de resultados  
}  

 

5. Buenas Prácticas

5.1 Optimización de Memoria:



override fun onDestroy() {  
    super.onDestroy()  
    tflite.close()  // ¡Evita memory leaks!  
} 

5.2 Manejo de Errores:



try {  
    val result = analyzeSentiment(text)  
} catch (e: Exception) {  
    Log.e("ML_ERROR", "Error en inferencia: ${e.message}")  
}  
 

6. Resultado Final

Conclusión

Integrar modelos de Hugging Face en Android nunca fue tan accesible. Con esta guía:

✔️ Aprovechas el poder de transformers en móviles

✔️ Creas interfaces modernas con Jetpack Compose

✔️ Optimizas recursos para dispositivos limitados

¡Tu turno! Clona el repositorio completo y personaliza el modelo para otros casos de uso: análisis de reseñas, filtrado de contenido, etc.

¿Preguntas? ¡Discutamos en los comentarios! 🚀

App.py>

 # app.py
from transformers import AutoTokenizer, TFAutoModelForSequenceClassification
import tensorflow as tf

# Cargar modelo y tokenizer
model_name = "nlptown/bert-base-multilingual-uncased-sentiment"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = TFAutoModelForSequenceClassification.from_pretrained(
    model_name,
    from_pt=True  # Convertir desde PyTorch si no hay modelo TF nativo
)

# Guardar modelo TensorFlow
model.save("sentiment_tf", save_format="tf")

# Convertir a TFLite
converter = tf.lite.TFLiteConverter.from_saved_model("sentiment_tf")
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_types = [tf.float16]  # Optimizar para móviles
converter.experimental_new_converter = True
tflite_model = converter.convert()

# Guardar modelo TFLite
with open("sentiment.tflite", "wb") as f:
    f.write(tflite_model)

# Guardar vocabulario del tokenizer
tokenizer.save_vocabulary(".")
print("Modelo y vocabulario guardados: sentiment.tflite, vocab.txt")

Crear_json

 # create_vocab.py
import json

# Leer vocab.txt
with open("vocab.txt", "r", encoding="utf-8") as f:
    vocab = {line.strip(): idx for idx, line in enumerate(f)}

# Seleccionar palabras comunes (español e inglés)
common_words = [
    "[PAD]", "[UNK]", "[CLS]", "[SEP]",
    "feliz", "triste", "bueno", "malo", "estoy", "qué", "día",
    "muy", "bien", "mal", "amor", "vida", "genial", "horrible",
    "happy", "sad", "good", "bad", "i", "am", "what", "day",
    "love", "life", "awesome", "terrible"
]
small_vocab = {word: vocab.get(word, vocab["[UNK]"]) for word in common_words}

# Guardar como vocab.json
with open("vocab.json", "w", encoding="utf-8") as f:
    json.dump(small_vocab, f, ensure_ascii=False)
print("vocab.json creado")

Android Studio

Paso a Paso y codigo complete

Paso #1

Copia los archivos: sentiment.tflite y vocab.json en el directory assets. Si assets no existe. Crealo.

Paso #2

Actualize el recurso strings.xml en el directorio res.



    SentimentApp
    Analiza tus emociones
    Inicio
    Historial
    Configuración
    Acerca de
    Menú
    Escribe cómo te sientes
    Analizar
    😊 ¡Estás de buena vibra!
    😔 Ánimo, brother!
    😐 Todo tranquilo.
    Confianza: %1$d%%
    No hay análisis guardados
    %1$s (%2$d%%)
    Eliminar
    Configura la sensibilidad del modelo (próximamente)
    Versión 1.0.0
    Analiza el sentimiento de tus textos con IA.\nDesarrollado con 💙 en 2025.

Pase # 3 Agrega las dependencia en el gradle

Dependencies

implementation("org.tensorflow:tensorflow-lite:2.16.1")
    implementation("org.tensorflow:tensorflow-lite-support:0.4.4")
    implementation("androidx.compose.material3:material3:1.2.1")
    implementation("androidx.compose.material:material-icons-extended:1.6.8")
    implementation("com.google.code.gson:gson:2.10.1")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") 

Sincroniza el Proyecto

Paso # 4 Crea la clase SimpleTokenizer en el paquete modelo. Cree el paquete modelo.

Tonizador



package com.example.sentimentapp.modelo



import android.content.Context
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.InputStreamReader
import java.util.Locale

class SimpleTokenizer(context: Context) {
    private val vocab: Map
    private val maxLength = 128

    init {
        vocab = try {
            val json = context.assets.open("vocab.json").use { inputStream ->
                InputStreamReader(inputStream).readText()
            }
            val type = object : TypeToken>() {}.type
            val rawVocab: Map = Gson().fromJson(json, type)
            rawVocab.mapValues { (_, value) ->
                when (value) {
                    is Double -> value.toInt()
                    is Int -> value
                    else -> throw IllegalStateException("Valor de vocab no es numérico: $value")
                }
            }
        } catch (e: Exception) {
            throw IllegalStateException("Error al cargar vocab.json: ${e.message}")
        }
    }

    fun tokenize(text: String): Pair {
        val tokens = text.lowercase(Locale.getDefault()).split(" ")
        val inputIds = mutableListOf(2) // [CLS]
        val attentionMask = mutableListOf(1)

        for (token in tokens) {
            if (inputIds.size >= maxLength - 1) break
            inputIds.add(vocab[token] ?: vocab["[UNK]"]!!)
            attentionMask.add(1)
        }
        inputIds.add(3) // [SEP]
        attentionMask.add(1)

        while (inputIds.size < maxLength) {
            inputIds.add(0) // [PAD]
            attentionMask.add(0)
        }

        return Pair(inputIds.toIntArray(), attentionMask.toIntArray())
    }
}

Paso # 5 Actualize el mainActivity con el codigo a continuacion

MainActivity


package com.example.sentimentapp


import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.tensorflow.lite.Interpreter
import java.io.FileInputStream
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel
import java.text.SimpleDateFormat
import java.util.*
import com.example.sentimentapp.modelo.SimpleTokenizer

class MainActivity : ComponentActivity() {
    private lateinit var tflite: Interpreter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val tfliteModel = loadModelFile(this)
        tflite = Interpreter(tfliteModel)
        val tokenizer = SimpleTokenizer(this)

        setContent {
            SentimentApp(tflite, tokenizer)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        tflite.close() // Liberar recursos
    }

    private fun loadModelFile(context: Context): MappedByteBuffer {
        val fileDescriptor = context.assets.openFd("sentiment.tflite")
        val inputStream = FileInputStream(fileDescriptor.fileDescriptor)
        val fileChannel = inputStream.channel
        val startOffset = fileDescriptor.startOffset
        val declaredLength = fileDescriptor.declaredLength
        return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength)
    }
}

@Composable
fun SentimentApp(tflite: Interpreter, tokenizer: SimpleTokenizer) {
    var showSplash by remember { mutableStateOf(true) }
    LaunchedEffect(Unit) {
        delay(2000)
        showSplash = false
    }
    MaterialTheme(
        colorScheme = darkColorScheme(
            primary = Color(0xFF00D4FF),
            secondary = Color(0xFFFF6B35),
            tertiary = Color(0xFF7B68EE),
            background = Color(0xFF0A0A0A),
            surface = Color(0xFF1A1A1A),
            onBackground = Color.White,
            onSurface = Color.White
        ),
        typography = Typography(
            bodyLarge = androidx.compose.ui.text.TextStyle(fontFamily = FontFamily.SansSerif),
            titleLarge = androidx.compose.ui.text.TextStyle(fontFamily = FontFamily.SansSerif)
        )
    ) {
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            if (showSplash) {
                SplashScreen()
            } else {
                MainAppContent(tflite, tokenizer)
            }
        }
    }
}

@Composable
fun SplashScreen() {
    val alpha by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(durationMillis = 1500)
    )
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(
                brush = androidx.compose.ui.graphics.Brush.verticalGradient(
                    colors = listOf(
                        MaterialTheme.colorScheme.primary,
                        MaterialTheme.colorScheme.tertiary
                    )
                )
            ),
        contentAlignment = Alignment.Center
    ) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.alpha(alpha)
        ) {
            Icon(
                Icons.Default.Mood,
                contentDescription = null,
                tint = Color.White,
                modifier = Modifier
                    .size(100.dp)
                    .clip(CircleShape)
                    .background(MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f))
                    .padding(16.dp)
            )
            Spacer(modifier = Modifier.height(16.dp))
            Text(
                text = stringResource(R.string.app_name),
                fontSize = 32.sp,
                fontWeight = FontWeight.Bold,
                color = Color.White,
                fontFamily = FontFamily.SansSerif
            )
            Text(
                text = stringResource(R.string.analyze_emotions),
                fontSize = 16.sp,
                color = Color.White.copy(alpha = 0.7f),
                fontFamily = FontFamily.SansSerif
            )
        }
    }
}

@Composable
fun MainAppContent(tflite: Interpreter, tokenizer: SimpleTokenizer) {
    var currentScreen by remember { mutableStateOf("home") }
    var analysisHistory by remember { mutableStateOf(listOf()) }
    val drawerState = rememberDrawerState(DrawerValue.Closed)
    val scope = rememberCoroutineScope()
    ModalNavigationDrawer(
        drawerState = drawerState,
        drawerContent = {
            DrawerContent(
                currentScreen = currentScreen,
                onNavigate = { route ->
                    currentScreen = route
                    scope.launch { drawerState.close() }
                }
            )
        }
    ) {
        when (currentScreen) {
            "home" -> HomeScreen(
                tflite = tflite,
                tokenizer = tokenizer,
                onAnalyze = { text ->
                    val result = analyzeSentiment(text, tflite, tokenizer)
                    analysisHistory = (analysisHistory + result).takeLast(100) // Limitar a 100
                },
                onMenuClick = { scope.launch { drawerState.open() } }
            )
            "history" -> HistoryScreen(
                history = analysisHistory,
                onHistoryChanged = { newHistory -> analysisHistory = newHistory },
                onMenuClick = { scope.launch { drawerState.open() } }
            )
            "settings" -> SettingsScreen(
                onMenuClick = { scope.launch { drawerState.open() } }
            )
            "about" -> AboutScreen(
                onMenuClick = { scope.launch { drawerState.open() } }
            )
        }
    }
}

data class SentimentResult(
    val text: String,
    val sentiment: String,
    val confidence: Float,
    val timestamp: Long = System.currentTimeMillis()
)

@Composable
fun DrawerContent(currentScreen: String, onNavigate: (String) -> Unit) {
    ModalDrawerSheet(
        modifier = Modifier
            .width(280.dp)
            .background(MaterialTheme.colorScheme.surface)
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 16.dp)
            ) {
                Icon(
                    Icons.Default.Mood,
                    contentDescription = null,
                    tint = MaterialTheme.colorScheme.primary,
                    modifier = Modifier.size(32.dp)
                )
                Spacer(modifier = Modifier.width(8.dp))
                Text(
                    text = stringResource(R.string.app_name),
                    fontSize = 20.sp,
                    fontWeight = FontWeight.Bold,
                    color = MaterialTheme.colorScheme.primary
                )
            }
            Divider(color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f))
            Spacer(modifier = Modifier.height(8.dp))
            DrawerItem(stringResource(R.string.home), Icons.Default.Home, "home", currentScreen, onNavigate)
            DrawerItem(stringResource(R.string.history), Icons.Default.History, "history", currentScreen, onNavigate)
            DrawerItem(stringResource(R.string.settings), Icons.Default.Settings, "settings", currentScreen, onNavigate)
            DrawerItem(stringResource(R.string.about), Icons.Default.Info, "about", currentScreen, onNavigate)
        }
    }
}

@Composable
fun DrawerItem(
    label: String,
    icon: ImageVector,
    route: String,
    currentScreen: String,
    onNavigate: (String) -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onNavigate(route) }
            .background(
                if (currentScreen == route) MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
                else Color.Transparent,
                shape = RoundedCornerShape(8.dp)
            )
            .padding(vertical = 12.dp, horizontal = 8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            imageVector = icon,
            contentDescription = null,
            tint = if (currentScreen == route) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
            modifier = Modifier.size(24.dp)
        )
        Spacer(modifier = Modifier.width(16.dp))
        Text(
            text = label,
            fontSize = 16.sp,
            color = if (currentScreen == route) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
            fontWeight = if (currentScreen == route) FontWeight.Bold else FontWeight.Medium
        )
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    tflite: Interpreter,
    tokenizer: SimpleTokenizer,
    onAnalyze: (String) -> Unit,
    onMenuClick: () -> Unit
) {
    var textInput by remember { mutableStateOf("") }
    var result by remember { mutableStateOf(null) }
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(stringResource(R.string.app_name), fontWeight = FontWeight.Bold) },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.surface,
                    titleContentColor = MaterialTheme.colorScheme.primary
                ),
                navigationIcon = {
                    IconButton(onClick = onMenuClick) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = stringResource(R.string.menu),
                            tint = MaterialTheme.colorScheme.primary
                        )
                    }
                }
            )
        }
    ) { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            OutlinedTextField(
                value = textInput,
                onValueChange = { textInput = it },
                label = { Text(stringResource(R.string.write_feelings)) },
                modifier = Modifier
                    .fillMaxWidth()
                    .height(120.dp),
                maxLines = 3
            )
            Spacer(modifier = Modifier.height(16.dp))
            Button(
                onClick = {
                    if (textInput.isNotBlank()) {
                        onAnalyze(textInput)
                        result = analyzeSentiment(textInput, tflite, tokenizer)
                    }
                },
                modifier = Modifier
                    .fillMaxWidth()
                    .height(56.dp),
                colors = ButtonDefaults.buttonColors(
                    containerColor = MaterialTheme.colorScheme.primary
                ),
                shape = RoundedCornerShape(8.dp)
            ) {
                Text(stringResource(R.string.analyze), fontWeight = FontWeight.Bold)
            }
            Spacer(modifier = Modifier.height(16.dp))
            result?.let { res ->
                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .animateContentSize(),
                    colors = CardDefaults.cardColors(
                        containerColor = when (res.sentiment) {
                            "Positivo" -> Color(0xFF4CAF50)
                            "Negativo" -> Color(0xFFF44336)
                            else -> Color(0xFF2196F3)
                        }.copy(alpha = 0.2f)
                    ),
                    shape = RoundedCornerShape(8.dp)
                ) {
                    Column(
                        modifier = Modifier.padding(16.dp),
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Text(
                            text = when (res.sentiment) {
                                "Positivo" -> stringResource(R.string.positive_mood)
                                "Negativo" -> stringResource(R.string.negative_mood)
                                else -> stringResource(R.string.neutral_mood)
                            },
                            fontSize = 20.sp,
                            fontWeight = FontWeight.Bold,
                            color = MaterialTheme.colorScheme.onSurface
                        )
                        Spacer(modifier = Modifier.height(8.dp))
                        Text(
                            text = stringResource(R.string.confidence, (res.confidence * 100).toInt()),
                            fontSize = 16.sp,
                            color = MaterialTheme.colorScheme.onSurface
                        )
                    }
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HistoryScreen(
    history: List,
    onHistoryChanged: (List) -> Unit,
    onMenuClick: () -> Unit
) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(stringResource(R.string.history), fontWeight = FontWeight.Bold) },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.surface,
                    titleContentColor = MaterialTheme.colorScheme.primary
                ),
                navigationIcon = {
                    IconButton(onClick = onMenuClick) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = stringResource(R.string.menu),
                            tint = MaterialTheme.colorScheme.primary
                        )
                    }
                }
            )
        }
    ) { padding ->
        if (history.isEmpty()) {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(padding)
                    .padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Icon(
                    Icons.Default.History,
                    contentDescription = null,
                    modifier = Modifier.size(64.dp),
                    tint = Color.Gray
                )
                Spacer(modifier = Modifier.height(16.dp))
                Text(
                    text = stringResource(R.string.no_history),
                    textAlign = TextAlign.Center,
                    color = Color.Gray
                )
            }
        } else {
            LazyColumn(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(padding)
                    .padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(history) { result ->
                    Card(
                        modifier = Modifier.fillMaxWidth(),
                        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
                        shape = RoundedCornerShape(8.dp)
                    ) {
                        Row(
                            modifier = Modifier.padding(16.dp),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            Column(modifier = Modifier.weight(1f)) {
                                Text(
                                    text = result.text,
                                    fontSize = 16.sp,
                                    fontWeight = FontWeight.Bold,
                                    color = MaterialTheme.colorScheme.onSurface
                                )
                                Text(
                                    text = stringResource(R.string.sentiment_result, result.sentiment, (result.confidence * 100).toInt()),
                                    fontSize = 14.sp,
                                    color = Color.Gray
                                )
                                Text(
                                    text = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault())
                                        .format(Date(result.timestamp)),
                                    fontSize = 12.sp,
                                    color = Color.Gray
                                )
                            }
                            IconButton(onClick = { onHistoryChanged(history - result) }) {
                                Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.delete), tint = Color.Red)
                            }
                        }
                    }
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(onMenuClick: () -> Unit) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(stringResource(R.string.settings), fontWeight = FontWeight.Bold) },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.surface,
                    titleContentColor = MaterialTheme.colorScheme.primary
                ),
                navigationIcon = {
                    IconButton(onClick = onMenuClick) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = stringResource(R.string.menu),
                            tint = MaterialTheme.colorScheme.primary
                        )
                    }
                }
            )
        }
    ) { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .padding(16.dp)
        ) {
            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
                shape = RoundedCornerShape(8.dp)
            ) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text(
                        text = stringResource(R.string.settings),
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Bold,
                        color = MaterialTheme.colorScheme.onSurface
                    )
                    Text(
                        text = stringResource(R.string.settings_coming_soon),
                        fontSize = 14.sp,
                        color = Color.Gray
                    )
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AboutScreen(onMenuClick: () -> Unit) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(stringResource(R.string.about), fontWeight = FontWeight.Bold) },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.surface,
                    titleContentColor = MaterialTheme.colorScheme.primary
                ),
                navigationIcon = {
                    IconButton(onClick = onMenuClick) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = stringResource(R.string.menu),
                            tint = MaterialTheme.colorScheme.primary
                        )
                    }
                }
            )
        }
    ) { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Icon(
                Icons.Default.Mood,
                contentDescription = null,
                tint = MaterialTheme.colorScheme.primary,
                modifier = Modifier
                    .size(80.dp)
                    .clip(CircleShape)
                    .background(MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f))
                    .padding(16.dp)
            )
            Spacer(modifier = Modifier.height(16.dp))
            Text(
                text = stringResource(R.string.app_name),
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold,
                color = MaterialTheme.colorScheme.primary
            )
            Text(
                text = stringResource(R.string.version),
                fontSize = 16.sp,
                color = Color.Gray
            )
            Spacer(modifier = Modifier.height(16.dp))
            Text(
                text = stringResource(R.string.about_description),
                fontSize = 14.sp,
                textAlign = TextAlign.Center,
                color = MaterialTheme.colorScheme.onSurface
            )
        }
    }
}

fun analyzeSentiment(text: String, tflite: Interpreter, tokenizer: SimpleTokenizer): SentimentResult {
    if (text.isBlank()) {
        return SentimentResult(text, "Neutral", 0f)
    }
    val trimmedText = text.trim().take(512) // Limitar longitud
    val (inputIds, attentionMask) = tokenizer.tokenize(trimmedText)

    val inputIdsArray = Array(1) { inputIds }
    val attentionMaskArray = Array(1) { attentionMask }
    val output = Array(1) { FloatArray(5) } // 5 clases (1 a 5 estrellas)

    tflite.runForMultipleInputsOutputs(
        arrayOf(inputIdsArray, attentionMaskArray),
        mapOf(0 to output)
    )

    val logits = output[0]
    val expLogits = logits.map { kotlin.math.exp(it) }
    val sumExpLogits = expLogits.sum()
    val probabilities = expLogits.map { it / sumExpLogits }

    val maxIndex = probabilities.indices.maxByOrNull { probabilities[it] } ?: 2
    val confidence = probabilities[maxIndex]

    val sentiment = when (maxIndex) {
        0, 1 -> "Negativo"
        2 -> "Neutral"
        3, 4 -> "Positivo"
        else -> "Neutral"
    }

    return SentimentResult(trimmedText, sentiment, confidence)
}
27 views

Comments

Leave a Comment