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
- Características destacadas:
- Splash Screen animado
- Historial persistente de análisis
- Interfaz Dark/Light automática
- Optimización para bajos recursos
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
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)
}
Comments