summaryrefslogtreecommitdiff
path: root/examples/llama.android/app/src/main/java/com
diff options
context:
space:
mode:
authorNeuman Vong <neuman.vong@gmail.com>2024-01-17 00:47:34 +1100
committerGitHub <noreply@github.com>2024-01-16 15:47:34 +0200
commit862f5e41ab1fdf12d6f59455aad3f5dd8258f805 (patch)
treef114f6cdfb36f0cc119952b0fcd1eca507b28d8e /examples/llama.android/app/src/main/java/com
parent3a48d558a69c88ac17efcaa5900cd9eb19596ac4 (diff)
android : introduce starter project example (#4926)
* Introduce starter project for Android Based on examples/llama.swiftui. * Add github workflow * Set NDK version * Only build arm64-v8a in CI * Sync bench code * Rename CI prop to skip-armeabi-v7a * Remove unused tests
Diffstat (limited to 'examples/llama.android/app/src/main/java/com')
-rw-r--r--examples/llama.android/app/src/main/java/com/example/llama/Downloadable.kt119
-rw-r--r--examples/llama.android/app/src/main/java/com/example/llama/Llm.kt172
-rw-r--r--examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt154
-rw-r--r--examples/llama.android/app/src/main/java/com/example/llama/MainViewModel.kt104
-rw-r--r--examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Color.kt11
-rw-r--r--examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Theme.kt70
-rw-r--r--examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Type.kt34
7 files changed, 664 insertions, 0 deletions
diff --git a/examples/llama.android/app/src/main/java/com/example/llama/Downloadable.kt b/examples/llama.android/app/src/main/java/com/example/llama/Downloadable.kt
new file mode 100644
index 00000000..78c231ae
--- /dev/null
+++ b/examples/llama.android/app/src/main/java/com/example/llama/Downloadable.kt
@@ -0,0 +1,119 @@
+package com.example.llama
+
+import android.app.DownloadManager
+import android.net.Uri
+import android.util.Log
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableDoubleStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.core.database.getLongOrNull
+import androidx.core.net.toUri
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.io.File
+
+data class Downloadable(val name: String, val source: Uri, val destination: File) {
+ companion object {
+ @JvmStatic
+ private val tag: String? = this::class.qualifiedName
+
+ sealed interface State
+ data object Ready: State
+ data class Downloading(val id: Long): State
+ data class Downloaded(val downloadable: Downloadable): State
+ data class Error(val message: String): State
+
+ @JvmStatic
+ @Composable
+ fun Button(viewModel: MainViewModel, dm: DownloadManager, item: Downloadable) {
+ var status: State by remember {
+ mutableStateOf(
+ if (item.destination.exists()) Downloaded(item)
+ else Ready
+ )
+ }
+ var progress by remember { mutableDoubleStateOf(0.0) }
+
+ val coroutineScope = rememberCoroutineScope()
+
+ suspend fun waitForDownload(result: Downloading, item: Downloadable): State {
+ while (true) {
+ val cursor = dm.query(DownloadManager.Query().setFilterById(result.id))
+
+ if (cursor == null) {
+ Log.e(tag, "dm.query() returned null")
+ return Error("dm.query() returned null")
+ }
+
+ if (!cursor.moveToFirst() || cursor.count < 1) {
+ cursor.close()
+ Log.i(tag, "cursor.moveToFirst() returned false or cursor.count < 1, download canceled?")
+ return Ready
+ }
+
+ val pix = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
+ val tix = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
+ val sofar = cursor.getLongOrNull(pix) ?: 0
+ val total = cursor.getLongOrNull(tix) ?: 1
+ cursor.close()
+
+ if (sofar == total) {
+ return Downloaded(item)
+ }
+
+ progress = (sofar * 1.0) / total
+
+ delay(1000L)
+ }
+ }
+
+ fun onClick() {
+ when (val s = status) {
+ is Downloaded -> {
+ viewModel.load(item.destination.path)
+ }
+
+ is Downloading -> {
+ coroutineScope.launch {
+ status = waitForDownload(s, item)
+ }
+ }
+
+ else -> {
+ item.destination.delete()
+
+ val request = DownloadManager.Request(item.source).apply {
+ setTitle("Downloading model")
+ setDescription("Downloading model: ${item.name}")
+ setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
+ setDestinationUri(item.destination.toUri())
+ }
+
+ viewModel.log("Saving ${item.name} to ${item.destination.path}")
+ Log.i(tag, "Saving ${item.name} to ${item.destination.path}")
+
+ val id = dm.enqueue(request)
+ status = Downloading(id)
+ onClick()
+ }
+ }
+ }
+
+ Button(onClick = { onClick() }, enabled = status !is Downloading) {
+ when (status) {
+ is Downloading -> Text(text = "Downloading ${(progress * 100).toInt()}%")
+ is Downloaded -> Text("Load ${item.name}")
+ is Ready -> Text("Download ${item.name}")
+ is Error -> Text("Download ${item.name}")
+ }
+ }
+ }
+
+ }
+}
diff --git a/examples/llama.android/app/src/main/java/com/example/llama/Llm.kt b/examples/llama.android/app/src/main/java/com/example/llama/Llm.kt
new file mode 100644
index 00000000..5f327037
--- /dev/null
+++ b/examples/llama.android/app/src/main/java/com/example/llama/Llm.kt
@@ -0,0 +1,172 @@
+package com.example.llama
+
+import android.util.Log
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.withContext
+import java.util.concurrent.Executors
+import kotlin.concurrent.thread
+
+class Llm {
+ private val tag: String? = this::class.simpleName
+
+ private val threadLocalState: ThreadLocal<State> = ThreadLocal.withInitial { State.Idle }
+
+ private val runLoop: CoroutineDispatcher = Executors.newSingleThreadExecutor {
+ thread(start = false, name = "Llm-RunLoop") {
+ Log.d(tag, "Dedicated thread for native code: ${Thread.currentThread().name}")
+
+ // No-op if called more than once.
+ System.loadLibrary("llama-android")
+
+ // Set llama log handler to Android
+ log_to_android()
+ backend_init(false)
+
+ Log.d(tag, system_info())
+
+ it.run()
+ }.apply {
+ uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, exception: Throwable ->
+ Log.e(tag, "Unhandled exception", exception)
+ }
+ }
+ }.asCoroutineDispatcher()
+
+ private val nlen: Int = 64
+
+ private external fun log_to_android()
+ private external fun load_model(filename: String): Long
+ private external fun free_model(model: Long)
+ private external fun new_context(model: Long): Long
+ private external fun free_context(context: Long)
+ private external fun backend_init(numa: Boolean)
+ private external fun backend_free()
+ private external fun free_batch(batch: Long)
+ private external fun new_batch(nTokens: Int, embd: Int, nSeqMax: Int): Long
+ private external fun bench_model(
+ context: Long,
+ model: Long,
+ batch: Long,
+ pp: Int,
+ tg: Int,
+ pl: Int,
+ nr: Int
+ ): String
+
+ private external fun system_info(): String
+
+ private external fun completion_init(
+ context: Long,
+ batch: Long,
+ text: String,
+ nLen: Int
+ ): Int
+
+ private external fun completion_loop(
+ context: Long,
+ batch: Long,
+ nLen: Int,
+ ncur: IntVar
+ ): String
+
+ private external fun kv_cache_clear(context: Long)
+
+ suspend fun bench(pp: Int, tg: Int, pl: Int, nr: Int = 1): String {
+ return withContext(runLoop) {
+ when (val state = threadLocalState.get()) {
+ is State.Loaded -> {
+ Log.d(tag, "bench(): $state")
+ bench_model(state.context, state.model, state.batch, pp, tg, pl, nr)
+ }
+
+ else -> throw IllegalStateException("No model loaded")
+ }
+ }
+ }
+
+ suspend fun load(pathToModel: String) {
+ withContext(runLoop) {
+ when (threadLocalState.get()) {
+ is State.Idle -> {
+ val model = load_model(pathToModel)
+ if (model == 0L) throw IllegalStateException("load_model() failed")
+
+ val context = new_context(model)
+ if (context == 0L) throw IllegalStateException("new_context() failed")
+
+ val batch = new_batch(512, 0, 1)
+ if (batch == 0L) throw IllegalStateException("new_batch() failed")
+
+ Log.i(tag, "Loaded model $pathToModel")
+ threadLocalState.set(State.Loaded(model, context, batch))
+ }
+ else -> throw IllegalStateException("Model already loaded")
+ }
+ }
+ }
+
+ fun send(message: String): Flow<String> = flow {
+ when (val state = threadLocalState.get()) {
+ is State.Loaded -> {
+ val ncur = IntVar(completion_init(state.context, state.batch, message, nlen))
+ while (ncur.value <= nlen) {
+ val str = completion_loop(state.context, state.batch, nlen, ncur)
+ if (str.isEmpty()) {
+ break
+ }
+ emit(str)
+ }
+ kv_cache_clear(state.context)
+ }
+ else -> {}
+ }
+ }.flowOn(runLoop)
+
+ /**
+ * Unloads the model and frees resources.
+ *
+ * This is a no-op if there's no model loaded.
+ */
+ suspend fun unload() {
+ withContext(runLoop) {
+ when (val state = threadLocalState.get()) {
+ is State.Loaded -> {
+ free_context(state.context)
+ free_model(state.model)
+ free_batch(state.batch)
+
+ threadLocalState.set(State.Idle)
+ }
+ else -> {}
+ }
+ }
+ }
+
+ companion object {
+ private class IntVar(value: Int) {
+ @Volatile
+ var value: Int = value
+ private set
+
+ fun inc() {
+ synchronized(this) {
+ value += 1
+ }
+ }
+ }
+
+ private sealed interface State {
+ data object Idle: State
+ data class Loaded(val model: Long, val context: Long, val batch: Long): State
+ }
+
+ // Enforce only one instance of Llm.
+ private val _instance: Llm = Llm()
+
+ fun instance(): Llm = _instance
+ }
+}
diff --git a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt
new file mode 100644
index 00000000..9da04f7d
--- /dev/null
+++ b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt
@@ -0,0 +1,154 @@
+package com.example.llama
+
+import android.app.ActivityManager
+import android.app.DownloadManager
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.StrictMode
+import android.os.StrictMode.VmPolicy
+import android.text.format.Formatter
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Button
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.core.content.getSystemService
+import com.example.llama.ui.theme.LlamaAndroidTheme
+import java.io.File
+
+class MainActivity(
+ activityManager: ActivityManager? = null,
+ downloadManager: DownloadManager? = null,
+ clipboardManager: ClipboardManager? = null,
+): ComponentActivity() {
+ private val tag: String? = this::class.simpleName
+
+ private val activityManager by lazy { activityManager ?: getSystemService<ActivityManager>()!! }
+ private val downloadManager by lazy { downloadManager ?: getSystemService<DownloadManager>()!! }
+ private val clipboardManager by lazy { clipboardManager ?: getSystemService<ClipboardManager>()!! }
+
+ private val viewModel: MainViewModel by viewModels()
+
+ // Get a MemoryInfo object for the device's current memory status.
+ private fun availableMemory(): ActivityManager.MemoryInfo {
+ return ActivityManager.MemoryInfo().also { memoryInfo ->
+ activityManager.getMemoryInfo(memoryInfo)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ StrictMode.setVmPolicy(
+ VmPolicy.Builder(StrictMode.getVmPolicy())
+ .detectLeakedClosableObjects()
+ .build()
+ )
+
+ val free = Formatter.formatFileSize(this, availableMemory().availMem)
+ val total = Formatter.formatFileSize(this, availableMemory().totalMem)
+
+ viewModel.log("Current memory: $free / $total")
+ viewModel.log("Downloads directory: ${getExternalFilesDir(null)}")
+
+ val extFilesDir = getExternalFilesDir(null)
+
+ val models = listOf(
+ Downloadable(
+ "Phi-2 7B (Q4_0, 1.6 GiB)",
+ Uri.parse("https://huggingface.co/ggml-org/models/resolve/main/phi-2/ggml-model-q4_0.gguf?download=true"),
+ File(extFilesDir, "phi-2-q4_0.gguf"),
+ ),
+ Downloadable(
+ "TinyLlama 1.1B (f16, 2.2 GiB)",
+ Uri.parse("https://huggingface.co/ggml-org/models/resolve/main/tinyllama-1.1b/ggml-model-f16.gguf?download=true"),
+ File(extFilesDir, "tinyllama-1.1-f16.gguf"),
+ ),
+ Downloadable(
+ "Phi 2 DPO (Q3_K_M, 1.48 GiB)",
+ Uri.parse("https://huggingface.co/TheBloke/phi-2-dpo-GGUF/resolve/main/phi-2-dpo.Q3_K_M.gguf?download=true"),
+ File(extFilesDir, "phi-2-dpo.Q3_K_M.gguf")
+ ),
+ )
+
+ setContent {
+ LlamaAndroidTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ MainCompose(
+ viewModel,
+ clipboardManager,
+ downloadManager,
+ models,
+ )
+ }
+
+ }
+ }
+ }
+}
+
+@Composable
+fun MainCompose(
+ viewModel: MainViewModel,
+ clipboard: ClipboardManager,
+ dm: DownloadManager,
+ models: List<Downloadable>
+) {
+ Column {
+ val scrollState = rememberLazyListState()
+
+ Box(modifier = Modifier.weight(1f)) {
+ LazyColumn(state = scrollState) {
+ items(viewModel.messages) {
+ Text(
+ it,
+ style = MaterialTheme.typography.bodyLarge.copy(color = LocalContentColor.current),
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+ }
+ }
+ OutlinedTextField(
+ value = viewModel.message,
+ onValueChange = { viewModel.updateMessage(it) },
+ label = { Text("Message") },
+ )
+ Row {
+ Button({ viewModel.send() }) { Text("Send") }
+ Button({ viewModel.bench(8, 4, 1) }) { Text("Bench") }
+ Button({ viewModel.clear() }) { Text("Clear") }
+ Button({
+ viewModel.messages.joinToString("\n").let {
+ clipboard.setPrimaryClip(ClipData.newPlainText("", it))
+ }
+ }) { Text("Copy") }
+ }
+
+ Column {
+ for (model in models) {
+ Downloadable.Button(viewModel, dm, model)
+ }
+ }
+ }
+}
diff --git a/examples/llama.android/app/src/main/java/com/example/llama/MainViewModel.kt b/examples/llama.android/app/src/main/java/com/example/llama/MainViewModel.kt
new file mode 100644
index 00000000..be95e222
--- /dev/null
+++ b/examples/llama.android/app/src/main/java/com/example/llama/MainViewModel.kt
@@ -0,0 +1,104 @@
+package com.example.llama
+
+import android.util.Log
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.launch
+
+class MainViewModel(private val llm: Llm = Llm.instance()): ViewModel() {
+ companion object {
+ @JvmStatic
+ private val NanosPerSecond = 1_000_000_000.0
+ }
+
+ private val tag: String? = this::class.simpleName
+
+ var messages by mutableStateOf(listOf("Initializing..."))
+ private set
+
+ var message by mutableStateOf("")
+ private set
+
+ override fun onCleared() {
+ super.onCleared()
+
+ viewModelScope.launch {
+ try {
+ llm.unload()
+ } catch (exc: IllegalStateException) {
+ messages += exc.message!!
+ }
+ }
+ }
+
+ fun send() {
+ val text = message
+ message = ""
+
+ // Add to messages console.
+ messages += text
+ messages += ""
+
+ viewModelScope.launch {
+ llm.send(text)
+ .catch {
+ Log.e(tag, "send() failed", it)
+ messages += it.message!!
+ }
+ .collect { messages = messages.dropLast(1) + (messages.last() + it) }
+ }
+ }
+
+ fun bench(pp: Int, tg: Int, pl: Int, nr: Int = 1) {
+ viewModelScope.launch {
+ try {
+ val start = System.nanoTime()
+ val warmupResult = llm.bench(pp, tg, pl, nr)
+ val end = System.nanoTime()
+
+ messages += warmupResult
+
+ val warmup = (end - start).toDouble() / NanosPerSecond
+ messages += "Warm up time: $warmup seconds, please wait..."
+
+ if (warmup > 5.0) {
+ messages += "Warm up took too long, aborting benchmark"
+ return@launch
+ }
+
+ messages += llm.bench(512, 128, 1, 3)
+ } catch (exc: IllegalStateException) {
+ Log.e(tag, "bench() failed", exc)
+ messages += exc.message!!
+ }
+ }
+ }
+
+ fun load(pathToModel: String) {
+ viewModelScope.launch {
+ try {
+ llm.load(pathToModel)
+ messages += "Loaded $pathToModel"
+ } catch (exc: IllegalStateException) {
+ Log.e(tag, "load() failed", exc)
+ messages += exc.message!!
+ }
+ }
+ }
+
+ fun updateMessage(newMessage: String) {
+ message = newMessage
+ }
+
+ fun clear() {
+ messages = listOf()
+ }
+
+ fun log(message: String) {
+ messages += message
+ }
+}
diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Color.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Color.kt
new file mode 100644
index 00000000..40c30e8d
--- /dev/null
+++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.example.llama.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Theme.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Theme.kt
new file mode 100644
index 00000000..e742220a
--- /dev/null
+++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Theme.kt
@@ -0,0 +1,70 @@
+package com.example.llama.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun LlamaAndroidTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Type.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Type.kt
new file mode 100644
index 00000000..0b87946c
--- /dev/null
+++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.example.llama.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)