整合全流程完整文档
本文档描述了通过单一代码提示词完成从主题生成、代码生成、代码执行到视频播放的全流程。主要包括数据库设计、数据接口定义、Java 后端逻辑(含大模型调用、Python 代码生成与修复、Linux 服务器代码执行),以及测试用例。下文对每个部分的代码做了详细说明,并解释了其主要逻辑。
1. 数据库设计
该系统主要涉及四张数据表,它们分别用于记录生成视频、生成场景、生成代码以及避免错误的提示词。
1.1 数据表说明
ef_ugvideo
存储生成的视频数据,包含视频时长、视频播放地址、标题、语言、语音设置、播放次数、创建人、创建时间等信息。ef_generate_sence
存储大模型生成的场景文本(即视频脚本或说明文本),记录了主题、md5 标识、生成的场景提示词以及其他元数据。ef_generate_code
存储基于主题生成的完整 Python 代码,同时保存相关大模型返回的信息,如错误、生成原因以及视频地址。ef_generate_code_avoid_error_prompt
当代码执行失败时,系统保存一份“避免错误的提示词”,作为后续再次调用大模型修正代码的参考。
1.2 数据库脚本
drop table if exists ef_ugvideo;
CREATE TABLE ef_ugvideo (
id BIGINT PRIMARY KEY,
video_length int4,
video_url VARCHAR,
cover_url VARCHAR,
title VARCHAR,
"language" VARCHAR,
voice_id VARCHAR,
view_count int4 DEFAULT 0 NULL,
user_id VARCHAR,
"type" VARCHAR,
"elapsed" bigint,
is_public bool DEFAULT FALSE,
search_ts tsvector,
"creator" VARCHAR ( 64 ) DEFAULT '',
"create_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" VARCHAR ( 64 ) DEFAULT '',
"update_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" SMALLINT DEFAULT 0,
"tenant_id" BIGINT NOT NULL DEFAULT 0
);
drop table if exists ef_generate_sence;
CREATE TABLE ef_generate_sence (
id BIGINT PRIMARY KEY,
topic VARCHAR,
md5 VARCHAR,
sence_prompt text,
"creator" VARCHAR ( 64 ) DEFAULT '',
"create_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" VARCHAR ( 64 ) DEFAULT '',
"update_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" SMALLINT DEFAULT 0,
"tenant_id" BIGINT NOT NULL DEFAULT 0
);
drop table if exists ef_generate_code;
CREATE TABLE ef_generate_code (
id BIGINT PRIMARY KEY,
topic VARCHAR,
md5 VARCHAR,
"language" VARCHAR,
"voice_provider" VARCHAR,
"voice_id" VARCHAR,
sence_prompt text,
python_code text,
video_url text,
error text,
reason text,
"creator" VARCHAR ( 64 ) DEFAULT '',
"create_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" VARCHAR ( 64 ) DEFAULT '',
"update_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" SMALLINT DEFAULT 0,
"tenant_id" BIGINT NOT NULL DEFAULT 0
);
drop table if exists ef_generate_code_avoid_error_prompt;
CREATE TABLE ef_generate_code_avoid_error_prompt (
id BIGINT PRIMARY KEY,
final_request_json text,
prompt VARCHAR,
"creator" VARCHAR ( 64 ) DEFAULT '',
"create_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" VARCHAR ( 64 ) DEFAULT '',
"update_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" SMALLINT DEFAULT 0,
"tenant_id" BIGINT NOT NULL DEFAULT 0
);
2. 数据接口
前端通过 /api/explanation/video
接口发送请求,提供内容为包含提示词与语音、语言等配置信息的 JSON 数据,后端通过 SSE(Server-Sent Events)方式返回生成的视频地址以及其他过程状态信息。
2.1 接口示例
{
"prompt": "Chemistry - Kjeldahl Method",
"voice_provider": "openai",
"voice_id": "default_voice",
"language": "english",
"user_id": ""
}
后端在返回数据时会依次发送多个 SSE 事件:
- progress:显示执行过程中产生的状态提示
- python_code:返回生成的 Python 代码(主要用于调试)
- main:最后返回生成视频的播放地址,前端据此播放视频
3. Java 后端代码
后端代码采用了 JFinal、Tio 框架以及自定义大模型调用相关模块。接下来分别介绍各个类的作用。
3.1 数据传输对象 ExplanationVo
该类用于封装前端传递的参数。默认设置了“openai”语音供应商、默认语音和语言为英文,支持两种构造函数,满足不同使用场景下的参数传递。
package com.litongjava.manim.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
//"voice_provider": "openai",
//"voice_id": "default_voice",
//"language": "english"
public class ExplanationVo {
private String prompt;
private String voice_provider = "openai";
private String voice_id = "default_voice";
private String language = "english";
private String user_id;
public ExplanationVo(String text) {
this.prompt = text;
}
public ExplanationVo(String user_id, String text) {
this.user_id = user_id;
this.prompt = text;
}
}
3.2 请求处理类 ExplanationHandler
该类用于解析 HTTP 请求,将 JSON 参数转换为 ExplanationVo 对象,然后调用 ExplanationVideoService
执行整个业务逻辑。利用 SSE 推送实时反馈信息给前端,同时开启异步线程执行后续任务。
package com.litongjava.manim.handler;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.manim.services.ExplanationVideoService;
import com.litongjava.manim.vo.ExplanationVo;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.server.util.CORSUtils;
import com.litongjava.tio.utils.json.JsonUtils;
import com.litongjava.tio.utils.thread.TioThreadUtils;
public class ExplanationHandler {
ExplanationVideoService mainimService = Aop.get(ExplanationVideoService.class);
public HttpResponse index(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
CORSUtils.enableCORS(response);
response.addServerSentEventsHeader();
Tio.bSend(request.channelContext, response);
response.setSend(false);
String bodyString = request.getBodyString();
ExplanationVo xplanationVo = JsonUtils.parse(bodyString, ExplanationVo.class);
request.channelContext.setAttribute("type", "SSE");
TioThreadUtils.execute(() -> {
try {
mainimService.index(xplanationVo, request.channelContext);
} catch (Exception e) {
e.printStackTrace();
}
});
return response;
}
}
3.3 核心服务类 ExplanationVideoService
该服务类封装了整个业务逻辑,主要步骤如下:
- 缓存检查:判断当前主题是否已经生成过视频(根据 md5 值、语言、语音参数等),如果命中缓存则直接返回视频地址。
- 生成系统提示词:通过读取预先定义好的文件以及数据库中存储的提示词,构造大模型调用用的系统提示词。
- 大模型生成代码:调用大模型,传入用户的主题和生成的系统提示词,生成符合要求的 Python 代码。
- 发送代码和执行状态:将生成的 Python 代码通过 SSE 事件发送到前端,同时调用 Linux 服务器执行代码。
- 错误处理与修复:若代码执行失败,则将错误日志传递给大模型,并请求其修复代码,同时生成一份“避免错误的提示词”再度执行。最多重试 10 次。
- 记录结果:执行成功后,将视频播放地址保存到数据库,并返回给前端,由前端播放视频。
下面是详细代码:
package com.litongjava.manim.services;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import com.google.common.util.concurrent.Striped;
import com.jfinal.kit.Kv;
import com.jfinal.template.Template;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.gemini.GeminiChatRequestVo;
import com.litongjava.gemini.GeminiChatResponseVo;
import com.litongjava.gemini.GeminiClient;
import com.litongjava.gemini.GoogleGeminiModels;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.linux.ProcessResult;
import com.litongjava.manim.vo.ExplanationVo;
import com.litongjava.openai.chat.ChatMessage;
import com.litongjava.template.PromptEngine;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.http.common.sse.SsePacket;
import com.litongjava.tio.http.server.util.SseEmitter;
import com.litongjava.tio.utils.SystemTimer;
import com.litongjava.tio.utils.crypto.Md5Utils;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.hutool.ResourceUtil;
import com.litongjava.tio.utils.hutool.StrUtil;
import com.litongjava.tio.utils.json.FastJson2Utils;
import com.litongjava.tio.utils.json.JsonUtils;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ExplanationVideoService {
private Striped<Lock> locks = Striped.lock(1024);
private LinuxService linuxService = Aop.get(LinuxService.class);
private String video_server_name = "https://manim.collegebot.ai";
public void index(ExplanationVo explanationVo, ChannelContext channelContext) {
String user_id = explanationVo.getUser_id();
String prompt = explanationVo.getPrompt();
String language = explanationVo.getLanguage();
long start = SystemTimer.currTime;
String videoUrl = this.index(explanationVo, false, channelContext);
long end = SystemTimer.currTime;
if (videoUrl != null) {
Row row = Row.by("id", SnowflakeIdUtils.id()).set("video_url", videoUrl).set("title", prompt).set("language", language)
.set("voice_id", explanationVo.getVoice_id()).set("user_id", user_id).set("elapsed", (end - start));
Db.save("ef_ugvideo", row);
}
}
public String index(ExplanationVo xplanationVo, boolean isTelegram, ChannelContext channelContext) {
String topic = xplanationVo.getPrompt();
String language = xplanationVo.getLanguage();
String voice_provider = xplanationVo.getVoice_provider();
String voice_id = xplanationVo.getVoice_id();
String md5 = Md5Utils.getMD5(topic);
String sql = "select video_url from ef_generate_code where md5=? and language=? and voice_provider=? and voice_id=?";
String output = Db.queryStr(sql, md5, language, voice_provider, voice_id);
if (output != null) {
log.info("hit cache ef_generate_code");
String url = video_server_name + output;
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("url", url));
SsePacket ssePacket = new SsePacket("main", jsonBytes);
Tio.bSend(channelContext, ssePacket);
SseEmitter.closeSeeConnection(channelContext);
}
return url;
}
// String generatedText = genSence(topic, md5);
// if (channelContext != null) {
// byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("sence", generatedText));
// SsePacket ssePacket = new SsePacket("sence", jsonBytes);
// Tio.bSend(channelContext, ssePacket);
// }
String prompt = getSystemPrompt();
String sence = topic + " \r\nThe generated subtitles and narration must use the " + language;
List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", sence));
//请求类
GeminiChatRequestVo geminiChatRequestVo = new GeminiChatRequestVo();
geminiChatRequestVo.setChatMessages(messages);
geminiChatRequestVo.setSystemPrompt(prompt);
// log.info("request:{}", JsonUtils.toSkipNullJson(geminiChatRequestVo));
String value = "Start generate python code";
log.info("value:{}", value);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info", value));
SsePacket ssePacket = new SsePacket("progress", jsonBytes);
Tio.bSend(channelContext, ssePacket);
}
String code = linuxService.genManaimCode(topic, md5, geminiChatRequestVo);
if (code == null) {
String info = "Failed to generate python code";
log.info(info);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("error", info));
SsePacket ssePacket = new SsePacket("error", jsonBytes);
Tio.bSend(channelContext, ssePacket);
SseEmitter.closeSeeConnection(channelContext);
}
}
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("python_code", code));
SsePacket ssePacket = new SsePacket("python_code", jsonBytes);
Tio.bSend(channelContext, ssePacket);
}
//log.info("code:{}", code);
String message = "Start run python code";
log.info("value:{}", value);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info", message));
SsePacket ssePacket = new SsePacket("progress", jsonBytes);
Tio.bSend(channelContext, ssePacket);
}
ProcessResult executeMainmCode = linuxService.executeCode(code);
String stdErr = executeMainmCode.getStdErr();
if (StrUtil.isBlank(executeMainmCode.getOutput())) {
executeMainmCode = linuxService.fixCodeAndRerun(topic, md5, code, stdErr, messages, geminiChatRequestVo, channelContext);
}
if (executeMainmCode != null && StrUtil.isNotBlank(executeMainmCode.getOutput())) {
output = executeMainmCode.getOutput();
String url = video_server_name + output;
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("url", url));
SsePacket ssePacket = new SsePacket("main", jsonBytes);
Tio.bSend(channelContext, ssePacket);
}
Row row = Row.by("id", SnowflakeIdUtils.id());
row.set("topic", topic).set("md5", md5);
row.set("video_url", output).set("language", language).set("voice_provider", voice_provider).set("voice_id", voice_id);
Db.save("ef_generate_code", row);
}
log.info("result:{}", output);
SseEmitter.closeSeeConnection(channelContext);
return output;
}
public String genSence(String text, String md5) {
String sql = "select sence_prompt from ef_generate_sence where md5=?";
String generatedText = Db.queryStr(sql, md5);
if (generatedText != null) {
log.info("Cache Hit ef_generate_sence");
return generatedText;
}
Lock lock = locks.get(md5);
lock.lock();
try {
// 生成场景
Template template = PromptEngine.getTemplate("gen_video_sence_en.txt");
String prompt = template.renderToString();
String userMessageText = text + " \r\nplease reply use this message language.If English is involved, use English vocabulary instead of Pinyin.";
List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", userMessageText));
GeminiChatRequestVo geminiChatRequestVo = new GeminiChatRequestVo();
geminiChatRequestVo.setChatMessages(messages);
geminiChatRequestVo.setSystemPrompt(prompt);
log.info("request:{}", JsonUtils.toSkipNullJson(geminiChatRequestVo));
GeminiChatResponseVo chatResponse = GeminiClient.generate(GoogleGeminiModels.GEMINI_2_0_FLASH, geminiChatRequestVo);
generatedText = chatResponse.getCandidates().get(0).getContent().getParts().get(0).getText();
Row row = Row.by("id", SnowflakeIdUtils.id()).set("topic", text).set("md5", md5).set("sence_prompt", generatedText);
Db.save("ef_generate_sence", row);
return generatedText;
} finally {
lock.unlock();
}
}
public String genSence(String topic) {
return this.genSence(topic, Md5Utils.getMD5(topic));
}
public String getSystemPrompt() {
// 生成代码
URL resource = ResourceUtil.getResource("prompts/gen_video_code_en.txt");
StringBuilder stringBuffer = FileUtil.readURLAsString(resource);
String sql = "select prompt from ef_generate_code_avoid_error_prompt";
List<String> prompts = Db.queryListString(sql);
if (prompts != null && prompts.size() > 0) {
for (String string : prompts) {
stringBuffer.append(string).append("\r\n");
}
}
URL code_example_01_url = ResourceUtil.getResource("prompts/code_example_01.txt");
StringBuilder code_example_01 = FileUtil.readURLAsString(code_example_01_url);
URL code_example_02_url = ResourceUtil.getResource("prompts/code_example_02.txt");
StringBuilder code_example_02 = FileUtil.readURLAsString(code_example_02_url);
URL code_example_03_url = ResourceUtil.getResource("prompts/code_example_03.txt");
StringBuilder code_example_03 = FileUtil.readURLAsString(code_example_03_url);
String prompt = stringBuffer.toString() + "\r\n## complete Python code example \r\n" + "\r\n### Example 1 \r\n" + code_example_01 + "\r\n### Example 2 \r\n" + code_example_02
+ "\r\n### Example 3 \r\n" + code_example_03;
return prompt;
}
}
3.4 Linux 代码服务 LinuxService
该类主要负责将大模型生成的 Python 代码发送至 Linux 服务器进行执行,并提供错误修复机制:
executeCode
调用 LinuxClient 远程执行 Python 代码,并返回执行结果。fixCodeAndRerun
如果首次代码执行失败,根据错误日志调用大模型修复代码,最多重试 10 次。每次修复过程会:- 将错误日志和当前代码加入消息队列,再次调用大模型生成新的代码;
- 再次执行生成的代码;
- 如执行成功则直接返回,否则继续进行修复直到达到最大尝试次数。
genManaimCode
根据用户的主题与大模型交互,提取生成的 Python 代码。如果返回格式为 Markdown 格式的代码块则提取代码内容;若返回的内容为 JSON 格式,则解析获取代码。genAvoidPromptCode
用于生成“避免错误”的提示词,并保存到数据库中,便于后续参考。
下面是完整代码:
package com.litongjava.manim.services;
import java.io.File;
import java.io.IOException;
import java.util.List;
import com.jfinal.kit.Kv;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.gemini.GeminiChatRequestVo;
import com.litongjava.gemini.GeminiChatResponseVo;
import com.litongjava.gemini.GeminiClient;
import com.litongjava.gemini.GeminiGenerationConfigVo;
import com.litongjava.gemini.GoogleGeminiModels;
import com.litongjava.linux.LinuxClient;
import com.litongjava.linux.ProcessResult;
import com.litongjava.openai.chat.ChatMessage;
import com.litongjava.template.PromptEngine;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.http.common.sse.SsePacket;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.hutool.StrUtil;
import com.litongjava.tio.utils.json.FastJson2Utils;
import com.litongjava.tio.utils.json.JsonUtils;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import com.litongjava.vo.ToolVo;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LinuxService {
/**
* 运行代码
* @param code
* @return
*/
public ProcessResult executeCode(String code) {
String apiBase = "http://13.216.69.13";
ProcessResult executeMainmCode = LinuxClient.executeMainmCode(apiBase, "123456", code);
return executeMainmCode;
}
/**
* 最多共会运行时10次
* @param topic
* @param code
* @param stdErr
* @param messages
* @param geminiChatRequestVo
* @param channelContext
* @return
*/
public ProcessResult fixCodeAndRerun(final String topic, String md5, String code, String stdErr, List<ChatMessage> messages, GeminiChatRequestVo geminiChatRequestVo, ChannelContext channelContext) {
// 初始错误日志和 SSE 提示
log.error("python 代码 第1次执行失败:{}", stdErr);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("progress", "Python code: 1st execution failed"));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
ProcessResult executeMainmCode = null;
final int maxAttempts = 10;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
//Row row = Row.by("id", SnowflakeIdUtils.id()).set("topic", topic).set("md5", md5).set("python_code", code).set("error", stdErr);
//Db.save("ef_generate_error_code", row);
messages.add(new ChatMessage("model", code));
messages.add(new ChatMessage("user", "代码执行遇到错误,请修复,并输出修复后的代码 " + stdErr));
geminiChatRequestVo.setChatMessages(messages);
//log.info("fix request {}: {}", attempt, JsonUtils.toSkipNullJson(geminiChatRequestVo));
String info = "start fix code " + attempt;
log.info(info);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info", info));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
code = genManaimCode(topic, md5, geminiChatRequestVo);
info = "finish fix code " + attempt;
log.info(info);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info", info));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
if (code == null) {
return null;
}
//log.info("修复后的代码: {}", code);
// 执行修复后的代码
String message = "start run code " + attempt;
log.info(message);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info", message));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
executeMainmCode = executeCode(code);
stdErr = executeMainmCode.getStdErr();
message = "run finished " + attempt;
log.info(message);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info ", message));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
String codeFixPrompt = PromptEngine.renderToString("code_fix_prompt.txt");
messages.add(new ChatMessage("model", code));
messages.add(new ChatMessage("user", codeFixPrompt));
geminiChatRequestVo.setChatMessages(messages);
message = "start generate avoid prompt " + attempt;
log.info(message);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info ", message));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
String avoidPrompt = genAvoidPromptCode(geminiChatRequestVo);
message = "finish generate avoid prompt " + attempt;
log.info(message);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info ", message));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
message = "start save to ef_generate_code_avoid_error_prompt " + attempt;
log.info(message);
String final_request_json = JsonUtils.toJson(geminiChatRequestVo);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info ", message));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
Row row2 = Row.by("id", SnowflakeIdUtils.id()).set("final_request_json", final_request_json).set("prompt", avoidPrompt);
Db.save("ef_generate_code_avoid_error_prompt", row2);
message = "finish save to ef_generate_code_avoid_error_prompt " + attempt;
log.info(message);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info ", message));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
messages.add(new ChatMessage("model", avoidPrompt));
if (StrUtil.isNotBlank(executeMainmCode.getOutput())) {
String value = "success " + attempt;
log.info(value);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("info ", value));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
log.info("success :{}:", executeMainmCode.getOutput());
return executeMainmCode;
}
log.error("python {} Failed, error:{}", attempt + 1, stdErr);
if (channelContext != null) {
byte[] jsonBytes = FastJson2Utils.toJSONBytes(Kv.by("error", "Python code: 第" + (attempt + 1) + "次执行失败"));
Tio.bSend(channelContext, new SsePacket("progress", jsonBytes));
}
}
return executeMainmCode;
}
private String genAvoidPromptCode(GeminiChatRequestVo geminiChatRequestVo) {
GeminiChatResponseVo chatResponse = null;
GeminiGenerationConfigVo geminiGenerationConfigVo = new GeminiGenerationConfigVo();
//设置参数
geminiGenerationConfigVo.setTemperature(0d);
geminiChatRequestVo.setGenerationConfig(geminiGenerationConfigVo);
try {
chatResponse = GeminiClient.generate(GoogleGeminiModels.GEMINI_2_5_PRO_PREVIEW_03_25, geminiChatRequestVo);
} catch (Exception e) {
log.error("Faile to generate code", e);
return null;
}
String generatedText = chatResponse.getCandidates().get(0).getContent().getParts().get(0).getText();
return generatedText;
}
public String genManaimCode(String topic, String md5, GeminiChatRequestVo geminiChatRequestVo) {
String sql = "select python_code from ef_generate_code where md5=?";
String pythonCode = Db.queryStr(sql, md5);
if (pythonCode != null) {
return pythonCode;
}
GeminiChatResponseVo chatResponse = null;
GeminiGenerationConfigVo geminiGenerationConfigVo = new GeminiGenerationConfigVo();
geminiGenerationConfigVo.setTemperature(0d);
geminiChatRequestVo.setGenerationConfig(geminiGenerationConfigVo);
try {
//chatResponse = GeminiClient.generate(GoogleGeminiModels.GEMINI_2_5_PRO_EXP_03_25, geminiChatRequestVo);
chatResponse = GeminiClient.generate(GoogleGeminiModels.GEMINI_2_5_PRO_PREVIEW_03_25, geminiChatRequestVo);
} catch (Exception e) {
log.error("Faile to generate code:{}", JsonUtils.toJson(geminiChatRequestVo), e);
return null;
}
String generatedText = chatResponse.getCandidates().get(0).getContent().getParts().get(0).getText();
String code = null;
int indexOf = generatedText.indexOf("```python");
if (indexOf == -1) {
if (generatedText.startsWith("{") && generatedText.endsWith("}")) {
code = generatedText;
ToolVo toolVo = null;
try {
toolVo = JsonUtils.parse(code.trim(), ToolVo.class);
} catch (Exception e) {
log.error("Failed to parse Json:{}", code);
return null;
}
return toolVo.getCode();
} else {
log.error("No valid JSON data found in the output.:{}", generatedText);
return null;
}
} else {
int lastIndexOf = generatedText.lastIndexOf("```");
log.info("index:{},{}", indexOf, lastIndexOf);
if (lastIndexOf > 9) {
try {
code = generatedText.substring(indexOf + 9, lastIndexOf);
} catch (Exception e) {
log.error("generated text:{}", generatedText, e);
return null;
}
} else {
try {
code = generatedText.substring(indexOf + 9);
} catch (Exception e) {
log.error("generated text:{}", generatedText, e);
return null;
}
}
new File("script").mkdirs();
try {
String path = "script/" + SnowflakeIdUtils.id() + ".py";
log.info("code file:{}", path);
FileUtil.writeString(code, path, "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
return code;
}
}
}
3.5 定义 Linux 执行结果 ProcessResult
定义了一个简单的 POJO 类用于封装 Linux 服务器执行 Python 代码的返回结果,包括任务 ID、退出码、标准输出、错误输出、返回的最终输出以及可能的图片、视频列表。
package com.litongjava.linux;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProcessResult {
private long taskId;
private int exitCode;
private String executeCode;
private String stdOut;
private String stdErr;
private String output;
private List<String> images;
private List<String> videos;
}
4. 测试类
通过以下测试类可以对系统核心流程进行验证。测试用例覆盖了不同主题的视频生成、语言设置以及系统提示词和场景生成等功能。
package com.litongjava.manim.services;
import org.junit.Test;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.manim.config.AdminAppConfig;
import com.litongjava.manim.vo.ExplanationVo;
import com.litongjava.tio.boot.testing.TioBootTest;
public class ExplanationVideoServiceTest {
@Test
public void test_Kjeldahl() {
TioBootTest.runWith(AdminAppConfig.class);
ExplanationVo explanationVo = new ExplanationVo("Chemistry - Kjeldahl Method");
Aop.get(ExplanationVideoService.class).index(explanationVo, false, null);
}
@Test
public void test_light() {
TioBootTest.runWith(AdminAppConfig.class);
ExplanationVo explanationVo = new ExplanationVo("generate a video about refraction of light");
explanationVo.setLanguage("English");
Aop.get(ExplanationVideoService.class).index(explanationVo, false, null);
}
@Test
public void trigonometricFunctions() {
TioBootTest.runWith(AdminAppConfig.class);
ExplanationVo explanationVo = new ExplanationVo("生成一个三角函数讲解视频");
Aop.get(ExplanationVideoService.class).index(explanationVo, false, null);
}
@Test
public void getSystemPrompt() {
TioBootTest.runWith(AdminAppConfig.class);
String systemPrompt = Aop.get(ExplanationVideoService.class).getSystemPrompt();
System.out.println(systemPrompt);
}
@Test
public void genSence() {
TioBootTest.runWith(AdminAppConfig.class);
Aop.get(ExplanationVideoService.class).genSence("什么是向量");
}
}
5. 主要逻辑说明
整体流程的主要逻辑如下:
系统提示词生成
后端通过getSystemPrompt()
方法加载存储在文件中的基础系统提示词,同时从数据库加载大模型之前总结的避免错误的提示词,并将多个代码示例追加至提示中,以确保生成的 Python 代码具备完整性。调用大模型生成代码
根据用户提交的主题,构造对话消息(含用户描述和系统提示词),调用大模型(如 Google Gemini 系列)生成 Python 代码。生成代码时支持两种返回格式:- 直接返回 Markdown 格式中的代码块
- 返回 JSON 格式数据,然后解析其中的代码字段
代码执行及错误处理
生成的代码通过LinuxService.executeCode()
发送至 Linux 服务器执行。如果执行过程中发生错误,系统调用fixCodeAndRerun()
方法,将错误信息反馈给大模型,请求修复代码,并同时生成一份“避免错误提示”保存于数据库。最多重试 10 次直至执行成功。结果保存与反馈
当 Python 代码成功执行后,生成的视频地址(拼接前缀后)通过 SSE 事件发送给前端,前端据此播放视频。同时,将视频信息保存至数据库表ef_ugvideo
和生成代码记录表ef_generate_code
,以便下次命中缓存,提升响应速度。前端响应
前端通过 SSE 接收到多次事件(包括代码、执行进度、错误提示及最终视频地址),最终在main
事件中获取到视频地址,并播放视频。
总结
本文档详细介绍了一个整合全流程的系统,从数据库设计、接口定义到后端逻辑实现,每个步骤都经过精心设计和充分考虑异常处理。整个系统利用大模型生成 Python 代码,并通过 Linux 服务器执行代码,形成一个自动化的视频生成与播放流程,同时结合 SSE 实现前后端的实时通信,确保用户可以及时获得反馈信息。