feat(i18n): 번역 작업 큐와 언어 감지 캐시를 도입한다
조회 중 외부 번역 호출을 줄이고 누락 번역을 비동기 job으로 처리한다.
This commit is contained in:
@@ -14,8 +14,6 @@ import kr.co.vividnext.sodalive.chat.character.dto.RecentCharacter
|
||||
import kr.co.vividnext.sodalive.chat.character.dto.RecentCharactersResponse
|
||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
|
||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslation
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRenderedPayload
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterBackground
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterDetail
|
||||
@@ -24,8 +22,8 @@ import kr.co.vividnext.sodalive.chat.room.service.ChatRoomService
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
@@ -46,7 +44,7 @@ class ChatCharacterController(
|
||||
private val characterCommentService: CharacterCommentService,
|
||||
private val curationQueryService: CharacterCurationQueryService,
|
||||
|
||||
private val translationService: PapagoTranslationService,
|
||||
private val resourceTranslationJobScheduler: ResourceTranslationJobScheduler,
|
||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
||||
|
||||
private val langContext: LangContext,
|
||||
@@ -212,89 +210,11 @@ class ChatCharacterController(
|
||||
tags = payload.tags
|
||||
)
|
||||
} else {
|
||||
val texts = mutableListOf<String>()
|
||||
texts.add(character.name)
|
||||
texts.add(character.description)
|
||||
texts.add(character.gender ?: "")
|
||||
|
||||
val hasPersonality = personality != null
|
||||
if (hasPersonality) {
|
||||
texts.add(personality!!.trait)
|
||||
texts.add(personality.description)
|
||||
}
|
||||
|
||||
val hasBackground = background != null
|
||||
if (hasBackground) {
|
||||
texts.add(background!!.topic)
|
||||
texts.add(background.description)
|
||||
}
|
||||
|
||||
texts.add(tags)
|
||||
|
||||
val sourceLanguage = character.languageCode ?: "ko"
|
||||
|
||||
val response = translationService.translate(
|
||||
request = TranslateRequest(
|
||||
texts = texts,
|
||||
sourceLanguage = sourceLanguage,
|
||||
targetLanguage = langContext.lang.code
|
||||
)
|
||||
resourceTranslationJobScheduler.scheduleResourceTranslation(
|
||||
resourceType = LanguageTranslationTargetType.CHARACTER,
|
||||
resourceId = character.id!!,
|
||||
targetLanguage = langContext.lang.code
|
||||
)
|
||||
|
||||
val translatedTexts = response.translatedText
|
||||
if (translatedTexts.size == texts.size) {
|
||||
var index = 0
|
||||
|
||||
val translatedName = translatedTexts[index++]
|
||||
val translatedDescription = translatedTexts[index++]
|
||||
val translatedGender = translatedTexts[index++]
|
||||
|
||||
var translatedPersonality: TranslatedAiCharacterPersonality? = null
|
||||
if (hasPersonality) {
|
||||
translatedPersonality = TranslatedAiCharacterPersonality(
|
||||
trait = translatedTexts[index++],
|
||||
description = translatedTexts[index++]
|
||||
)
|
||||
}
|
||||
|
||||
var translatedBackground: TranslatedAiCharacterBackground? = null
|
||||
if (hasBackground) {
|
||||
translatedBackground = TranslatedAiCharacterBackground(
|
||||
topic = translatedTexts[index++],
|
||||
description = translatedTexts[index++]
|
||||
)
|
||||
}
|
||||
|
||||
val translatedTags = translatedTexts[index]
|
||||
|
||||
val payload = AiCharacterTranslationRenderedPayload(
|
||||
name = translatedName,
|
||||
description = translatedDescription,
|
||||
gender = translatedGender,
|
||||
personalityTrait = translatedPersonality?.trait ?: "",
|
||||
personalityDescription = translatedPersonality?.description ?: "",
|
||||
backgroundTopic = translatedBackground?.topic ?: "",
|
||||
backgroundDescription = translatedBackground?.description ?: "",
|
||||
tags = translatedTags
|
||||
)
|
||||
|
||||
val entity = AiCharacterTranslation(
|
||||
characterId = character.id!!,
|
||||
locale = langContext.lang.code,
|
||||
renderedPayload = payload
|
||||
)
|
||||
|
||||
aiCharacterTranslationRepository.save(entity)
|
||||
|
||||
translated = TranslatedAiCharacterDetail(
|
||||
name = translatedName,
|
||||
description = translatedDescription,
|
||||
gender = translatedGender,
|
||||
personality = translatedPersonality,
|
||||
background = translatedBackground,
|
||||
tags = translatedTags
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
package kr.co.vividnext.sodalive.chat.original.service
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.original.OriginalWork
|
||||
import kr.co.vividnext.sodalive.chat.original.translation.OriginalWorkTranslation
|
||||
import kr.co.vividnext.sodalive.chat.original.translation.OriginalWorkTranslationPayload
|
||||
import kr.co.vividnext.sodalive.chat.original.translation.OriginalWorkTranslationRepository
|
||||
import kr.co.vividnext.sodalive.chat.original.translation.TranslatedOriginalWork
|
||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
|
||||
import org.slf4j.LoggerFactory
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class OriginalWorkTranslationService(
|
||||
private val translationRepository: OriginalWorkTranslationRepository,
|
||||
private val papagoTranslationService: PapagoTranslationService
|
||||
private val resourceTranslationJobScheduler: ResourceTranslationJobScheduler
|
||||
) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
/**
|
||||
* 원작의 언어와 요청 언어가 다를 때 번역 데이터를 확보하고 반환한다.
|
||||
* - 기존 번역이 있으면 그대로 사용
|
||||
* - 없으면 파파고 번역 수행 후 저장
|
||||
* - 없으면 누락 번역 job 예약 후 null 반환
|
||||
* - 실패/불필요 시 null 반환
|
||||
*/
|
||||
@Transactional
|
||||
@@ -55,70 +49,11 @@ class OriginalWorkTranslationService(
|
||||
}
|
||||
}
|
||||
|
||||
// 파파고 번역 수행
|
||||
return try {
|
||||
val tags = originalWork.tagMappings.map { it.tag.tag }.filter { it.isNotBlank() }
|
||||
val texts = buildList {
|
||||
add(originalWork.title)
|
||||
add(originalWork.contentType)
|
||||
add(originalWork.category)
|
||||
add(originalWork.description)
|
||||
addAll(tags)
|
||||
}
|
||||
|
||||
val response = papagoTranslationService.translate(
|
||||
TranslateRequest(
|
||||
texts = texts,
|
||||
sourceLanguage = source,
|
||||
targetLanguage = target
|
||||
)
|
||||
)
|
||||
|
||||
val out = response.translatedText
|
||||
if (out.isEmpty()) return null
|
||||
|
||||
// 앞 4개는 필드, 나머지는 태그
|
||||
val title = out.getOrNull(0)?.trim().orEmpty()
|
||||
val contentType = out.getOrNull(1)?.trim().orEmpty()
|
||||
val category = out.getOrNull(2)?.trim().orEmpty()
|
||||
val description = out.getOrNull(3)?.trim().orEmpty()
|
||||
val translatedTags = if (out.size > 4) {
|
||||
out.drop(4).map { it.trim() }.filter { it.isNotEmpty() }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val hasAny = title.isNotEmpty() || contentType.isNotEmpty() ||
|
||||
category.isNotEmpty() || description.isNotEmpty() || translatedTags.isNotEmpty()
|
||||
if (!hasAny) return null
|
||||
|
||||
val payload = OriginalWorkTranslationPayload(
|
||||
title = title,
|
||||
contentType = contentType,
|
||||
category = category,
|
||||
description = description,
|
||||
tags = translatedTags
|
||||
)
|
||||
|
||||
val entity = existed?.apply { this.renderedPayload = payload }
|
||||
?: OriginalWorkTranslation(
|
||||
originalWorkId = originalWork.id!!,
|
||||
locale = target,
|
||||
renderedPayload = payload
|
||||
)
|
||||
|
||||
translationRepository.save(entity)
|
||||
|
||||
TranslatedOriginalWork(
|
||||
title = title,
|
||||
contentType = contentType,
|
||||
category = category,
|
||||
description = description,
|
||||
tags = translatedTags
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
log.warn("Failed to translate OriginalWork(id={}) from {} to {}: {}", originalWork.id, source, target, e.message)
|
||||
null
|
||||
}
|
||||
resourceTranslationJobScheduler.scheduleResourceTranslation(
|
||||
resourceType = LanguageTranslationTargetType.ORIGINAL_WORK,
|
||||
resourceId = originalWork.id!!,
|
||||
targetLanguage = target
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user