Skip to content

Commit

Permalink
feat: processorの分割 (#156)
Browse files Browse the repository at this point in the history
* feat: processorの分割 (WIP)

* feat: add reply, sticker test

* feat: add inline voice, markdown format test

* feat: add more markdown format test

* chore: add todo

* feat: add mention to channel, user, role test

* chore: うまくいかない attachment download test

* chore: remove attachment test

* test: add replaceMessageUrl, replaceChannelUrl, replaceEventDirectUrl

* test: add replaceTweetUrl

* test: add replaceEmoji, replaceInviteUrl, replaceSteamAppUrl, replaceYouTubeUrl, replaceYouTubePlaylistUrl, replaceGoogleSearchUrl, replaceUrl

* chore: コメントとか追加

* test: テストケース名の変更。関数のみ記載だった箇所は明確化し、文とするため末尾にピリオドを追加

* fix: Narrator.process内の処理をforからfold文に変更

* fix: isImmediatelyをisImmediatelyReadに変更

* fix: isImmediateReadに変更

* fix: shouldIgnoreOnをまとめた

* Update src/test/kotlin/SteamTest.kt

Co-authored-by: yuuaHP <[email protected]>

* Update src/test/kotlin/TwitterTest.kt

Co-authored-by: yuuaHP <[email protected]>

* Update src/test/kotlin/TwitterTest.kt

Co-authored-by: yuuaHP <[email protected]>

* Update src/test/kotlin/TwitterTest.kt

Co-authored-by: yuuaHP <[email protected]>

* Apply suggestions from code review

Co-authored-by: yuuaHP <[email protected]>

* fix: fileとattachmentの混在をattachmentに寄せる

* Apply suggestions from code review

Co-authored-by: yuuaHP <[email protected]>

* fix: ignoreのtestでbefore, afterでcontextを分割

* Apply suggestions from code review

Co-authored-by: yuuaHP <[email protected]>

* Apply suggestions from code review

Co-authored-by: yuuaHP <[email protected]>

* Apply suggestions from code review

Co-authored-by: yuuaHP <[email protected]>

* Apply suggestions from code review

Co-authored-by: yuuaHP <[email protected]>

* fix: テスト後フォルダ削除処理の修正

* chore: レビューを受けたテストケース名の更新

---------

Co-authored-by: yuuaHP <[email protected]>
  • Loading branch information
book000 and yuuahp authored Jul 7, 2024
1 parent dfae678 commit deb1b05
Show file tree
Hide file tree
Showing 37 changed files with 2,536 additions and 464 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
kotlin("jvm") version "1.9.24"
kotlin("plugin.serialization") version "2.0.0"
id("io.kotest.multiplatform") version "5.0.2"
application
}

Expand Down
12 changes: 12 additions & 0 deletions src/main/kotlin/com/jaoafa/vcspeaker/StringUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.jaoafa.vcspeaker

object StringUtils {
fun String.substringByCodePoints(start: Int, end: Int): String {
val codePoints = codePoints().toArray()
return String(codePoints.copyOfRange(start, end), 0, end - start)
}

fun String.lengthByCodePoints(): Long {
return codePoints().count()
}
}
4 changes: 2 additions & 2 deletions src/main/kotlin/com/jaoafa/vcspeaker/commands/ParseCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import com.jaoafa.vcspeaker.tools.discord.DiscordExtensions.respondEmbed
import com.jaoafa.vcspeaker.tools.discord.DiscordExtensions.successColor
import com.jaoafa.vcspeaker.tools.discord.Options
import com.jaoafa.vcspeaker.tools.discord.SlashCommandExtensions.publicSlashCommand
import com.jaoafa.vcspeaker.tts.TextProcessor
import com.jaoafa.vcspeaker.tts.Token
import com.jaoafa.vcspeaker.tts.processors.ReplacerProcessor
import com.kotlindiscord.kord.extensions.checks.anyGuild
import com.kotlindiscord.kord.extensions.commands.converters.impl.string
import com.kotlindiscord.kord.extensions.extensions.Extension
Expand Down Expand Up @@ -109,7 +109,7 @@ class ParseCommand : Extension() {
}

// step 2: apply alias
val tokens = TextProcessor.replacers.fold(mutableListOf(Token(text))) { tokens, replacer ->
val tokens = ReplacerProcessor().replacers.fold(mutableListOf(Token(text))) { tokens, replacer ->
replacer.replace(tokens, guildId)
}

Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/com/jaoafa/vcspeaker/features/Ignore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.jaoafa.vcspeaker.features

import com.jaoafa.vcspeaker.stores.IgnoreStore
import com.jaoafa.vcspeaker.stores.IgnoreType
import dev.kord.common.entity.Snowflake

object Ignore {
fun String.shouldIgnoreOn(guildId: Snowflake) =
IgnoreStore.filter(guildId).any {
when (it.type) {
IgnoreType.Equals -> this == it.search
IgnoreType.Contains -> contains(it.search)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ data class GuildScheduledEvent(
@SerialName("sku_ids")
val skuIds: List<String>,
@SerialName("auto_start")
val autoStart: Boolean,
val autoStart: Boolean = false,
@SerialName("entity_metadata")
val entityMetadata: EntityMetadata,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class SteamAppDetail(
val success: Boolean,
val data: SteamAppDetailData
val data: SteamAppDetailData?
)

@Serializable
Expand Down
5 changes: 4 additions & 1 deletion src/main/kotlin/com/jaoafa/vcspeaker/tools/Steam.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ object Steam {
HttpStatusCode.OK -> {
val mapSerializer = MapSerializer(String.serializer(), SteamAppDetail.serializer())
val map = JsonConfiguration.decodeFromString(mapSerializer, response.body())
map[appId] ?: return null
if (map.isEmpty()) return null
val app = map[appId]
if (app?.success == false) return null
app
}

else -> null
Expand Down
10 changes: 9 additions & 1 deletion src/main/kotlin/com/jaoafa/vcspeaker/tools/VisionApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ data class VisionTextAnnotation(
val vertices: List<VisionVertex>,
)

class VisionApi {
object VisionApi {
/**
* Vision APIにリクエストを送信し、VisionTextAnnotationのリストを取得する。
*
Expand Down Expand Up @@ -170,4 +170,12 @@ class VisionApi {

/** File の加算拡張関数: `this + file` で `this` と `file` を連結する。 */
private operator fun File.plus(file: File) = File(this, file.name)

// Systemがモックできないので、ラップする
// https://toranoana-lab.hatenablog.com/entry/2023/09/26/100000
// https://github.com/mockk/mockk/issues/98
/** Google Application Credentials が存在するか確認する */
fun isGoogleAppCredentialsExist(): Boolean {
return File(System.getenv("GOOGLE_APPLICATION_CREDENTIALS")).exists()
}
}
71 changes: 0 additions & 71 deletions src/main/kotlin/com/jaoafa/vcspeaker/tts/TextProcessor.kt

This file was deleted.

44 changes: 30 additions & 14 deletions src/main/kotlin/com/jaoafa/vcspeaker/tts/narrators/Narrator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import com.jaoafa.vcspeaker.VCSpeaker
import com.jaoafa.vcspeaker.stores.GuildStore
import com.jaoafa.vcspeaker.stores.VoiceStore
import com.jaoafa.vcspeaker.tools.discord.DiscordExtensions.asChannelOf
import com.jaoafa.vcspeaker.tts.MessageProcessor.processMessage
import com.jaoafa.vcspeaker.tools.getClassesIn
import com.jaoafa.vcspeaker.tts.Scheduler
import com.jaoafa.vcspeaker.tts.TextProcessor.extractInlineVoice
import com.jaoafa.vcspeaker.tts.TextProcessor.processText
import com.jaoafa.vcspeaker.tts.TrackType
import com.jaoafa.vcspeaker.tts.Voice
import com.jaoafa.vcspeaker.tts.narrators.Narrators.narrator
import com.jaoafa.vcspeaker.tts.processors.BaseProcessor
import com.kotlindiscord.kord.extensions.utils.addReaction
import com.kotlindiscord.kord.extensions.utils.deleteOwnReaction
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer
Expand All @@ -24,6 +23,7 @@ import dev.kord.voice.VoiceConnection
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.reflect.full.createInstance

/**
* 読み上げを管理するクラスです。
Expand Down Expand Up @@ -79,6 +79,7 @@ class Narrator @OptIn(KordVoice::class) constructor(
suspend fun scheduleAsUser(message: Message) =
schedule(
message = message,
text = message.content,
voice = VoiceStore.byIdOrDefault(message.author!!.id),
guild = message.getGuild(),
type = TrackType.User
Expand All @@ -93,32 +94,47 @@ class Narrator @OptIn(KordVoice::class) constructor(
*/
private suspend fun schedule(
message: Message? = null,
text: String? = null,
text: String,
voice: Voice,
guild: Guild,
type: TrackType
) {
val content = processMessage(message) ?: text ?: return

// extract inline voice
val (extractedText, inlineVoice) = extractInlineVoice(content, voice)

// process text
val replacedText = processText(guildId, extractedText) ?: return

if (replacedText.isBlank()) return
val (processText, processVoice) = process(message, text, voice)

CoroutineScope(Dispatchers.Default).launch {
message?.addReaction("👀")
}

scheduler.queue(message, replacedText, inlineVoice, guild, type)
scheduler.queue(message, processText, processVoice, guild, type)

CoroutineScope(Dispatchers.Default).launch {
message?.deleteOwnReaction("👀")
}
}

/**
* テキストを処理します。
*
* @param message メッセージ
* @param text 処理するテキスト
* @param voice 処理する音声
* @return 処理後のテキストと音声
*/
suspend fun process(message: Message? = null, text: String, voice: Voice): Pair<String, Voice> {
val processors = getClassesIn<BaseProcessor>("com.jaoafa.vcspeaker.tts.processors")
.mapNotNull {
it.kotlin.createInstance()
}.sortedBy { it.priority }

return processors.fold(text to voice) { (processText, processVoice), processor ->
val (processedText, processedVoice) = processor.process(message, processText, processVoice)
if (processor.isCancelled()) return@fold processText to processVoice // キャンセルされた場合は、このProcessorだけをスキップする
if (processor.isImmediately()) return processedText to processedVoice // 即座に返す場合は、このProcessorを最後とし読み上げる

processedText to processedVoice
}
}

/**
* 読み上げ中のメッセージをスキップします。
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,28 @@
package com.jaoafa.vcspeaker.tts
package com.jaoafa.vcspeaker.tts.processors

import com.jaoafa.vcspeaker.StringUtils.substringByCodePoints
import com.jaoafa.vcspeaker.stores.VisionApiCounterStore
import com.jaoafa.vcspeaker.tools.VisionApi
import com.jaoafa.vcspeaker.tts.TextProcessor.substringByCodePoints
import com.jaoafa.vcspeaker.tts.Voice
import com.kotlindiscord.kord.extensions.utils.download
import com.sksamuel.scrimage.ImmutableImage
import com.sksamuel.scrimage.nio.PngWriter
import dev.kord.core.behavior.reply
import dev.kord.core.entity.Message
import dev.kord.core.entity.effectiveName
import dev.kord.rest.builder.message.EmbedBuilder
import dev.kord.rest.builder.message.addFile
import java.io.File
import java.nio.file.Path

object MessageProcessor {
suspend fun processMessage(message: Message?): String? {
if (message == null) return null

val stickers = message.stickers
val attachments = message.attachments
val content = message.content
val replyReadText = if (message.referencedMessage != null) {
val replyToName = message.referencedMessage!!.author?.effectiveName ?: "だれか"
"$replyToName への返信、"
} else {
""
}

if (stickers.isNotEmpty())
return replyReadText + stickers.joinToString(" ") { "スタンプ ${it.name}" }
class AttachmentProcessor : BaseProcessor() {
override val priority = 30

if (attachments.isNotEmpty()) {
val fileText = getReadFileText(message)

// ファイルのみ送信の場合、返信先とファイル名を読み上げる。ファイル送信と合わせてメッセージがある場合は、返信先 + メッセージ + ファイル名を読み上げる
return if (content.isBlank()) replyReadText + fileText else "$replyReadText$content $fileText"
}
override suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice> {
val attachments = message?.attachments ?: return content to voice
if (attachments.isEmpty()) return content to voice

return replyReadText + content.ifBlank { null }
val fileText = getReadFileText(message) ?: return content to voice
return if (content.isBlank()) fileText to voice else "$content $fileText" to voice
}

/**
Expand All @@ -60,16 +44,15 @@ object MessageProcessor {
return "添付ファイル ${firstAttachment.filename} $moreFileRead"
}

if (!File(System.getenv("GOOGLE_APPLICATION_CREDENTIALS")).exists()) {
if (!VisionApi.isGoogleAppCredentialsExist()) {
return "画像ファイル ${firstAttachment.filename} $moreFileRead"
}

// 画像解析を行う
val isSpoiler = firstAttachment.isSpoiler
val binaryArray = firstAttachment.download()
try {
val visionApi = VisionApi()
val textAnnotations = visionApi.getTextAnnotations(binaryArray)
val textAnnotations = VisionApi.getTextAnnotations(binaryArray)
// 改行は半角スペースに置換する
val firstDescription = textAnnotations.firstOrNull()?.description?.replace("\n", " ") ?: ""
val shortDescription =
Expand All @@ -78,7 +61,7 @@ object MessageProcessor {
if (firstDescription.length > 1000) firstDescription.substringByCodePoints(0, 1000) + "..." else firstDescription

// 画像解析結果を返信する
val editedImage = visionApi.drawTextAnnotations(binaryArray)
val editedImage = VisionApi.drawTextAnnotations(binaryArray)
val filePath = editedImage.outputTempFile(isSpoiler)
val visionApiCounterStore = VisionApiCounterStore.get()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.jaoafa.vcspeaker.tts.processors

import com.jaoafa.vcspeaker.tts.Voice
import dev.kord.core.entity.Message

/**
* メッセージを処理する基底クラス
*/
abstract class BaseProcessor {
abstract val priority: Int
private var isCancelled: Boolean = false
private var isImmediateRead: Boolean = false

abstract suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice>

fun isCancelled() = isCancelled
fun isImmediately() = isImmediateRead

fun cancel() {
isCancelled = true
}

fun immediateRead() {
isImmediateRead = true
}
}
Loading

0 comments on commit deb1b05

Please sign in to comment.