Skip to content

Commit c2dd837

Browse files
authored
Merge pull request #982 from maxrave-dev/dev
v0.2.16/development - New version
2 parents 520a1f7 + 70f9954 commit c2dd837

File tree

97 files changed

+5450
-3807
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+5450
-3807
lines changed

MediaServiceCore

Submodule MediaServiceCore updated 29 files

aiService/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

aiService/build.gradle.kts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import com.android.build.gradle.internal.tasks.CompileArtProfileTask
2+
3+
plugins {
4+
alias(libs.plugins.android.library)
5+
alias(libs.plugins.kotlin.android)
6+
kotlin("plugin.serialization")
7+
8+
}
9+
10+
android {
11+
namespace = "org.simpmusic.aiservice"
12+
compileSdk = 35
13+
14+
defaultConfig {
15+
minSdk = 26
16+
17+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18+
consumerProguardFiles("consumer-rules.pro")
19+
}
20+
21+
buildTypes {
22+
release {
23+
isMinifyEnabled = false
24+
proguardFiles(
25+
getDefaultProguardFile("proguard-android-optimize.txt"),
26+
"proguard-rules.pro"
27+
)
28+
}
29+
}
30+
compileOptions {
31+
sourceCompatibility = JavaVersion.VERSION_11
32+
targetCompatibility = JavaVersion.VERSION_11
33+
}
34+
kotlinOptions {
35+
jvmTarget = "11"
36+
}
37+
}
38+
39+
dependencies {
40+
41+
implementation(libs.core.ktx)
42+
implementation(libs.appcompat)
43+
implementation(libs.material)
44+
testImplementation(libs.junit)
45+
androidTestImplementation(libs.androidx.junit)
46+
androidTestImplementation(libs.espresso.core)
47+
48+
implementation(libs.ktor.client.core)
49+
implementation(libs.ktor.client.okhttp)
50+
implementation(libs.gemini.kotlin)
51+
52+
implementation(project(":lyricsProviders"))
53+
}
54+
tasks.withType<CompileArtProfileTask> {
55+
enabled = false
56+
}

aiService/consumer-rules.pro

Whitespace-only changes.

aiService/proguard-rules.pro

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-keep class kotlinx.coroutines.CoroutineExceptionHandler
2+
-keep class kotlinx.coroutines.internal.MainDispatcherFactory
3+
# Keep `Companion` object fields of serializable classes.
4+
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
5+
-if @kotlinx.serialization.Serializable class **
6+
-keepclassmembers class <1> {
7+
static <1>$Companion Companion;
8+
}
9+
10+
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
11+
-if @kotlinx.serialization.Serializable class ** {
12+
static **$* *;
13+
}
14+
-keepclassmembers class <2>$<3> {
15+
kotlinx.serialization.KSerializer serializer(...);
16+
}
17+
18+
# Keep `INSTANCE.serializer()` of serializable objects.
19+
-if @kotlinx.serialization.Serializable class ** {
20+
public static ** INSTANCE;
21+
}
22+
-keepclassmembers class <1> {
23+
public static <1> INSTANCE;
24+
kotlinx.serialization.KSerializer serializer(...);
25+
}
26+
27+
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
28+
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
29+
30+
# Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes
31+
# See also https://github.com/Kotlin/kotlinx.serialization/issues/1900
32+
-dontnote kotlinx.serialization.**
33+
34+
# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes.
35+
# If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning.
36+
# However, since in this case they will not be used, we can disable these warnings
37+
-dontwarn kotlinx.serialization.internal.ClassValueReferences
38+
-dontwarn org.slf4j.impl.StaticLoggerBinder
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
<uses-permission android:name="android.permission.INTERNET"/>
4+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
5+
</manifest>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.simpmusic.aiservice
2+
3+
import com.maxrave.lyricsproviders.models.lyrics.Lyrics
4+
5+
class AiClient() {
6+
private var aiService: AiService? = null
7+
var host = AIHost.GEMINI
8+
set(value) {
9+
field = value
10+
apiKey?.let {
11+
aiService = AiService(
12+
aiHost = value,
13+
apiKey = it
14+
)
15+
}
16+
}
17+
var apiKey: String? = null
18+
set(value) {
19+
field = value
20+
aiService = if (value != null) {
21+
AiService(
22+
aiHost = host,
23+
apiKey = value
24+
)
25+
} else {
26+
null
27+
}
28+
}
29+
var customModelId: String? = null
30+
set(value) {
31+
field = value
32+
aiService = if (apiKey != null) {
33+
AiService(
34+
aiHost = host,
35+
apiKey = apiKey!!,
36+
customModelId = value
37+
)
38+
} else {
39+
null
40+
}
41+
}
42+
43+
suspend fun translateLyrics(
44+
inputLyrics: Lyrics,
45+
targetLanguage: String
46+
): Result<Lyrics> = runCatching {
47+
aiService?.translateLyrics(inputLyrics, targetLanguage).also {
48+
if (it?.lyrics?.lines?.map { it.words }?.containsAll(
49+
inputLyrics.lyrics?.lines?.map { it.words } ?: emptyList()
50+
) == true) {
51+
throw IllegalStateException("Translation failed or returned empty lyrics.")
52+
}
53+
}
54+
?: throw IllegalStateException("AI service is not initialized. Please set host and apiKey.")
55+
}
56+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package org.simpmusic.aiservice
2+
3+
import com.aallam.openai.api.chat.ChatCompletion
4+
import com.aallam.openai.api.chat.ChatResponseFormat
5+
import com.aallam.openai.api.chat.JsonSchema
6+
import com.aallam.openai.api.chat.chatCompletionRequest
7+
import com.aallam.openai.api.model.ModelId
8+
import com.aallam.openai.client.OpenAI
9+
import com.aallam.openai.client.OpenAIHost.Companion.Gemini
10+
import com.maxrave.lyricsproviders.models.lyrics.Lyrics
11+
import kotlinx.serialization.json.Json
12+
import kotlinx.serialization.json.JsonObject
13+
import kotlinx.serialization.json.JsonPrimitive
14+
import kotlinx.serialization.json.add
15+
import kotlinx.serialization.json.buildJsonArray
16+
import kotlinx.serialization.json.buildJsonObject
17+
import kotlinx.serialization.json.put
18+
import kotlinx.serialization.json.putJsonArray
19+
import kotlinx.serialization.json.putJsonObject
20+
21+
class AiService(
22+
private val aiHost: AIHost = AIHost.GEMINI,
23+
private val apiKey: String,
24+
private val customModelId: String? = null
25+
) {
26+
private val json = Json {
27+
ignoreUnknownKeys = true
28+
isLenient = true
29+
}
30+
private val openAI: OpenAI by lazy {
31+
when (aiHost) {
32+
AIHost.GEMINI -> OpenAI(host = Gemini, token = apiKey)
33+
AIHost.OPENAI -> OpenAI(token = apiKey)
34+
}
35+
}
36+
37+
private val model by lazy {
38+
if (!customModelId.isNullOrEmpty()) {
39+
ModelId(customModelId)
40+
} else {
41+
when (aiHost) {
42+
AIHost.GEMINI -> ModelId("gemini-2.0-flash-lite")
43+
AIHost.OPENAI -> ModelId("gpt-4o")
44+
}
45+
}
46+
}
47+
48+
suspend fun translateLyrics(
49+
inputLyrics: Lyrics,
50+
targetLanguage: String
51+
): Lyrics {
52+
val request = chatCompletionRequest {
53+
this.model = this@AiService.model
54+
responseFormat = ChatResponseFormat.jsonSchema(aiResponseJsonSchema)
55+
messages {
56+
system {
57+
content =
58+
"You are a translation assistant. Translate the below JSON-serialized lyrics into the target language while preserving the exact same JSON structure. Only translate the actual text fields such as `words` and `syllables` (if present). Do not change keys, nesting, timestamps, or any other metadata.\\n\\nThe output must be valid JSON with the same structure as the input. Do not include explanations or extra commentary—only return the resulting JSON."
59+
}
60+
user {
61+
content {
62+
text("Target language: $targetLanguage")
63+
}
64+
content {
65+
text("Input lyrics: ${json.encodeToString(inputLyrics)}")
66+
}
67+
}
68+
}
69+
}
70+
val completion: ChatCompletion = openAI.chatCompletion(request)
71+
val jsonContent =
72+
completion.choices
73+
.firstOrNull()
74+
?.message
75+
?.content ?: throw IllegalStateException("No response from AI")
76+
val jsonData =
77+
Regex(
78+
"```json\\s*([\\s\\S]*?)```"
79+
).find(jsonContent)
80+
?.groups
81+
?.firstOrNull()
82+
?.value ?: jsonContent
83+
val aiResponse =
84+
json.decodeFromString<Lyrics>(
85+
jsonData
86+
.replace("```json", "")
87+
.replace("```", "")
88+
)
89+
return aiResponse
90+
}
91+
92+
companion object {
93+
private val translationJsonSchema: JsonObject =
94+
buildJsonObject {
95+
put("type", "object")
96+
putJsonObject("properties") {
97+
putJsonObject("lyrics") {
98+
put("type", "object")
99+
putJsonObject("properties") {
100+
putJsonObject("lines") {
101+
put("type", "array")
102+
putJsonObject("items") {
103+
put("type", "object")
104+
putJsonObject("properties") {
105+
putJsonObject("startTimeMs") {
106+
put("type", "string")
107+
}
108+
putJsonObject("endTimeMs") {
109+
put("type", "string")
110+
}
111+
putJsonObject("syllables") {
112+
put("type", "array")
113+
putJsonObject("items") {
114+
put("type", "string")
115+
}
116+
}
117+
putJsonObject("words") {
118+
put("type", "string")
119+
}
120+
}
121+
putJsonArray("required") {
122+
add("startTimeMs")
123+
add("endTimeMs")
124+
add("words")
125+
// `syllables` is optional if it's nullable
126+
}
127+
}
128+
}
129+
putJsonObject("syncType") {
130+
put("type", "string")
131+
}
132+
}
133+
putJsonArray("required") {
134+
add("lines")
135+
add("syncType")
136+
}
137+
}
138+
}
139+
putJsonArray("required") {
140+
add("lyrics")
141+
}
142+
}
143+
private val aiResponseJsonSchema = JsonSchema(
144+
name = "ai_translation_schema", // Give your schema a name
145+
schema = translationJsonSchema,
146+
strict = true // Recommended for better adherence
147+
)
148+
}
149+
}
150+
151+
enum class AIHost {
152+
GEMINI,
153+
OPENAI
154+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.simpmusic.aiservice
2+
3+
import com.maxrave.lyricsproviders.models.lyrics.Line
4+
import com.maxrave.lyricsproviders.models.lyrics.Lyrics
5+
import kotlinx.coroutines.Dispatchers
6+
import kotlinx.coroutines.runBlocking
7+
import org.junit.Before
8+
import org.junit.Test
9+
10+
class AiClientTest {
11+
// private val aiClient = AiClient()
12+
// @Before
13+
// fun setUp() {
14+
// aiClient.host = AIHost.GEMINI
15+
// aiClient.apiKey = ""
16+
// }
17+
//
18+
// @Test
19+
// fun testTranslateLyrics() {
20+
// // Example lyrics input
21+
// val inputLyrics = Lyrics(
22+
// lyrics = Lyrics.LyricsX(
23+
// lines = listOf(
24+
// Line(
25+
// startTimeMs = "0",
26+
// endTimeMs = "1000",
27+
// words = "Hello world",
28+
// syllables = listOf("Hel", "lo", "world")
29+
// ),
30+
// Line(
31+
// startTimeMs = "1000",
32+
// endTimeMs = "2000",
33+
// words = "This is a test",
34+
// syllables = listOf("This", "is", "a", "test")
35+
// )
36+
// ),
37+
// syncType = "LINE_SYNCED"
38+
// )
39+
// )
40+
// val targetLanguage = "vi" // Vietnamese
41+
//
42+
// runBlocking(Dispatchers.IO) {
43+
// // Call the translateLyrics method
44+
// aiClient.translateLyrics(inputLyrics, targetLanguage).onSuccess {
45+
// // Print the translated lyrics
46+
// println("Translated Lyrics: $it")
47+
// }.onFailure { exception ->
48+
// // Print the error message
49+
// println("Error translating lyrics: ${exception.message}")
50+
// }
51+
// }
52+
// }
53+
}

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ dependencies {
237237
// Other module
238238
implementation(project(mapOf("path" to ":kotlinYtmusicScraper")))
239239
implementation(project(mapOf("path" to ":spotify")))
240+
implementation(project(mapOf("path" to ":aiService")))
240241

241242
implementation(libs.lifecycle.livedata.ktx)
242243
implementation(libs.lifecycle.viewmodel.ktx)

0 commit comments

Comments
 (0)