LaiTool/src/main/Service/writing.ts
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

567 lines
18 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.

let path = require('path')
let fspromises = require('fs').promises
import { Tools } from '../tools'
import { DEFINE_STRING } from '../../define/define_string'
import { PublicMethod } from '../Public/publicMethod'
import { define } from '../../define/define'
import { get, has, isEmpty } from 'lodash'
import { errorMessage, successMessage } from '../Public/generalTools'
import { ServiceBase } from '../../define/db/service/serviceBase'
import {
GetDoubaoErrorResponse,
GetKimiErrorResponse,
GetOpenAISuccessResponse,
GetRixApiErrorResponse
} from '../../define/response/openAIResponse'
import axios from 'axios'
import { ValidateJson } from '../../define/Tools/validate'
import { RetryWithBackoff } from '../../define/Tools/common'
const { v4: uuidv4 } = require('uuid') // 引入UUID库来生成唯一标识符
let tools = new Tools()
export class Writing extends ServiceBase {
pm: PublicMethod
constructor(global) {
super()
this.pm = new PublicMethod(global)
axios.defaults.baseURL = define.lms
}
/**
* AI 请求发送
* @param {*} setting
* @param {*} aiData
* @param {*} word
* @returns
*/
async AIRequest(setting, aiData, word): Promise<string> {
// 开始请求AI
let axiosRes = await axios.post('/lms/Forward/ForwardWord', {
promptTypeId: setting.gptType,
promptId: setting.gptData,
gptUrl: aiData.gpt_url + '/v1/chat/completions',
model: aiData.model,
machineId: global.machineId,
apiKey: aiData.api_key,
word: word
})
// 判断返回的状态,如果是失败的话直接返回错误信息
if (axiosRes.status != 200) {
throw new Error('请求失败')
}
let dataRes = axiosRes.data
if (dataRes.code == 1) {
// 获取成功
// 解析返回的数据
return GetOpenAISuccessResponse(dataRes.data);
} else {
// 系统报错
if (dataRes.code == 5000) {
throw new Error('系统错误,错误信息如下:' + dataRes.message)
} else {
// 处理不同类型的错误消息
if (setting.gptAI == 'laiapi') {
throw new Error(GetRixApiErrorResponse(dataRes.data))
} else if (setting.gptAI == 'kimi') {
throw new Error(GetKimiErrorResponse(dataRes.data))
} else if (setting.gptAI == 'doubao') {
throw new Error(GetDoubaoErrorResponse(dataRes.data))
} else {
throw new Error(dataRes.data)
}
}
}
}
/**
* 流式请求接口
* @param setting
* @param aiData
* @param word
*/
async AIRequestStream(setting, aiData, word, oldData: string) {
let body = {
promptTypeId: setting.gptType,
promptId: setting.gptData,
gptUrl: aiData.gpt_url,
model: aiData.model,
machineId: global.machineId,
apiKey: aiData.api_key,
word: word
}
var myHeaders = new Headers();
myHeaders.append("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
myHeaders.append("Content-Type", "application/json");
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: JSON.stringify(body),
};
let resData = '\n\n';
return new Promise((resolve, reject) => {
fetch(define.lms + "/lms/Forward/ForwardWordStream", requestOptions)
.then(response => {
if (!response.body) {
throw new Error('ReadableStream not yet supported in this browser.');
}
const reader = response.body.getReader();
return new ReadableStream({
start(controller) {
function push() {
reader.read().then(({
done,
value
}) => {
if (done) {
controller.close();
resolve(resData)
return;
}
// 假设服务器发送的是文本数据
const text = new TextDecoder().decode(value);
resData += text
// 将数据返回前端
global.newWindow[0].win.webContents.send(DEFINE_STRING.GPT.GPT_STREAM_RETURN, oldData + resData)
controller.enqueue(value); // 可选:将数据块放入流中
push();
}).catch(err => {
controller.error(err);
reject(err)
});
}
push();
}
});
})
.catch(error => {
reject(error)
});
})
}
async SplitWord(word: string, wordCount: number) {
let word_list = word.split('\n')
let new_word = []
let tmp_str = ''
let result = []
for (let i = 0; i < word_list.length; i++) {
const element = word_list[i];
if (tmp_str.length + element.length > wordCount) {
result.push({
index: i,
word: new_word.join('\n')
})
new_word = []
tmp_str = ""
new_word.push(element);
} else {
tmp_str += ',' + element
new_word.push(element);
}
}
result.push({
index: word_list.length,
word: new_word.join('\n')
})
return result
}
/**
* 文案处理GPT进行推理
* @param {*} setting
* @param {*} word
* @returns
*/
async ActionStart(setting, word) {
try {
await this.InitService()
// console.log(setting, word)
if (isEmpty(setting.gptType)) {
throw new Error('请选择GPT类型')
}
if (isEmpty(setting.gptData)) {
throw new Error('请选择GPT预设')
}
if (isEmpty(setting.gptAI)) {
throw new Error('请选择使用的AI')
}
// 判断对应的AI的设置是不是有
let aiSetting = this.softService.GetSoftWarePropertyData('aiSetting')
if (isEmpty(aiSetting)) {
throw new Error('请先设置AI设置')
}
let tryP = ValidateJson(aiSetting)
if (!tryP) {
throw new Error('AI设置的数据格式不正确')
}
aiSetting = JSON.parse(aiSetting)
// 判断是不是有对应的AI设置
let aiData = get(aiSetting, setting.gptAI)
for (const aid in aiData) {
if (isEmpty(aid)) {
throw new Error('请先设置AI设置')
}
}
if (isEmpty(word)) {
throw new Error('请先设置文案')
}
let result = ''
if (setting.isSplit) {
// 这边拆分文案
let spiltWord = await this.SplitWord(word, setting.splitNumber)
for (let i = 0; i < spiltWord.length; i++) {
const element = spiltWord[i];
if (setting.isStream) {
result += await RetryWithBackoff(async () => {
return await this.AIRequestStream(setting, aiData, element.word, result)
}, 3, 1000) + '\n'
} else {
result = await RetryWithBackoff(async () => {
return await this.AIRequest(setting, aiData, word)
}, 3, 1000)
}
}
} else {
if (setting.isStream) {
result += await RetryWithBackoff(async () => {
return await this.AIRequestStream(setting, aiData, word, '')
}, 3, 1000) + '\r\n\n'
} else {
result = await RetryWithBackoff(async () => {
return await this.AIRequest(setting, aiData, word)
}, 3, 1000)
}
}
return successMessage(result, "执行文案相关任务成功", 'Writing_ActionStart');
} catch (error) {
return errorMessage(
'执行文案相关任务失败,失败信息如下:' + error.toString(),
'Writing_ActionStart'
)
}
}
//#region 下面是文案的处理相关
/**
* 将文案信息写入到本地的文案文件中
* @param {*} value
*/
async SaveWordTxt(value) {
try {
let word_path = path.join(global.config.project_path, '文案.txt')
await tools.writeArrayToFile(value, word_path)
return {
code: 1,
message: '保存成功'
}
} catch (error) {
throw new Error(error)
}
}
/**
* 将分镜的时间信息添加道配置文件中
* @param {*} value 是一个数组0 :写入的数据 1写入的属性 2是否需要解析
*/
async SaveCopywritingInformation(value) {
try {
return await this.pm.SaveConfigJsonProperty(value)
} catch (error) {
return {
code: 0,
message: error.toString()
}
}
}
/**
* 获取Config.json文件中指定的属性
* @param {Array} value 传入的值 0 需要获取的属性 1 返回的默认值
* @returns
*/
async GetConfigJson(value) {
try {
return await this.pm.GetConfigJson(value, false)
} catch (error) {
return {
code: 0,
message: error.toString()
}
}
}
/**
* 获取当前项目下面的文案
*/
async GetProjectWord() {
try {
// 先判断当前的项目文件下面是不是又配置文件。没有才读取文案
let srt_config_path = path.join(global.config.project_path, 'scripts/config.json')
let isExist = await tools.checkExists(srt_config_path)
let data = null
let isImformation = false
if (isExist) {
let config_1 = JSON.parse(await fspromises.readFile(srt_config_path))
isImformation = has(config_1, 'srt_time_information')
if (isImformation) {
data = JSON.parse(await fspromises.readFile(srt_config_path)).srt_time_information
}
}
if (!isExist || !isImformation) {
let word_path = path.join(global.config.project_path, '文案.txt')
let isExistWord = await tools.checkExists(word_path)
if (!isExistWord) {
return {
code: 0,
message: '没有文案文件'
}
}
let data = await fspromises.readFile(word_path, { encoding: 'utf-8' })
let lines = data.split(/\r?\n/)
// 打印或返回这个数组
// console.log(lines);
// 判断是不是有洗稿后的文件
let new_srt_path = path.join(global.config.project_path, 'new_word.txt')
let isExistAfterGPTWord = await tools.checkExists(new_srt_path)
let after_data = null
if (isExistAfterGPTWord) {
after_data = (await fspromises.readFile(new_srt_path, { encoding: 'utf-8' })).split(
/\r?\n/
)
}
// 判断抽帧文件是不是存在
// 返回图片信息
let old_image_path_list = await tools.getFilesWithExtensions(
path.join(global.config.project_path, 'tmp/input_crop'),
'.png'
)
let res = []
let lastId = ''
// 处理数据
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
let id = uuidv4()
let after_gpt = null
if (after_data != null) {
after_gpt = after_data[i]
}
let img_path = null
if (old_image_path_list != null) {
img_path = old_image_path_list[i]
}
let obj = {
no: i + 1,
id: id,
lastId: lastId,
word: line,
old_image: img_path,
after_gpt: after_gpt,
start_time: null,
end_time: null,
timeLimit: null,
subValue: []
}
res.push(obj)
lastId = id
}
return {
code: 1,
type: 0,
data: res
}
} else {
let data = JSON.parse(await fspromises.readFile(srt_config_path)).srt_time_information
return {
code: 1,
type: 1,
data: data
}
}
} catch (error) {
throw new Error(error)
}
}
/**
* 搭导入srt。然后加载时间轴。完全匹配失败的将会还是会导入然后手动手动切换
* @param {文案洗稿界面信息} textData
*/
async ImportSrtAndGetTime(data) {
let textData = data[0]
let init_num = textData.length
let srt_path = data[1]
let current_text = ''
let iii = 0
let sss = ''
try {
if (!srt_path) {
// 获取项目下面的所有的srt
let srtfiles = await tools.getFilesWithExtensions(global.config.project_path, '.srt')
if (srtfiles.length <= 0) {
throw new Error('没有SRT文件')
}
srt_path = srtfiles[0]
}
let srt_data = (await fspromises.readFile(srt_path, 'utf-8')).toString('utf-8')
const entries = srt_data.replace(/\r\n/g, '\n').split('\n\n')
let data = entries
.map((entry) => {
const lines = entry.split('\n')
if (lines.length >= 3) {
const times = lines[1]
const text = lines.slice(2).join(' ')
const [start, end] = times.split(' --> ').map((time) => {
const [hours, minutes, seconds] = time.split(':')
const [sec, millis] = seconds.split(',')
return (
(parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(sec)) * 1000 +
parseInt(millis)
)
})
return { start, end, text, id: uuidv4() }
}
})
.filter((entry) => entry)
// 开始匹配(洗稿后的)
let srt_list = []
let srt_obj = null
let text_count = 0
let tmp_str = ''
for (let i = 0; i < data.length;) {
iii = i
sss = data[i].after_gpt
let srt_value = data[i].text
current_text = `字幕: “${srt_value}” 和文案第${text_count + 1} 行数据 “${textData[text_count].after_gpt
}” 数据不匹配(检查一下上下文)`
let start_time = data[i].start
let end_time = data[i].end
let obj = {
start_time,
end_time,
srt_value,
id: data[i].id
}
// 判断当前字幕是不是在当前句
// 不能用简单的包含,而是将数据进行去除特殊符号拼接后判断是不是相同
tmp_str += srt_value
if (
tools
.removePunctuationIncludingEllipsis(textData[text_count].after_gpt)
.startsWith(tools.removePunctuationIncludingEllipsis(tmp_str))
) {
if (srt_obj == null) {
srt_obj = {}
srt_obj.id = uuidv4()
srt_obj.start_time = start_time
srt_obj.value = srt_value
srt_obj.subValue = [obj]
} else {
srt_obj.value = srt_obj.value + srt_value
srt_obj.subValue = [...srt_obj.subValue, obj]
}
textData[text_count].start_time = srt_obj.start_time
textData[text_count].subValue = srt_obj.subValue
srt_list.push(obj)
i++
} else {
// 判断下一句文件是不是以当当前巨开头。是的话继续。不是的话。直接返回后面的所有信息
if (
tools
.removePunctuationIncludingEllipsis(textData[text_count + 1].after_gpt)
.startsWith(tools.removePunctuationIncludingEllipsis(srt_value))
) {
textData[text_count].end_time = srt_list[srt_list.length - 1].end_time
text_count++
srt_obj = null
tmp_str = ''
} else {
// 将下面的数据直接 添加到textData后面。
// 修改当前行数据的结束事件为
if (srt_list.length > 0) {
textData[text_count].end_time = srt_list[srt_list.length - 1].end_time
text_count++
}
// 将后面的数据直接添加
let lastId = textData[textData.length - 1].id
for (let j = i; j < data.length; j++) {
// 直接修改原有数据
if (text_count < init_num) {
textData[text_count].subValue = [
{
start_time: data[j].start,
end_time: data[j].end,
id: data[j].id,
srt_value: data[j].text
}
]
textData[text_count].start_time = data[j].start
textData[text_count].end_time = data[j].end
text_count++
} else {
let id = uuidv4()
// 添加
let obj = {
no: j + 1,
id: id,
word: null,
lastId: lastId,
old_image: path.normalize(define.zhanwei_image),
after_gpt: null,
start_time: data[j].start,
end_time: data[j].end,
subValue: [
{
start_time: data[j].start,
end_time: data[j].end,
id: data[j].id,
srt_value: data[j].text
}
]
}
lastId = id
textData.push(obj)
}
}
global.newWindow[0].win.webContents.send(DEFINE_STRING.SHOW_MESSAGE_DIALOG, {
code: 0,
message: current_text
})
return {
code: 1,
data: textData
}
}
}
}
// 最后对齐
textData[textData.length - 1].end_time = srt_list[srt_list.length - 1].end_time
// 返回数据
return {
code: 1,
data: textData
}
} catch (error) {
// console.log(iii)
// console.log(sss)
return {
code: 0,
message: error.toString()
}
}
}
//#endregion
}