本周工作:本周开始AIGC部分的工作。我负责对框架的搭建、LoRA训练模型的搭建以及其中可视化故事板层的实现。其中框架搭建、训练模型搭建已完工,可视化故事板层的实现正在进一步开发中。
本周迎来了我们项目最重要的部分:AIGC模块,也是我个人觉得最难的部分。为此,我查阅了很多资料和文献、也看了很多教学视频。
一、AIGC模块架构
我们项目的基础功能是用户选择几个单词后,DeepSeek生成和这几个单词有关的一个英文小故事,然后生成四页的漫画分镜描述,将这些信息传给微调过的Stable Diffusion模型,然后由Stable Diffusion模型生成一幅四格漫画,虽然看着这个功能似乎挺简单的,但是在实际开发过程中,我还是费了较大的精力。一开始设计的架构是这样的:
我初步的想法是直接在领域层实现两个模型的交互,一边接收DeepSeek生成的故事,然后将故事直接用作Stable Diffusion模型的prompt。但是这样简单地传递整个故事给Stable Diffusion效果非常差,因为Stable Diffusionm模型更擅长处理一系列单独的词,而不是自然语言。
于是我考虑将领域层分解为两个模块,一个更负责接收完整故事,并将其拆解为四个分镜描述,一个负责将四个描述转换为四个prompt表单,传给Stable Diffusion模型。但是,我很快发现,将拆解为四个分镜描述这里必须再次调用Deepseek API实现,这样的话这个模块将涉及多次与Deepseek的交互,并且故事生成和图像生成的处理混在一起,Story → Prompt 类似的逻辑在多个地方反复写了好几遍,非常不清晰。
于是我想到,需要单独的一层负责把故事变成镜头语言。可以在领域层和表示层之间加一层“可视化故事板层”,专门处理语义理解到视觉语境的转换,即Story → Prompt。这一层将负责调用Deepseek接口,拆分故事为四个分镜描述,并将描述转换为供Stable Diffusion使用的prompt。这样一来,可视化数据板层充当了“翻译官”的角色,Deepseek写的故事Stable Diffusion看不懂,可视化数据板层作为翻译官把故事转成“画面描述”。
解决了prompt的问题后还需要解决绘图风格问题,Stable Diffusion基底模型具有不稳定的特点,想要保证每次画出来的四幅漫画风格、人物特征一致,就需要用到LoRA微调技术。
大体思路为预训练好若干漫画风格和人物特征的LoRA模型,并将其存放于服务器数据库中,每次选用固定的一个风格模型和几个人物模型生成一组四幅漫画。在Stable Diffusion标准工作流模型的基底模型加载层和CLIP层之间添加一层LoRA层,专门负责处理这一逻辑。
完整架构:
二、LoRA训练与推理模型搭建
1.训练模型
训练模型下载:
kijai/ComfyUI-KJNodes: Various custom nodes for ComfyUI
rgthree/rgthree-comfy: Making ComfyUI more comfortable!
载入训练数据:使用数据添加器载入训练数据,支持不同分辨率
载入模型并初始化:载入Flux模型,设置统一桶分辨率,并在StringConstantMultiline模块中设置prompt
训练:使用LoRA模型迭代训练,并在每500次迭代完后预览图像
完整工作流:(2000次迭代)
2.推理模型
只需要在标准工作流(详情见山东大学创新项目实训(2)本地部署Stable Diffusion模型-CSDN博客)的加载层和CLIP层之间添加一层LoRA层:
参考:
DiffSensei: Bridging Multi-Modal LLMs and Diffusion Models for Customized Manga Generation
使用 ComfyUI 进行本地 FLUX.1 LoRA 的训练 - 知乎
三、数据层实现
定义API交互逻辑:
interface DeepSeekApiService {
@POST("generate_story")
suspend fun generateStory(@Body request: StoryRequest): StoryResponse
}
data class StoryRequest(
val keywords: List<String>
)
data class StoryResponse(
val story: String
)
class DeepSeekRemoteSource(private val apiService: DeepSeekApiService) {
suspend fun getStory(keywords: List<String>): StoryResponse {
return apiService.generateStory(StoryRequest(keywords))
}
}
调用Deepseek API:
class DeepSeekService(
private val apiBase: String = "http://10.2.8.77:3000/v1",
private val apiKey: String = "sk-xxxxxxx",
private val model: String = "DeepSeek-R1"
)
网络通信逻辑:
private val client = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
fun streamResponse(prompt: String): Flow<String> = callbackFlow {
val url = "$apiBase/chat/completions"
val payload = JSONObject().apply {
put("model", model)
put("messages", JSONArray().apply {
put(JSONObject().apply {
put("role", "user")
put("content", prompt)
})
})
put("temperature", 0.7)
put("max_tokens", 1024)
put("stream", true)
}
val request = withContext(Dispatchers.IO) {
Request.Builder()
.url(url)
.header("Authorization", "Bearer $apiKey")
.header("Content-Type", "application/json")
.post(payload.toString().toRequestBody())
.build()
}
val eventSourceFactory = EventSources.createFactory(client)
val finalResponse = StringBuilder()
val eventSourceListener = object : EventSourceListener() {
override fun onEvent(
eventSource: EventSource,
id: String?,
type: String?,
data: String
)
四、领域层实现
功能封装:
interface GenerateComicUseCase {
suspend fun execute(keywords: List<String>): ComicResult
}
data class ComicResult(
val story: String,
val frames: List<StoryboardFrame>
)
class StoryboardProcessor {
fun processStory(story: String): List<StoryboardFrame> {
...
}
}
接收完整故事:
class GenerateComicUseCaseImpl(
private val deepSeekRemoteSource: DeepSeekRemoteSource,
private val storyboardProcessor: StoryboardProcessor,
private val sdApiService: StableDiffusionApiService
) : GenerateComicUseCase {
override suspend fun execute(keywords: List<String>): ComicResult {
val storyResponse = deepSeekRemoteSource.getStory(keywords)
val frames = storyboardProcessor.processStory(storyResponse.story)
val images = sdApiService.generateImages(frames)
return ComicResult(storyResponse.story, frames)
}
}
五、可视化故事板层的实现
开发中...