Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Currently Dicio answers questions about:
- **media**: play, pause, previous, next song
- **translation**: translate from/to any language with **Lingva** - _How do I say Football in German?_
- **wake word control**: turn on/off the wakeword - _Stop listening_
- **definitions**: get word definitions - _Define serendipity_

## Speech to text

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/kotlin/org/stypox/dicio/eval/SkillHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.stypox.dicio.settings.datastore.UserSettings
import org.stypox.dicio.settings.datastore.UserSettingsModule
import org.stypox.dicio.skills.calculator.CalculatorInfo
import org.stypox.dicio.skills.current_time.CurrentTimeInfo
import org.stypox.dicio.skills.definition.DefinitionInfo
import org.stypox.dicio.skills.fallback.text.TextFallbackInfo
import org.stypox.dicio.skills.listening.ListeningInfo
import org.stypox.dicio.skills.lyrics.LyricsInfo
Expand All @@ -45,6 +46,7 @@ class SkillHandler @Inject constructor(
WeatherInfo,
SearchInfo,
LyricsInfo,
DefinitionInfo,
OpenInfo,
CalculatorInfo,
NavigationInfo,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.stypox.dicio.skills.definition

import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MenuBook
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import org.dicio.skill.context.SkillContext
import org.dicio.skill.skill.Skill
import org.dicio.skill.skill.SkillInfo
import org.stypox.dicio.R
import org.stypox.dicio.sentences.Sentences

object DefinitionInfo : SkillInfo("definition") {
override fun name(context: Context) =
context.getString(R.string.skill_name_definition)

override fun sentenceExample(context: Context) =
context.getString(R.string.skill_sentence_example_definition)

@Composable
override fun icon() =
rememberVectorPainter(Icons.Default.MenuBook)

override fun isAvailable(ctx: SkillContext): Boolean {
return Sentences.Definition[ctx.sentencesLanguage] != null
}

override fun build(ctx: SkillContext): Skill<*> {
return DefinitionSkill(DefinitionInfo, Sentences.Definition[ctx.sentencesLanguage]!!)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.stypox.dicio.skills.definition

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.dicio.skill.context.SkillContext
import org.dicio.skill.skill.SkillOutput
import org.stypox.dicio.R
import org.stypox.dicio.io.graphical.HeadlineSpeechSkillOutput
import org.stypox.dicio.util.getString

sealed interface DefinitionOutput : SkillOutput {
data class Success(
val word: String,
val definitions: List<PartOfSpeechDefinition>
) : DefinitionOutput {
override fun getSpeechOutput(ctx: SkillContext): String {
val firstDefinition = definitions.firstOrNull()?.definitions?.firstOrNull()
return if (firstDefinition != null) {
ctx.getString(
R.string.skill_definition_found,
word,
definitions.first().partOfSpeech,
firstDefinition
)
} else {
ctx.getString(R.string.skill_definition_not_found, word)
}
}

@Composable
override fun GraphicalOutput(ctx: SkillContext) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = word,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))

definitions.forEach { posDefinition ->
Text(
text = posDefinition.partOfSpeech,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(4.dp))

posDefinition.definitions.forEachIndexed { index, definition ->
Text(
text = "${index + 1}. $definition",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(4.dp))
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}

data class NotFound(
val word: String
) : DefinitionOutput, HeadlineSpeechSkillOutput {
override fun getSpeechOutput(ctx: SkillContext): String = ctx.getString(
R.string.skill_definition_not_found, word
)
}

data class NetworkError(
val word: String
) : DefinitionOutput, HeadlineSpeechSkillOutput {
override fun getSpeechOutput(ctx: SkillContext): String = ctx.getString(
R.string.skill_definition_network_error, word
)
}

data class ParseError(
val word: String
) : DefinitionOutput, HeadlineSpeechSkillOutput {
override fun getSpeechOutput(ctx: SkillContext): String = ctx.getString(
R.string.skill_definition_parse_error, word
)
}
}

data class PartOfSpeechDefinition(
val partOfSpeech: String,
val definitions: List<String>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.stypox.dicio.skills.definition

import org.dicio.skill.context.SkillContext
import org.dicio.skill.skill.SkillInfo
import org.dicio.skill.skill.SkillOutput
import org.dicio.skill.standard.StandardRecognizerData
import org.dicio.skill.standard.StandardRecognizerSkill
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import org.stypox.dicio.sentences.Sentences.Definition
import org.stypox.dicio.util.ConnectionUtils
import java.io.FileNotFoundException
import java.io.IOException
import java.util.Locale

class DefinitionSkill(correspondingSkillInfo: SkillInfo, data: StandardRecognizerData<Definition>) :
StandardRecognizerSkill<Definition>(correspondingSkillInfo, data) {

override suspend fun generateOutput(ctx: SkillContext, inputData: Definition): SkillOutput {
val word: String = when (inputData) {
is Definition.Query -> inputData.word ?: return DefinitionOutput.NotFound(word = "")
}

// Get language code from locale (e.g., "en", "fr", "de")
val languageCode = ctx.locale.language.lowercase(Locale.getDefault())

// Build Wiktionary API URL based on user's locale
val apiUrl = "https://$languageCode.wiktionary.org/api/rest_v1/page/definition/" +
ConnectionUtils.percentEncode(word.trim())

return try {
val definitionData = ConnectionUtils.getPageJson(apiUrl)
parseDefinitions(word, definitionData)
} catch (e: FileNotFoundException) {
// 404 - word not found in Wiktionary
DefinitionOutput.NotFound(word = word)
} catch (e: IOException) {
// Network error
DefinitionOutput.NetworkError(word = word)
} catch (e: JSONException) {
// Failed to parse response
DefinitionOutput.ParseError(word = word)
}
}

private fun parseDefinitions(word: String, data: JSONObject): SkillOutput {
try {
// Wiktionary API returns language-specific definitions
// The structure is: { "en": [ { "partOfSpeech": "...", "definitions": [...] }, ... ] }
// or sometimes just an array at the root level

val languageKeys = data.keys()
if (!languageKeys.hasNext()) {
return DefinitionOutput.NotFound(word = word)
}

// Get the first language's definitions (usually matches the Wiktionary language)
val firstLanguageKey = languageKeys.next()
val definitionsArray: JSONArray = data.getJSONArray(firstLanguageKey)

if (definitionsArray.length() == 0) {
return DefinitionOutput.NotFound(word = word)
}

val posDefinitions = mutableListOf<PartOfSpeechDefinition>()

// Parse each part of speech
for (i in 0 until definitionsArray.length()) {
val posObject = definitionsArray.getJSONObject(i)
val partOfSpeech = posObject.optString("partOfSpeech", "Unknown")
val defsArray = posObject.optJSONArray("definitions")

if (defsArray != null && defsArray.length() > 0) {
val definitions = mutableListOf<String>()

// Only take the first 3 definitions per part of speech
val maxDefinitions = minOf(3, defsArray.length())
for (j in 0 until maxDefinitions) {
val defObject = defsArray.getJSONObject(j)
val definition = defObject.optString("definition", "")
if (definition.isNotEmpty()) {
// Clean up the definition text (remove HTML tags if any)
definitions.add(cleanDefinitionText(definition))
}
}

if (definitions.isNotEmpty()) {
posDefinitions.add(
PartOfSpeechDefinition(
partOfSpeech = partOfSpeech,
definitions = definitions
)
)
}
}
}

return if (posDefinitions.isNotEmpty()) {
DefinitionOutput.Success(word = word, definitions = posDefinitions)
} else {
DefinitionOutput.NotFound(word = word)
}
} catch (e: JSONException) {
return DefinitionOutput.ParseError(word = word)
}
}

private fun cleanDefinitionText(text: String): String {
// Remove HTML tags and extra whitespace
return text
.replace(Regex("<[^>]*>"), "") // Remove HTML tags
.replace(Regex("\\s+"), " ") // Normalize whitespace
.trim()
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@
<string name="stt_say_something">Say something…</string>
<string name="stt_did_not_understand">I could not understand, try again</string>
<string name="stt_popup">Speech to text popup</string>
<string name="skill_name_definition">Word definition</string>
<string name="skill_sentence_example_definition">Define serendipity</string>
<string name="skill_name_search">Search</string>
<string name="skill_sentence_example_search">Search how to feed a cat</string>
<string name="skill_name_weather">Weather</string>
Expand Down Expand Up @@ -229,6 +231,10 @@
<string name="skill_translation_to">to</string>
<string name="skill_name_translation">Translate</string>
<string name="skill_sentence_example_translation">Translate Hola from Spanish to English</string>
<string name="skill_definition_found">%1$s, %2$s: %3$s</string>
<string name="skill_definition_not_found">Could not find definition for %1$s</string>
<string name="skill_definition_network_error">Could not connect to Wiktionary to look up %1$s</string>
<string name="skill_definition_parse_error">Could not understand the definition for %1$s</string>
<string name="pref_weather_temperature_unit">Temperature unit</string>
<string name="use_system_default">Use system default</string>
<string name="pref_weather_unit_system_instructions">Uses the unit configured in \"System settings > System > Languages &amp; input > Regional preferences\"</string>
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/sentences/en/definition.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query:
- define .word.
- (what does|what is|what s|whats) .word. mean|means?
- (what is|what s|whats) the? (definition|meaning) (of|for) .word.
- (the? definition|meaning) of .word.
- .word. definition|meaning
- look up .word.
- (can you )?(define|explain) .word. (for me|to me)?
8 changes: 8 additions & 0 deletions app/src/main/sentences/skill_definitions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ skills:
sentences:
- id: query

- id: definition
specificity: high
sentences:
- id: query
captures:
- id: word
type: string

- id: calculator
specificity: medium
sentences:
Expand Down
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/full_description.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Dicio answers questions about:
<li><b>jokes</b>: tells you a joke - <i>Tell me a joke</i></li>
<li><b>media</b>: play, pause, previous, next song - <i>Next Song</i></li>
<li><b>translation</b>: translate from/to any language with <b>Lingva</b> - <i>How do I say Football in German?</i></li>
<li><b>definitions</b>: get word definitions - <i>Define serendipity</i></li>
</ul>

Dicio can receive input through a text box or through <a href="https://github.com/alphacep/vosk-api/">Vosk</a> <i>speech to text</i>, and can talk using toasts or the Android <i>speech synthesis</i> engine. <b>Interactive graphical output</b> is provided by skills when they answer a question.
Expand Down