本周工作:本周我主要实现了基于DeepSeek的故事翻译以及自动标注动重点词汇功能。包括一键对生成的故事提供逐句翻译功能和自动对故事中涉及的重点词汇和短语进行标注的功能。

对于英语基础较差的用户来说,生成的故事阅读起来比较困难,通过自动逐句翻译并解析句式,能够帮助用户不仅记住单词,还能了解它们在句子中的实际应用,提升语言的综合能力。然后再通过逐句解析和重点词汇标注,用户可以及时纠正自己在理解和使用词汇时的错误,避免误学。

思路:得益于对DeepSeek历史上下文的维护,只需要通过不同接口的组合使用,即可实现对生成的故事进行翻译、对重点词汇进行标注和解析的功能。

一、故事逐句翻译功能

在生成故事后,下一步是将英文故事逐句翻译成中文。通过连续对话,模型可以理解和记住先前生成的内容,进而逐句翻译。在用户要求翻译时,模型会对每一段话进行翻译,用户可以看到单词和句子的多语言对比,从而更好地理解它们的含义和用法。

实现:

在生成故事后添加故事信息到临时对话储存变量中,将其作为历史信息发送给DeepSeek API:

//在生成故事后添加故事信息到临时对话储存变量中
mutex.withLock {
                        viewModelScope.launch {
                            GlobalTracker.appendContent("$buffer\n")
                        }
                    }
...
uiState.fullResponse=globalContent;//携带生成的故事信息
...
val history = uiState.fullResponse.split("\n\n\n").mapNotNull {
                val parts = it.split(": ", limit = 2)
                if (parts.size == 2) parts[1] else null
            }.chunked(2).map {
                it[0] to (it.getOrNull(1) ?: "")
            }

            deepSeekService.streamResponse(history, prompt)

示例请求如下:

{
  "model": "deepseek-chat",
  "messages": [
    { "role": "system", "content": "你是一个英语学习助手,..." },
    { "role": "user", "content": "请将刚才的故事逐句翻译成中文。" },
    { "role": "assistant", "content": "好的!这是你请求的故事翻译:\n\n1. (故事内容)..." }
  ],
  "max_tokens": 1024,
  "temperature": 0.7
}

二、自动标注重点词汇功能

生成翻译后,再次返回历史信息,让DeepSeek标注重点词汇,并对句子结构进行解析。这样一来每当模型生成一句话时,不仅会翻译它,还会标记出每个新词汇的翻译并解析句子的语法结构。

实现:

在生成故事以及生成翻译后分别添加这些信息到临时对话储存变量中,将其作为历史信息发送给DeepSeek API:

//在生成故事后添加故事信息到临时对话储存变量中
mutex.withLock {
                        viewModelScope.launch {
                            GlobalTracker.appendContent("$buffer\n")
                        }
                    }
...
//在生成中文翻译后添加故事信息到临时对话储存变量中
mutex.withLock {
                        viewModelScope.launch {
                            GlobalTracker.appendContent("$buffer\n")
                        }
                    }
...
uiState.fullResponse=globalContent;//携带故事及其翻译信息
...
val history = uiState.fullResponse.split("\n\n\n").mapNotNull {
                val parts = it.split(": ", limit = 2)
                if (parts.size == 2) parts[1] else null
            }.chunked(2).map {
                it[0] to (it.getOrNull(1) ?: "")
            }

            deepSeekService.streamResponse(history, prompt)

示例请求如下:

{
  "model": "deepseek-chat",
  "messages": [
    { "role": "system", "content": "你是一个英语学习助手,..." },
    { "role": "user", "content": "请在故事中标注每个新单词的翻译,并解析句式。" }
  ],{ "role": "assistant", "content": "好的!这是你请求的重点词汇标注:\n\n1. (故事以及翻译内容)..." }
  "max_tokens": 1024,
  "temperature": 0.7
}

小tips:互斥锁(mutex.withLock)的使用

在每次添加历史对话记录时,需要使用互斥锁确保线程安全,如:

mutex.withLock {
                        viewModelScope.launch {
                            GlobalTracker.appendContent("$buffer\n")
                        }
                    }

写入数据库前:

fun appendContent(role: String, message: String) {
        CoroutineScope(Dispatchers.IO).launch {
            mutex.withLock {
                _globalContent += "\n\n\n$role: $message"
                database.chatHistoryDao().insertMessage(
                    ChatHistoryEntity(
                        role = role,
                        content = message
                    )
                )
            }
        }
    }

GlobalTracker 存储了共享的历史对话记录 _globalContent,这个变量是由多个协程进行读写操作的。在用户学单词的过程中,存在至少两条对话主线:

  • 生成故事->生成故事分镜描述
  • 生成故事->生成翻译->生成重点词汇标注

也就是说,其中的 appendContent 方法可能被多个协程并发调用,或者同时多个协程在不同的生命周期中执行。如果两个协程同时写入 _globalContent,没有线程安全保护,就会导致其中一个协程的电话内容覆盖另一个对话内容。结果导致历史不完整,生成的内容不正确。

Kotlin中的Mutux就是互斥锁用于控制协程之间对共享资源的访问,可以确保在任意时刻只有一个线程能够访问受保护的资源,而其他线程必须等待该资源释放,避免导致资源冲突和数据不一致的问题。

三、界面设计

将界面分为“故事”界面和“漫画”界面,并使用HorizontalPager组件来实现左右滑动的效果:

HorizontalPager(
                pageCount = 2,
                state = pagerState,
                pageSpacing = 16.dp,
                modifier = Modifier.weight(1f)
            ) { pageIndex ->
                when (pageIndex) {
                    0 -> {
                        // 漫画界面
                        ComicPage(imageUrl, generationState)
                    }
                    1 -> {
                        // 故事界面
                        StoryPage(viewModel = viewModel)
                    }
                }
            }

使用WebView渲染DeepSeek生成的HTML格式的文本:

AndroidView(
                    factory = { context ->
                        WebView(context).apply {
                            settings.javaScriptEnabled = true
                            settings.setSupportZoom(false)
                            settings.builtInZoomControls = false
                            settings.displayZoomControls = false

                            loadDataWithBaseURL(
                                null,
                                formattedResponse.ifBlank { "暂无生成内容。" },
                                "text/html",
                                "UTF-8",
                                null
                            )
                        }
                    },
                    modifier = Modifier
                        .fillMaxWidth()
                        .weight(1f)
                )

内容过滤:筛选仅显示DeepSeek回复的内容

val formattedResponse = remember(uiState.fullResponse) {
        uiState.fullResponse
            .split("\n\n\n")
            .filter { it.startsWith("助手:") }
            .joinToString("\n") { it.removePrefix("助手:").trim() }
    }

重写界面跳转逻辑:

故事生成界面->漫画/故事展示界面(->故事翻译)(->返回故事生成界面)

Button(
           onClick = {
                isLoading = true
                viewModel.sendR()
                coroutineScope.launch {
                    isLoading = false
                    navHostController.navigate(LandingDestination.Main.Cartoon.route)
                }
            },
composable(LandingDestination.Main.Cartoon.route) {
            val viewModel = hiltViewModel<DeepSeekViewModel>()
            val generationState = remember { mutableStateOf(GenerationState.Idle) }
            CartoonScreen(
                viewModel = viewModel,
                navigateToTerms = {
                    navHostController.navigate(
                        LandingDestination.Main.Cartoon.getNavTermsRoute(it)
                    ) {
                        launchSingleTop = true
                    }
                },
                navigateTo = {
                    navHostController.navigate(it) {
                        popUpTo(LandingDestination.Main.Home.route) {
                            saveState = true
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                },onRefreshComic = {
                },
                onRefreshStory = {
                },
                generationState = generationState
            )
        }

效果:

点击生成漫画,跳转漫画界面:

向右滑动到故事界面,等待生成逐句翻译和重点词汇标注:

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐