V 3.2.3
1.优化文案处理逻辑,重构界面 2.修复批量导出草稿只能导出一个的bug 3.添加自动 推理人物 场景 方便快速生成标签 4.(聚合推文) 修复删除数据bug 5.新增推理国内转发接口(包括翻译) 6.新增文案导入时导入SRT后可手动校验一遍时间数据,简化简单过程 7.语音服务那边添加字符不生效,格式化不生效 8.优化语音服务(数据结构优化,可设置合成超时时间)
This commit is contained in:
parent
1ce665a3e5
commit
b0eb7795e4
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "laitool",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "laitool",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.3",
|
||||
"description": "An AI tool for image processing, video processing, and other functions.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "laitool.cn",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -6,6 +6,9 @@
|
||||
*/
|
||||
export function ValidateJson(str: string): boolean {
|
||||
try {
|
||||
if (str == null) {
|
||||
return false;
|
||||
}
|
||||
JSON.parse(str);
|
||||
return true
|
||||
} catch (e) {
|
||||
|
||||
@ -11,6 +11,7 @@ import { OtherData } from '../../../enum/softwareEnum.js'
|
||||
import { BookBackTaskList } from '../../model/Book/BookBackTaskListModel.js'
|
||||
import { Book } from '../../../../model/book/book.js'
|
||||
import { GeneralResponse } from '../../../../model/generalResponse.js'
|
||||
import { TaskModal } from '@/model/task.js'
|
||||
const { v4: uuidv4 } = require('uuid')
|
||||
|
||||
export class BookBackTaskListService extends BaseRealmService {
|
||||
|
||||
@ -232,7 +232,8 @@ export class BookService extends BaseRealmService {
|
||||
updateTime: new Date(),
|
||||
createTime: new Date(),
|
||||
version: version,
|
||||
imageCategory: imageCategory
|
||||
imageCategory: imageCategory,
|
||||
openVideoGenerate: false
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
|
||||
79
src/define/db/service/SoftWare/optionRealmService.ts
Normal file
79
src/define/db/service/SoftWare/optionRealmService.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import Realm from 'realm'
|
||||
import { isEmpty, cloneDeep } from 'lodash'
|
||||
import { OptionType } from '@/define/enum/option'
|
||||
import { BaseSoftWareService } from './softwareBasic'
|
||||
import { OptionModel } from '@/model/option/option'
|
||||
|
||||
export class OptionRealmService extends BaseSoftWareService {
|
||||
static instance: OptionRealmService | null = null
|
||||
declare realm: Realm
|
||||
private constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前实例对象,为空则创建一个新的
|
||||
* @returns
|
||||
*/
|
||||
public static async getInstance() {
|
||||
if (OptionRealmService.instance === null) {
|
||||
OptionRealmService.instance = new OptionRealmService()
|
||||
await super.getInstance()
|
||||
}
|
||||
await OptionRealmService.instance.open()
|
||||
return OptionRealmService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定的Option,通过key,不存在返回null
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
public GetOptionByKey(key: string): OptionModel.OptionItem | null {
|
||||
if (isEmpty(key)) {
|
||||
return null
|
||||
}
|
||||
let res = this.realm.objects('Options').filtered(`key = "${key}"`);
|
||||
|
||||
if (res.length > 0) {
|
||||
let resData = Array.from(res).map((item) => {
|
||||
let resObj = {
|
||||
...item
|
||||
}
|
||||
return cloneDeep(resObj)
|
||||
})
|
||||
return resData[0] as OptionModel.OptionItem
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改指定的Option,通过key,不存在则创建
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
public ModifyOptionByKey(key: string, value: string, type: OptionType = OptionType.STRING) {
|
||||
if (isEmpty(key)) {
|
||||
return false
|
||||
}
|
||||
let option = this.realm.objectForPrimaryKey('Options', key);
|
||||
if (option) {
|
||||
this.realm.write(() => {
|
||||
option.value = value;
|
||||
option.type = type;
|
||||
})
|
||||
} else {
|
||||
this.realm.write(() => {
|
||||
this.realm.create('Options', {
|
||||
key: key,
|
||||
value: value,
|
||||
type: type
|
||||
})
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import SETTING from "./settingDefineString"
|
||||
import BOOK from "./bookDefineString"
|
||||
import WRITE from "./writeDefineString"
|
||||
import DB from "./dbDefineString"
|
||||
import OPTIONS from "./optionsDefineString"
|
||||
|
||||
export const DEFINE_STRING = {
|
||||
SYSTEM: SYSTEM,
|
||||
@ -14,6 +15,7 @@ export const DEFINE_STRING = {
|
||||
SETTING: SETTING,
|
||||
WRITE: WRITE,
|
||||
DB: DB,
|
||||
OPTIONS:OPTIONS,
|
||||
SHOW_GLOBAL_MESSAGE: "SHOW_GLOBAL_MESSAGE",
|
||||
SHOW_GLOBAL_MAIN_NOTIFICATION: 'SHOW_GLOBAL_MAIN_NOTIFICATION',
|
||||
OPEN_DEV_TOOLS_PASSWORD: 'OPEN_DEV_TOOLS_PASSWORD',
|
||||
|
||||
19
src/define/define_string/optionsDefineString.ts
Normal file
19
src/define/define_string/optionsDefineString.ts
Normal file
@ -0,0 +1,19 @@
|
||||
const OPTIONS = {
|
||||
|
||||
/**
|
||||
* 获取指定的Option,通过key,不存在返回null
|
||||
*/
|
||||
GET_OPTION_BY_KEY: 'GET_OPTION_BY_KEY',
|
||||
|
||||
/**
|
||||
* 修改指定的Option,通过key,不存在则创建
|
||||
*/
|
||||
MODIFY_OPTION_BY_KEY: 'MODIFY_OPTION_BY_KEY',
|
||||
|
||||
/**
|
||||
* 同步文案处理中的AI设置旧数据到新的数据表中
|
||||
*/
|
||||
INIT_COPY_WRITING_AI_SETTING: "INIT_COPY_WRITING_AI_SETTING"
|
||||
}
|
||||
|
||||
export default OPTIONS
|
||||
@ -1,7 +1,6 @@
|
||||
const WRITE = {
|
||||
GET_WRITE_CONFIG: 'GET_WRITE_CONFIG',
|
||||
SAVE_WRITE_CONFIG: 'SAVE_WRITE_CONFIG',
|
||||
ACTION_START: 'ACTION_START',
|
||||
GET_SUBTITLE_SETTING: "GET_SUBTITLE_SETTING",
|
||||
RESET_SUBTITLE_SETTING: "RESET_SUBTITLE_SETTING",
|
||||
SAVE_SUBTITLE_SETTING: "SAVE_SUBTITLE_SETTING",
|
||||
@ -9,7 +8,16 @@ const WRITE = {
|
||||
/** 生成洗稿后文案 */
|
||||
GENERATE_AFTER_GPT_WORD: "GENERATE_AFTER_GPT_WORD",
|
||||
/** 生成洗稿后文案返回数据,前端接收 */
|
||||
GENERATE_AFTER_GPT_WORD_RESPONSE: "GENERATE_AFTER_GPT_WORD_RESPONSE"
|
||||
GENERATE_AFTER_GPT_WORD_RESPONSE: "GENERATE_AFTER_GPT_WORD_RESPONSE",
|
||||
|
||||
//#region 文案改写
|
||||
|
||||
/**
|
||||
* AI处理文案
|
||||
*/
|
||||
COPY_WRITING_AI_GENERATION: "COPY_WRITING_AI_GENERATION",
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export default WRITE
|
||||
@ -1,7 +1,40 @@
|
||||
/** option 中的type的类型 */
|
||||
/**
|
||||
* Option Value的数据类型,用于数据的格式化
|
||||
*/
|
||||
export enum OptionType {
|
||||
STRING = 'string',
|
||||
NUMBER = 'number',
|
||||
BOOLEAN = 'boolean',
|
||||
JOSN = 'json'
|
||||
}
|
||||
|
||||
export enum OptionKeyName {
|
||||
|
||||
//#region 文案处理
|
||||
|
||||
/**
|
||||
* 文案处理的AI设置
|
||||
*/
|
||||
CW_AISetting = 'CW_AISetting',
|
||||
|
||||
/**
|
||||
* 文案处理数据界面数据
|
||||
*/
|
||||
CW_AISimpleSetting = 'CW_AISimpleSetting',
|
||||
|
||||
/**
|
||||
* 格式化的特殊字符数据
|
||||
*/
|
||||
CW_FormatSpecialChar = 'CW_FormatSpecialChar',
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region TTS
|
||||
|
||||
/**
|
||||
* TTS界面视图数据
|
||||
*/
|
||||
TTS_GlobalSetting = 'TTS_GlobalSetting',
|
||||
|
||||
//#endregion
|
||||
}
|
||||
@ -8,7 +8,20 @@ import { apiUrl } from './api/apiUrlDefine'
|
||||
export const gptDefine = {
|
||||
// Add properties and methods to the shared object
|
||||
characterSystemContent: `{textContent}\r查看上面的文本,然后扮演一个文本编辑来回答问题。`,
|
||||
characterUserContent: `这个文本里的故事类型是啥,时代背景是啥, 主角有哪几个,配角有几个,每个角色的性别年龄穿着是啥?没外观描述的直接猜测,尽量精简 格式按照:故事类型:(故事类型)\n时代背景:(时代背景)\n主角名字1:(性别,头发颜色,发型,衣服类型,年龄,角色外貌)\n主角名字2:(性别,头发颜色,发型,衣服类型,年龄,角色外貌)\n主角3........\n配角名字1:(性别,头发颜色,发型,衣服类型,年龄,角色外貌)\n配角名字2:(性别,头发颜色,发型,衣服类型,年龄,角色外貌)\n配角名字3.... ,不知道的直接猜测设定,不能出不详和未知这两个词,150字内,中文回答。`,
|
||||
characterUserContent: `这个文本里的故事类型是什么,时代背景是什么, 上面文本中存在哪些场景,主角有哪几个,配角有几个,每个角色的性别年龄穿着是啥?没外观描述的直接猜测,尽量精简
|
||||
格式按照:
|
||||
故事类型:(故事类型)
|
||||
时代背景:(时代背景)
|
||||
主角名字1:(性别,头发颜色,发型,衣服类型,年龄,角色外貌,若未提及则合理推测)
|
||||
主角名字2:(性别,头发颜色,发型,衣服类型,年龄,角色外貌,若未提及则合理推测)
|
||||
主角3........
|
||||
配角名字1:(性别,头发颜色,发型,衣服类型,年龄,角色外貌,若未提及则合理推测)
|
||||
配角名字2:(性别,头发颜色,发型,衣服类型,年龄,角色外貌,若未提及则合理推测)
|
||||
配角名字3.... ,
|
||||
场景1:(地点,环境状况,光线条件,氛围特点,所处时间,若无明确信息则合理推测)
|
||||
场景2:(地点,环境状况,光线条件,氛围特点,所处时间,若无明确信息则合理推测)
|
||||
场景3......
|
||||
不知道的直接猜测设定,不能出不详和未知这两个词,250字内,中文回答。`,
|
||||
|
||||
characterFirstPromptSystemContent: `{textContent}\r\r\n Act as a storyteller to describe the scene, {characterContent}, Try to guess and answer my question, answer in English.`,
|
||||
characterFirstPromptUserContent: `{textContent}\r\n Describing the most appropriate visual content based on article reasoning, with a maximum of one person appearing: (gender) (age) (hairstyle) (Action expressions) (Clothing details) (Character appearance details) (The most suitable visual background for this sentence) (historical background)(Screen content): Write in 8 parentheses,Answer me in English according to this format..{wordCount}words`,
|
||||
|
||||
@ -17,6 +17,7 @@ import { TTSIpc } from './ttsIpc'
|
||||
import { DBIpc } from './dbIpc'
|
||||
import { PresetIpc } from './presetIpc'
|
||||
import { TaskIpc } from './taskIpc'
|
||||
import { OptionsIpc } from './optionsIpc'
|
||||
|
||||
export async function RegisterIpc(createWindow) {
|
||||
PromptIpc()
|
||||
@ -38,4 +39,5 @@ export async function RegisterIpc(createWindow) {
|
||||
SystemIpc()
|
||||
BookIpc()
|
||||
TTSIpc()
|
||||
OptionsIpc()
|
||||
}
|
||||
|
||||
33
src/main/IPCEvent/optionsIpc.ts
Normal file
33
src/main/IPCEvent/optionsIpc.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { DEFINE_STRING } from '../../define/define_string'
|
||||
import OptionHandle from '../Service/Options/index'
|
||||
import { OptionType } from '@/define/enum/option'
|
||||
|
||||
function OptionsIpc() {
|
||||
|
||||
/**
|
||||
* 获取指定的Option,通过key,不存在返回null
|
||||
*/
|
||||
ipcMain.handle(
|
||||
DEFINE_STRING.OPTIONS.GET_OPTION_BY_KEY,
|
||||
async (_, key: string) => await OptionHandle.GetOptionByKey(key)
|
||||
)
|
||||
|
||||
/**
|
||||
* 修改指定的Option,通过key,不存在则创建
|
||||
*/
|
||||
ipcMain.handle(
|
||||
DEFINE_STRING.OPTIONS.MODIFY_OPTION_BY_KEY,
|
||||
async (_, key: string, value: string, type: OptionType) =>
|
||||
await OptionHandle.ModifyOptionByKey(key, value, type)
|
||||
)
|
||||
|
||||
/**
|
||||
* 同步文案处理中的AI设置旧数据到新的数据表中
|
||||
*/
|
||||
ipcMain.handle(
|
||||
DEFINE_STRING.OPTIONS.INIT_COPY_WRITING_AI_SETTING,
|
||||
async () => await OptionHandle.InitCopyWritingAISetting()
|
||||
)
|
||||
}
|
||||
export { OptionsIpc }
|
||||
@ -5,19 +5,11 @@ import { TTS } from '../Service/tts'
|
||||
const tts = new TTS()
|
||||
|
||||
export function TTSIpc() {
|
||||
// 获取当前的TTS配置数据
|
||||
ipcMain.handle(DEFINE_STRING.TTS.GET_TTS_CONFIG, async () => await tts.GetTTSCOnfig())
|
||||
|
||||
// 保存TTS配置
|
||||
ipcMain.handle(
|
||||
DEFINE_STRING.TTS.SAVE_TTS_CONFIG,
|
||||
async (event, data) => await tts.SaveTTSConfig(data)
|
||||
)
|
||||
|
||||
// 生成音频
|
||||
ipcMain.handle(
|
||||
DEFINE_STRING.TTS.GENERATE_AUDIO,
|
||||
async (event, text) => await tts.GenerateAudio(text)
|
||||
async (event) => await tts.GenerateAudio()
|
||||
)
|
||||
|
||||
// 生成SRT字幕文件
|
||||
|
||||
@ -9,6 +9,8 @@ import { BookPrompt } from '../Service/Book/bookPrompt'
|
||||
let subtitleService = new SubtitleService()
|
||||
const bookPrompt = new BookPrompt();
|
||||
|
||||
import CopyWritingService from '@/main/Service/copywriting/index'
|
||||
|
||||
function WritingIpc() {
|
||||
// 监听分镜时间的保存
|
||||
ipcMain.handle(
|
||||
@ -69,11 +71,18 @@ function WritingIpc() {
|
||||
async (event, subtitleSetting) => await subtitleService.SaveSubtitleSetting(subtitleSetting)
|
||||
)
|
||||
|
||||
//#region 文案处理
|
||||
|
||||
/**
|
||||
* AI处理文案
|
||||
*/
|
||||
ipcMain.handle(
|
||||
DEFINE_STRING.WRITE.ACTION_START,
|
||||
async (event, aiSetting, word) => await writing.ActionStart(aiSetting, word)
|
||||
DEFINE_STRING.WRITE.COPY_WRITING_AI_GENERATION,
|
||||
async (event, ids: string[]) => await CopyWritingService.CopyWritingAIGeneration(ids)
|
||||
)
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 文案洗稿相关
|
||||
|
||||
/** 生成洗稿后文案 */
|
||||
|
||||
@ -351,6 +351,37 @@ export class Translate {
|
||||
content: translateData
|
||||
})
|
||||
|
||||
let content = ''
|
||||
// 判断整体是不是需要LMS转发
|
||||
if (global.config.useTransfer) {
|
||||
let url = define.lms + '/lms/Forward/SimpleTransfer'
|
||||
let config = {
|
||||
method: 'post',
|
||||
url: url,
|
||||
maxBodyLength: Infinity,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify({
|
||||
url: this.translationBusiness,
|
||||
apiKey: token,
|
||||
dataString: JSON.stringify(data)
|
||||
})
|
||||
}
|
||||
// 重试机制
|
||||
let res = await RetryWithBackoff(
|
||||
async () => {
|
||||
return await axios.request(config)
|
||||
},
|
||||
5,
|
||||
2000
|
||||
)
|
||||
|
||||
if (res.data.code != 1) {
|
||||
throw new Error(res.data.message)
|
||||
}
|
||||
content = GetOpenAISuccessResponse(res.data.data)
|
||||
} else {
|
||||
let config = {
|
||||
method: 'post',
|
||||
maxBodyLength: Infinity,
|
||||
@ -369,9 +400,10 @@ export class Translate {
|
||||
2000
|
||||
)
|
||||
// 将返回的数据进行拼接数据处理
|
||||
content = GetOpenAISuccessResponse(res.data)
|
||||
}
|
||||
|
||||
let res_data = []
|
||||
let content = GetOpenAISuccessResponse(res.data)
|
||||
|
||||
if (to == 'zh') {
|
||||
res_data.push({
|
||||
|
||||
@ -101,9 +101,14 @@ export class BookBasic {
|
||||
try {
|
||||
let book = await this.bookServiceBasic.GetBookDataById(bookId)
|
||||
// 获取所有的小说批次
|
||||
let bookTasks = (await this.bookServiceBasic.GetBookTaskData({
|
||||
let bookTasksObj = (await this.bookServiceBasic.GetBookTaskData({
|
||||
bookId: bookId
|
||||
})).bookTasks;
|
||||
}, true));
|
||||
// 删除之前判断是不是有子批次 没有直接退出
|
||||
let bookTasks = bookTasksObj.bookTasks;
|
||||
if (bookTasks.length == 0) {
|
||||
return successMessage('未找到小说批次数据,正常退出', 'BookBasic_ResetBookData');
|
||||
}
|
||||
// 重置批次任务
|
||||
for (let i = 0; i < bookTasks.length; i++) {
|
||||
const element = bookTasks[i];
|
||||
@ -179,14 +184,19 @@ export class BookBasic {
|
||||
if (resetRes.code == 0) {
|
||||
throw new Error(resetRes.message)
|
||||
}
|
||||
let bookTasks = (await this.bookServiceBasic.GetBookTaskData({
|
||||
let bookTasksObj = (await this.bookServiceBasic.GetBookTaskData({
|
||||
bookId: bookId
|
||||
})).bookTasks;
|
||||
}, true))
|
||||
let bookTasks = bookTasksObj.bookTasks;
|
||||
|
||||
// 有数据才删除
|
||||
if (bookTasks.length > 0) {
|
||||
// 删除遗留重置的小说批次任务
|
||||
for (let i = 0; i < bookTasks.length; i++) {
|
||||
const element = bookTasks[i];
|
||||
await this.bookServiceBasic.DeleteBookTaskData(element.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始删除数据
|
||||
await this.bookServiceBasic.DeleteBookData(bookId);
|
||||
|
||||
@ -436,7 +436,7 @@ export class BookPrompt {
|
||||
})
|
||||
}
|
||||
// 分批次执行异步任务
|
||||
let res = await ExecuteConcurrently(tasks, global.config.task_number)
|
||||
await ExecuteConcurrently(tasks, global.config.task_number)
|
||||
// 执行完毕
|
||||
return successMessage(null, "推理所有数据完成", 'BookPrompt_OriginalGetPrompt')
|
||||
|
||||
|
||||
@ -227,6 +227,7 @@ export class BookTask {
|
||||
this.bookServiceBasic.transaction((realm) => {
|
||||
for (let i = 0; i < bookTasks.length; i++) {
|
||||
const element = bookTasks[i];
|
||||
element.openVideoGenerate = false
|
||||
realm.create('BookTask', element)
|
||||
}
|
||||
for (let i = 0; i < bookTaskDetail.length; i++) {
|
||||
@ -409,6 +410,7 @@ export class BookTask {
|
||||
suffixPrompt: sourceBookTask.suffixPrompt,
|
||||
version: sourceBookTask.version,
|
||||
imageCategory: sourceBookTask.imageCategory,
|
||||
openVideoGenerate: sourceBookTask.openVideoGenerate == null ? false : sourceBookTask.openVideoGenerate,
|
||||
} as Book.SelectBookTask
|
||||
|
||||
addBookTask.push(addOneBookTask)
|
||||
@ -517,7 +519,7 @@ export class BookTask {
|
||||
return successMessage(returnBookTask, "复制小说任务成功", "BookBasic_CopyNewBookTask")
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw error
|
||||
return errorMessage("复制小说任务失败,失败信息如下:" + error.message, "BookBasic_CopyNewBookTask")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -262,8 +262,9 @@ export class BookVideo {
|
||||
if (repalceObject && repalceObject.length > 0) {
|
||||
await this.jianyingService.ReplaceDraftMaterialImageToVideo(book.name + "_" + element.name, repalceObject);
|
||||
}
|
||||
return successMessage(result, `${result.join('\n')} ${'\n'} 剪映草稿添加成功`, "BookTask_AddJianyingDraft")
|
||||
}
|
||||
// 所有的草稿都添加完毕之后开始返回
|
||||
return successMessage(result, `${result.join('\n')} ${'\n'} 剪映草稿添加成功`, "BookTask_AddJianyingDraft")
|
||||
} catch (error) {
|
||||
return errorMessage('添加剪映草稿失败,错误信息如下:' + error.toString(), "BookTask_AddJianyingDraft");
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { isEmpty } from "lodash";
|
||||
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相关的服务都在这边
|
||||
@ -11,6 +12,7 @@ export class GptService {
|
||||
gptUrl: string = undefined
|
||||
gptModel: string = undefined
|
||||
gptApiKey: string = undefined
|
||||
useTransfer: boolean = false
|
||||
|
||||
|
||||
//#region GPT 设置
|
||||
@ -42,10 +44,12 @@ export class GptService {
|
||||
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
|
||||
gptModel: this.gptModel,
|
||||
useTransfer: this.useTransfer
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,6 +105,16 @@ export class GptService {
|
||||
}
|
||||
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;
|
||||
}
|
||||
@ -126,6 +140,27 @@ export class GptService {
|
||||
"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',
|
||||
@ -141,6 +176,7 @@ export class GptService {
|
||||
let res = await axios.request(config);
|
||||
let content = this.GetResponseContent(res, this.gptUrl);
|
||||
return content;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
41
src/main/Service/Options/index.ts
Normal file
41
src/main/Service/Options/index.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { OptionType } from "@/define/enum/option"
|
||||
import { OptionServices } from "./optionServices"
|
||||
|
||||
class OptionHandle {
|
||||
optionServices: OptionServices
|
||||
constructor() {
|
||||
this.optionServices = new OptionServices()
|
||||
}
|
||||
|
||||
//#region 和数据库的option操作
|
||||
/**
|
||||
* 获取指定的Option,通过key,不存在返回null
|
||||
* @param key 指定的Key的值
|
||||
* @returns
|
||||
*/
|
||||
GetOptionByKey = async (key: string) => await this.optionServices.GetOptionByKey(key)
|
||||
|
||||
/**
|
||||
* 修改指定的Option,通过key,不存在则创建
|
||||
* @param key 要修改的Key
|
||||
* @param value 修改Key指定的值
|
||||
* @param type 值的类型
|
||||
* @returns
|
||||
*/
|
||||
ModifyOptionByKey = async (key: string, value: string, type: OptionType) =>
|
||||
await this.optionServices.ModifyOptionByKey(key, value, type)
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 其他的Option操作
|
||||
|
||||
/**
|
||||
* 同步文案处理中的AI设置旧数据到新的数据表中
|
||||
* @returns
|
||||
*/
|
||||
InitCopyWritingAISetting = async () => await this.optionServices.InitCopyWritingAISetting()
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export default new OptionHandle()
|
||||
116
src/main/Service/Options/optionServices.ts
Normal file
116
src/main/Service/Options/optionServices.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { OptionRealmService } from '@/define/db/service/SoftWare/optionRealmService'
|
||||
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
||||
import { ValidateJson } from '@/define/Tools/validate'
|
||||
import { errorMessage, successMessage } from '@/main/Public/generalTools'
|
||||
import { ErrorItem, GeneralResponse, SuccessItem } from '@/model/generalResponse'
|
||||
export class OptionServices {
|
||||
optionRealmService!: OptionRealmService
|
||||
constructor() { }
|
||||
|
||||
/** 初始化数据库服务 */
|
||||
async InitService() {
|
||||
if (!this.optionRealmService) {
|
||||
this.optionRealmService = await OptionRealmService.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定的Option,通过key,不存在返回null
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
public async GetOptionByKey(key: string): Promise<GeneralResponse.ErrorItem | SuccessItem> {
|
||||
try {
|
||||
await this.InitService()
|
||||
let res = this.optionRealmService.GetOptionByKey(key)
|
||||
return successMessage(res, '获取成功 OptionKey: ' + key, 'OptionOptions.GetOptionByKey')
|
||||
} catch (error: any) {
|
||||
return errorMessage(
|
||||
'获取失败 OptionKey: ' + key + ',失败信息如下 : ' + error.message,
|
||||
'OptionOptions.GetOptionByKey'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改指定的Option,通过key,不存在则创建
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
public async ModifyOptionByKey(
|
||||
key: string,
|
||||
value: string,
|
||||
type: OptionType
|
||||
): Promise<ErrorItem | SuccessItem> {
|
||||
try {
|
||||
await this.InitService()
|
||||
if (type == OptionType.BOOLEAN) {
|
||||
value = value.toString()
|
||||
}
|
||||
let res = this.optionRealmService.ModifyOptionByKey(key, value, type)
|
||||
return successMessage(res, '修改成功 OptionKey: ' + key, 'OptionOptions.ModifyOptionByKey')
|
||||
} catch (error: any) {
|
||||
return errorMessage(
|
||||
`修改失败 OptionKey: ${key} , 失败信息如下: ${error.message}`,
|
||||
'OptionOptions.ModifyOptionByKey'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 同步文案处理中的AI设置旧数据到新的数据表中
|
||||
* @returns
|
||||
*/
|
||||
public async InitCopyWritingAISetting(): Promise<ErrorItem | SuccessItem> {
|
||||
try {
|
||||
await this.InitService()
|
||||
|
||||
// 没有数据 也没有数据同步 需要初始化
|
||||
let aiSetting = {
|
||||
"laiapi": {
|
||||
"gpt_url": "https://api.laitool.cc",
|
||||
"api_key": "你的LAI API的API Key",
|
||||
"model": "你要使用的API 模型名称,不是令牌名"
|
||||
}
|
||||
}
|
||||
let CW_AISetting = this.optionRealmService.GetOptionByKey(OptionKeyName.CW_AISetting);
|
||||
if (CW_AISetting != null) {
|
||||
let CW_AISettingData = CW_AISetting.value as string
|
||||
|
||||
// 判断已有数据能不能格式化,如果可以格式化则不需要初始化
|
||||
if (ValidateJson(CW_AISettingData)) {
|
||||
return successMessage(JSON.parse(CW_AISettingData), "数据已存在,无需再次同步或初始化", "OptionOptions.InitCopyWritingAISetting")
|
||||
} else {
|
||||
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, JSON.stringify(aiSetting), OptionType.JOSN);
|
||||
return successMessage(aiSetting, "数据已存在,但是数据格式不正确,已重新初始化", "OptionOptions.InitCopyWritingAISetting")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 同步旧文案处理AI设置
|
||||
let software = this.optionRealmService.realm.objects('Software');
|
||||
if (software.length > 0) {
|
||||
// 有数据 同步之前的数据
|
||||
let softwareData = software.toJSON()[0]
|
||||
let SynchronizeAISetting = softwareData["aiSetting"] as string
|
||||
if (ValidateJson(SynchronizeAISetting)) {
|
||||
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, SynchronizeAISetting, OptionType.JOSN);
|
||||
return successMessage(JSON.parse(SynchronizeAISetting), "同步旧文案处理AI设置数据成功", "OptionOptions.InitCopyWritingAISetting")
|
||||
} else {
|
||||
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, JSON.stringify(aiSetting), OptionType.JOSN);
|
||||
return successMessage(aiSetting, "旧的文案处理AI设置无效,已重新重置", "OptionOptions.InitCopyWritingAISetting")
|
||||
}
|
||||
}
|
||||
|
||||
// 新设置
|
||||
this.optionRealmService.ModifyOptionByKey(OptionKeyName.CW_AISetting, JSON.stringify(aiSetting), OptionType.JOSN);
|
||||
return successMessage(aiSetting, '初始化文案处理AI设置成功', 'OptionOptions.SynchronizeAISettingOldData')
|
||||
} catch (error: any) {
|
||||
return errorMessage(
|
||||
'同步失败,失败信息如下:' + error.message,
|
||||
'OptionOptions.SynchronizeAISettingOldData'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ import { DEFINE_STRING } from "../../../define/define_string";
|
||||
import { OtherData, ResponseMessageType } from "../../../define/enum/softwareEnum";
|
||||
import util from 'util';
|
||||
import { exec } from 'child_process';
|
||||
import { TaskModal } from "@/model/task";
|
||||
const execAsync = util.promisify(exec)
|
||||
const fspromise = fs.promises
|
||||
|
||||
|
||||
@ -55,7 +55,14 @@ class BookServiceBasic {
|
||||
//#region 批次任务任务
|
||||
|
||||
GetBookTaskDataById = async (bookTaskId: string) => await this.bookTaskServiceBasic.GetBookTaskDataById(bookTaskId);
|
||||
GetBookTaskData = async (bookTaskCondition: Book.QueryBookTaskCondition) => await this.bookTaskServiceBasic.GetBookTaskData(bookTaskCondition);
|
||||
/**
|
||||
* 通过查询条件获取小说批次任务数据
|
||||
* @param bookTaskCondition 查询的小说条件
|
||||
* @param returnEmpry 是不是返回空数据,默认是false,没有数据直接报错,true的话返回空数据
|
||||
* @returns
|
||||
*/
|
||||
GetBookTaskData = async (bookTaskCondition: Book.QueryBookTaskCondition, returnEmpry: boolean = false) => await this.bookTaskServiceBasic.GetBookTaskData(bookTaskCondition, returnEmpry);
|
||||
|
||||
GetMaxBookTaskNo = async (bookId: string) => await this.bookTaskServiceBasic.GetMaxBookTaskNo(bookId);
|
||||
UpdetedBookTaskData = async (bookTaskId: string, data: Book.SelectBookTask) => await this.bookTaskServiceBasic.UpdetedBookTaskData(bookTaskId, data);
|
||||
ResetBookTask = async (bookTaskId: string) => await this.bookTaskServiceBasic.ResetBookTask(bookTaskId);
|
||||
|
||||
@ -33,15 +33,21 @@ export default class BookTaskServiceBasic {
|
||||
|
||||
/**
|
||||
* 通过查询条件获取小说批次任务数据
|
||||
* @param bookTaskCondition 小说批次的查询条件
|
||||
* @param bookTaskCondition 查询的小说条件
|
||||
* @param returnEmpry 是不是返回空数据,默认是false,没有数据直接报错,true的话返回空数据
|
||||
* @returns
|
||||
*/
|
||||
async GetBookTaskData(bookTaskCondition: Book.QueryBookTaskCondition): Promise<{ bookTasks: Book.SelectBookTask[], total: number }> {
|
||||
async GetBookTaskData(bookTaskCondition: Book.QueryBookTaskCondition, returnEmpry: boolean = false): Promise<{ bookTasks: Book.SelectBookTask[], total: number }> {
|
||||
|
||||
await this.InitService();
|
||||
let bookTasks = this.bookTaskService.GetBookTaskData(bookTaskCondition)
|
||||
if (returnEmpry) {
|
||||
return { bookTasks: [], total: 0 }
|
||||
} else {
|
||||
if (bookTasks.data.bookTasks.length <= 0 || bookTasks.data.total <= 0) {
|
||||
throw new Error("未找到对应的小说批次任务数据,请检查")
|
||||
}
|
||||
}
|
||||
return bookTasks.data
|
||||
}
|
||||
|
||||
|
||||
@ -405,9 +405,8 @@ export class SubtitleService {
|
||||
btd.timeLimit = element.timeLimit
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return successMessage(null, "保存文案数据成功", 'SubtitleService_SaveCopywriting')
|
||||
} catch (error) {
|
||||
return errorMessage("保存文案数据失败,失败信息如下:" + error.toString(), 'SubtitleService_SaveCopywriting')
|
||||
}
|
||||
|
||||
@ -193,6 +193,33 @@ export class Translate {
|
||||
"content": value.text
|
||||
});
|
||||
|
||||
let content = "";
|
||||
// 判断整体是不是需要LMS转发
|
||||
if (global.config.useTransfer) {
|
||||
let url = define.lms + "/lms/Forward/SimpleTransfer";
|
||||
let config = {
|
||||
method: 'post',
|
||||
url: url,
|
||||
maxBodyLength: Infinity,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify({
|
||||
url: this.translationBusiness,
|
||||
apiKey: token,
|
||||
dataString: JSON.stringify(data)
|
||||
})
|
||||
}
|
||||
// 重试机制
|
||||
let res = await RetryWithBackoff(async () => {
|
||||
return await axios.request(config);
|
||||
}, 5, 2000)
|
||||
|
||||
if (res.data.code != 1) {
|
||||
throw new Error(res.data.message);
|
||||
}
|
||||
content = GetOpenAISuccessResponse(res.data.data);
|
||||
} else {
|
||||
let config = {
|
||||
method: 'post',
|
||||
maxBodyLength: Infinity,
|
||||
@ -206,11 +233,11 @@ export class Translate {
|
||||
let res = await RetryWithBackoff(async () => {
|
||||
return await axios.request(config);
|
||||
}, 5, 2000)
|
||||
// let res = await axios.request(config);
|
||||
// 将返回的数据进行拼接数据处理
|
||||
|
||||
content = GetOpenAISuccessResponse(res.data);
|
||||
}
|
||||
let res_data = [];
|
||||
let content = GetOpenAISuccessResponse(res.data);
|
||||
|
||||
res_data.push({
|
||||
src: value.text,
|
||||
dst: content
|
||||
|
||||
210
src/main/Service/copywriting/copywritingAIGenerationService.ts
Normal file
210
src/main/Service/copywriting/copywritingAIGenerationService.ts
Normal file
@ -0,0 +1,210 @@
|
||||
import { OptionKeyName } from "@/define/enum/option";
|
||||
import { RetryWithBackoff } from "@/define/Tools/common";
|
||||
import { errorMessage, successMessage } from "@/main/Public/generalTools";
|
||||
import OptionHandle from "@/main/Service/Options/index";
|
||||
import { OptionModel } from "@/model/option/option";
|
||||
import { get, isEmpty } from "lodash";
|
||||
import { define } from "@/define/define"
|
||||
import { DEFINE_STRING } from "@/define/define_string";
|
||||
import { GetDoubaoErrorResponse, GetKimiErrorResponse, GetOpenAISuccessResponse, GetRixApiErrorResponse } from "@/define/response/openAIResponse";
|
||||
import axios from "axios";
|
||||
|
||||
export class CopywritingAIGenerationService {
|
||||
|
||||
//#region 文案处理相关
|
||||
|
||||
/**
|
||||
* AI处理文案
|
||||
* @param idS 需要改写的文案ID
|
||||
*/
|
||||
async CopyWritingAIGeneration(ids: string[]) {
|
||||
try {
|
||||
if (ids.length === 0) {
|
||||
throw new Error("没有需要处理的文案ID")
|
||||
}
|
||||
|
||||
// 加载文案处理数据
|
||||
let CW_AISimpleSetting = await OptionHandle.GetOptionByKey(OptionKeyName.CW_AISimpleSetting);
|
||||
if (CW_AISimpleSetting.code !== 1) {
|
||||
throw new Error("加载文案处理数据失败,失败原因如下:" + CW_AISimpleSetting.message);
|
||||
}
|
||||
let CW_AISimpleSettingData = JSON.parse(CW_AISimpleSetting.data.value) as OptionModel.CW_AISimpleSettingModel;
|
||||
|
||||
if (isEmpty(CW_AISimpleSettingData.gptType) || isEmpty(CW_AISimpleSettingData.gptData) || isEmpty(CW_AISimpleSettingData.gptAI)) {
|
||||
throw new Error("设置数据不完整,请检查提示词类型,提示词预设,请求AI数据是否完整");
|
||||
}
|
||||
|
||||
let wordStruct = CW_AISimpleSettingData.wordStruct;
|
||||
let filterWordStruct = wordStruct.filter((item) => ids.includes(item.id));
|
||||
if (filterWordStruct.length === 0) {
|
||||
throw new Error("没有找到需要处理的文案ID对应的数据,请检查数据是否正确");
|
||||
}
|
||||
|
||||
let CW_AISetting = await OptionHandle.GetOptionByKey(OptionKeyName.CW_AISetting);
|
||||
if (CW_AISetting.code !== 1) {
|
||||
throw new Error("加载AI设置数据失败,失败原因如下:" + CW_AISetting.message);
|
||||
}
|
||||
let CW_AISettingData = JSON.parse(CW_AISetting.data.value);
|
||||
let aiSetting = get(CW_AISettingData, CW_AISimpleSettingData.gptAI, {});
|
||||
for (const aid in aiSetting) {
|
||||
if (isEmpty(aid)) {
|
||||
throw new Error('请先设置AI设置')
|
||||
}
|
||||
}
|
||||
|
||||
// 开始循环请求AI
|
||||
for (let ii = 0; ii < filterWordStruct.length; ii++) {
|
||||
const element = filterWordStruct[ii];
|
||||
if (CW_AISimpleSettingData.isStream) {
|
||||
// 流式请求
|
||||
let returnData = await RetryWithBackoff(async () => {
|
||||
return await this.AIRequestStream(CW_AISimpleSettingData, aiSetting, element, "")
|
||||
}, 3, 1000) + '\n'
|
||||
// 这边将数据保存
|
||||
element.newWord = returnData
|
||||
} else {
|
||||
// 非流式请求
|
||||
let returnData = await RetryWithBackoff(async () => {
|
||||
return await this.AIRequest(CW_AISimpleSettingData, aiSetting, element.oldWord)
|
||||
}, 3, 1000) + '\n'
|
||||
// 这边将数据保存
|
||||
element.newWord = returnData
|
||||
console.log(returnData)
|
||||
// 将非流的数据返回
|
||||
global.newWindow[0].win.webContents.send(DEFINE_STRING.GPT.GPT_STREAM_RETURN, {
|
||||
id: element.id,
|
||||
newWord: returnData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理完毕 返回数据。这边不做任何的保存动作
|
||||
return successMessage(wordStruct, "AI处理文案成功", "CopywritingAIGenerationService_CopyWritingAIGeneration")
|
||||
|
||||
} catch (error) {
|
||||
return errorMessage("AI处理文案失败,失败原因如下:" + error.message, "CopywritingAIGenerationService_CopyWritingAIGeneration")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, wordStruct: OptionModel.CW_AISimpleSettingModel_WordStruct, 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: wordStruct.oldWord,
|
||||
}
|
||||
|
||||
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 = '';
|
||||
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, {
|
||||
id: wordStruct.id,
|
||||
newWord: resData
|
||||
})
|
||||
controller.enqueue(value); // 可选:将数据块放入流中
|
||||
push();
|
||||
}).catch(err => {
|
||||
controller.error(err);
|
||||
reject(err)
|
||||
});
|
||||
}
|
||||
push();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
24
src/main/Service/copywriting/index.ts
Normal file
24
src/main/Service/copywriting/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
import { CopywritingAIGenerationService } from "./copywritingAIGenerationService";
|
||||
|
||||
class CopyWritingService {
|
||||
|
||||
copywritingAIGenerationService: CopywritingAIGenerationService
|
||||
|
||||
constructor() {
|
||||
this.copywritingAIGenerationService = new CopywritingAIGenerationService();
|
||||
}
|
||||
|
||||
//#region 文案处理
|
||||
|
||||
/**
|
||||
* AI处理文案
|
||||
* @param idS 需要改写的文案ID
|
||||
*/
|
||||
CopyWritingAIGeneration = async (idS: string[]) => await this.copywritingAIGenerationService.CopyWritingAIGeneration(idS);
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export default new CopyWritingService();
|
||||
@ -14,12 +14,20 @@ import { tts } from '../../model/tts'
|
||||
import { GeneralResponse } from '../../model/generalResponse'
|
||||
import axios from 'axios'
|
||||
import { GetEdgeTTSRole } from '../../define/tts/ttsDefine'
|
||||
import { OptionServices } from "@/main/Service/Options/optionServices"
|
||||
import { OptionKeyName } from '@/define/enum/option'
|
||||
import { OptionModel } from '@/model/option/option'
|
||||
|
||||
export class TTS {
|
||||
softService: SoftwareService
|
||||
ttsService: TTSService
|
||||
|
||||
constructor() { }
|
||||
optionServices: OptionServices
|
||||
|
||||
|
||||
constructor() {
|
||||
this.optionServices = new OptionServices()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化TTS服务
|
||||
@ -62,72 +70,12 @@ export class TTS {
|
||||
throw new Error("获取TTS角色配置失败")
|
||||
}
|
||||
if (isEmpty(data[0].value) || !ValidateJson(data[0].value)) {
|
||||
return successMessage(GetEdgeTTSRole(), "获取远程配置失败,获取默认配音角色", "TTS_GetTTSCOnfig"); // 使用默认值
|
||||
return successMessage(GetEdgeTTSRole(), "获取远程配置失败,获取默认配音角色", "TTS_GetTTSOptions"); // 使用默认值
|
||||
}
|
||||
// 返回远程值
|
||||
return successMessage(JSON.parse(data[0].value), '获取TTS配置成功', 'TTS_GetTTSCOnfig')
|
||||
return successMessage(JSON.parse(data[0].value), '获取TTS配置成功', 'TTS_GetTTSOptions')
|
||||
} catch (error) {
|
||||
return errorMessage('获取TTS配置失败,错误信息如下:' + error.toString(), 'TTS_GetTTSCOnfig')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化TTS设置
|
||||
*/
|
||||
async InitTTSSetting() {
|
||||
let defaultData = {
|
||||
selectModel: 'edge-tts',
|
||||
edgeTTS: {
|
||||
value: 'zh-CN-XiaoxiaoNeural',
|
||||
gender: 'Female',
|
||||
label: '晓晓',
|
||||
lang: 'zh-CN',
|
||||
saveSubtitles: true,
|
||||
pitch: 0, // 语调
|
||||
rate: 10, // 倍速
|
||||
volumn: 0 // 音量
|
||||
}
|
||||
}
|
||||
await this.SaveTTSConfig(defaultData)
|
||||
return defaultData
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取TTS配置
|
||||
*/
|
||||
// @ts-ignore
|
||||
async GetTTSCOnfig(): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
|
||||
try {
|
||||
await this.InitService()
|
||||
let res = this.softService.GetSoftWarePropertyData('ttsSetting')
|
||||
let resObj = undefined
|
||||
if (isEmpty(res)) {
|
||||
// 没有数据,需要初始化
|
||||
resObj = await this.InitTTSSetting()
|
||||
} else {
|
||||
let tryParse = ValidateJson(res)
|
||||
if (!tryParse) {
|
||||
throw new Error('解析TTS配置失败,数据格式不正确')
|
||||
}
|
||||
resObj = JSON.parse(res)
|
||||
}
|
||||
return successMessage(resObj, '获取TTS配置成功', 'TTS_GetTTSCOnfig')
|
||||
} catch (error) {
|
||||
return errorMessage('获取TTS配置失败,错误信息如下:' + error.toString(), 'TTS_GetTTSCOnfig')
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 保存TTS配置
|
||||
* @param {*} data 要保存的数据
|
||||
*/
|
||||
// @ts-ignore
|
||||
async SaveTTSConfig(data: TTSSettingModel.TTSSetting) {
|
||||
try {
|
||||
await this.InitService()
|
||||
let res = this.softService.SaveSoftwarePropertyData('ttsSetting', JSON.stringify(data))
|
||||
return res
|
||||
} catch (error) {
|
||||
return errorMessage('保存TTS配置失败,错误信息如下:' + error.toString(), 'TTS_SaveTTSConfig')
|
||||
return errorMessage('获取TTS配置失败,错误信息如下:' + error.toString(), 'TTS_GetTTSOptions')
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,12 +86,17 @@ export class TTS {
|
||||
* 生成音频
|
||||
* @param text 要生成的文本
|
||||
*/
|
||||
async GenerateAudio(text: string) {
|
||||
async GenerateAudio() {
|
||||
try {
|
||||
await this.InitService()
|
||||
let ttsSetting = await this.GetTTSCOnfig()
|
||||
if (ttsSetting.code === 0) {
|
||||
return ttsSetting
|
||||
let TTS_GlobalSetting = await this.optionServices.GetOptionByKey(OptionKeyName.TTS_GlobalSetting);
|
||||
if (TTS_GlobalSetting.code == 0) {
|
||||
throw new Error(TTS_GlobalSetting.message);
|
||||
}
|
||||
let TTS_GlobalSettingData = JSON.parse(TTS_GlobalSetting.data.value) as OptionModel.TTS_GlobalSettingModel
|
||||
let text = TTS_GlobalSettingData.ttsText;
|
||||
if (isEmpty(text)) {
|
||||
throw new Error('生成音频失败,文本为空')
|
||||
}
|
||||
let res = undefined
|
||||
|
||||
@ -156,13 +109,13 @@ export class TTS {
|
||||
await fs.promises.writeFile(textPath, text, 'utf-8')
|
||||
|
||||
let audioPath = path.join(define.tts_path, `${thisId}/${thisId}.mp3`)
|
||||
let selectModel = ttsSetting.data.selectModel as TTSSelectModel
|
||||
let selectModel = TTS_GlobalSettingData.selectModel;
|
||||
|
||||
let hasSrt = true
|
||||
switch (selectModel) {
|
||||
case TTSSelectModel.edgeTTS:
|
||||
hasSrt = ttsSetting.data.edgeTTS.saveSubtitles
|
||||
res = await this.GenerateAudioByEdgeTTS(text, ttsSetting.data.edgeTTS, audioPath)
|
||||
hasSrt = TTS_GlobalSettingData.edgeTTS.saveSubtitles
|
||||
res = await this.GenerateAudioByEdgeTTS(text, TTS_GlobalSettingData.edgeTTS, audioPath)
|
||||
break
|
||||
default:
|
||||
throw new Error('未知的TTS模式')
|
||||
@ -182,7 +135,7 @@ export class TTS {
|
||||
id: thisId,
|
||||
textPath: textPath ? path.relative(define.tts_path, textPath) : null
|
||||
})
|
||||
return res
|
||||
return successMessage(res, '生成音频成功', 'TTS_GenerateAudio')
|
||||
} catch (error) {
|
||||
return errorMessage('生成音频失败,错误信息如下:' + error.toString(), 'TTS_GenerateAudio')
|
||||
}
|
||||
@ -194,9 +147,9 @@ export class TTS {
|
||||
* @param edgeTTS edgetts的设置
|
||||
* @returns
|
||||
*/
|
||||
async GenerateAudioByEdgeTTS(text: string, edgeTTS: TTSSettingModel.EdgeTTSSetting, mp3Path: string) {
|
||||
async GenerateAudioByEdgeTTS(text: string, edgeTTS: OptionModel.TTS_EdgeTTSModel, mp3Path: string) {
|
||||
try {
|
||||
const tts = new EdgeTTS({
|
||||
const edgeTts = new EdgeTTS({
|
||||
voice: edgeTTS.value,
|
||||
lang: edgeTTS.lang,
|
||||
outputFormat: 'audio-24khz-96kbitrate-mono-mp3',
|
||||
@ -204,10 +157,9 @@ export class TTS {
|
||||
pitch: `${edgeTTS.pitch}%`,
|
||||
rate: `${edgeTTS.rate}%`,
|
||||
volume: `${edgeTTS.volumn}%`,
|
||||
timeout : 100000
|
||||
timeout: edgeTTS.timeOut ?? 100000
|
||||
})
|
||||
let ttsRes = await tts.ttsPromise(text, mp3Path)
|
||||
console.log(ttsRes)
|
||||
await edgeTts.ttsPromise(text, mp3Path)
|
||||
return {
|
||||
mp3Path: mp3Path,
|
||||
srtJsonPath: mp3Path + '.json'
|
||||
|
||||
@ -5,7 +5,6 @@ import { DEFINE_STRING } from '../../define/define_string'
|
||||
import { PublicMethod } from '../Public/publicMethod'
|
||||
import { define } from '../../define/define'
|
||||
import { get, has, isEmpty } from 'lodash'
|
||||
import { ClipSetting } from '../../define/setting/clipSetting'
|
||||
import { errorMessage, successMessage } from '../Public/generalTools'
|
||||
import { ServiceBase } from '../../define/db/service/serviceBase'
|
||||
import {
|
||||
@ -25,7 +24,7 @@ export class Writing extends ServiceBase {
|
||||
constructor(global) {
|
||||
super()
|
||||
this.pm = new PublicMethod(global)
|
||||
axios.defaults.baseURL = define.serverUrl
|
||||
axios.defaults.baseURL = define.lms
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,7 +104,7 @@ export class Writing extends ServiceBase {
|
||||
|
||||
let resData = '\n\n';
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(define.serverUrl + "/lms/Forward/ForwardWordStream", requestOptions)
|
||||
fetch(define.lms + "/lms/Forward/ForwardWordStream", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.body) {
|
||||
throw new Error('ReadableStream not yet supported in this browser.');
|
||||
@ -241,10 +240,6 @@ export class Writing extends ServiceBase {
|
||||
}, 3, 1000)
|
||||
}
|
||||
}
|
||||
// let tasks =
|
||||
|
||||
// console.log("ActionStart", result);
|
||||
// ExecuteConcurrently
|
||||
return successMessage(result, "执行文案相关任务成功", 'Writing_ActionStart');
|
||||
} catch (error) {
|
||||
return errorMessage(
|
||||
|
||||
@ -17,7 +17,7 @@ export class GptSetting extends ServiceBase {
|
||||
subtitleService: SubtitleService
|
||||
constructor() {
|
||||
super()
|
||||
axios.defaults.baseURL = define.serverUrl
|
||||
axios.defaults.baseURL = define.lms
|
||||
this.softWareServiceBasic = new SoftWareServiceBasic();
|
||||
this.subtitleService = new SubtitleService()
|
||||
}
|
||||
|
||||
1
src/model/Setting/softwareSetting.d.ts
vendored
1
src/model/Setting/softwareSetting.d.ts
vendored
@ -21,6 +21,7 @@ declare namespace SoftwareSettingModel {
|
||||
project_name: string = undefined // 项目名称
|
||||
gpt_business: string = undefined // GPT服务商ID
|
||||
gpt_model: string = undefined // GPT模型
|
||||
useTransfer: boolean = false // 是不是使用转发
|
||||
task_number: number = undefined // 任务数量
|
||||
theme: string = undefined // 主题
|
||||
gpt_auto_inference: string = undefined // GPT自动推理模式
|
||||
|
||||
13
src/model/generalResponse.d.ts
vendored
13
src/model/generalResponse.d.ts
vendored
@ -36,3 +36,16 @@ declare namespace GeneralResponse {
|
||||
data?: MJ.MJResponseToFront | Buffer | string | TranslateModel.TranslateResponseMessageModel | ProgressResponse | SubtitleProgressResponse
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type SuccessItem = {
|
||||
code: number
|
||||
message?: string
|
||||
data: any
|
||||
}
|
||||
|
||||
export type ErrorItem = {
|
||||
code: number
|
||||
message: string
|
||||
data?: any
|
||||
}
|
||||
110
src/model/option/option.d.ts
vendored
110
src/model/option/option.d.ts
vendored
@ -1,10 +1,116 @@
|
||||
import { OptionType } from "@/define/enum/option"
|
||||
|
||||
declare namespace Option {
|
||||
/** option的model */
|
||||
declare namespace OptionModel {
|
||||
/**
|
||||
* Option的模型
|
||||
*/
|
||||
type OptionItem = {
|
||||
key: string,
|
||||
value: string,
|
||||
type: OptionType
|
||||
}
|
||||
|
||||
//#region 文案处理
|
||||
|
||||
/**
|
||||
* 文案处理 AI设置模型
|
||||
*/
|
||||
type CW_AISettingModel = {
|
||||
/** API key */
|
||||
api_key: string,
|
||||
/** 调用的AI地址,支持 OPEN AI 请求格式的 */
|
||||
gpt_url: string,
|
||||
/** 调用的模型名字 */
|
||||
model: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 文案处理数据的数据格式数据类型
|
||||
*/
|
||||
type CW_AISimpleSettingModel_WordStruct = {
|
||||
/** ID */
|
||||
id: string,
|
||||
/** AI改写前的文案 */
|
||||
oldWord: string | undefined,
|
||||
/** AI输出的文案 */
|
||||
newWord: string | undefined
|
||||
/** AI改写前的文案的字数 */
|
||||
oldWordCount: number,
|
||||
/** AI输出的文案的字数 */
|
||||
newWordCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 文案处理 简单AI设置模型
|
||||
*/
|
||||
type CW_AISimpleSettingModel = {
|
||||
/** 预设的类型 */
|
||||
gptType: string | undefined,
|
||||
/** 选择的预设 */
|
||||
gptData: string | undefined,
|
||||
/** 选择的AI站点,默认LAI API */
|
||||
gptAI: string | undefined,
|
||||
/** 是不是流式请求 */
|
||||
isStream: boolean,
|
||||
/** 是不是对文案内容进行分割 按照设置的 splitNumber,默认为500 */
|
||||
isSplit: boolean,
|
||||
/** 分割字符 */
|
||||
splitNumber: number,
|
||||
/** AI改写前的文案 */
|
||||
oldWord: string | undefined,
|
||||
/** AI输出的文案 */
|
||||
newWord: string | undefined,
|
||||
/** AI改写前的文案的字数 */
|
||||
oldWordCount: number,
|
||||
/** AI输出的文案的字数 */
|
||||
newWordCount: number,
|
||||
/** 文案数据的数据格式数据类型,数组 */
|
||||
wordStruct: Array<CW_AISimpleSettingModel_WordStruct>
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region TTS
|
||||
|
||||
/**
|
||||
* tts所有的配置数据
|
||||
*/
|
||||
type TTS_GlobalSettingModel = {
|
||||
/** 选择的TTS模型 */
|
||||
selectModel: string,
|
||||
/** TTS模型的数据 */
|
||||
edgeTTS: TTS_EdgeTTSModel,
|
||||
/** 合成语音的文本 */
|
||||
ttsText?: string,
|
||||
/** 保存的音频文件路径 */
|
||||
saveAudioPath?: string,
|
||||
}
|
||||
|
||||
/** EdgeTTS模型的设置 */
|
||||
type TTS_EdgeTTSModel = {
|
||||
/** 选择的TTS模型值 */
|
||||
value: string,
|
||||
/** 选择的TTS模型的性别 */
|
||||
gender: string,
|
||||
/** 显示的名称 */
|
||||
label: string,
|
||||
/** 语言 */
|
||||
lang: string,
|
||||
/** 是否保存字幕 */
|
||||
saveSubtitles: boolean,
|
||||
/** 音调 */
|
||||
pitch: number,
|
||||
/** 语速 */
|
||||
rate: number,
|
||||
/** 音量 */
|
||||
volumn: number,
|
||||
/** 超时时间,单位 毫秒 */
|
||||
timeOut: number
|
||||
}
|
||||
|
||||
|
||||
|
||||
//#endregion
|
||||
|
||||
}
|
||||
@ -16,6 +16,7 @@ import { db } from './db'
|
||||
import { translate } from './translate'
|
||||
import { preset } from './preset'
|
||||
import { task } from './task'
|
||||
import { options } from './options'
|
||||
// Custom APIs for renderer
|
||||
|
||||
let events = []
|
||||
@ -478,6 +479,7 @@ if (process.contextIsolated) {
|
||||
contextBridge.exposeInMainWorld('db', db)
|
||||
contextBridge.exposeInMainWorld('preset', preset)
|
||||
contextBridge.exposeInMainWorld('task', task)
|
||||
contextBridge.exposeInMainWorld('options', options)
|
||||
contextBridge.exposeInMainWorld('darkMode', {
|
||||
toggle: (value) => ipcRenderer.invoke('dark-mode:toggle', value)
|
||||
})
|
||||
@ -502,4 +504,5 @@ if (process.contextIsolated) {
|
||||
window.preset = preset
|
||||
window.task = task
|
||||
window.translate = translate
|
||||
window.options = options
|
||||
}
|
||||
|
||||
22
src/preload/options.ts
Normal file
22
src/preload/options.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { DEFINE_STRING } from '../define/define_string'
|
||||
import { OptionType } from '@/define/enum/option'
|
||||
|
||||
const options = {
|
||||
/** 通过Key获取指定的option */
|
||||
GetOptionByKey: async (key: string) =>
|
||||
await ipcRenderer.invoke(DEFINE_STRING.OPTIONS.GET_OPTION_BY_KEY, key),
|
||||
|
||||
/**
|
||||
* 修改指定的Option 通过key
|
||||
*/
|
||||
ModifyOptionByKey: async (key: string, value: string, type: OptionType) =>
|
||||
await ipcRenderer.invoke(DEFINE_STRING.OPTIONS.MODIFY_OPTION_BY_KEY, key, value, type),
|
||||
|
||||
/**
|
||||
* 初始化文案处理AI设置
|
||||
* @returns
|
||||
*/
|
||||
InitCopyWritingAISetting: async () => await ipcRenderer.invoke(DEFINE_STRING.OPTIONS.INIT_COPY_WRITING_AI_SETTING)
|
||||
}
|
||||
export { options }
|
||||
@ -2,14 +2,9 @@ import { ipcRenderer } from 'electron'
|
||||
import { DEFINE_STRING } from '../define/define_string'
|
||||
|
||||
const tts = {
|
||||
// 获取当前的TTS配置数据
|
||||
GetTTSCOnfig: async () => await ipcRenderer.invoke(DEFINE_STRING.TTS.GET_TTS_CONFIG),
|
||||
|
||||
// 保存TTS配置
|
||||
SaveTTSConfig: async (data) => await ipcRenderer.invoke(DEFINE_STRING.TTS.SAVE_TTS_CONFIG, data),
|
||||
|
||||
// 生成音频
|
||||
GenerateAudio: async (text) => await ipcRenderer.invoke(DEFINE_STRING.TTS.GENERATE_AUDIO, text),
|
||||
GenerateAudio: async () => await ipcRenderer.invoke(DEFINE_STRING.TTS.GENERATE_AUDIO),
|
||||
|
||||
// 生成SRT字幕
|
||||
GenerateSrt: async (text) => await ipcRenderer.invoke(DEFINE_STRING.TTS.GENERATE_SRT, text),
|
||||
|
||||
@ -27,10 +27,15 @@ const write = {
|
||||
|
||||
//#region AI相关的任务
|
||||
|
||||
// 开始执行API相关的一系列任务
|
||||
ActionStart(aiSetting, word) {
|
||||
return ipcRenderer.invoke(DEFINE_STRING.WRITE.ACTION_START, aiSetting, word)
|
||||
/**
|
||||
* AI处理文案
|
||||
* @param ids 需要改写的文案ID
|
||||
* @returns
|
||||
*/
|
||||
CopyWritingAIGeneration(ids: string[]) {
|
||||
return ipcRenderer.invoke(DEFINE_STRING.WRITE.COPY_WRITING_AI_GENERATION, ids)
|
||||
},
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 文案洗稿相关
|
||||
|
||||
72
src/renderer/src/common/copyWriting.ts
Normal file
72
src/renderer/src/common/copyWriting.ts
Normal file
@ -0,0 +1,72 @@
|
||||
// @ts-nocheck
|
||||
import { OptionKeyName, OptionType } from "@/define/enum/option";
|
||||
import { useOptionStore } from "@/stores/option";
|
||||
import { isEmpty } from "lodash";
|
||||
import TextCommon from "./text";
|
||||
|
||||
/**
|
||||
* 保存CWAISimpleSetting
|
||||
*/
|
||||
async function SaveCWAISimpleSetting() {
|
||||
let optionStore = useOptionStore();
|
||||
let saveRes = await window.options.ModifyOptionByKey(OptionKeyName.CW_AISimpleSetting, JSON.stringify(optionStore.CW_AISimpleSetting), OptionType.JOSN);
|
||||
if (saveRes.code == 0) {
|
||||
throw new Error(saveRes.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新分割或者是合并旧文案
|
||||
* @param isSplit 这个默认为
|
||||
*/
|
||||
async function SplitOrMergeOldText(isSplit: boolean = true, inputWord: string | undefined = undefined) {
|
||||
let optionStore = useOptionStore();
|
||||
let wordStr = "";
|
||||
|
||||
if (inputWord) {
|
||||
wordStr = inputWord
|
||||
} else {
|
||||
let wordStruct = optionStore.CW_AISimpleSetting.wordStruct
|
||||
if (!wordStruct || wordStruct.length <= 0) {
|
||||
throw new Error('分割合并文案失败:没有数据(wordStruct)')
|
||||
}
|
||||
wordStr = wordStruct.map((item) => item.oldWord).join('\n')
|
||||
if (isEmpty(wordStr)) {
|
||||
throw new Error('分割合并文案失败:没有数据(wordStr)')
|
||||
}
|
||||
}
|
||||
// 分割文案
|
||||
if (isSplit) {
|
||||
let splits = TextCommon.SplitTextIntoChunks(
|
||||
wordStr,
|
||||
optionStore.CW_AISimpleSetting.splitNumber
|
||||
)
|
||||
|
||||
optionStore.CW_AISimpleSetting.wordStruct = []
|
||||
|
||||
splits.map((item) => {
|
||||
optionStore.CW_AISimpleSetting.wordStruct.push({
|
||||
oldWord: item,
|
||||
newWord: '',
|
||||
id: crypto.randomUUID()
|
||||
})
|
||||
})
|
||||
await CopyWriting.SaveCWAISimpleSetting()
|
||||
} else {
|
||||
optionStore.CW_AISimpleSetting.wordStruct = []
|
||||
// 将文案合并为一个文案
|
||||
optionStore.CW_AISimpleSetting.wordStruct.push({
|
||||
oldWord: wordStr,
|
||||
newWord: '',
|
||||
id: crypto.randomUUID()
|
||||
})
|
||||
await CopyWriting.SaveCWAISimpleSetting()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let CopyWriting = {
|
||||
SaveCWAISimpleSetting,
|
||||
SplitOrMergeOldText
|
||||
};
|
||||
export default CopyWriting;
|
||||
97
src/renderer/src/common/initCommon.ts
Normal file
97
src/renderer/src/common/initCommon.ts
Normal file
@ -0,0 +1,97 @@
|
||||
// @ts-nocheck
|
||||
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
|
||||
/**
|
||||
* 初始化特殊字符数据,用于文案的相关处理
|
||||
* @returns
|
||||
*/
|
||||
async function InitSpecialCharacters() {
|
||||
let optionStore = useOptionStore()
|
||||
let specialCharacters = `。,“”‘’!?【】「」《》()…—;,''""!?[]<>()-:;╰*°▽°*╯′,ノ﹏<o‵゚Д゚,ノ,へ ̄╬▔`
|
||||
|
||||
let specialCharactersData = await window.options.GetOptionByKey(
|
||||
OptionKeyName.CW_FormatSpecialChar
|
||||
)
|
||||
if (specialCharactersData.code == 0) {
|
||||
// 获取失败
|
||||
window.api.showGlobalMessageDialog(specialCharactersData)
|
||||
return
|
||||
}
|
||||
|
||||
if (specialCharactersData.data == null) {
|
||||
// 没有数据初始化
|
||||
let saveRes = await window.options.ModifyOptionByKey(
|
||||
OptionKeyName.CW_FormatSpecialChar,
|
||||
specialCharacters,
|
||||
OptionType.STRING
|
||||
)
|
||||
if (saveRes.code == 0) {
|
||||
window.api.showGlobalMessageDialog(saveRes)
|
||||
return
|
||||
} else {
|
||||
optionStore.CW_FormatSpecialChar = specialCharacters
|
||||
}
|
||||
} else {
|
||||
optionStore.CW_FormatSpecialChar = specialCharactersData.data.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化TTS设置
|
||||
* @returns
|
||||
*/
|
||||
async function InitTTSGlobalSetting() {
|
||||
debugger
|
||||
let optionStore = useOptionStore()
|
||||
let initData = {
|
||||
selectModel: "edge-tts",
|
||||
edgeTTS: {
|
||||
"value": "zh-CN-XiaoyiNeural",
|
||||
"gender": "Female",
|
||||
"label": "中文-女-小宜",
|
||||
"lang": "zh-CN",
|
||||
"saveSubtitles": true,
|
||||
"pitch": 0,
|
||||
"rate": 10,
|
||||
"volumn": 0,
|
||||
"timeOut": 120000
|
||||
},
|
||||
ttsText: "你好,我是你的智能语音助手!",
|
||||
/** 保存的音频文件路径 */
|
||||
saveAudioPath: undefined,
|
||||
};
|
||||
|
||||
let TTS_GlobalSetting = await window.options.GetOptionByKey(
|
||||
OptionKeyName.TTS_GlobalSetting
|
||||
)
|
||||
if (TTS_GlobalSetting.code == 0) {
|
||||
// 获取失败
|
||||
window.api.showGlobalMessageDialog(TTS_GlobalSetting)
|
||||
return
|
||||
}
|
||||
|
||||
if (TTS_GlobalSetting.data == null) {
|
||||
// 没有数据初始化
|
||||
let saveRes = await window.options.ModifyOptionByKey(
|
||||
OptionKeyName.TTS_GlobalSetting,
|
||||
JSON.stringify(initData),
|
||||
OptionType.JOSN
|
||||
)
|
||||
if (saveRes.code == 0) {
|
||||
window.api.showGlobalMessageDialog(saveRes)
|
||||
return
|
||||
} else {
|
||||
optionStore.TTS_GlobalSetting = initData
|
||||
}
|
||||
} else {
|
||||
optionStore.TTS_GlobalSetting = JSON.parse(TTS_GlobalSetting.data.value)
|
||||
}
|
||||
}
|
||||
|
||||
let InitCommon = {
|
||||
InitSpecialCharacters,
|
||||
InitTTSGlobalSetting
|
||||
}
|
||||
export default InitCommon
|
||||
133
src/renderer/src/common/text.ts
Normal file
133
src/renderer/src/common/text.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { useOptionStore } from "@/stores/option"
|
||||
|
||||
/**
|
||||
* 格式化文本,通过自定义的特殊字符进行格式化
|
||||
* @param oldText 需要格式化的文本
|
||||
* @returns 返回格式化后的文本
|
||||
*/
|
||||
async function FormatText(oldText: string): Promise<string> {
|
||||
// 专用正则转义函数
|
||||
function escapeRegExp(char) {
|
||||
const regexSpecialChars = [
|
||||
'\\',
|
||||
'.',
|
||||
'*',
|
||||
'+',
|
||||
'?',
|
||||
'^',
|
||||
'$',
|
||||
'{',
|
||||
'}',
|
||||
'(',
|
||||
')',
|
||||
'[',
|
||||
']',
|
||||
'|',
|
||||
'/'
|
||||
]
|
||||
return regexSpecialChars.includes(char) ? `\\${char}` : char
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
let optionStore = useOptionStore()
|
||||
// 1. 获取特殊字符数组并过滤数字(可选)
|
||||
const specialChars = Array.from(optionStore.CW_FormatSpecialChar)
|
||||
// 如果确定不要数字可以加过滤:.filter(c => !/\d/.test(c))
|
||||
|
||||
// 2. 处理连字符的特殊情况
|
||||
const processedChars = specialChars.map((char) => {
|
||||
// 优先处理连字符(必须第一个处理)
|
||||
if (char === '-') return { char, escaped: '\\-' }
|
||||
return { char, escaped: escapeRegExp(char) }
|
||||
})
|
||||
|
||||
// 3. 构建正则表达式字符集
|
||||
const regexParts = []
|
||||
processedChars.forEach(({ char, escaped }) => {
|
||||
// 单独处理连字符位置
|
||||
if (char === '-') {
|
||||
regexParts.unshift(escaped) // 将连字符放在字符集开头
|
||||
} else {
|
||||
regexParts.push(escaped)
|
||||
}
|
||||
})
|
||||
|
||||
// 4. 创建正则表达式
|
||||
const regex = new RegExp(`[${regexParts.join('')}]`, 'gu')
|
||||
|
||||
// 5. 后续替换和过滤逻辑保持不变...
|
||||
let content = oldText.replace(regex, '\n')
|
||||
|
||||
const lines = content
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line !== '')
|
||||
|
||||
// word.value = lines.join('\n')
|
||||
let newContent = lines.join('\n')
|
||||
return newContent
|
||||
} catch (error) {
|
||||
throw new Error("格式化文本失败,失败信息如下:" + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文本按最大长度分割成多个块
|
||||
* @param text 要分割的文本
|
||||
* @param maxLength 每个块的最大长度
|
||||
* @returns 分割后的文本块数组
|
||||
*/
|
||||
function SplitTextIntoChunks(text: string, maxLength: number): string[] {
|
||||
const lines = text.split('\n');
|
||||
const result: string[] = [];
|
||||
let currentBlock: string[] = [];
|
||||
let currentLength = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const lineLength = line.length;
|
||||
// 计算添加当前行后的新长度(包括换行符)
|
||||
const newLength = currentLength === 0
|
||||
? lineLength
|
||||
: currentLength + 1 + lineLength;
|
||||
|
||||
if (newLength > maxLength) {
|
||||
if (currentBlock.length > 0) {
|
||||
// 提交当前块并重置
|
||||
result.push(currentBlock.join('\n'));
|
||||
currentBlock = [];
|
||||
currentLength = 0;
|
||||
|
||||
// 重新尝试添加当前行到新块
|
||||
if (lineLength > maxLength) {
|
||||
// 行单独超过最大长度,直接作为独立块
|
||||
result.push(line);
|
||||
} else {
|
||||
currentBlock.push(line);
|
||||
currentLength = lineLength;
|
||||
}
|
||||
} else {
|
||||
// 当前块为空且行超过最大长度,强制作为独立块
|
||||
result.push(line);
|
||||
}
|
||||
} else {
|
||||
// 可以安全添加到当前块
|
||||
currentBlock.push(line);
|
||||
currentLength = newLength;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后一个未提交的块
|
||||
if (currentBlock.length > 0) {
|
||||
result.push(currentBlock.join('\n'));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let TextCommon = {
|
||||
FormatText,
|
||||
SplitTextIntoChunks
|
||||
}
|
||||
|
||||
export default TextCommon;
|
||||
20
src/renderer/src/common/ttsCommon.ts
Normal file
20
src/renderer/src/common/ttsCommon.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import { OptionKeyName, OptionType } from "@/define/enum/option";
|
||||
import { useOptionStore } from "@/stores/option";
|
||||
|
||||
/**
|
||||
* 保存TTS的全局视图数据到数据库
|
||||
*/
|
||||
async function SaveTTSGlobalSetting() {
|
||||
let optionStore = useOptionStore();
|
||||
let saveRes = await window.options.ModifyOptionByKey(OptionKeyName.TTS_GlobalSetting, JSON.stringify(optionStore.TTS_GlobalSetting), OptionType.JOSN);
|
||||
if (saveRes.code == 0) {
|
||||
throw new Error(saveRes.message);
|
||||
}
|
||||
}
|
||||
|
||||
let TTSCommon = {
|
||||
SaveTTSGlobalSetting
|
||||
};
|
||||
|
||||
export default TTSCommon;
|
||||
@ -163,13 +163,77 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '时间范围',
|
||||
title: () => {
|
||||
return h('div', { style: 'display: flex; align-items: center' }, [
|
||||
h('span', '时间范围'),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
style: 'margin-left: 4px',
|
||||
strong: true,
|
||||
secondary: true,
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
CheckTimeLime()
|
||||
}
|
||||
},
|
||||
{ default: () => '时间轴检查' }
|
||||
)
|
||||
])
|
||||
},
|
||||
key: 'timeLimit',
|
||||
width: 120
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制对齐字幕的时间轴
|
||||
*/
|
||||
function CheckTimeLime() {
|
||||
dialog.create({
|
||||
type: 'warning',
|
||||
title: '警告',
|
||||
showIcon: true,
|
||||
content: '确定要检查时间轴吗?该操作会更具对应的字幕进行时间的强制对齐,是不是继续操作?',
|
||||
style: `width : 400px;`,
|
||||
maskClosable: false,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
// 检查时间轴逻辑
|
||||
console.log('CheckTimeLime', data.value)
|
||||
for (let i = 0; i < data.value.length; i++) {
|
||||
const element = data.value[i]
|
||||
if (!element.subValue || element.subValue.length <= 0) {
|
||||
message.error(`第${i + 1}行字幕为空,请检查`)
|
||||
return
|
||||
}
|
||||
let startTime = element.subValue[0].start_time ?? 0
|
||||
let endTime = element.subValue[element.subValue.length - 1].end_time ?? 0
|
||||
if (endTime == 0) {
|
||||
message.error(`第${i + 1}行字幕结束时间为空,请检查`)
|
||||
return
|
||||
}
|
||||
|
||||
// 开始写入时间
|
||||
data.value[i].start_time = startTime
|
||||
data.value[i].end_time = endTime
|
||||
data.value[i].timeLimit = `${startTime} -- ${endTime}`
|
||||
}
|
||||
// 提示
|
||||
window.api.showGlobalMessageDialog({
|
||||
code: 1,
|
||||
message: '时间轴检查和改写完成,请手动保存!!!'
|
||||
})
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
message.info('取消操作')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 设置窗体的高度
|
||||
function setHeight() {
|
||||
let div = document.getElementById('import_word_and_srt')
|
||||
@ -210,7 +274,6 @@ export default defineComponent({
|
||||
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
|
||||
maskClosable: false,
|
||||
onClose: async () => {
|
||||
|
||||
console.log(wenkeRef.value.word)
|
||||
let word = wenkeRef.value.data
|
||||
if (word == null || word == '') {
|
||||
@ -565,7 +628,6 @@ export default defineComponent({
|
||||
})
|
||||
}
|
||||
} else if (type.value == 'mj_reverse' || type.value == 'sd_reverse') {
|
||||
|
||||
if (data.value.length != reverseManageStore.selectBookTaskDetail.length) {
|
||||
message.error('检测到导入的字幕数据和分镜对不上,请先对齐')
|
||||
return
|
||||
|
||||
@ -98,7 +98,6 @@ async function ResetBookData(e) {
|
||||
|
||||
async function DeleteBookData(e) {
|
||||
e.stopPropagation()
|
||||
message.info('删除小说数据 ' + book.value.id)
|
||||
let da = dialog.warning({
|
||||
title: '删除小说数据警告',
|
||||
content:
|
||||
@ -107,7 +106,6 @@ async function DeleteBookData(e) {
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
da?.destroy()
|
||||
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '正在删除小说数据。。。'
|
||||
let res = await window.book.DeleteBookData(book.value.id)
|
||||
@ -116,7 +114,6 @@ async function DeleteBookData(e) {
|
||||
message.error(res.message)
|
||||
return
|
||||
}
|
||||
|
||||
// 这边直接把数据删除了就行
|
||||
reverseManageStore.bookData = reverseManageStore.bookData.filter(
|
||||
(item) => item.id != book.value.id
|
||||
|
||||
@ -940,7 +940,9 @@ async function ImportWordAndSrtFunc() {
|
||||
subValue: element.subValue ? element.subValue : [],
|
||||
name: element.name + '.png',
|
||||
prompt: element.prompt,
|
||||
timeLimit: element.timeLimit
|
||||
timeLimit: element.timeLimit,
|
||||
start_time: element.startTime,
|
||||
end_time: element.endTime
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -134,7 +134,9 @@ async function ImportWord() {
|
||||
subValue: element.subValue ? element.subValue : [],
|
||||
name: element.name + '.png',
|
||||
prompt: element.prompt,
|
||||
timeLimit: element.timeLimit
|
||||
timeLimit: element.timeLimit,
|
||||
start_time: element.startTime,
|
||||
end_time: element.endTime
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
126
src/renderer/src/components/CopyWriting/CWInputWord.vue
Normal file
126
src/renderer/src/components/CopyWriting/CWInputWord.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="cw-input-word">
|
||||
<div class="formatting-word">
|
||||
<n-button color="#b6a014" size="small" @click="formatWrite" style="margin-right: 5px"
|
||||
>一键格式化</n-button
|
||||
>
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button quaternary circle size="tiny" color="#b6a014" @click="AddSplitChar">
|
||||
<template #icon>
|
||||
<n-icon size="25"> <AddCircleOutline /> </n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<span>添加分割标识符</span>
|
||||
</n-popover>
|
||||
</div>
|
||||
<n-input
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 20,
|
||||
maxRows: 20
|
||||
}"
|
||||
v-model:value="word"
|
||||
showCount
|
||||
placeholder="请输入文案"
|
||||
style="width: 100%; margin-top: 10px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, h } from 'vue'
|
||||
import { NInput, NIcon, NButton, NPopover, useDialog, useMessage } from 'naive-ui'
|
||||
import { AddCircleOutline } from '@vicons/ionicons5'
|
||||
import InitCommon from '../../common/initCommon'
|
||||
import InputDialogContent from '../Original/Components/InputDialogContent.vue'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
||||
import TextCommon from '../../common/text'
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
let dialog = useDialog()
|
||||
let message = useMessage()
|
||||
|
||||
let word = ref('')
|
||||
|
||||
let split_ref = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
await InitCommon.InitSpecialCharacters()
|
||||
await InitWord()
|
||||
})
|
||||
|
||||
/**
|
||||
* 整合文案数据
|
||||
*/
|
||||
async function InitWord() {
|
||||
debugger
|
||||
let wordStruct = optionStore.CW_AISimpleSetting.wordStruct
|
||||
if (!wordStruct || wordStruct.length <= 0) {
|
||||
return
|
||||
}
|
||||
let wordArr = []
|
||||
for (let i = 0; i < wordStruct.length; i++) {
|
||||
wordArr.push(wordStruct[i].oldWord)
|
||||
}
|
||||
word.value = wordArr.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文案
|
||||
*/
|
||||
async function formatWrite() {
|
||||
try {
|
||||
let newText = await TextCommon.FormatText(word.value)
|
||||
word.value = newText
|
||||
message.success('格式化成功')
|
||||
} catch (error) {
|
||||
message.error('格式化失败,失败原因:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加分割符号
|
||||
*/
|
||||
async function AddSplitChar() {
|
||||
// 判断当前数据是不是存在
|
||||
// 处理数据。获取当前的所有的数据
|
||||
let dialogWidth = 400
|
||||
let dialogHeight = 150
|
||||
dialog.create({
|
||||
title: '添加分割符',
|
||||
showIcon: false,
|
||||
closeOnEsc: false,
|
||||
content: () =>
|
||||
h(InputDialogContent, {
|
||||
ref: split_ref,
|
||||
initData: optionStore.CW_FormatSpecialChar,
|
||||
placeholder: '请输入分割符'
|
||||
}),
|
||||
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
|
||||
maskClosable: false,
|
||||
onClose: async () => {
|
||||
optionStore.CW_FormatSpecialChar = split_ref.value.data
|
||||
let saveRes = await window.options.ModifyOptionByKey(
|
||||
OptionKeyName.CW_FormatSpecialChar,
|
||||
optionStore.CW_FormatSpecialChar,
|
||||
OptionType.STRING
|
||||
)
|
||||
if (saveRes.code == 0) {
|
||||
window.api.showGlobalMessageDialog(saveRes)
|
||||
// 报错 不能关闭
|
||||
return false
|
||||
} else {
|
||||
message.success('数据保存成功')
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
word
|
||||
})
|
||||
</script>
|
||||
452
src/renderer/src/components/CopyWriting/CopyWritingContent.vue
Normal file
452
src/renderer/src/components/CopyWriting/CopyWritingContent.vue
Normal file
@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<div class="copy-writing-content">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="optionStore.CW_AISimpleSetting.wordStruct"
|
||||
:bordered="false"
|
||||
:max-height="maxHeight"
|
||||
scroll-x="1500"
|
||||
style="margin-bottom: 20px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, h, onMounted } from 'vue'
|
||||
import { NDataTable, NInput, NButton, NIcon, useMessage, useDialog, NSpace } from 'naive-ui'
|
||||
import { TrashBinOutline, RefreshOutline } from '@vicons/ionicons5'
|
||||
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
import { useSoftwareStore } from '@/stores/software'
|
||||
let softwareStore = useSoftwareStore()
|
||||
let optionStore = useOptionStore()
|
||||
import CWInputWord from './CWInputWord.vue'
|
||||
import CopyWritingShowAIGenerate from './CopyWritingShowAIGenerate.vue'
|
||||
import { isEmpty } from 'lodash'
|
||||
import CopyWriting from '../../common/copyWriting'
|
||||
import { TimeDelay } from '@/define/Tools/time'
|
||||
|
||||
let message = useMessage()
|
||||
let dialog = useDialog()
|
||||
|
||||
let maxHeight = ref(0)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 80,
|
||||
render: (_, index) => index + 1
|
||||
},
|
||||
{
|
||||
title: (row, index) => {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: 'display: flex; align-items: center ,justify-content: center;'
|
||||
},
|
||||
[
|
||||
h('div', { style: { marginBottom: '8px' } }, '处理前文本'),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
strong: true,
|
||||
secondary: true,
|
||||
style: 'margin-left: 5px',
|
||||
type: 'info',
|
||||
onClick: ImportText
|
||||
},
|
||||
{ default: () => '导入文本' }
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
strong: true,
|
||||
secondary: true,
|
||||
style: 'margin-left: 5px',
|
||||
type: 'info',
|
||||
onClick: TextSplit
|
||||
},
|
||||
{ default: () => '文案分割' }
|
||||
)
|
||||
]
|
||||
)
|
||||
},
|
||||
key: 'oldWord',
|
||||
width: 500,
|
||||
render: (row) =>
|
||||
h(NInput, {
|
||||
type: 'textarea',
|
||||
autosize: { minRows: 10, maxRows: 10 },
|
||||
value: row.oldWord,
|
||||
showCount: true,
|
||||
onUpdateValue: (value) => (row.oldWord = value),
|
||||
style: { minWidth: '200px' },
|
||||
placeholder: '请输入文本'
|
||||
})
|
||||
},
|
||||
{
|
||||
title: (row, index) => {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: 'display: flex; align-items: center ,justify-content: center;'
|
||||
},
|
||||
[
|
||||
h('div', { style: { marginBottom: '8px' } }, '处理后文本'),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
strong: true,
|
||||
secondary: true,
|
||||
style: 'margin-left: 5px',
|
||||
type: 'info',
|
||||
onClick: ShowAIGenerateText
|
||||
},
|
||||
{ default: () => '显示生成文本' }
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
strong: true,
|
||||
secondary: true,
|
||||
style: 'margin-left: 5px',
|
||||
type: 'info',
|
||||
onClick: CopyGenerationText
|
||||
},
|
||||
{ default: () => '复制生成文本' }
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
strong: true,
|
||||
secondary: true,
|
||||
style: 'margin-left: 5px',
|
||||
type: 'info',
|
||||
onClick: async () => {
|
||||
// 获取AI生成为空的文本
|
||||
let ids = optionStore.CW_AISimpleSetting.wordStruct
|
||||
.filter((item) => isEmpty(item.newWord))
|
||||
.map((item) => item.id)
|
||||
if (ids <= 0) {
|
||||
message.error('生成失败:不存在未生成的文本')
|
||||
return
|
||||
}
|
||||
handleGenerate(ids)
|
||||
}
|
||||
},
|
||||
{ default: () => '生成空文本' }
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
strong: true,
|
||||
secondary: true,
|
||||
style: 'margin-left: 5px',
|
||||
type: 'error',
|
||||
onClick: ClearAIGeneration
|
||||
},
|
||||
{ default: () => '清空' }
|
||||
)
|
||||
]
|
||||
)
|
||||
},
|
||||
key: 'newWord',
|
||||
width: 500,
|
||||
render: (row) =>
|
||||
h(NInput, {
|
||||
type: 'textarea',
|
||||
autosize: { minRows: 10, maxRows: 10 },
|
||||
value: row.newWord,
|
||||
showCount: true,
|
||||
onUpdateValue: (value) => (row.newWord = value),
|
||||
style: { minWidth: '200px' },
|
||||
placeholder: 'AI 改写后的文件'
|
||||
})
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
render: (row) =>
|
||||
h(NSpace, {}, () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
onClick: () => handleGenerate([row.id])
|
||||
},
|
||||
() => [h(NIcon, { size: 16, component: RefreshOutline }), ' 生成']
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
onClick: () => handleDelete(row.id)
|
||||
},
|
||||
() => [h(NIcon, { size: 16, component: TrashBinOutline }), ' 清空']
|
||||
)
|
||||
])
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 计算表格的最大高度
|
||||
*/
|
||||
function calcHeight() {
|
||||
// 视口的高度
|
||||
let height = window.innerHeight
|
||||
maxHeight.value = height - 240
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
calcHeight()
|
||||
})
|
||||
|
||||
function ShowAIGenerateText() {
|
||||
dialog.info({
|
||||
title: 'AI生成文本',
|
||||
content: () => h(CopyWritingShowAIGenerate),
|
||||
style: 'width: 800px;height: 610px;',
|
||||
showIcon: false,
|
||||
maskClosable: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空AI生成的文本
|
||||
*/
|
||||
function ClearAIGeneration() {
|
||||
dialog.warning({
|
||||
title: '温馨提示',
|
||||
content: '确定要清空所有的AI生成文本吗?清空后不可恢复,是否继续?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
optionStore.CW_AISimpleSetting.wordStruct.forEach((item) => {
|
||||
item.newWord = ''
|
||||
})
|
||||
await CopyWriting.SaveCWAISimpleSetting()
|
||||
message.success('清空成功')
|
||||
} catch (error) {
|
||||
message.error('清空失败:' + error.message)
|
||||
}
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
message.info('取消清空')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制AI生成的文本 做个简单的拼接
|
||||
*/
|
||||
function CopyGenerationText() {
|
||||
dialog.warning({
|
||||
title: '温馨提示',
|
||||
content:
|
||||
'直接复制会将所有的AI生成后的数据直接进行复制,不会进行格式之类的调整,若有需求可以再下面表格直接修改,或者是再左边的显示生成文本中修改,是否继续复制?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
let wordStruct = optionStore.CW_AISimpleSetting.wordStruct
|
||||
// 循环判断是不是有空的数据,有的话提示
|
||||
let isHaveEmpty = wordStruct.some((item) => {
|
||||
return isEmpty(item.newWord)
|
||||
})
|
||||
if (isHaveEmpty) {
|
||||
message.error('复制失败:存在未生成的文本,请先生成文本')
|
||||
return false
|
||||
}
|
||||
// 获取当前的文本
|
||||
let newWordAll = wordStruct.map((item) => {
|
||||
return item.newWord
|
||||
})
|
||||
let newWordStr = newWordAll.join('\n')
|
||||
// 删除里面的空行
|
||||
let newWord = newWordStr.split('\n').filter((item) => {
|
||||
return !isEmpty(item)
|
||||
})
|
||||
await navigator.clipboard.writeText(newWord.join('\n'))
|
||||
message.success('复制成功')
|
||||
} catch (error) {
|
||||
message.error(
|
||||
'复制失败,请在左边的显示生成文本中进行手动复制,失败信息如图:' + error.message
|
||||
)
|
||||
} finally {
|
||||
softwareStore.spin.spinning = false
|
||||
}
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
message.info('取消删除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行生成AI后文本的方法
|
||||
* @param id
|
||||
*/
|
||||
function handleGenerate(rowIds) {
|
||||
let da = dialog.warning({
|
||||
title: '温馨提示',
|
||||
content:
|
||||
'确定重新生成当前行的AI生成文本吗?重新生成后会清空之前的生成文本并且不可恢复,是否继续?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
debugger
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '生成中......'
|
||||
let ids = []
|
||||
optionStore.CW_AISimpleSetting.wordStruct.forEach((item) => {
|
||||
if (rowIds.includes(item.id)) {
|
||||
ids.push(item.id)
|
||||
}
|
||||
})
|
||||
da.destroy()
|
||||
|
||||
let res = await window.write.CopyWritingAIGeneration(ids)
|
||||
|
||||
if (res.code == 0) {
|
||||
message.error(res.message)
|
||||
window.api.showGlobalMessageDialog(res)
|
||||
softwareStore.spin.spinning = false
|
||||
return
|
||||
}
|
||||
// 这边直接保存一下
|
||||
await CopyWriting.SaveCWAISimpleSetting()
|
||||
window.api.showGlobalMessageDialog(res)
|
||||
await TimeDelay(200)
|
||||
} catch (error) {
|
||||
message.error('生成失败:' + error.message)
|
||||
} finally {
|
||||
softwareStore.spin.spinning = false
|
||||
}
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
message.info('取消删除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当行的AI生成文本
|
||||
* @param id
|
||||
*/
|
||||
const handleDelete = (id) => {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要删除当前行的AI生成文本吗?数据删除后不可恢复,是否继续?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
let index = optionStore.CW_AISimpleSetting.wordStruct.findIndex((item) => item.id == id)
|
||||
if (index == -1) {
|
||||
message.error('删除失败:未找到对应的数据')
|
||||
return false
|
||||
}
|
||||
optionStore.CW_AISimpleSetting.wordStruct[index].newWord = ''
|
||||
// 保存数据
|
||||
await CopyWriting.SaveCWAISimpleSetting()
|
||||
message.success('删除成功')
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
message.info('取消删除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入文本按钮的具体实现
|
||||
*/
|
||||
function ImportText() {
|
||||
let cwInputWordRef = ref(null)
|
||||
dialog.info({
|
||||
title: '导入文本',
|
||||
content: () =>
|
||||
h('div', {}, [
|
||||
h(CWInputWord, { type: 'textarea', ref: cwInputWordRef, placeholder: '请输入文本' })
|
||||
]),
|
||||
style: 'width: 800px;height: 610px;',
|
||||
showIcon: false,
|
||||
maskClosable: false,
|
||||
positiveText: '导入',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
let inputWord = cwInputWordRef.value.word
|
||||
if (isEmpty(inputWord)) {
|
||||
message.error('导入失败:文本不能为空')
|
||||
return false
|
||||
}
|
||||
// 判断当前是不是又数据存在,存在的话提示
|
||||
if (
|
||||
optionStore.CW_AISimpleSetting.wordStruct &&
|
||||
optionStore.CW_AISimpleSetting.wordStruct.length > 0
|
||||
) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content:
|
||||
'当前已经存在数据,继续操作会删除之前的数据,包括生成之后的数据,若只是简单调整数据,可在外面显示的表格中进行直接修改,是否继续?',
|
||||
positiveText: '导入',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await CopyWriting.SplitOrMergeOldText(
|
||||
optionStore.CW_AISimpleSetting.isSplit,
|
||||
inputWord
|
||||
)
|
||||
message.success('更新导入成功')
|
||||
}
|
||||
})
|
||||
return false
|
||||
} else {
|
||||
await CopyWriting.SplitOrMergeOldText(optionStore.CW_AISimpleSetting.isSplit, inputWord)
|
||||
await CopyWriting.SaveCWAISimpleSetting()
|
||||
message.info(inputWord)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('导入失败:' + err.message)
|
||||
}
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
message.info('取消导入')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 文案分割按钮的具体实现
|
||||
*/
|
||||
function TextSplit() {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content:
|
||||
'确定要将当前文本按照设定的单次最大次数进行分割吗?分割后会清空已生成的内容,是否继续?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await CopyWriting.SplitOrMergeOldText(true)
|
||||
message.success('分割成功')
|
||||
} catch (err) {
|
||||
message.error('分割失败:' + err.message)
|
||||
}
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
message.info('取消分割')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
106
src/renderer/src/components/CopyWriting/CopyWritingHome.vue
Normal file
106
src/renderer/src/components/CopyWriting/CopyWritingHome.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="copy-writing-home">
|
||||
<CopyWritingSimpleSetting />
|
||||
<CopyWritingContent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useSoftwareStore } from '@/stores/software'
|
||||
import { TimeDelay } from '@/define/Tools/time'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
import CopyWritingSimpleSetting from './CopyWritingSimpleSetting.vue'
|
||||
import CopyWritingContent from './CopyWritingContent.vue'
|
||||
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
||||
|
||||
let softwareStore = useSoftwareStore()
|
||||
let message = useMessage()
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '数据加载中'
|
||||
|
||||
// 这边同步数据和加载文案处理AI设置数据
|
||||
await InitCopyWritingAISetting()
|
||||
// 初始化界面数据
|
||||
await InitCopyWritingData()
|
||||
|
||||
await TimeDelay(1000)
|
||||
} catch (error) {
|
||||
window.api.showGlobalMessageDialog({
|
||||
code: 0,
|
||||
message: '数据加载失败,请切换左侧菜单重试,错误信息:' + error.message
|
||||
})
|
||||
} finally {
|
||||
softwareStore.spin.spinning = false
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始化文案处理AI设置
|
||||
*/
|
||||
async function InitCopyWritingAISetting() {
|
||||
try {
|
||||
let initRes = await window.options.InitCopyWritingAISetting()
|
||||
if (initRes.code == 0) {
|
||||
window.api.showGlobalMessageDialog(initRes)
|
||||
return
|
||||
}
|
||||
optionStore.CW_AISetting = initRes.data
|
||||
message.success('同步/初始化/加载 文案处理AI设置成功')
|
||||
} catch (error) {
|
||||
throw new Error('初始化文案处理AI设置失败,错误信息:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化文案处理界面数据
|
||||
*/
|
||||
async function InitCopyWritingData() {
|
||||
try {
|
||||
let initRes = await window.options.GetOptionByKey(OptionKeyName.CW_AISimpleSetting)
|
||||
if (initRes.code == 0) {
|
||||
window.api.showGlobalMessageDialog(initRes)
|
||||
return
|
||||
}
|
||||
console.log(initRes)
|
||||
if (initRes.data == null) {
|
||||
// 初始化数据
|
||||
optionStore.CW_AISimpleSetting = {
|
||||
gptType: undefined,
|
||||
gptData: undefined,
|
||||
gptAI: 'laiapi',
|
||||
isStream: false,
|
||||
isSplit: false,
|
||||
splitNumber: 500,
|
||||
oldWord: '',
|
||||
newWord: '',
|
||||
oldWordCount: 0,
|
||||
newWordCount: 0,
|
||||
wordStruct: []
|
||||
}
|
||||
// 保存到数据库
|
||||
let saveRes = await window.options.ModifyOptionByKey(
|
||||
OptionKeyName.CW_AISimpleSetting,
|
||||
JSON.stringify(optionStore.CW_AISimpleSetting),
|
||||
OptionType.JOSN
|
||||
)
|
||||
if (saveRes.code == 0) {
|
||||
throw new Error('初始化文案处理界面数据失败,错误信息:' + saveRes.message)
|
||||
} else {
|
||||
message.success('未找到文案处理界面数据,已初始化数据')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
optionStore.CW_AISimpleSetting = JSON.parse(initRes.data.value)
|
||||
}
|
||||
message.success('同步/初始化/加载 文案处理界面数据成功')
|
||||
} catch (error) {
|
||||
throw new Error('初始化文案处理界面数据失败,错误信息:' + error.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="copy-writing-show">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; flex-direction: row; gap: 10px">
|
||||
<n-button type="info" size="small" @click="CopyNewData"> 复制 </n-button>
|
||||
<n-button type="info" size="small" @click="FormatOutput"> 一键格式化 </n-button>
|
||||
<span style="font-size: 16px; color: red">
|
||||
注意:这边的格式化不一定会完全格式化,需要自己手动检查
|
||||
</span>
|
||||
</div>
|
||||
<n-input
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 18,
|
||||
maxRows: 18
|
||||
}"
|
||||
style="margin-top: 10px"
|
||||
v-model:value="word"
|
||||
placeholder="请输入内容"
|
||||
:rows="4"
|
||||
/>
|
||||
</div>
|
||||
<div style="font-size: 20px; color: red">
|
||||
注意:当前弹窗的修改和格式化只在改界面有效,关闭或重新打开会重新加载同步外部表格数据,之前修改的数据会试下
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { NButton, NInput, useMessage } from 'naive-ui'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
import { isEmpty } from 'lodash'
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
let word = ref('')
|
||||
let message = useMessage()
|
||||
|
||||
onMounted(() => {
|
||||
word.value = optionStore.CW_AISimpleSetting.wordStruct.map((item) => item.newWord).join('\n')
|
||||
})
|
||||
|
||||
/**
|
||||
* 复制新数据
|
||||
*/
|
||||
async function CopyNewData() {
|
||||
try {
|
||||
let copyData = word.value
|
||||
await navigator.clipboard.writeText(copyData)
|
||||
message.success('复制成功')
|
||||
} catch (error) {
|
||||
message.error('复制失败,错误信息:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化输出
|
||||
*/
|
||||
async function FormatOutput() {
|
||||
let splitData = word.value.split('\n').filter((item) => {
|
||||
return !isEmpty(item)
|
||||
})
|
||||
let isNumberedFormat = (str) => {
|
||||
return /^\d+\./.test(str)
|
||||
}
|
||||
let isTextFormat = (str) => {
|
||||
return /^【文本】/.test(str)
|
||||
}
|
||||
let type = undefined
|
||||
|
||||
splitData = splitData.map((item) => {
|
||||
if (isNumberedFormat(item)) {
|
||||
type = 'startNumber'
|
||||
return item.replace(/^\d+\./, '')
|
||||
} else if (isTextFormat(item)) {
|
||||
type = 'startText'
|
||||
return item.replace('&【', '\n【')
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
})
|
||||
if (type == 'startNumber') {
|
||||
word.value = splitData.join('\n')
|
||||
} else {
|
||||
word.value = splitData.join('\n\n')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div style="min-width: 800px; overflow: auto">
|
||||
<div style="width: 100%">
|
||||
<n-form ref="formRef" :model="formValue" inline label-placement="left">
|
||||
<n-form ref="formRef" :model="optionStore.CW_AISimpleSetting" inline label-placement="left">
|
||||
<n-form-item label="选择类型" path="gptType">
|
||||
<n-select
|
||||
style="width: 160px"
|
||||
v-model:value="formValue.gptType"
|
||||
v-model:value="optionStore.CW_AISimpleSetting.gptType"
|
||||
@update:value="UpdateSelectPromptType"
|
||||
:options="gptTypeOptions"
|
||||
placeholder="请选择提示词类型"
|
||||
@ -15,7 +15,7 @@
|
||||
<n-form-item label="选择预设" path="gptData">
|
||||
<n-select
|
||||
style="width: 200px"
|
||||
v-model:value="formValue.gptData"
|
||||
v-model:value="optionStore.CW_AISimpleSetting.gptData"
|
||||
:options="gptDataOptions"
|
||||
placeholder="请选择提示词数据"
|
||||
>
|
||||
@ -24,7 +24,7 @@
|
||||
<n-form-item label="选择请求AI" path="gptAI">
|
||||
<n-select
|
||||
style="width: 160px"
|
||||
v-model:value="formValue.gptAI"
|
||||
v-model:value="optionStore.CW_AISimpleSetting.gptAI"
|
||||
:options="gptOptions"
|
||||
placeholder="请选择AI"
|
||||
>
|
||||
@ -34,100 +34,80 @@
|
||||
></n-button>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="ActionStart">开始生成</n-button>
|
||||
<n-button type="primary" @click="SaveData">保存数据</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="ActionStart">开始 AI 生成</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-form :model="formValue" inline label-placement="left">
|
||||
<n-form :model="optionStore.CW_AISimpleSetting" inline label-placement="left">
|
||||
<n-form-item path="isStream">
|
||||
<n-checkbox label="是否流式发送" v-model:checked="formValue.isStream" />
|
||||
<n-checkbox
|
||||
label="是否流式发送"
|
||||
v-model:checked="optionStore.CW_AISimpleSetting.isStream"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="isSplit">
|
||||
<n-checkbox label="是否拆分发送" v-model:checked="formValue.isSplit" />
|
||||
<n-checkbox
|
||||
label="是否拆分发送"
|
||||
v-model:checked="optionStore.CW_AISimpleSetting.isSplit"
|
||||
@update:checked="handleCheckedChange"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="每次发送字符" path="splitNumber">
|
||||
<n-form-item label="单次最大字符数" path="splitNumber">
|
||||
<n-input-number
|
||||
v-model:value="formValue.splitNumber"
|
||||
v-model:value="optionStore.CW_AISimpleSetting.splitNumber"
|
||||
:min="1"
|
||||
:show-button="false"
|
||||
:max="99999"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item path="splitNumber">
|
||||
<div style="color: red">注意:爆款开头不要分段发送</div>
|
||||
<div style="color: red; font-size: 20px">注意:爆款开头不要拆分发送</div>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
<div style="display: flex; width: 100%">
|
||||
<div style="flex: 1; margin-right: 10px">
|
||||
<n-input
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 30, maxRows: 30 }"
|
||||
v-model:value="oldWord"
|
||||
placeholder="请输入内容"
|
||||
/>
|
||||
</div>
|
||||
<div style="flex: 1">
|
||||
<n-input
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 30, maxRows: 30 }"
|
||||
v-model:value="newWord"
|
||||
placeholder="请输入内容"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; align-items: center; margin-right: 20px">
|
||||
<n-button type="primary" @click="FormatOutput()"> 格式化 </n-button>
|
||||
<div style="color: red; margin-left: 5px">
|
||||
注意:由于GPT输出的格式化有太多的不确定,不一定可以完全格式,需要手动检查
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch, h } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, toRaw, h } from 'vue'
|
||||
import {
|
||||
useMessage,
|
||||
useDialog,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NSelect,
|
||||
NButton,
|
||||
NIcon,
|
||||
NCheckbox,
|
||||
NInputNumber
|
||||
} from 'naive-ui'
|
||||
import { SettingsOutline } from '@vicons/ionicons5'
|
||||
import { Copy, SettingsOutline } from '@vicons/ionicons5'
|
||||
import ManageAISetting from './ManageAISetting.vue'
|
||||
import { useSoftwareStore } from '../../../../stores/software'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { DEFINE_STRING } from '../../../../define/define_string'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
||||
import CopyWriting from '../../common/copyWriting'
|
||||
import { TimeDelay } from '@/define/Tools/time'
|
||||
|
||||
let optionStore = useOptionStore()
|
||||
let softwareStore = useSoftwareStore()
|
||||
|
||||
let message = useMessage()
|
||||
let dialog = useDialog()
|
||||
let formValue = ref({
|
||||
gptType: undefined,
|
||||
gptData: undefined,
|
||||
gptAI: undefined,
|
||||
isStream: true,
|
||||
isSplit: false,
|
||||
splitNumber: 1000
|
||||
})
|
||||
let oldWord = ref('')
|
||||
let newWord = ref('')
|
||||
let gptTypeOptions = ref([])
|
||||
let gptDataOptions = ref([])
|
||||
let gptAllData = undefined
|
||||
let formRef = ref(null)
|
||||
let gptOptions = ref([
|
||||
{ label: 'LAI API', value: 'laiapi' },
|
||||
{ label: 'KIMI', value: 'kimi' }
|
||||
])
|
||||
let softwareStore = useSoftwareStore()
|
||||
let gptOptions = ref([{ label: 'LAI API', value: 'laiapi' }])
|
||||
let allPromptDataOptions = ref([])
|
||||
|
||||
// 加载服务端数据
|
||||
/**
|
||||
* 加载远程提示词数据,包括提示词预设等
|
||||
*/
|
||||
async function InitServerGptOptions() {
|
||||
let gptRes = await window.gpt.InitServerGptOptions()
|
||||
if (gptRes.code == 0) {
|
||||
@ -168,7 +148,13 @@ function debounce(func, wait) {
|
||||
}
|
||||
|
||||
let UpdateWord = debounce((value) => {
|
||||
newWord.value = value
|
||||
debugger
|
||||
let index = optionStore.CW_AISimpleSetting.wordStruct.findIndex((item) => item.id == value.id)
|
||||
if (index == -1) {
|
||||
return
|
||||
}
|
||||
optionStore.CW_AISimpleSetting.wordStruct[index].newWord = value.newWord
|
||||
// newWord.value += value
|
||||
}, 300)
|
||||
|
||||
onMounted(async () => {
|
||||
@ -183,46 +169,38 @@ onUnmounted(() => {
|
||||
window.api.removeEventListen(DEFINE_STRING.GPT.GPT_STREAM_RETURN)
|
||||
})
|
||||
|
||||
let ruleObj = (errorMessage) => {
|
||||
return [
|
||||
{
|
||||
required: true,
|
||||
validator(rule, value) {
|
||||
if (value == null || value == '') return new Error(errorMessage)
|
||||
return true
|
||||
},
|
||||
trigger: ['input', 'blur', 'change']
|
||||
async function handleCheckedChange(checked) {
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '数据处理中......'
|
||||
try {
|
||||
if (checked) {
|
||||
await CopyWriting.SplitOrMergeOldText(true)
|
||||
} else {
|
||||
await CopyWriting.SplitOrMergeOldText(false)
|
||||
}
|
||||
await TimeDelay(500)
|
||||
} catch (error) {
|
||||
message.error(error.message)
|
||||
} finally {
|
||||
softwareStore.spin.spinning = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async function FormatOutput() {
|
||||
let splitData = newWord.value.split('\n').filter((item) => {
|
||||
return !isEmpty(item)
|
||||
})
|
||||
let isNumberedFormat = (str) => {
|
||||
return /^\d+\./.test(str)
|
||||
}
|
||||
let isTextFormat = (str) => {
|
||||
return /^【文本】/.test(str)
|
||||
}
|
||||
let type = undefined
|
||||
|
||||
splitData = splitData.map((item) => {
|
||||
if (isNumberedFormat(item)) {
|
||||
type = 'startNumber'
|
||||
return item.replace(/^\d+\./, '')
|
||||
} else if (isTextFormat(item)) {
|
||||
type = 'startText'
|
||||
return item.replace('&【', '\n【')
|
||||
/**
|
||||
* 保存数据
|
||||
*/
|
||||
async function SaveData() {
|
||||
let res = await window.options.ModifyOptionByKey(
|
||||
OptionKeyName.CW_AISimpleSetting,
|
||||
JSON.stringify(optionStore.CW_AISimpleSetting),
|
||||
OptionType.JOSN
|
||||
)
|
||||
if (res.code == 0) {
|
||||
window.api.showGlobalMessageDialog(res)
|
||||
return false
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
})
|
||||
if (type == 'startNumber') {
|
||||
newWord.value = splitData.join('\n')
|
||||
} else {
|
||||
newWord.value = splitData.join('\n\n')
|
||||
message.success('数据保存成功')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,13 +211,12 @@ async function AISetting() {
|
||||
// 判断当前数据是不是存在
|
||||
// 处理数据。获取当前的所有的数据
|
||||
let dialogWidth = 800
|
||||
let dialogHeight = 600
|
||||
dialog.create({
|
||||
title: 'AI设置',
|
||||
showIcon: false,
|
||||
closeOnEsc: false,
|
||||
content: () => h(ManageAISetting, { type: formValue.value.gptAI }),
|
||||
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
|
||||
content: () => h(ManageAISetting, { type: optionStore.CW_AISimpleSetting.gptAI }),
|
||||
style: `width : ${dialogWidth}px`,
|
||||
maskClosable: false,
|
||||
onClose: async () => {}
|
||||
})
|
||||
@ -249,25 +226,30 @@ async function AISetting() {
|
||||
* 开始执行GPT
|
||||
*/
|
||||
async function ActionStart() {
|
||||
// 检查是不是有数据
|
||||
if (
|
||||
isEmpty(formValue.value.gptType) ||
|
||||
isEmpty(formValue.value.gptData) ||
|
||||
isEmpty(formValue.value.gptAI)
|
||||
) {
|
||||
message.error('请选择完整的数据')
|
||||
return
|
||||
}
|
||||
try {
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '生成中......'
|
||||
let res = await window.write.ActionStart(toRaw(formValue.value), oldWord.value)
|
||||
softwareStore.spin.spinning = false
|
||||
let ids = []
|
||||
optionStore.CW_AISimpleSetting.wordStruct.forEach((item) => {
|
||||
ids.push(item.id)
|
||||
})
|
||||
let res = await window.write.CopyWritingAIGeneration(ids)
|
||||
|
||||
if (res.code == 0) {
|
||||
message.error(res.message)
|
||||
window.api.showGlobalMessageDialog(res)
|
||||
softwareStore.spin.spinning = false
|
||||
return
|
||||
}
|
||||
newWord.value = res.data
|
||||
// 这边直接保存一下
|
||||
await CopyWriting.SaveCWAISimpleSetting()
|
||||
window.api.showGlobalMessageDialog(res)
|
||||
await TimeDelay(200)
|
||||
} catch (error) {
|
||||
message.error('生成失败:' + error.message)
|
||||
} finally {
|
||||
softwareStore.spin.spinning = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3,56 +3,22 @@
|
||||
<n-card title="LAI API 设置">
|
||||
<div style="display: flex">
|
||||
<n-input
|
||||
v-model:value="aiSetting.laiapi.gpt_url"
|
||||
v-model:value="optionStore.CW_AISetting.laiapi.gpt_url"
|
||||
style="margin-right: 10px"
|
||||
type="text"
|
||||
placeholder="请输入GPT URL"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="aiSetting.laiapi.api_key"
|
||||
v-model:value="optionStore.CW_AISetting.laiapi.api_key"
|
||||
style="margin-right: 10px"
|
||||
type="text"
|
||||
placeholder="请输入API KEY"
|
||||
/>
|
||||
<n-input v-model:value="aiSetting.laiapi.model" type="text" placeholder="请输入Model" />
|
||||
</div>
|
||||
</n-card>
|
||||
<!-- <n-card title="豆包设置">
|
||||
<div style="display: flex">
|
||||
<n-input
|
||||
v-model:value="aiSetting.doubao.gpt_url"
|
||||
v-model:value="optionStore.CW_AISetting.laiapi.model"
|
||||
type="text"
|
||||
style="margin-right: 10px"
|
||||
placeholder="请输入豆包的调用网址"
|
||||
placeholder="请输入Model"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="aiSetting.doubao.api_key"
|
||||
type="text"
|
||||
style="margin-right: 10px"
|
||||
placeholder="请输入豆包的APIKEY"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="aiSetting.doubao.model"
|
||||
type="text"
|
||||
placeholder="请输入豆包的模型"
|
||||
/>
|
||||
</div>
|
||||
</n-card> -->
|
||||
<n-card title="KIMI设置">
|
||||
<div style="display: flex">
|
||||
<n-input
|
||||
v-model:value="aiSetting.kimi.gpt_url"
|
||||
type="text"
|
||||
style="margin-right: 10px"
|
||||
placeholder="请输入KIMI的网址"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="aiSetting.kimi.api_key"
|
||||
type="text"
|
||||
style="margin-right: 10px"
|
||||
placeholder="请输入KIMI的APIKEY"
|
||||
/>
|
||||
<n-input v-model:value="aiSetting.kimi.model" type="text" placeholder="请输入KIMI的模型" />
|
||||
</div>
|
||||
</n-card>
|
||||
<div style="display: flex; justify-content: flex-end; margin: 20px">
|
||||
@ -61,75 +27,46 @@
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch } from 'vue'
|
||||
import { useMessage, NCard, NSpace, NInput, NSelect, NButton } from 'naive-ui'
|
||||
<script setup>
|
||||
import { useMessage, useDialog, NCard, NSpace, NInput, NSelect, NButton } from 'naive-ui'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
export default defineComponent({
|
||||
components: { NCard, NSpace, NInput, NSelect, NButton },
|
||||
props: ['type'],
|
||||
setup(props) {
|
||||
let message = useMessage()
|
||||
let aiSetting = ref({
|
||||
laiapi: {
|
||||
gpt_url: undefined,
|
||||
api_key: undefined,
|
||||
model: undefined
|
||||
},
|
||||
kimi: {
|
||||
gpt_url: undefined,
|
||||
api_key: undefined,
|
||||
model: undefined
|
||||
},
|
||||
doubao: { gpt_url: undefined, api_key: undefined, model: undefined }
|
||||
})
|
||||
let message = useMessage()
|
||||
let dialog = useDialog()
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
// 获取AIsetting
|
||||
let res = await window.gpt.GetAISetting()
|
||||
if (res.code == 0) {
|
||||
message.error(res.message)
|
||||
return
|
||||
}
|
||||
aiSetting.value = res.data
|
||||
})
|
||||
|
||||
function checkValue(obj) {
|
||||
for (const d in obj) {
|
||||
if (isEmpty(d)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
async function SaveAISetting() {
|
||||
let da = dialog.warning({
|
||||
title: '提示',
|
||||
content: '确认保存AI设置?这边不会检测数据的可用性,请确保数据填写正确!!!',
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onNegativeClick: () => {
|
||||
message.info('用户取消操作')
|
||||
return true
|
||||
}
|
||||
|
||||
async function SaveAISetting() {
|
||||
// 判断当前是选择是哪个AI,判断是不是有填写
|
||||
let checkRes = true
|
||||
if (props.type == 'laiapi') {
|
||||
checkRes = checkValue(Object.values(aiSetting.value.laiapi))
|
||||
} else if (props.type == 'kimi') {
|
||||
checkRes = checkValue(Object.values(aiSetting.value.kimi))
|
||||
} else if (props.type == 'doubao') {
|
||||
checkRes = checkValue(Object.values(aiSetting.value.doubao))
|
||||
}
|
||||
|
||||
if (!checkRes) {
|
||||
},
|
||||
onPositiveClick: async () => {
|
||||
da.destroy()
|
||||
// 保存数据
|
||||
let aiSetting = optionStore.CW_AISetting.laiapi
|
||||
if (isEmpty(aiSetting.gpt_url) || isEmpty(aiSetting.api_key) || isEmpty(aiSetting.model)) {
|
||||
message.error('请填写完整选择的AI相关的设置')
|
||||
return
|
||||
}
|
||||
|
||||
let res = await window.gpt.SaveAISetting(toRaw(aiSetting.value))
|
||||
// 直接保存
|
||||
let res = await window.options.ModifyOptionByKey(
|
||||
OptionKeyName.CW_AISetting,
|
||||
JSON.stringify(optionStore.CW_AISetting),
|
||||
OptionType.JSON
|
||||
)
|
||||
if (res.code == 0) {
|
||||
message.error(res.message)
|
||||
return
|
||||
window.api.showGlobalMessageDialog(res)
|
||||
} else {
|
||||
message.success('保存AI设置成功')
|
||||
}
|
||||
message.success('保存成功')
|
||||
}
|
||||
|
||||
return { aiSetting, SaveAISetting }
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -112,15 +112,16 @@
|
||||
v-if="formValue.gpt_business == 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65'"
|
||||
style="width: 120px; margin-left: 30px"
|
||||
path="gpt_key"
|
||||
label="LAI 站点选择"
|
||||
label="是否国内转发"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="formValue.laiApiSelect"
|
||||
placeholder="选择LAIAPI的请求站点"
|
||||
style="width: 200px"
|
||||
:options="laiApiOptions"
|
||||
>
|
||||
</n-select>
|
||||
v-model:value="formValue.useTransfer"
|
||||
:options="[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false }
|
||||
]"
|
||||
style="width: 100px"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item style="margin-left: 30px" path="gpt_model" label="GPT模型">
|
||||
<n-select
|
||||
@ -225,6 +226,7 @@ let formValue = ref({
|
||||
character_select_model: window.config.character_select_model,
|
||||
window_wh_bm_remember: window.config.window_wh_bm_remember,
|
||||
laiApiSelect: window.config.laiApiSelect ? window.config.laiApiSelect : LaiAPIType.MAIN,
|
||||
useTransfer: window.config.useTransfer ?? false,
|
||||
hdScale: window.config.hdScale ?? 2,
|
||||
defaultImageMode: window.config.defaultImageMode ?? 'mj',
|
||||
defaultVideoMode: window.config.defaultVideoMode ?? ImageToVideoModels.RUNWAY
|
||||
|
||||
@ -2,53 +2,56 @@
|
||||
<div>
|
||||
<n-form
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
label-width="100"
|
||||
require-mark-placement="right-hanging"
|
||||
:model="settingStore.ttsSetting.edgeTTS"
|
||||
:model="optionStore.TTS_GlobalSetting.selectModel"
|
||||
>
|
||||
<n-form-item label="合成角色">
|
||||
<n-select
|
||||
@update:value="ChangeCharacter"
|
||||
v-model:value="settingStore.ttsSetting.edgeTTS.value"
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.value"
|
||||
:options="roleOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="音量">
|
||||
<n-input-number
|
||||
v-model:value="settingStore.ttsSetting.edgeTTS.volumn"
|
||||
:min="0"
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.volumn"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="语速">
|
||||
<n-input-number v-model:value="settingStore.ttsSetting.edgeTTS.rate" :min="0" :max="100" />
|
||||
<n-input-number
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.rate"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="语调">
|
||||
<n-input-number v-model:value="settingStore.ttsSetting.edgeTTS.pitch" :min="0" :max="100" />
|
||||
<n-input-number
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.pitch"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="超时时间(ms)">
|
||||
<n-input-number
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.timeOut"
|
||||
:min="0"
|
||||
:max="10000000"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, defineComponent, watch, inject } from 'vue'
|
||||
import {
|
||||
useMessage,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInputNumber,
|
||||
NSelect,
|
||||
NButton,
|
||||
NIcon,
|
||||
NPopover,
|
||||
NCheckbox
|
||||
} from 'naive-ui'
|
||||
import { GetEdgeTTSRole } from '../../../../define/tts/ttsDefine'
|
||||
import { ReaderOutline } from '@vicons/ionicons5'
|
||||
import { useSettingStore } from '../../../../stores/setting'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useMessage, NForm, NFormItem, NInputNumber, NSelect } from 'naive-ui'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
|
||||
let message = useMessage()
|
||||
let settingStore = useSettingStore()
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
async function SwitchTTSOptions(key) {
|
||||
console.log('SwitchTTSOptions', key)
|
||||
@ -73,9 +76,9 @@ onMounted(async () => {
|
||||
*/
|
||||
function ChangeCharacter(value) {
|
||||
let role = roleOptions.value.find((item) => item.value === value)
|
||||
settingStore.ttsSetting.edgeTTS.value = role.value
|
||||
settingStore.ttsSetting.edgeTTS.label = role.label
|
||||
settingStore.ttsSetting.edgeTTS.lang = role.lang
|
||||
settingStore.ttsSetting.edgeTTS.gender = role.gender
|
||||
optionStore.TTS_GlobalSetting.edgeTTS.value = role.value
|
||||
optionStore.TTS_GlobalSetting.edgeTTS.label = role.label
|
||||
optionStore.TTS_GlobalSetting.edgeTTS.lang = role.lang
|
||||
optionStore.TTS_GlobalSetting.edgeTTS.gender = role.gender
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,66 +1,28 @@
|
||||
<template>
|
||||
<div style="display: flex; min-width: 900px; overflow: auto">
|
||||
<div class="text-input">
|
||||
<n-input
|
||||
v-model:value="text"
|
||||
type="textarea"
|
||||
placeholder="请输入配音的文本内容"
|
||||
:autosize="{
|
||||
minRows: 30,
|
||||
maxRows: 30
|
||||
}"
|
||||
show-count
|
||||
></n-input>
|
||||
<div class="tts-options">
|
||||
<n-button
|
||||
:color="softwareStore.SoftColor.BROWN_YELLOW"
|
||||
size="small"
|
||||
@click="FormatWordString"
|
||||
>
|
||||
格式化文档
|
||||
</n-button>
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button quaternary circle color="#b6a014" @click="ModifySplitChar">
|
||||
<template #icon>
|
||||
<n-icon size="25"> <AddCircleOutline /> </n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<span>添加分割标识符</span>
|
||||
</n-popover>
|
||||
<n-button
|
||||
style="margin-right: 10px"
|
||||
:color="softwareStore.SoftColor.BROWN_YELLOW"
|
||||
size="small"
|
||||
@click="ClearText"
|
||||
>
|
||||
清空内容
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<TTSTextInput />
|
||||
<div class="audio-setting">
|
||||
<div class="param-setting">
|
||||
<n-form label-placement="left">
|
||||
<n-form-item label="选择配音渠道">
|
||||
<n-select
|
||||
placeholder="请选择配音渠道"
|
||||
v-model:value="settingStore.ttsSetting.selectModel"
|
||||
v-model:value="optionStore.TTS_GlobalSetting.selectModel"
|
||||
:options="ttsOptions"
|
||||
></n-select>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<EdgeTTS v-if="settingStore.ttsSetting.selectModel == 'edge-tts'" />
|
||||
<AzureTTS
|
||||
<EdgeTTS v-if="optionStore.TTS_GlobalSetting.selectModel == 'edge-tts'" />
|
||||
<!-- <AzureTTS
|
||||
:azureTTS="settingStore.ttsSetting.azureTTS"
|
||||
v-else-if="settingStore.ttsSetting.selectModel == 'azure-tts'"
|
||||
/>
|
||||
v-else-if="optionStore.TTS_GlobalSetting.selectModel == 'azure-tts'"
|
||||
/> -->
|
||||
</div>
|
||||
<div class="autio-button">
|
||||
<n-button
|
||||
:color="softwareStore.SoftColor.BROWN_YELLOW"
|
||||
style="margin-right: 10px"
|
||||
@click="SaveTTSConfig"
|
||||
@click="SaveTTSGlobalSetting"
|
||||
>
|
||||
保存配置信息
|
||||
</n-button>
|
||||
@ -80,17 +42,21 @@
|
||||
</n-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<audio ref="audio" :src="audioUrl" controls style="width: 100%"></audio>
|
||||
<audio
|
||||
ref="audio"
|
||||
:src="optionStore.TTS_GlobalSetting.saveAudioPath"
|
||||
controls
|
||||
style="width: 100%"
|
||||
></audio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, defineComponent, onUnmounted, toRaw, reactive, h } from 'vue'
|
||||
import { ref, onMounted, toRaw, h } from 'vue'
|
||||
import {
|
||||
useMessage,
|
||||
NInput,
|
||||
NSelect,
|
||||
NFormItem,
|
||||
NForm,
|
||||
@ -101,155 +67,92 @@ import {
|
||||
} from 'naive-ui'
|
||||
import EdgeTTS from './EdgeTTS.vue'
|
||||
import AzureTTS from './AzureTTS.vue'
|
||||
import { GetTTSSelect } from '../../../../define/tts/ttsDefine'
|
||||
import { useSoftwareStore } from '../../../../stores/software'
|
||||
import { AddCircleOutline } from '@vicons/ionicons5'
|
||||
import InputDialogContent from '../Original/Components/InputDialogContent.vue'
|
||||
import { useSettingStore } from '../../../../stores/setting'
|
||||
import TTSHistory from './TTSHistory.vue'
|
||||
import { FormatWord } from '../../../../define/Tools/write'
|
||||
import TTSTextInput from './TTSTextInput.vue'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
import { TimeDelay } from '@/define/Tools/time'
|
||||
import InitCommon from '../../common/initCommon'
|
||||
import TTSCommon from '../../common/ttsCommon'
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
let message = useMessage()
|
||||
let dialog = useDialog()
|
||||
|
||||
let softwareStore = useSoftwareStore()
|
||||
let text = ref('你好,我是你的智能语音助手')
|
||||
let ttsOptions = ref([
|
||||
{
|
||||
label: 'EdgeTTS(免费)',
|
||||
value: 'edge-tts'
|
||||
}
|
||||
])
|
||||
let splitRef = ref(null)
|
||||
let settingStore = useSettingStore()
|
||||
let writeSetting = ref({
|
||||
split_char: '。,“”‘’!?【】《》()…—:;.,\'\'""!?[]<>()...-:;',
|
||||
merge_count: 3,
|
||||
merge_char: ',',
|
||||
end_char: '。'
|
||||
})
|
||||
let audioUrl = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '正在加载,请稍等...'
|
||||
try {
|
||||
// 加载服务端的TTS配置(目前的TTS配置是全局的)
|
||||
let res = await window.tts.GetTTSCOnfig()
|
||||
if (res.code == 0) {
|
||||
message.error(res.message)
|
||||
} else {
|
||||
settingStore.ttsSetting = res.data
|
||||
}
|
||||
await InitTTSGlobalSetting()
|
||||
})
|
||||
|
||||
// 加载文字设置
|
||||
let writeSettingRes = await window.write.GetWriteCOnfig()
|
||||
if (writeSettingRes.code == 0) {
|
||||
message.error(writeSettingRes.message)
|
||||
} else {
|
||||
writeSetting.value = writeSettingRes.data
|
||||
}
|
||||
/**
|
||||
* 初始化TTS配置信息
|
||||
*/
|
||||
async function InitTTSGlobalSetting() {
|
||||
try {
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '正在加载数据,请稍等...'
|
||||
await InitCommon.InitTTSGlobalSetting()
|
||||
|
||||
await TimeDelay(300)
|
||||
} catch (error) {
|
||||
message.error('加载失败,失败原因:' + error.toString())
|
||||
window.api.showGlobalMessageDialog({
|
||||
code: 0,
|
||||
message: '加载或者是初始化TTS信息失败,失败原因:' + error.toString()
|
||||
})
|
||||
} finally {
|
||||
softwareStore.spin.spinning = false
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 修改分割符
|
||||
*/
|
||||
async function ModifySplitChar() {
|
||||
// 判断当前数据是不是存在
|
||||
// 处理数据。获取当前的所有的数据
|
||||
let dialogWidth = 400
|
||||
let dialogHeight = 150
|
||||
dialog.create({
|
||||
title: '添加分割符',
|
||||
showIcon: false,
|
||||
closeOnEsc: false,
|
||||
content: () =>
|
||||
h(InputDialogContent, {
|
||||
ref: splitRef,
|
||||
initData: writeSetting.value.split_char,
|
||||
placeholder: '请输入分割符'
|
||||
}),
|
||||
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
|
||||
maskClosable: false,
|
||||
onClose: async () => {
|
||||
writeSetting.value.split_char = splitRef.value.data
|
||||
// 保存数据
|
||||
let saveRes = await window.write.SaveWriteConfig(toRaw(writeSetting.value))
|
||||
if (saveRes.code == 0) {
|
||||
message.error(saveRes.message)
|
||||
return
|
||||
}
|
||||
message.success('分隔符保存成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析/格式化文档
|
||||
* 调用方法保存全部配置信息
|
||||
*/
|
||||
async function FormatWordString() {
|
||||
async function SaveTTSGlobalSetting() {
|
||||
try {
|
||||
let wordSrr = FormatWord(text.value)
|
||||
text.value = wordSrr.join('\n')
|
||||
message.success('文本格式化成功')
|
||||
await TTSCommon.SaveTTSGlobalSetting()
|
||||
message.success('保存配置信息成功')
|
||||
} catch (error) {
|
||||
message.error('文本格式化失败,失败原因:' + error.toString())
|
||||
message.error('保存配置信息失败,失败原因:' + error.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文本内容
|
||||
*/
|
||||
async function ClearText() {
|
||||
text.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存TTS配置信息
|
||||
*/
|
||||
async function SaveTTSConfig() {
|
||||
let saveRes = await window.tts.SaveTTSConfig(toRaw(settingStore.ttsSetting))
|
||||
if (saveRes.code == 0) {
|
||||
message.error(saveRes.message)
|
||||
return
|
||||
}
|
||||
message.success('TTS配置保存成功')
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始合成音频
|
||||
*/
|
||||
async function GenerateAudio() {
|
||||
if (text.value == '') {
|
||||
try {
|
||||
if (optionStore.TTS_GlobalSetting.ttsText == '') {
|
||||
message.error('文本内容不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '正在合成音频,请稍等...'
|
||||
|
||||
// 保存配置信息
|
||||
let saveRes = await window.tts.SaveTTSConfig(toRaw(settingStore.ttsSetting))
|
||||
if (saveRes.code == 0) {
|
||||
softwareStore.spin.spinning = false
|
||||
message.error(saveRes.message)
|
||||
return
|
||||
}
|
||||
await TTSCommon.SaveTTSGlobalSetting()
|
||||
|
||||
// 开始真正的合成音频
|
||||
let generateRes = await window.tts.GenerateAudio(text.value)
|
||||
if (generateRes.code == 0) {
|
||||
softwareStore.spin.spinning = false
|
||||
message.error(generateRes.message)
|
||||
return
|
||||
let generateRes = await window.tts.GenerateAudio()
|
||||
if (generateRes.code == 1) {
|
||||
optionStore.TTS_GlobalSetting.saveAudioPath = generateRes.data.mp3Path
|
||||
// 合成完毕保存数据
|
||||
await TTSCommon.SaveTTSGlobalSetting()
|
||||
}
|
||||
|
||||
audioUrl.value = generateRes.mp3Path
|
||||
window.api.showGlobalMessageDialog(generateRes)
|
||||
} catch (error) {
|
||||
message.error('合成音频失败,失败原因:' + error.toString())
|
||||
} finally {
|
||||
softwareStore.spin.spinning = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示配音的历史记录
|
||||
@ -273,19 +176,8 @@ function ShowHistory() {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
}
|
||||
.text-input {
|
||||
flex: 4;
|
||||
height: 100%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.audio-setting {
|
||||
flex: 2;
|
||||
margin: 0 20px;
|
||||
}
|
||||
.tts-options {
|
||||
margin-top: 10px;
|
||||
margin-right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
145
src/renderer/src/components/TTS/TTSTextInput.vue
Normal file
145
src/renderer/src/components/TTS/TTSTextInput.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="text-input">
|
||||
<n-input
|
||||
v-model:value="optionStore.TTS_GlobalSetting.ttsText"
|
||||
type="textarea"
|
||||
placeholder="请输入配音的文本内容"
|
||||
:autosize="{
|
||||
minRows: 30,
|
||||
maxRows: 30
|
||||
}"
|
||||
show-count
|
||||
></n-input>
|
||||
<div class="tts-options">
|
||||
<n-button :color="softwareStore.SoftColor.BROWN_YELLOW" size="small" @click="formatWrite">
|
||||
格式化文档
|
||||
</n-button>
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button quaternary circle color="#b6a014" @click="AddSplitChar">
|
||||
<template #icon>
|
||||
<n-icon size="25"> <AddCircleOutline /> </n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<span>添加分割标识符</span>
|
||||
</n-popover>
|
||||
<n-button
|
||||
style="margin-right: 10px"
|
||||
:color="softwareStore.SoftColor.BROWN_YELLOW"
|
||||
size="small"
|
||||
@click="ClearText"
|
||||
>
|
||||
清空内容
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, h } from 'vue'
|
||||
import { NInput, NButton, NPopover, NIcon, useMessage, useDialog } from 'naive-ui'
|
||||
import { AddCircleOutline } from '@vicons/ionicons5'
|
||||
import InitCommon from '../../common/initCommon'
|
||||
import TextCommon from '../../common/text'
|
||||
import InputDialogContent from '../Original/Components/InputDialogContent.vue'
|
||||
|
||||
import { useSoftwareStore } from '@/stores/software'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
import { OptionKeyName, OptionType } from '@/define/enum/option'
|
||||
import TTSCommon from '../../common/ttsCommon'
|
||||
|
||||
let softwareStore = useSoftwareStore()
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
let dialog = useDialog()
|
||||
let message = useMessage()
|
||||
|
||||
let split_ref = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
await InitCommon.InitSpecialCharacters()
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加分割符号
|
||||
*/
|
||||
async function AddSplitChar() {
|
||||
// 判断当前数据是不是存在
|
||||
// 处理数据。获取当前的所有的数据
|
||||
let dialogWidth = 400
|
||||
let dialogHeight = 150
|
||||
dialog.create({
|
||||
title: '添加分割符',
|
||||
showIcon: false,
|
||||
closeOnEsc: false,
|
||||
content: () =>
|
||||
h(InputDialogContent, {
|
||||
ref: split_ref,
|
||||
initData: optionStore.CW_FormatSpecialChar,
|
||||
placeholder: '请输入分割符'
|
||||
}),
|
||||
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
|
||||
maskClosable: false,
|
||||
onClose: async () => {
|
||||
optionStore.CW_FormatSpecialChar = split_ref.value.data
|
||||
let saveRes = await window.options.ModifyOptionByKey(
|
||||
OptionKeyName.CW_FormatSpecialChar,
|
||||
optionStore.CW_FormatSpecialChar,
|
||||
OptionType.STRING
|
||||
)
|
||||
if (saveRes.code == 0) {
|
||||
window.api.showGlobalMessageDialog(saveRes)
|
||||
// 报错 不能关闭
|
||||
return false
|
||||
} else {
|
||||
message.success('数据保存成功')
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文案
|
||||
*/
|
||||
async function formatWrite() {
|
||||
try {
|
||||
let newText = await TextCommon.FormatText(optionStore.TTS_GlobalSetting.ttsText)
|
||||
optionStore.TTS_GlobalSetting.ttsText = newText
|
||||
await TTSCommon.SaveTTSGlobalSetting()
|
||||
message.success('格式化成功')
|
||||
} catch (error) {
|
||||
message.error('格式化失败,失败原因:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空数据库信息
|
||||
*/
|
||||
async function ClearText() {
|
||||
try {
|
||||
optionStore.TTS_GlobalSetting.ttsText = ''
|
||||
optionStore.TTS_GlobalSetting.saveAudioPath = ''
|
||||
await TTSCommon.SaveTTSGlobalSetting()
|
||||
message.success('清空成功')
|
||||
} catch (error) {
|
||||
message.error('清空失败,失败原因:' + error.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-input {
|
||||
flex: 4;
|
||||
height: 100%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.tts-options {
|
||||
margin-top: 10px;
|
||||
margin-right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@ -16,7 +16,7 @@ const routes = [
|
||||
{
|
||||
path: '/gptCopywriting',
|
||||
name: 'gptCopywriting',
|
||||
component: () => import('./components/CopyWriting/CopyWriting.vue')
|
||||
component: () => import('./components/CopyWriting/CopyWritingHome.vue')
|
||||
},
|
||||
{
|
||||
path: '/global_setting',
|
||||
|
||||
74
src/stores/option.ts
Normal file
74
src/stores/option.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { OptionKeyName } from "@/define/enum/option";
|
||||
import { OptionModel } from "@/model/option/option";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export type OptionStoreModel = {
|
||||
//#region
|
||||
|
||||
/** 文案处理 AI设置 */
|
||||
[OptionKeyName.CW_AISetting]: {
|
||||
laiapi: OptionModel.CW_AISettingModel
|
||||
};
|
||||
/** 文案处理数据界面数据 */
|
||||
[OptionKeyName.CW_AISimpleSetting]: OptionModel.CW_AISimpleSettingModel | undefined;
|
||||
|
||||
/** 格式化的特殊字符数据 */
|
||||
[OptionKeyName.CW_FormatSpecialChar]: string | undefined;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region TTS
|
||||
|
||||
/** TTS界面视图数据 */
|
||||
[OptionKeyName.TTS_GlobalSetting]: OptionModel.TTS_GlobalSettingModel | undefined;
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export const useOptionStore = defineStore('option', {
|
||||
state: () => ({
|
||||
[OptionKeyName.CW_AISetting]: {
|
||||
laiapi: {
|
||||
api_key: '',
|
||||
gpt_url: '',
|
||||
model: ''
|
||||
}
|
||||
},
|
||||
[OptionKeyName.CW_AISimpleSetting]: {
|
||||
gptType: undefined,
|
||||
gptData: undefined,
|
||||
gptAI: 'laiapi',
|
||||
isStream: false,
|
||||
isSplit: false,
|
||||
splitNumber: 500,
|
||||
oldWord: '',
|
||||
newWord: '',
|
||||
oldWordCount: 0,
|
||||
newWordCount: 0,
|
||||
wordStruct: []
|
||||
},
|
||||
[OptionKeyName.CW_FormatSpecialChar]: undefined,
|
||||
[OptionKeyName.TTS_GlobalSetting]: {
|
||||
selectModel: "edge-tts",
|
||||
edgeTTS: {
|
||||
"value": "zh-CN-XiaoyiNeural",
|
||||
"gender": "Female",
|
||||
"label": "中文-女-小宜",
|
||||
"lang": "zh-CN",
|
||||
"saveSubtitles": true,
|
||||
"pitch": 0,
|
||||
"rate": 10,
|
||||
"volumn": 0,
|
||||
timeOut: 120000,
|
||||
},
|
||||
ttsText: "你好,我是你的智能语音助手!",
|
||||
/** 保存的音频文件路径 */
|
||||
saveAudioPath: undefined,
|
||||
}
|
||||
|
||||
} as unknown as OptionStoreModel),
|
||||
getters: {
|
||||
},
|
||||
actions: {
|
||||
}
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user