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

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "laitool", "name": "laitool",
"version": "3.2.2", "version": "3.2.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@ -1,6 +1,6 @@
{ {
"name": "laitool", "name": "laitool",
"version": "3.2.2", "version": "3.2.3",
"description": "An AI tool for image processing, video processing, and other functions.", "description": "An AI tool for image processing, video processing, and other functions.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "laitool.cn", "author": "laitool.cn",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,6 +6,9 @@
*/ */
export function ValidateJson(str: string): boolean { export function ValidateJson(str: string): boolean {
try { try {
if (str == null) {
return false;
}
JSON.parse(str); JSON.parse(str);
return true return true
} catch (e) { } catch (e) {

View File

@ -11,6 +11,7 @@ import { OtherData } from '../../../enum/softwareEnum.js'
import { BookBackTaskList } from '../../model/Book/BookBackTaskListModel.js' import { BookBackTaskList } from '../../model/Book/BookBackTaskListModel.js'
import { Book } from '../../../../model/book/book.js' import { Book } from '../../../../model/book/book.js'
import { GeneralResponse } from '../../../../model/generalResponse.js' import { GeneralResponse } from '../../../../model/generalResponse.js'
import { TaskModal } from '@/model/task.js'
const { v4: uuidv4 } = require('uuid') const { v4: uuidv4 } = require('uuid')
export class BookBackTaskListService extends BaseRealmService { export class BookBackTaskListService extends BaseRealmService {

View File

@ -232,7 +232,8 @@ export class BookService extends BaseRealmService {
updateTime: new Date(), updateTime: new Date(),
createTime: new Date(), createTime: new Date(),
version: version, version: version,
imageCategory: imageCategory imageCategory: imageCategory,
openVideoGenerate: false
} }
// 添加任务 // 添加任务

View 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
}
/**
* Optionkeynull
* @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;
}
}
/**
* Optionkey
* @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
}
}

View File

@ -5,6 +5,7 @@ import SETTING from "./settingDefineString"
import BOOK from "./bookDefineString" import BOOK from "./bookDefineString"
import WRITE from "./writeDefineString" import WRITE from "./writeDefineString"
import DB from "./dbDefineString" import DB from "./dbDefineString"
import OPTIONS from "./optionsDefineString"
export const DEFINE_STRING = { export const DEFINE_STRING = {
SYSTEM: SYSTEM, SYSTEM: SYSTEM,
@ -14,6 +15,7 @@ export const DEFINE_STRING = {
SETTING: SETTING, SETTING: SETTING,
WRITE: WRITE, WRITE: WRITE,
DB: DB, DB: DB,
OPTIONS:OPTIONS,
SHOW_GLOBAL_MESSAGE: "SHOW_GLOBAL_MESSAGE", SHOW_GLOBAL_MESSAGE: "SHOW_GLOBAL_MESSAGE",
SHOW_GLOBAL_MAIN_NOTIFICATION: 'SHOW_GLOBAL_MAIN_NOTIFICATION', SHOW_GLOBAL_MAIN_NOTIFICATION: 'SHOW_GLOBAL_MAIN_NOTIFICATION',
OPEN_DEV_TOOLS_PASSWORD: 'OPEN_DEV_TOOLS_PASSWORD', OPEN_DEV_TOOLS_PASSWORD: 'OPEN_DEV_TOOLS_PASSWORD',

View File

@ -0,0 +1,19 @@
const OPTIONS = {
/**
* Optionkeynull
*/
GET_OPTION_BY_KEY: 'GET_OPTION_BY_KEY',
/**
* Optionkey
*/
MODIFY_OPTION_BY_KEY: 'MODIFY_OPTION_BY_KEY',
/**
* AI设置旧数据到新的数据表中
*/
INIT_COPY_WRITING_AI_SETTING: "INIT_COPY_WRITING_AI_SETTING"
}
export default OPTIONS

View File

@ -1,7 +1,6 @@
const WRITE = { const WRITE = {
GET_WRITE_CONFIG: 'GET_WRITE_CONFIG', GET_WRITE_CONFIG: 'GET_WRITE_CONFIG',
SAVE_WRITE_CONFIG: 'SAVE_WRITE_CONFIG', SAVE_WRITE_CONFIG: 'SAVE_WRITE_CONFIG',
ACTION_START: 'ACTION_START',
GET_SUBTITLE_SETTING: "GET_SUBTITLE_SETTING", GET_SUBTITLE_SETTING: "GET_SUBTITLE_SETTING",
RESET_SUBTITLE_SETTING: "RESET_SUBTITLE_SETTING", RESET_SUBTITLE_SETTING: "RESET_SUBTITLE_SETTING",
SAVE_SUBTITLE_SETTING: "SAVE_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: "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 export default WRITE

View File

@ -1,7 +1,40 @@
/** option 中的type的类型 */ /**
* Option Value的数据类型
*/
export enum OptionType { export enum OptionType {
STRING = 'string', STRING = 'string',
NUMBER = 'number', NUMBER = 'number',
BOOLEAN = 'boolean', BOOLEAN = 'boolean',
JOSN = 'json' 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
}

View File

@ -8,7 +8,20 @@ import { apiUrl } from './api/apiUrlDefine'
export const gptDefine = { export const gptDefine = {
// Add properties and methods to the shared object // Add properties and methods to the shared object
characterSystemContent: `{textContent}\r查看上面的文本,然后扮演一个文本编辑来回答问题。`, 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.`, 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`, 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`,

View File

@ -17,6 +17,7 @@ import { TTSIpc } from './ttsIpc'
import { DBIpc } from './dbIpc' import { DBIpc } from './dbIpc'
import { PresetIpc } from './presetIpc' import { PresetIpc } from './presetIpc'
import { TaskIpc } from './taskIpc' import { TaskIpc } from './taskIpc'
import { OptionsIpc } from './optionsIpc'
export async function RegisterIpc(createWindow) { export async function RegisterIpc(createWindow) {
PromptIpc() PromptIpc()
@ -38,4 +39,5 @@ export async function RegisterIpc(createWindow) {
SystemIpc() SystemIpc()
BookIpc() BookIpc()
TTSIpc() TTSIpc()
OptionsIpc()
} }

View 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() {
/**
* Optionkeynull
*/
ipcMain.handle(
DEFINE_STRING.OPTIONS.GET_OPTION_BY_KEY,
async (_, key: string) => await OptionHandle.GetOptionByKey(key)
)
/**
* Optionkey
*/
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 }

View File

@ -5,19 +5,11 @@ import { TTS } from '../Service/tts'
const tts = new TTS() const tts = new TTS()
export function TTSIpc() { 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( ipcMain.handle(
DEFINE_STRING.TTS.GENERATE_AUDIO, DEFINE_STRING.TTS.GENERATE_AUDIO,
async (event, text) => await tts.GenerateAudio(text) async (event) => await tts.GenerateAudio()
) )
// 生成SRT字幕文件 // 生成SRT字幕文件

View File

@ -9,6 +9,8 @@ import { BookPrompt } from '../Service/Book/bookPrompt'
let subtitleService = new SubtitleService() let subtitleService = new SubtitleService()
const bookPrompt = new BookPrompt(); const bookPrompt = new BookPrompt();
import CopyWritingService from '@/main/Service/copywriting/index'
function WritingIpc() { function WritingIpc() {
// 监听分镜时间的保存 // 监听分镜时间的保存
ipcMain.handle( ipcMain.handle(
@ -69,11 +71,18 @@ function WritingIpc() {
async (event, subtitleSetting) => await subtitleService.SaveSubtitleSetting(subtitleSetting) async (event, subtitleSetting) => await subtitleService.SaveSubtitleSetting(subtitleSetting)
) )
//#region 文案处理
/**
* AI处理文案
*/
ipcMain.handle( ipcMain.handle(
DEFINE_STRING.WRITE.ACTION_START, DEFINE_STRING.WRITE.COPY_WRITING_AI_GENERATION,
async (event, aiSetting, word) => await writing.ActionStart(aiSetting, word) async (event, ids: string[]) => await CopyWritingService.CopyWritingAIGeneration(ids)
) )
//#endregion
//#region 文案洗稿相关 //#region 文案洗稿相关
/** 生成洗稿后文案 */ /** 生成洗稿后文案 */

View File

@ -351,27 +351,59 @@ export class Translate {
content: translateData content: translateData
}) })
let config = { let content = ''
method: 'post', // 判断整体是不是需要LMS转发
maxBodyLength: Infinity, if (global.config.useTransfer) {
url: this.translationBusiness, let url = define.lms + '/lms/Forward/SimpleTransfer'
headers: { let config = {
'Content-Type': 'application/json', method: 'post',
Authorization: `Bearer ${token}` url: url,
}, maxBodyLength: Infinity,
data: JSON.stringify(data) 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,
url: this.translationBusiness,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
data: JSON.stringify(data)
}
let res = await RetryWithBackoff(
async () => {
return await axios.request(config)
},
5,
2000
)
// 将返回的数据进行拼接数据处理
content = GetOpenAISuccessResponse(res.data)
} }
let res = await RetryWithBackoff(
async () => {
return await axios.request(config)
},
5,
2000
)
// 将返回的数据进行拼接数据处理
let res_data = [] let res_data = []
let content = GetOpenAISuccessResponse(res.data)
if (to == 'zh') { if (to == 'zh') {
res_data.push({ res_data.push({

View File

@ -101,9 +101,14 @@ export class BookBasic {
try { try {
let book = await this.bookServiceBasic.GetBookDataById(bookId) let book = await this.bookServiceBasic.GetBookDataById(bookId)
// 获取所有的小说批次 // 获取所有的小说批次
let bookTasks = (await this.bookServiceBasic.GetBookTaskData({ let bookTasksObj = (await this.bookServiceBasic.GetBookTaskData({
bookId: bookId bookId: bookId
})).bookTasks; }, true));
// 删除之前判断是不是有子批次 没有直接退出
let bookTasks = bookTasksObj.bookTasks;
if (bookTasks.length == 0) {
return successMessage('未找到小说批次数据,正常退出', 'BookBasic_ResetBookData');
}
// 重置批次任务 // 重置批次任务
for (let i = 0; i < bookTasks.length; i++) { for (let i = 0; i < bookTasks.length; i++) {
const element = bookTasks[i]; const element = bookTasks[i];
@ -179,13 +184,18 @@ export class BookBasic {
if (resetRes.code == 0) { if (resetRes.code == 0) {
throw new Error(resetRes.message) throw new Error(resetRes.message)
} }
let bookTasks = (await this.bookServiceBasic.GetBookTaskData({ let bookTasksObj = (await this.bookServiceBasic.GetBookTaskData({
bookId: bookId bookId: bookId
})).bookTasks; }, true))
// 删除遗留重置的小说批次任务 let bookTasks = bookTasksObj.bookTasks;
for (let i = 0; i < bookTasks.length; i++) {
const element = bookTasks[i]; // 有数据才删除
await this.bookServiceBasic.DeleteBookTaskData(element.id); if (bookTasks.length > 0) {
// 删除遗留重置的小说批次任务
for (let i = 0; i < bookTasks.length; i++) {
const element = bookTasks[i];
await this.bookServiceBasic.DeleteBookTaskData(element.id);
}
} }
// 开始删除数据 // 开始删除数据

View File

@ -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') return successMessage(null, "推理所有数据完成", 'BookPrompt_OriginalGetPrompt')

View File

@ -227,6 +227,7 @@ export class BookTask {
this.bookServiceBasic.transaction((realm) => { this.bookServiceBasic.transaction((realm) => {
for (let i = 0; i < bookTasks.length; i++) { for (let i = 0; i < bookTasks.length; i++) {
const element = bookTasks[i]; const element = bookTasks[i];
element.openVideoGenerate = false
realm.create('BookTask', element) realm.create('BookTask', element)
} }
for (let i = 0; i < bookTaskDetail.length; i++) { for (let i = 0; i < bookTaskDetail.length; i++) {
@ -409,6 +410,7 @@ export class BookTask {
suffixPrompt: sourceBookTask.suffixPrompt, suffixPrompt: sourceBookTask.suffixPrompt,
version: sourceBookTask.version, version: sourceBookTask.version,
imageCategory: sourceBookTask.imageCategory, imageCategory: sourceBookTask.imageCategory,
openVideoGenerate: sourceBookTask.openVideoGenerate == null ? false : sourceBookTask.openVideoGenerate,
} as Book.SelectBookTask } as Book.SelectBookTask
addBookTask.push(addOneBookTask) addBookTask.push(addOneBookTask)
@ -517,7 +519,7 @@ export class BookTask {
return successMessage(returnBookTask, "复制小说任务成功", "BookBasic_CopyNewBookTask") return successMessage(returnBookTask, "复制小说任务成功", "BookBasic_CopyNewBookTask")
} catch (error) { } catch (error) {
console.log(error) console.log(error)
throw error return errorMessage("复制小说任务失败,失败信息如下:" + error.message, "BookBasic_CopyNewBookTask")
} }
} }

View File

@ -262,8 +262,9 @@ export class BookVideo {
if (repalceObject && repalceObject.length > 0) { if (repalceObject && repalceObject.length > 0) {
await this.jianyingService.ReplaceDraftMaterialImageToVideo(book.name + "_" + element.name, repalceObject); 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) { } catch (error) {
return errorMessage('添加剪映草稿失败,错误信息如下:' + error.toString(), "BookTask_AddJianyingDraft"); return errorMessage('添加剪映草稿失败,错误信息如下:' + error.toString(), "BookTask_AddJianyingDraft");
} }

View File

@ -1,8 +1,9 @@
import { isEmpty } from "lodash"; import { isEmpty, method } from "lodash";
import { gptDefine } from "../../../define/gptDefine"; import { gptDefine } from "../../../define/gptDefine";
import axios from "axios"; import axios from "axios";
import { RetryWithBackoff } from "../../../define/Tools/common"; import { RetryWithBackoff } from "../../../define/Tools/common";
import { Book } from "../../../model/book/book"; import { Book } from "../../../model/book/book";
import { define } from "@/define/define";
/** /**
* GPT相关的服务都在这边 * GPT相关的服务都在这边
@ -11,6 +12,7 @@ export class GptService {
gptUrl: string = undefined gptUrl: string = undefined
gptModel: string = undefined gptModel: string = undefined
gptApiKey: string = undefined gptApiKey: string = undefined
useTransfer: boolean = false
//#region GPT 设置 //#region GPT 设置
@ -42,10 +44,12 @@ export class GptService {
this.gptUrl = all_options[index].gpt_url; this.gptUrl = all_options[index].gpt_url;
this.gptApiKey = global.config.gpt_key; this.gptApiKey = global.config.gpt_key;
this.gptModel = global.config.gpt_model; this.gptModel = global.config.gpt_model;
this.useTransfer = global.config.useTransfer;
return { return {
gptUrl: this.gptUrl, gptUrl: this.gptUrl,
gptApiKey: this.gptApiKey, 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")) { if (gpt_url.includes("dashscope.aliyuncs.com")) {
content = res.data.output.choices[0].message.content; 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 { } else {
content = res.data.choices[0].message.content; content = res.data.choices[0].message.content;
} }
@ -126,21 +140,43 @@ export class GptService {
"messages": message "messages": message
}; };
data = this.ModifyData(data, gpt_url); if (this.useTransfer) {
let config = { // 转发到LMS中过一遍
method: 'post', let url = define.lms + "/lms/Forward/SimpleTransfer";
maxBodyLength: Infinity, let config = {
url: gpt_url ? gpt_url : this.gptUrl, method: 'post',
headers: { url: url,
'Authorization': `Bearer ${gpt_key ? gpt_key : this.gptApiKey}`, maxBodyLength: Infinity,
'Content-Type': 'application/json' headers: {
}, 'Content-Type': 'application/json'
data: JSON.stringify(data) },
}; data: JSON.stringify({
url: gpt_url ? gpt_url : this.gptUrl,
apiKey: gpt_key ? gpt_key : this.gptApiKey,
dataString: JSON.stringify(data)
})
}
let res = await axios.request(config);
let content = this.GetResponseContent(res, gpt_url);
return content;
} else {
// 不转发 直接请求原接口
data = this.ModifyData(data, gpt_url);
let config = {
method: 'post',
maxBodyLength: Infinity,
url: gpt_url ? gpt_url : this.gptUrl,
headers: {
'Authorization': `Bearer ${gpt_key ? gpt_key : this.gptApiKey}`,
'Content-Type': 'application/json'
},
data: JSON.stringify(data)
};
let res = await axios.request(config); let res = await axios.request(config);
let content = this.GetResponseContent(res, this.gptUrl); let content = this.GetResponseContent(res, this.gptUrl);
return content; return content;
}
} catch (error) { } catch (error) {
throw error; throw error;
} }

View 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操作
/**
* Optionkeynull
* @param key Key的值
* @returns
*/
GetOptionByKey = async (key: string) => await this.optionServices.GetOptionByKey(key)
/**
* Optionkey
* @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()

View 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()
}
}
/**
* Optionkeynull
* @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'
)
}
}
/**
* Optionkey
* @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'
)
}
}
}

View File

@ -17,6 +17,7 @@ import { DEFINE_STRING } from "../../../define/define_string";
import { OtherData, ResponseMessageType } from "../../../define/enum/softwareEnum"; import { OtherData, ResponseMessageType } from "../../../define/enum/softwareEnum";
import util from 'util'; import util from 'util';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { TaskModal } from "@/model/task";
const execAsync = util.promisify(exec) const execAsync = util.promisify(exec)
const fspromise = fs.promises const fspromise = fs.promises

View File

@ -55,7 +55,14 @@ class BookServiceBasic {
//#region 批次任务任务 //#region 批次任务任务
GetBookTaskDataById = async (bookTaskId: string) => await this.bookTaskServiceBasic.GetBookTaskDataById(bookTaskId); GetBookTaskDataById = async (bookTaskId: string) => await this.bookTaskServiceBasic.GetBookTaskDataById(bookTaskId);
GetBookTaskData = async (bookTaskCondition: Book.QueryBookTaskCondition) => await this.bookTaskServiceBasic.GetBookTaskData(bookTaskCondition); /**
*
* @param bookTaskCondition
* @param returnEmpry falsetrue的话返回空数据
* @returns
*/
GetBookTaskData = async (bookTaskCondition: Book.QueryBookTaskCondition, returnEmpry: boolean = false) => await this.bookTaskServiceBasic.GetBookTaskData(bookTaskCondition, returnEmpry);
GetMaxBookTaskNo = async (bookId: string) => await this.bookTaskServiceBasic.GetMaxBookTaskNo(bookId); GetMaxBookTaskNo = async (bookId: string) => await this.bookTaskServiceBasic.GetMaxBookTaskNo(bookId);
UpdetedBookTaskData = async (bookTaskId: string, data: Book.SelectBookTask) => await this.bookTaskServiceBasic.UpdetedBookTaskData(bookTaskId, data); UpdetedBookTaskData = async (bookTaskId: string, data: Book.SelectBookTask) => await this.bookTaskServiceBasic.UpdetedBookTaskData(bookTaskId, data);
ResetBookTask = async (bookTaskId: string) => await this.bookTaskServiceBasic.ResetBookTask(bookTaskId); ResetBookTask = async (bookTaskId: string) => await this.bookTaskServiceBasic.ResetBookTask(bookTaskId);

View File

@ -33,14 +33,20 @@ export default class BookTaskServiceBasic {
/** /**
* *
* @param bookTaskCondition * @param bookTaskCondition
* @param returnEmpry falsetrue的话返回空数据
* @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(); await this.InitService();
let bookTasks = this.bookTaskService.GetBookTaskData(bookTaskCondition) let bookTasks = this.bookTaskService.GetBookTaskData(bookTaskCondition)
if (bookTasks.data.bookTasks.length <= 0 || bookTasks.data.total <= 0) { if (returnEmpry) {
throw new Error("未找到对应的小说批次任务数据,请检查") return { bookTasks: [], total: 0 }
} else {
if (bookTasks.data.bookTasks.length <= 0 || bookTasks.data.total <= 0) {
throw new Error("未找到对应的小说批次任务数据,请检查")
}
} }
return bookTasks.data return bookTasks.data
} }

View File

@ -405,9 +405,8 @@ export class SubtitleService {
btd.timeLimit = element.timeLimit btd.timeLimit = element.timeLimit
} }
}) })
} }
return successMessage(null, "保存文案数据成功", 'SubtitleService_SaveCopywriting')
} catch (error) { } catch (error) {
return errorMessage("保存文案数据失败,失败信息如下:" + error.toString(), 'SubtitleService_SaveCopywriting') return errorMessage("保存文案数据失败,失败信息如下:" + error.toString(), 'SubtitleService_SaveCopywriting')
} }

View File

@ -193,24 +193,51 @@ export class Translate {
"content": value.text "content": value.text
}); });
let config = { let content = "";
method: 'post', // 判断整体是不是需要LMS转发
maxBodyLength: Infinity, if (global.config.useTransfer) {
url: this.translationBusiness, let url = define.lms + "/lms/Forward/SimpleTransfer";
headers: { let config = {
'Content-Type': 'application/json', method: 'post',
'Authorization': `Bearer ${token}` url: url,
}, maxBodyLength: Infinity,
data: JSON.stringify(data) headers: {
}; 'Content-Type': 'application/json'
let res = await RetryWithBackoff(async () => { },
return await axios.request(config); data: JSON.stringify({
}, 5, 2000) url: this.translationBusiness,
// let res = await axios.request(config); 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,
url: this.translationBusiness,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
data: JSON.stringify(data)
};
let res = await RetryWithBackoff(async () => {
return await axios.request(config);
}, 5, 2000)
// 将返回的数据进行拼接数据处理
content = GetOpenAISuccessResponse(res.data);
}
let res_data = []; let res_data = [];
let content = GetOpenAISuccessResponse(res.data);
res_data.push({ res_data.push({
src: value.text, src: value.text,
dst: content dst: content

View 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
}

View 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();

View File

@ -14,12 +14,20 @@ import { tts } from '../../model/tts'
import { GeneralResponse } from '../../model/generalResponse' import { GeneralResponse } from '../../model/generalResponse'
import axios from 'axios' import axios from 'axios'
import { GetEdgeTTSRole } from '../../define/tts/ttsDefine' 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 { export class TTS {
softService: SoftwareService softService: SoftwareService
ttsService: TTSService ttsService: TTSService
constructor() { } optionServices: OptionServices
constructor() {
this.optionServices = new OptionServices()
}
/** /**
* TTS服务 * TTS服务
@ -62,72 +70,12 @@ export class TTS {
throw new Error("获取TTS角色配置失败") throw new Error("获取TTS角色配置失败")
} }
if (isEmpty(data[0].value) || !ValidateJson(data[0].value)) { 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) { } catch (error) {
return errorMessage('获取TTS配置失败错误信息如下' + error.toString(), 'TTS_GetTTSCOnfig') return errorMessage('获取TTS配置失败错误信息如下' + error.toString(), 'TTS_GetTTSOptions')
}
}
/**
* 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')
} }
} }
@ -138,12 +86,17 @@ export class TTS {
* *
* @param text * @param text
*/ */
async GenerateAudio(text: string) { async GenerateAudio() {
try { try {
await this.InitService() await this.InitService()
let ttsSetting = await this.GetTTSCOnfig() let TTS_GlobalSetting = await this.optionServices.GetOptionByKey(OptionKeyName.TTS_GlobalSetting);
if (ttsSetting.code === 0) { if (TTS_GlobalSetting.code == 0) {
return ttsSetting 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 let res = undefined
@ -156,13 +109,13 @@ export class TTS {
await fs.promises.writeFile(textPath, text, 'utf-8') await fs.promises.writeFile(textPath, text, 'utf-8')
let audioPath = path.join(define.tts_path, `${thisId}/${thisId}.mp3`) 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 let hasSrt = true
switch (selectModel) { switch (selectModel) {
case TTSSelectModel.edgeTTS: case TTSSelectModel.edgeTTS:
hasSrt = ttsSetting.data.edgeTTS.saveSubtitles hasSrt = TTS_GlobalSettingData.edgeTTS.saveSubtitles
res = await this.GenerateAudioByEdgeTTS(text, ttsSetting.data.edgeTTS, audioPath) res = await this.GenerateAudioByEdgeTTS(text, TTS_GlobalSettingData.edgeTTS, audioPath)
break break
default: default:
throw new Error('未知的TTS模式') throw new Error('未知的TTS模式')
@ -182,7 +135,7 @@ export class TTS {
id: thisId, id: thisId,
textPath: textPath ? path.relative(define.tts_path, textPath) : null textPath: textPath ? path.relative(define.tts_path, textPath) : null
}) })
return res return successMessage(res, '生成音频成功', 'TTS_GenerateAudio')
} catch (error) { } catch (error) {
return errorMessage('生成音频失败,错误信息如下:' + error.toString(), 'TTS_GenerateAudio') return errorMessage('生成音频失败,错误信息如下:' + error.toString(), 'TTS_GenerateAudio')
} }
@ -194,9 +147,9 @@ export class TTS {
* @param edgeTTS edgetts的设置 * @param edgeTTS edgetts的设置
* @returns * @returns
*/ */
async GenerateAudioByEdgeTTS(text: string, edgeTTS: TTSSettingModel.EdgeTTSSetting, mp3Path: string) { async GenerateAudioByEdgeTTS(text: string, edgeTTS: OptionModel.TTS_EdgeTTSModel, mp3Path: string) {
try { try {
const tts = new EdgeTTS({ const edgeTts = new EdgeTTS({
voice: edgeTTS.value, voice: edgeTTS.value,
lang: edgeTTS.lang, lang: edgeTTS.lang,
outputFormat: 'audio-24khz-96kbitrate-mono-mp3', outputFormat: 'audio-24khz-96kbitrate-mono-mp3',
@ -204,10 +157,9 @@ export class TTS {
pitch: `${edgeTTS.pitch}%`, pitch: `${edgeTTS.pitch}%`,
rate: `${edgeTTS.rate}%`, rate: `${edgeTTS.rate}%`,
volume: `${edgeTTS.volumn}%`, volume: `${edgeTTS.volumn}%`,
timeout : 100000 timeout: edgeTTS.timeOut ?? 100000
}) })
let ttsRes = await tts.ttsPromise(text, mp3Path) await edgeTts.ttsPromise(text, mp3Path)
console.log(ttsRes)
return { return {
mp3Path: mp3Path, mp3Path: mp3Path,
srtJsonPath: mp3Path + '.json' srtJsonPath: mp3Path + '.json'

View File

@ -5,7 +5,6 @@ import { DEFINE_STRING } from '../../define/define_string'
import { PublicMethod } from '../Public/publicMethod' import { PublicMethod } from '../Public/publicMethod'
import { define } from '../../define/define' import { define } from '../../define/define'
import { get, has, isEmpty } from 'lodash' import { get, has, isEmpty } from 'lodash'
import { ClipSetting } from '../../define/setting/clipSetting'
import { errorMessage, successMessage } from '../Public/generalTools' import { errorMessage, successMessage } from '../Public/generalTools'
import { ServiceBase } from '../../define/db/service/serviceBase' import { ServiceBase } from '../../define/db/service/serviceBase'
import { import {
@ -25,7 +24,7 @@ export class Writing extends ServiceBase {
constructor(global) { constructor(global) {
super() super()
this.pm = new PublicMethod(global) 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'; let resData = '\n\n';
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch(define.serverUrl + "/lms/Forward/ForwardWordStream", requestOptions) fetch(define.lms + "/lms/Forward/ForwardWordStream", requestOptions)
.then(response => { .then(response => {
if (!response.body) { if (!response.body) {
throw new Error('ReadableStream not yet supported in this browser.'); throw new Error('ReadableStream not yet supported in this browser.');
@ -241,10 +240,6 @@ export class Writing extends ServiceBase {
}, 3, 1000) }, 3, 1000)
} }
} }
// let tasks =
// console.log("ActionStart", result);
// ExecuteConcurrently
return successMessage(result, "执行文案相关任务成功", 'Writing_ActionStart'); return successMessage(result, "执行文案相关任务成功", 'Writing_ActionStart');
} catch (error) { } catch (error) {
return errorMessage( return errorMessage(

View File

@ -17,7 +17,7 @@ export class GptSetting extends ServiceBase {
subtitleService: SubtitleService subtitleService: SubtitleService
constructor() { constructor() {
super() super()
axios.defaults.baseURL = define.serverUrl axios.defaults.baseURL = define.lms
this.softWareServiceBasic = new SoftWareServiceBasic(); this.softWareServiceBasic = new SoftWareServiceBasic();
this.subtitleService = new SubtitleService() this.subtitleService = new SubtitleService()
} }

View File

@ -21,6 +21,7 @@ declare namespace SoftwareSettingModel {
project_name: string = undefined // 项目名称 project_name: string = undefined // 项目名称
gpt_business: string = undefined // GPT服务商ID gpt_business: string = undefined // GPT服务商ID
gpt_model: string = undefined // GPT模型 gpt_model: string = undefined // GPT模型
useTransfer: boolean = false // 是不是使用转发
task_number: number = undefined // 任务数量 task_number: number = undefined // 任务数量
theme: string = undefined // 主题 theme: string = undefined // 主题
gpt_auto_inference: string = undefined // GPT自动推理模式 gpt_auto_inference: string = undefined // GPT自动推理模式

View File

@ -36,3 +36,16 @@ declare namespace GeneralResponse {
data?: MJ.MJResponseToFront | Buffer | string | TranslateModel.TranslateResponseMessageModel | ProgressResponse | SubtitleProgressResponse 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
}

View File

@ -1,10 +1,116 @@
import { OptionType } from "@/define/enum/option" import { OptionType } from "@/define/enum/option"
declare namespace Option { declare namespace OptionModel {
/** option的model */ /**
* Option的模型
*/
type OptionItem = { type OptionItem = {
key: string, key: string,
value: string, value: string,
type: OptionType 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
} }

View File

@ -16,6 +16,7 @@ import { db } from './db'
import { translate } from './translate' import { translate } from './translate'
import { preset } from './preset' import { preset } from './preset'
import { task } from './task' import { task } from './task'
import { options } from './options'
// Custom APIs for renderer // Custom APIs for renderer
let events = [] let events = []
@ -478,6 +479,7 @@ if (process.contextIsolated) {
contextBridge.exposeInMainWorld('db', db) contextBridge.exposeInMainWorld('db', db)
contextBridge.exposeInMainWorld('preset', preset) contextBridge.exposeInMainWorld('preset', preset)
contextBridge.exposeInMainWorld('task', task) contextBridge.exposeInMainWorld('task', task)
contextBridge.exposeInMainWorld('options', options)
contextBridge.exposeInMainWorld('darkMode', { contextBridge.exposeInMainWorld('darkMode', {
toggle: (value) => ipcRenderer.invoke('dark-mode:toggle', value) toggle: (value) => ipcRenderer.invoke('dark-mode:toggle', value)
}) })
@ -502,4 +504,5 @@ if (process.contextIsolated) {
window.preset = preset window.preset = preset
window.task = task window.task = task
window.translate = translate window.translate = translate
window.options = options
} }

22
src/preload/options.ts Normal file
View 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 }

View File

@ -2,14 +2,9 @@ import { ipcRenderer } from 'electron'
import { DEFINE_STRING } from '../define/define_string' import { DEFINE_STRING } from '../define/define_string'
const tts = { 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字幕 // 生成SRT字幕
GenerateSrt: async (text) => await ipcRenderer.invoke(DEFINE_STRING.TTS.GENERATE_SRT, text), GenerateSrt: async (text) => await ipcRenderer.invoke(DEFINE_STRING.TTS.GENERATE_SRT, text),

View File

@ -27,10 +27,15 @@ const write = {
//#region AI相关的任务 //#region AI相关的任务
// 开始执行API相关的一系列任务 /**
ActionStart(aiSetting, word) { * AI处理文案
return ipcRenderer.invoke(DEFINE_STRING.WRITE.ACTION_START, aiSetting, word) * @param ids ID
* @returns
*/
CopyWritingAIGeneration(ids: string[]) {
return ipcRenderer.invoke(DEFINE_STRING.WRITE.COPY_WRITING_AI_GENERATION, ids)
}, },
//#endregion //#endregion
//#region 文案洗稿相关 //#region 文案洗稿相关

View 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;

View 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

View 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;

View 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;

View File

@ -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', key: 'timeLimit',
width: 120 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() { function setHeight() {
let div = document.getElementById('import_word_and_srt') let div = document.getElementById('import_word_and_srt')
@ -210,7 +274,6 @@ export default defineComponent({
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`, style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
maskClosable: false, maskClosable: false,
onClose: async () => { onClose: async () => {
console.log(wenkeRef.value.word) console.log(wenkeRef.value.word)
let word = wenkeRef.value.data let word = wenkeRef.value.data
if (word == null || word == '') { if (word == null || word == '') {
@ -565,7 +628,6 @@ export default defineComponent({
}) })
} }
} else if (type.value == 'mj_reverse' || type.value == 'sd_reverse') { } else if (type.value == 'mj_reverse' || type.value == 'sd_reverse') {
if (data.value.length != reverseManageStore.selectBookTaskDetail.length) { if (data.value.length != reverseManageStore.selectBookTaskDetail.length) {
message.error('检测到导入的字幕数据和分镜对不上,请先对齐') message.error('检测到导入的字幕数据和分镜对不上,请先对齐')
return return

View File

@ -98,7 +98,6 @@ async function ResetBookData(e) {
async function DeleteBookData(e) { async function DeleteBookData(e) {
e.stopPropagation() e.stopPropagation()
message.info('删除小说数据 ' + book.value.id)
let da = dialog.warning({ let da = dialog.warning({
title: '删除小说数据警告', title: '删除小说数据警告',
content: content:
@ -107,7 +106,6 @@ async function DeleteBookData(e) {
negativeText: '取消', negativeText: '取消',
onPositiveClick: async () => { onPositiveClick: async () => {
da?.destroy() da?.destroy()
softwareStore.spin.spinning = true softwareStore.spin.spinning = true
softwareStore.spin.tip = '正在删除小说数据。。。' softwareStore.spin.tip = '正在删除小说数据。。。'
let res = await window.book.DeleteBookData(book.value.id) let res = await window.book.DeleteBookData(book.value.id)
@ -116,7 +114,6 @@ async function DeleteBookData(e) {
message.error(res.message) message.error(res.message)
return return
} }
// //
reverseManageStore.bookData = reverseManageStore.bookData.filter( reverseManageStore.bookData = reverseManageStore.bookData.filter(
(item) => item.id != book.value.id (item) => item.id != book.value.id

View File

@ -940,7 +940,9 @@ async function ImportWordAndSrtFunc() {
subValue: element.subValue ? element.subValue : [], subValue: element.subValue ? element.subValue : [],
name: element.name + '.png', name: element.name + '.png',
prompt: element.prompt, prompt: element.prompt,
timeLimit: element.timeLimit timeLimit: element.timeLimit,
start_time: element.startTime,
end_time: element.endTime
}) })
} }

View File

@ -134,7 +134,9 @@ async function ImportWord() {
subValue: element.subValue ? element.subValue : [], subValue: element.subValue ? element.subValue : [],
name: element.name + '.png', name: element.name + '.png',
prompt: element.prompt, prompt: element.prompt,
timeLimit: element.timeLimit timeLimit: element.timeLimit,
start_time: element.startTime,
end_time: element.endTime
}) })
} }

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -1,11 +1,11 @@
<template> <template>
<div style="min-width: 800px; overflow: auto"> <div style="min-width: 800px; overflow: auto">
<div style="width: 100%"> <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-form-item label="选择类型" path="gptType">
<n-select <n-select
style="width: 160px" style="width: 160px"
v-model:value="formValue.gptType" v-model:value="optionStore.CW_AISimpleSetting.gptType"
@update:value="UpdateSelectPromptType" @update:value="UpdateSelectPromptType"
:options="gptTypeOptions" :options="gptTypeOptions"
placeholder="请选择提示词类型" placeholder="请选择提示词类型"
@ -15,7 +15,7 @@
<n-form-item label="选择预设" path="gptData"> <n-form-item label="选择预设" path="gptData">
<n-select <n-select
style="width: 200px" style="width: 200px"
v-model:value="formValue.gptData" v-model:value="optionStore.CW_AISimpleSetting.gptData"
:options="gptDataOptions" :options="gptDataOptions"
placeholder="请选择提示词数据" placeholder="请选择提示词数据"
> >
@ -24,7 +24,7 @@
<n-form-item label="选择请求AI" path="gptAI"> <n-form-item label="选择请求AI" path="gptAI">
<n-select <n-select
style="width: 160px" style="width: 160px"
v-model:value="formValue.gptAI" v-model:value="optionStore.CW_AISimpleSetting.gptAI"
:options="gptOptions" :options="gptOptions"
placeholder="请选择AI" placeholder="请选择AI"
> >
@ -34,100 +34,80 @@
></n-button> ></n-button>
</n-form-item> </n-form-item>
<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-item>
</n-form> </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-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>
<n-form-item path="isSplit"> <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>
<n-form-item label="每次发送字符" path="splitNumber"> <n-form-item label="单次最大字符数" path="splitNumber">
<n-input-number <n-input-number
v-model:value="formValue.splitNumber" v-model:value="optionStore.CW_AISimpleSetting.splitNumber"
:min="1" :min="1"
:show-button="false" :show-button="false"
:max="99999" :max="99999"
></n-input-number> ></n-input-number>
</n-form-item> </n-form-item>
<n-form-item path="splitNumber"> <n-form-item path="splitNumber">
<div style="color: red">注意爆款开头不要发送</div> <div style="color: red; font-size: 20px">注意爆款开头不要分发送</div>
</n-form-item> </n-form-item>
</n-form> </n-form>
</div> </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> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch, h } from 'vue' import { ref, onMounted, onUnmounted, toRaw, h } from 'vue'
import { import {
useMessage, useMessage,
useDialog, useDialog,
NForm, NForm,
NFormItem, NFormItem,
NInput,
NSelect, NSelect,
NButton, NButton,
NIcon, NIcon,
NCheckbox, NCheckbox,
NInputNumber NInputNumber
} from 'naive-ui' } from 'naive-ui'
import { SettingsOutline } from '@vicons/ionicons5' import { Copy, SettingsOutline } from '@vicons/ionicons5'
import ManageAISetting from './ManageAISetting.vue' import ManageAISetting from './ManageAISetting.vue'
import { useSoftwareStore } from '../../../../stores/software' import { useSoftwareStore } from '../../../../stores/software'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { DEFINE_STRING } from '../../../../define/define_string' 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 message = useMessage()
let dialog = useDialog() 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 gptTypeOptions = ref([])
let gptDataOptions = ref([]) let gptDataOptions = ref([])
let gptAllData = undefined let gptAllData = undefined
let formRef = ref(null) let formRef = ref(null)
let gptOptions = ref([ let gptOptions = ref([{ label: 'LAI API', value: 'laiapi' }])
{ label: 'LAI API', value: 'laiapi' },
{ label: 'KIMI', value: 'kimi' }
])
let softwareStore = useSoftwareStore()
let allPromptDataOptions = ref([]) let allPromptDataOptions = ref([])
// /**
* 加载远程提示词数据包括提示词预设等
*/
async function InitServerGptOptions() { async function InitServerGptOptions() {
let gptRes = await window.gpt.InitServerGptOptions() let gptRes = await window.gpt.InitServerGptOptions()
if (gptRes.code == 0) { if (gptRes.code == 0) {
@ -168,7 +148,13 @@ function debounce(func, wait) {
} }
let UpdateWord = debounce((value) => { 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) }, 300)
onMounted(async () => { onMounted(async () => {
@ -183,46 +169,38 @@ onUnmounted(() => {
window.api.removeEventListen(DEFINE_STRING.GPT.GPT_STREAM_RETURN) window.api.removeEventListen(DEFINE_STRING.GPT.GPT_STREAM_RETURN)
}) })
let ruleObj = (errorMessage) => { async function handleCheckedChange(checked) {
return [ softwareStore.spin.spinning = true
{ softwareStore.spin.tip = '数据处理中......'
required: true, try {
validator(rule, value) { if (checked) {
if (value == null || value == '') return new Error(errorMessage) await CopyWriting.SplitOrMergeOldText(true)
return true } else {
}, await CopyWriting.SplitOrMergeOldText(false)
trigger: ['input', 'blur', 'change']
} }
] 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) */
}) async function SaveData() {
let isNumberedFormat = (str) => { let res = await window.options.ModifyOptionByKey(
return /^\d+\./.test(str) OptionKeyName.CW_AISimpleSetting,
} JSON.stringify(optionStore.CW_AISimpleSetting),
let isTextFormat = (str) => { OptionType.JOSN
return /^【文本】/.test(str) )
} if (res.code == 0) {
let type = undefined window.api.showGlobalMessageDialog(res)
return false
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') {
newWord.value = splitData.join('\n')
} else { } else {
newWord.value = splitData.join('\n\n') message.success('数据保存成功')
return true
} }
} }
@ -233,13 +211,12 @@ async function AISetting() {
// //
// //
let dialogWidth = 800 let dialogWidth = 800
let dialogHeight = 600
dialog.create({ dialog.create({
title: 'AI设置', title: 'AI设置',
showIcon: false, showIcon: false,
closeOnEsc: false, closeOnEsc: false,
content: () => h(ManageAISetting, { type: formValue.value.gptAI }), content: () => h(ManageAISetting, { type: optionStore.CW_AISimpleSetting.gptAI }),
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`, style: `width : ${dialogWidth}px`,
maskClosable: false, maskClosable: false,
onClose: async () => {} onClose: async () => {}
}) })
@ -249,25 +226,30 @@ async function AISetting() {
* 开始执行GPT * 开始执行GPT
*/ */
async function ActionStart() { async function ActionStart() {
// try {
if ( softwareStore.spin.spinning = true
isEmpty(formValue.value.gptType) || softwareStore.spin.tip = '生成中......'
isEmpty(formValue.value.gptData) || let ids = []
isEmpty(formValue.value.gptAI) optionStore.CW_AISimpleSetting.wordStruct.forEach((item) => {
) { ids.push(item.id)
message.error('请选择完整的数据') })
return let res = await window.write.CopyWritingAIGeneration(ids)
}
softwareStore.spin.spinning = true if (res.code == 0) {
softwareStore.spin.tip = '生成中......' message.error(res.message)
let res = await window.write.ActionStart(toRaw(formValue.value), oldWord.value) window.api.showGlobalMessageDialog(res)
softwareStore.spin.spinning = false softwareStore.spin.spinning = false
if (res.code == 0) { return
message.error(res.message) }
//
await CopyWriting.SaveCWAISimpleSetting()
window.api.showGlobalMessageDialog(res)
await TimeDelay(200)
} catch (error) {
message.error('生成失败:' + error.message)
} finally {
softwareStore.spin.spinning = false softwareStore.spin.spinning = false
return
} }
newWord.value = res.data
} }
/** /**

View File

@ -3,56 +3,22 @@
<n-card title="LAI API 设置"> <n-card title="LAI API 设置">
<div style="display: flex"> <div style="display: flex">
<n-input <n-input
v-model:value="aiSetting.laiapi.gpt_url" v-model:value="optionStore.CW_AISetting.laiapi.gpt_url"
style="margin-right: 10px" style="margin-right: 10px"
type="text" type="text"
placeholder="请输入GPT URL" placeholder="请输入GPT URL"
/> />
<n-input <n-input
v-model:value="aiSetting.laiapi.api_key" v-model:value="optionStore.CW_AISetting.laiapi.api_key"
style="margin-right: 10px" style="margin-right: 10px"
type="text" type="text"
placeholder="请输入API KEY" 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 <n-input
v-model:value="aiSetting.doubao.gpt_url" v-model:value="optionStore.CW_AISetting.laiapi.model"
type="text" type="text"
style="margin-right: 10px" placeholder="请输入Model"
placeholder="请输入豆包的调用网址"
/> />
<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> </div>
</n-card> </n-card>
<div style="display: flex; justify-content: flex-end; margin: 20px"> <div style="display: flex; justify-content: flex-end; margin: 20px">
@ -61,75 +27,46 @@
</n-space> </n-space>
</template> </template>
<script> <script setup>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch } from 'vue' import { useMessage, useDialog, NCard, NSpace, NInput, NSelect, NButton } from 'naive-ui'
import { useMessage, NCard, NSpace, NInput, NSelect, NButton } from 'naive-ui'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { useOptionStore } from '@/stores/option'
import { OptionKeyName, OptionType } from '@/define/enum/option'
let optionStore = useOptionStore()
export default defineComponent({ let message = useMessage()
components: { NCard, NSpace, NInput, NSelect, NButton }, let dialog = useDialog()
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 }
})
onMounted(async () => { async function SaveAISetting() {
let da = dialog.warning({
// AIsetting title: '提示',
let res = await window.gpt.GetAISetting() content: '确认保存AI设置这边不会检测数据的可用性请确保数据填写正确',
if (res.code == 0) { positiveText: '确认',
message.error(res.message) negativeText: '取消',
return onNegativeClick: () => {
} message.info('用户取消操作')
aiSetting.value = res.data
})
function checkValue(obj) {
for (const d in obj) {
if (isEmpty(d)) {
return false
}
}
return true return true
} },
onPositiveClick: async () => {
async function SaveAISetting() { da.destroy()
// AI //
let checkRes = true let aiSetting = optionStore.CW_AISetting.laiapi
if (props.type == 'laiapi') { if (isEmpty(aiSetting.gpt_url) || isEmpty(aiSetting.api_key) || isEmpty(aiSetting.model)) {
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) {
message.error('请填写完整选择的AI相关的设置') message.error('请填写完整选择的AI相关的设置')
return 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) { if (res.code == 0) {
message.error(res.message) window.api.showGlobalMessageDialog(res)
return } else {
message.success('保存AI设置成功')
} }
message.success('保存成功')
} }
})
return { aiSetting, SaveAISetting } }
}
})
</script> </script>

View File

@ -112,15 +112,16 @@
v-if="formValue.gpt_business == 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65'" v-if="formValue.gpt_business == 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65'"
style="width: 120px; margin-left: 30px" style="width: 120px; margin-left: 30px"
path="gpt_key" path="gpt_key"
label="LAI 站点选择" label="是否国内转发"
> >
<n-select <n-select
v-model:value="formValue.laiApiSelect" v-model:value="formValue.useTransfer"
placeholder="选择LAIAPI的请求站点" :options="[
style="width: 200px" { label: '是', value: true },
:options="laiApiOptions" { label: '否', value: false }
> ]"
</n-select> style="width: 100px"
/>
</n-form-item> </n-form-item>
<n-form-item style="margin-left: 30px" path="gpt_model" label="GPT模型"> <n-form-item style="margin-left: 30px" path="gpt_model" label="GPT模型">
<n-select <n-select
@ -225,6 +226,7 @@ let formValue = ref({
character_select_model: window.config.character_select_model, character_select_model: window.config.character_select_model,
window_wh_bm_remember: window.config.window_wh_bm_remember, window_wh_bm_remember: window.config.window_wh_bm_remember,
laiApiSelect: window.config.laiApiSelect ? window.config.laiApiSelect : LaiAPIType.MAIN, laiApiSelect: window.config.laiApiSelect ? window.config.laiApiSelect : LaiAPIType.MAIN,
useTransfer: window.config.useTransfer ?? false,
hdScale: window.config.hdScale ?? 2, hdScale: window.config.hdScale ?? 2,
defaultImageMode: window.config.defaultImageMode ?? 'mj', defaultImageMode: window.config.defaultImageMode ?? 'mj',
defaultVideoMode: window.config.defaultVideoMode ?? ImageToVideoModels.RUNWAY defaultVideoMode: window.config.defaultVideoMode ?? ImageToVideoModels.RUNWAY

View File

@ -2,53 +2,56 @@
<div> <div>
<n-form <n-form
label-placement="left" label-placement="left"
label-width="auto" label-width="100"
require-mark-placement="right-hanging" require-mark-placement="right-hanging"
:model="settingStore.ttsSetting.edgeTTS" :model="optionStore.TTS_GlobalSetting.selectModel"
> >
<n-form-item label="合成角色"> <n-form-item label="合成角色">
<n-select <n-select
@update:value="ChangeCharacter" @update:value="ChangeCharacter"
v-model:value="settingStore.ttsSetting.edgeTTS.value" v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.value"
:options="roleOptions" :options="roleOptions"
/> />
</n-form-item> </n-form-item>
<n-form-item label="音量"> <n-form-item label="音量">
<n-input-number <n-input-number
v-model:value="settingStore.ttsSetting.edgeTTS.volumn" v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.volumn"
:min="0" :min="-100"
:max="100" :max="100"
/> />
</n-form-item> </n-form-item>
<n-form-item label="语速"> <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>
<n-form-item label="语调"> <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-item>
</n-form> </n-form>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, defineComponent, watch, inject } from 'vue' import { ref, onMounted } from 'vue'
import { import { useMessage, NForm, NFormItem, NInputNumber, NSelect } from 'naive-ui'
useMessage, import { useOptionStore } from '@/stores/option'
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'
let message = useMessage() let message = useMessage()
let settingStore = useSettingStore() let optionStore = useOptionStore()
async function SwitchTTSOptions(key) { async function SwitchTTSOptions(key) {
console.log('SwitchTTSOptions', key) console.log('SwitchTTSOptions', key)
@ -73,9 +76,9 @@ onMounted(async () => {
*/ */
function ChangeCharacter(value) { function ChangeCharacter(value) {
let role = roleOptions.value.find((item) => item.value === value) let role = roleOptions.value.find((item) => item.value === value)
settingStore.ttsSetting.edgeTTS.value = role.value optionStore.TTS_GlobalSetting.edgeTTS.value = role.value
settingStore.ttsSetting.edgeTTS.label = role.label optionStore.TTS_GlobalSetting.edgeTTS.label = role.label
settingStore.ttsSetting.edgeTTS.lang = role.lang optionStore.TTS_GlobalSetting.edgeTTS.lang = role.lang
settingStore.ttsSetting.edgeTTS.gender = role.gender optionStore.TTS_GlobalSetting.edgeTTS.gender = role.gender
} }
</script> </script>

View File

@ -1,66 +1,28 @@
<template> <template>
<div style="display: flex; min-width: 900px; overflow: auto"> <div style="display: flex; min-width: 900px; overflow: auto">
<div class="text-input"> <TTSTextInput />
<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>
<div class="audio-setting"> <div class="audio-setting">
<div class="param-setting"> <div class="param-setting">
<n-form label-placement="left"> <n-form label-placement="left">
<n-form-item label="选择配音渠道"> <n-form-item label="选择配音渠道">
<n-select <n-select
placeholder="请选择配音渠道" placeholder="请选择配音渠道"
v-model:value="settingStore.ttsSetting.selectModel" v-model:value="optionStore.TTS_GlobalSetting.selectModel"
:options="ttsOptions" :options="ttsOptions"
></n-select> ></n-select>
</n-form-item> </n-form-item>
</n-form> </n-form>
<EdgeTTS v-if="settingStore.ttsSetting.selectModel == 'edge-tts'" /> <EdgeTTS v-if="optionStore.TTS_GlobalSetting.selectModel == 'edge-tts'" />
<AzureTTS <!-- <AzureTTS
:azureTTS="settingStore.ttsSetting.azureTTS" :azureTTS="settingStore.ttsSetting.azureTTS"
v-else-if="settingStore.ttsSetting.selectModel == 'azure-tts'" v-else-if="optionStore.TTS_GlobalSetting.selectModel == 'azure-tts'"
/> /> -->
</div> </div>
<div class="autio-button"> <div class="autio-button">
<n-button <n-button
:color="softwareStore.SoftColor.BROWN_YELLOW" :color="softwareStore.SoftColor.BROWN_YELLOW"
style="margin-right: 10px" style="margin-right: 10px"
@click="SaveTTSConfig" @click="SaveTTSGlobalSetting"
> >
保存配置信息 保存配置信息
</n-button> </n-button>
@ -80,17 +42,21 @@
</n-button> </n-button>
</div> </div>
<div style="display: flex; align-items: center"> <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> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, reactive, h } from 'vue' import { ref, onMounted, toRaw, h } from 'vue'
import { import {
useMessage, useMessage,
NInput,
NSelect, NSelect,
NFormItem, NFormItem,
NForm, NForm,
@ -101,155 +67,92 @@ import {
} from 'naive-ui' } from 'naive-ui'
import EdgeTTS from './EdgeTTS.vue' import EdgeTTS from './EdgeTTS.vue'
import AzureTTS from './AzureTTS.vue' import AzureTTS from './AzureTTS.vue'
import { GetTTSSelect } from '../../../../define/tts/ttsDefine'
import { useSoftwareStore } from '../../../../stores/software' import { useSoftwareStore } from '../../../../stores/software'
import { AddCircleOutline } from '@vicons/ionicons5'
import InputDialogContent from '../Original/Components/InputDialogContent.vue'
import { useSettingStore } from '../../../../stores/setting' import { useSettingStore } from '../../../../stores/setting'
import TTSHistory from './TTSHistory.vue' 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 message = useMessage()
let dialog = useDialog() let dialog = useDialog()
let softwareStore = useSoftwareStore() let softwareStore = useSoftwareStore()
let text = ref('你好,我是你的智能语音助手')
let ttsOptions = ref([ let ttsOptions = ref([
{ {
label: 'EdgeTTS免费', label: 'EdgeTTS免费',
value: 'edge-tts' value: 'edge-tts'
} }
]) ])
let splitRef = ref(null)
let settingStore = useSettingStore() let settingStore = useSettingStore()
let writeSetting = ref({
split_char: '。,“”‘’!?【】《》()…—:;.,\'\'""!?[]<>()...-:;',
merge_count: 3,
merge_char: '',
end_char: '。'
})
let audioUrl = ref(null)
onMounted(async () => { onMounted(async () => {
softwareStore.spin.spinning = true await InitTTSGlobalSetting()
softwareStore.spin.tip = '正在加载,请稍等...' })
try {
// TTSTTS
let res = await window.tts.GetTTSCOnfig()
if (res.code == 0) {
message.error(res.message)
} else {
settingStore.ttsSetting = res.data
}
// /**
let writeSettingRes = await window.write.GetWriteCOnfig() * 初始化TTS配置信息
if (writeSettingRes.code == 0) { */
message.error(writeSettingRes.message) async function InitTTSGlobalSetting() {
} else { try {
writeSetting.value = writeSettingRes.data softwareStore.spin.spinning = true
} softwareStore.spin.tip = '正在加载数据,请稍等...'
await InitCommon.InitTTSGlobalSetting()
await TimeDelay(300)
} catch (error) { } catch (error) {
message.error('加载失败,失败原因:' + error.toString()) window.api.showGlobalMessageDialog({
code: 0,
message: '加载或者是初始化TTS信息失败失败原因' + error.toString()
})
} finally { } finally {
softwareStore.spin.spinning = false 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 { try {
let wordSrr = FormatWord(text.value) await TTSCommon.SaveTTSGlobalSetting()
text.value = wordSrr.join('\n') message.success('保存配置信息成功')
message.success('文本格式化成功')
} catch (error) { } 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() { async function GenerateAudio() {
if (text.value == '') { try {
message.error('文本内容不能为空') if (optionStore.TTS_GlobalSetting.ttsText == '') {
return message.error('文本内容不能为空')
} return
}
softwareStore.spin.spinning = true
softwareStore.spin.tip = '正在合成音频,请稍等...'
softwareStore.spin.spinning = true //
softwareStore.spin.tip = '正在合成音频,请稍等...' await TTSCommon.SaveTTSGlobalSetting()
// //
let saveRes = await window.tts.SaveTTSConfig(toRaw(settingStore.ttsSetting)) let generateRes = await window.tts.GenerateAudio()
if (saveRes.code == 0) { if (generateRes.code == 1) {
optionStore.TTS_GlobalSetting.saveAudioPath = generateRes.data.mp3Path
//
await TTSCommon.SaveTTSGlobalSetting()
}
window.api.showGlobalMessageDialog(generateRes)
} catch (error) {
message.error('合成音频失败,失败原因:' + error.toString())
} finally {
softwareStore.spin.spinning = false softwareStore.spin.spinning = false
message.error(saveRes.message)
return
} }
//
let generateRes = await window.tts.GenerateAudio(text.value)
if (generateRes.code == 0) {
softwareStore.spin.spinning = false
message.error(generateRes.message)
return
}
audioUrl.value = generateRes.mp3Path
softwareStore.spin.spinning = false
} }
// //
@ -273,19 +176,8 @@ function ShowHistory() {
margin-bottom: 10px; margin-bottom: 10px;
display: flex; display: flex;
} }
.text-input {
flex: 4;
height: 100%;
margin-right: 10px;
}
.audio-setting { .audio-setting {
flex: 2; flex: 2;
margin: 0 20px; margin: 0 20px;
} }
.tts-options {
margin-top: 10px;
margin-right: 0;
display: flex;
align-items: center;
}
</style> </style>

View 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>

View File

@ -16,7 +16,7 @@ const routes = [
{ {
path: '/gptCopywriting', path: '/gptCopywriting',
name: 'gptCopywriting', name: 'gptCopywriting',
component: () => import('./components/CopyWriting/CopyWriting.vue') component: () => import('./components/CopyWriting/CopyWritingHome.vue')
}, },
{ {
path: '/global_setting', path: '/global_setting',

74
src/stores/option.ts Normal file
View 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: {
}
});