lq1405 b0eb7795e4 V 3.2.3
1.优化文案处理逻辑,重构界面
2.修复批量导出草稿只能导出一个的bug
3.添加自动 推理人物 场景 方便快速生成标签
4.(聚合推文) 修复删除数据bug
5.新增推理国内转发接口(包括翻译)
6.新增文案导入时导入SRT后可手动校验一遍时间数据,简化简单过程
7.语音服务那边添加字符不生效,格式化不生效
8.优化语音服务(数据结构优化,可设置合成超时时间)
2025-02-17 18:26:47 +08:00

423 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { isEmpty, method } from "lodash";
import { gptDefine } from "../../../define/gptDefine";
import axios from "axios";
import { RetryWithBackoff } from "../../../define/Tools/common";
import { Book } from "../../../model/book/book";
import { define } from "@/define/define";
/**
* 一些GPT相关的服务都在这边
*/
export class GptService {
gptUrl: string = undefined
gptModel: string = undefined
gptApiKey: string = undefined
useTransfer: boolean = false
//#region GPT 设置
/**
* 获取GPT的所有的服务商
* @param type 获取的类型就是all
* @param callback 这个是个回调函数,干嘛的不知道
* @returns
*/
private async GetGPTBusinessOption(type: string, callback: Function = null): Promise<any> {
let res = await gptDefine.getGptDataByTypeAndProperty(type, "gpt_options", []);
if (res.code == 0) {
throw new Error(res.message)
} else {
if (callback) {
callback(res.data)
}
return res.data
}
}
async RefreshGptSetting() {
let all_options = await this.GetGPTBusinessOption("all", (value) => value.gpt_url);
let index = all_options.findIndex(item => item.value == global.config.gpt_business && item.gpt_url)
if (index < 0) {
throw new Error("没有找到指定的GPT服务商的配置请检查")
}
this.gptUrl = all_options[index].gpt_url;
this.gptApiKey = global.config.gpt_key;
this.gptModel = global.config.gpt_model;
this.useTransfer = global.config.useTransfer;
return {
gptUrl: this.gptUrl,
gptApiKey: this.gptApiKey,
gptModel: this.gptModel,
useTransfer: this.useTransfer
}
}
/**
* 初始化GPT的设置
*/
async InitGptSetting(refresh = false) {
if (refresh) {
await this.RefreshGptSetting()
} else {
// 判断是不是存在必要信息
if (isEmpty(this.gptUrl) || isEmpty(this.gptModel) || isEmpty(this.gptApiKey)) {
await this.RefreshGptSetting();
}
}
}
/**
* 适配一些请求体中的参数
* @param data
* @param gpt_url
* @returns
*/
ModifyData(data: any, gpt_url: string = null) {
let res = data;
if (!gpt_url) {
gpt_url = this.gptUrl
}
if (gpt_url.includes("dashscope.aliyuncs.com")) {
res = {
"model": data.model,
"input": {
"messages": data.messages,
},
"parameters": {
"result_format": "message"
}
}
}
return res;
}
/**
* 适配返回来的数据
* @param res 返回的数据
* @param gpt_url 请求的URL
* @returns
*/
GetResponseContent(res: any, gpt_url: string = null) {
let content = "";
if (!gpt_url) {
gpt_url = this.gptUrl
}
if (gpt_url.includes("dashscope.aliyuncs.com")) {
content = res.data.output.choices[0].message.content;
} else if (this.useTransfer) {
// 是不是有用 LMS 转发
console.log(res)
let data = res.data;
if (data.code != 1) {
throw new Error(data.message)
}
let aiContentStr = res.data.data;
let aiContent = JSON.parse(aiContentStr);
content = aiContent.choices[0].message.content;
} else {
content = res.data.choices[0].message.content;
}
return content;
}
//#endregion
//#region GPT 通用请求
/**
* 发送GPT请求
* @param {*} message 请求的信息
* @param {*} gpt_url gpt的url默认在global中取
* @param {*} gpt_key gpt的key默认在global中取
* @param {*} gpt_model gpt的model默认在global中取
* @returns
*/
async FetchGpt(message: any, gpt_model: string = null, gpt_key: string = null, gpt_url: string = null): Promise<string> {
try {
await this.InitGptSetting();
let data = {
"model": gpt_model ? gpt_model : this.gptModel,
"messages": message
};
if (this.useTransfer) {
// 转发到LMS中过一遍
let url = define.lms + "/lms/Forward/SimpleTransfer";
let config = {
method: 'post',
url: url,
maxBodyLength: Infinity,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
url: gpt_url ? gpt_url : this.gptUrl,
apiKey: gpt_key ? gpt_key : this.gptApiKey,
dataString: JSON.stringify(data)
})
}
let res = await axios.request(config);
let content = this.GetResponseContent(res, gpt_url);
return content;
} else {
// 不转发 直接请求原接口
data = this.ModifyData(data, gpt_url);
let config = {
method: 'post',
maxBodyLength: Infinity,
url: gpt_url ? gpt_url : this.gptUrl,
headers: {
'Authorization': `Bearer ${gpt_key ? gpt_key : this.gptApiKey}`,
'Content-Type': 'application/json'
},
data: JSON.stringify(data)
};
let res = await axios.request(config);
let content = this.GetResponseContent(res, this.gptUrl);
return content;
}
} catch (error) {
throw error;
}
}
//#endregion
//#region 原创推理
/**
* 获取当前分镜的上下文数据
* @param currentBookTaskDetail 当前分镜数据
* @param bookTaskDetails 所有的小说分镜数据
* @param contextCount 上下文行数
*/
GetBookTaskDetailContextData(currentBookTaskDetail: Book.SelectBookTaskDetail, bookTaskDetails: Book.SelectBookTaskDetail[], contextCount: number): string {
let prefix = "";
// 拼接一个word
let i = currentBookTaskDetail.no - 1
if (i <= contextCount) {
prefix = bookTaskDetails
.filter((item, index) => index < i)
.map((item) => item.afterGpt)
.join('\r\n')
} else if (i > contextCount) {
prefix = bookTaskDetails
.filter((item, index) => i - index <= contextCount && i - index > 0)
.map((item) => item.afterGpt)
.join('\r\n')
}
let suffix = "";
let o_i = bookTaskDetails.length - i
if (o_i <= contextCount) {
suffix = bookTaskDetails
.filter((item, index) => index > i)
.map((item) => item.afterGpt)
.join('\r\n')
} else if (o_i > contextCount) {
suffix = bookTaskDetails
.filter((item, index) => index - i <= contextCount && index - i > 0)
.map((item) => item.afterGpt)
.join('\r\n')
}
return `${prefix}\r\n${currentBookTaskDetail.afterGpt}\r\n${suffix}`;
}
/**
* 返回当前推理数据的请求体中的message
* @param currentBookTaskDetail 当前推理的提示词数据
* @param contextData 上下文数据
* @param autoAnalyzeCharacter 自动分析的角色数据
* @returns
*/
GetGPTRequestMessage(currentBookTaskDetail: Book.SelectBookTaskDetail, contextData: string, autoAnalyzeCharacter: string): any[] {
let message = []
if (
['superSinglePrompt', 'onlyPromptMJ', 'superSinglePromptChinese', "superPromptOverall", "superPromptCharacterEnhancement", "superPromptAdvancedEffects", "superPromptNotWord"].includes(
global.config.gpt_auto_inference
)
) {
// 有返回案例的
message = gptDefine.GetExamplePromptMessage(global.config.gpt_auto_inference)
// 加当前提问的
message.push({
role: 'user',
content: currentBookTaskDetail.afterGpt
})
} else {
// 直接返回,没有案例的
message = [
{
role: 'system',
content: gptDefine.getSystemContentByType(global.config.gpt_auto_inference, {
textContent: contextData,
characterContent: autoAnalyzeCharacter
})
},
{
role: 'user',
content: gptDefine.getUserContentByType(global.config.gpt_auto_inference, {
textContent: currentBookTaskDetail.afterGpt,
wordCount:
global.config.gpt_model && global.config.gpt_model.includes('gpt-4')
? '20'
: '40'
})
}
]
}
return message
}
/**
* 原创推理提示词数据
* @param currentBookTaskDetail 要推理的小说分镜任务
* @param bookTaskDetails 所有的小说分镜任务
* @param contextCount 上下文的数量
* @param autoAnalyzeCharacter 自动分析的角色数据字符串
*/
async OriginalInferencePrompt(currentBookTaskDetail: Book.SelectBookTaskDetail, bookTaskDetails: Book.SelectBookTaskDetail[], contextCount: number, autoAnalyzeCharacter: string) {
let gptPromptType = global.config.gpt_auto_inference;
let message = []
if (gptPromptType == "customize") { //自定义模式
// 自定义模式
// 获取当前自定义的推理提示词
let customize_gpt_prompt = (
await gptDefine.getGptDataByTypeAndProperty('dynamic', 'customize_gpt_prompt', [])
).data
let index = customize_gpt_prompt.findIndex(
(item: any) => item.id == global.config.customize_gpt_prompt
)
if (global.config.customize_gpt_prompt && index < 0) {
throw new Error('自定义推理时要选择对应的自定义推理词')
}
message = gptDefine.CustomizeGptPrompt(customize_gpt_prompt[index], currentBookTaskDetail.afterGpt)
message.push({
role: 'user',
content: currentBookTaskDetail.afterGpt
})
} else { // 内置模式
let context = this.GetBookTaskDetailContextData(currentBookTaskDetail, bookTaskDetails, contextCount);
message = this.GetGPTRequestMessage(currentBookTaskDetail, context, autoAnalyzeCharacter);
}
// 开始请求
let res = await RetryWithBackoff(async () => {
return this.FetchGpt(message)
}, 5, 1000)
if (res) {
if (res) {
res = res
.replace(/\)\s*\(/g, ', ')
.replace(/^\(/, '')
.replace(/\)$/, '')
.replaceAll('*', '')
.replaceAll('--', ' ')
}
}
return res
}
//#endregion
//#region 中文 繁体转简体
/**
* 将繁体中文转换为简体中文
* @param traditionalText 繁体中文文本
* @param apiKey Lai API的 Key
* @param baseUrl 请求的baseurl
* @returns
*/
async ChineseTraditionalToSimplified(traditionalText: string, apiKey: string, baseUrl: string = null): Promise<string> {
try {
let message = [
{
"role": "system",
"content": '我想让你充当中文繁体转简体专家用简体中文100%还原繁体中文,不要加其他的联想,只把原有的繁体中文转换为简体中文,请检查所有信息是否准确,并在回答时保持简活,不需要任何其他反馈。'
}, {
"role": "user",
"content": '上研究生後,發現導師竟然是曾經網戀的前男友。'
}, {
"role": "assistant",
"content": '上研究生后,发现导师竟然是曾经网恋的前男友。'
}, {
"role": "user",
"content": traditionalText
}
]
let baseSubUrl = baseUrl ? (baseUrl.endsWith('/') ? baseUrl + 'v1/chat/completions' : baseUrl + '/v1/chat/completions') : null;
let url = baseSubUrl ? baseSubUrl : "https://api.laitool.cc/v1/chat/completions"
// 开始请求这个默认是使用的是LAI API的gpt-4o-mini
let content = await RetryWithBackoff<string>(async () => {
return await this.FetchGpt(message, 'gpt-4o-mini', apiKey, url);
}, 5, 2000)
return content
} catch (error) {
throw error
}
}
//#endregion
//#region 聚合推文 文案洗稿
/**
* 聚合推文的文案洗稿
* @param word 之前的提示词
* @returns
*/
async GenerateAfterGptWordByGPT(word: string,) {
try {
let prompt = `## - Role: 文案去重
<Background>: 用户需要将小说的文本进行细致的分析,并将原文文本内容进行同义转换,换一种描述,不要改变文案的结构,不改变原来的意思,要求参考原文案的写作风格,不要书面词,要尽可能的口语化输出,但不要太过于运用东北话的一些词语,比如“瞅“,“爱咋咋地“,“咋地“,等类似的词语。
## 要求
严格执行以下要求进行小说内容的段落分行:
1.严格要求根据原文进行同义输出:不允许将原文一行内容所表达的内容分成多行来输出。
2.仅对原文中的话换一种描述,但不能完全脱离这句话的意思。
3.输出的内容每一句话与原文的字数尽量保持一致,严格遵守不能远少于原文每个句子的字数,再次强调不能少于原文每句话的字数。
4.输出的文案的相似度必须低于百分之五十
## 示例
输入1这个世界的女人每结一次婚就会在手上戴一枚戒指手上的戒指越多那么就代表着她的老公越多。
输出1: 雌性每洞房一次,就会带上一枚戒指,手上的戒指越多,就代表被她宠幸的雄性越多。
输入2我重生到末日前的第一件事就是从学校后山徒手抓了一条黑曼巴蛇并丧心病狂的将之活生生吞下。
输出2: 我重生后的第一件事就是生吞羊辣子,然后挖出自己双眼,再割掉自己头颅,众人以为我是疯子得了精神病,但只有我知道我是重生者。
输入3我明明觉醒了SSS级异能紫霄神器却被全校所有人嘲笑原因我觉醒的异能在别人眼里只是一个人形电击棒。
输出3: 我明明觉醒了sss级暗黑系异能却被超神学院五大院长同时除名就连唯一的妹妹夜哭着劝我放弃修炼。
输入3我的奶奶觉醒级凶兽烈焰巨龙我的爷爷觉醒级凶兽暴虐滔天而我却在这个觉醒凶兽的世界觉醒了个破烂石板
输出3: 我爷爷觉醒了s级凶兽黄金朱厌奶奶觉醒了ss级神兽泰坦巨猿可我却在这个人人觉醒泰坦战士的世界
输入5: 一个先天满属性的天才却被称为废物,只因我在转职时觉醒了史上最垃圾的道士职业,就连重金挖我来的学校也将我当成了耻辱
输出5: 一个先天满属性的天才却被称为废物,只因我在转职时觉醒了史上最垃圾的道士职业,就连重金挖我来的学校也将我当成了耻辱
##
最后再强调,你作为一位优秀的小说改写者,每一次输出都要严格遵守<要求>,一步一步慢慢思考,参考<示例>的格式,一步一步思考,按顺序执行<要求>不需要做解释说明只呈现最后的结果严格要求理解全文之后再进行分组无需要在输出用户直接输出Ai部分。
`;
let message = [
{
"role": "system",
"content": prompt,
}, {
"role": "user",
"content": word
}
]
// 开始请求这个默认是使用的是LAI API的gpt-4o-mini
let content = await RetryWithBackoff<string>(async () => {
return await this.FetchGpt(message, null, null, null);
}, 5, 2000)
return content;
} catch (error) {
throw error
}
}
//#endregion
}