1.优化文案处理逻辑,重构界面
2.修复批量导出草稿只能导出一个的bug
3.添加自动 推理人物 场景 方便快速生成标签
4.(聚合推文) 修复删除数据bug
5.新增推理国内转发接口(包括翻译)
6.新增文案导入时导入SRT后可手动校验一遍时间数据,简化简单过程
7.语音服务那边添加字符不生效,格式化不生效
8.优化语音服务(数据结构优化,可设置合成超时时间)
This commit is contained in:
2025-02-17 18:26:47 +08:00
parent 1ce665a3e5
commit b0eb7795e4
66 changed files with 2599 additions and 608 deletions
+72
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;
+97
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
+133
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;
+20
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;
@@ -61,7 +61,7 @@ export default defineComponent({
setup(props) {
let hh = props.height
let maxHeight = props.height - 80
let data = ref(JSON.parse(JSON.stringify(props.initData)))
const message = useMessage()
let dialog = useDialog()
@@ -163,13 +163,77 @@ export default defineComponent({
}
},
{
title: '时间范围',
title: () => {
return h('div', { style: 'display: flex; align-items: center' }, [
h('span', '时间范围'),
h(
NButton,
{
size: 'tiny',
style: 'margin-left: 4px',
strong: true,
secondary: true,
type: 'info',
onClick: () => {
CheckTimeLime()
}
},
{ default: () => '时间轴检查' }
)
])
},
key: 'timeLimit',
width: 120
}
]
}
/**
* 强制对齐字幕的时间轴
*/
function CheckTimeLime() {
dialog.create({
type: 'warning',
title: '警告',
showIcon: true,
content: '确定要检查时间轴吗?该操作会更具对应的字幕进行时间的强制对齐,是不是继续操作?',
style: `width : 400px;`,
maskClosable: false,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
// 检查时间轴逻辑
console.log('CheckTimeLime', data.value)
for (let i = 0; i < data.value.length; i++) {
const element = data.value[i]
if (!element.subValue || element.subValue.length <= 0) {
message.error(`${i + 1}行字幕为空,请检查`)
return
}
let startTime = element.subValue[0].start_time ?? 0
let endTime = element.subValue[element.subValue.length - 1].end_time ?? 0
if (endTime == 0) {
message.error(`${i + 1}行字幕结束时间为空,请检查`)
return
}
// 开始写入时间
data.value[i].start_time = startTime
data.value[i].end_time = endTime
data.value[i].timeLimit = `${startTime} -- ${endTime}`
}
// 提示
window.api.showGlobalMessageDialog({
code: 1,
message: '时间轴检查和改写完成,请手动保存!!!'
})
},
onNegativeClick: () => {
message.info('取消操作')
}
})
}
// 设置窗体的高度
function setHeight() {
let div = document.getElementById('import_word_and_srt')
@@ -210,7 +274,6 @@ export default defineComponent({
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
maskClosable: false,
onClose: async () => {
console.log(wenkeRef.value.word)
let word = wenkeRef.value.data
if (word == null || word == '') {
@@ -565,7 +628,6 @@ export default defineComponent({
})
}
} else if (type.value == 'mj_reverse' || type.value == 'sd_reverse') {
if (data.value.length != reverseManageStore.selectBookTaskDetail.length) {
message.error('检测到导入的字幕数据和分镜对不上,请先对齐')
return
@@ -98,7 +98,6 @@ async function ResetBookData(e) {
async function DeleteBookData(e) {
e.stopPropagation()
message.info('删除小说数据 ' + book.value.id)
let da = dialog.warning({
title: '删除小说数据警告',
content:
@@ -107,7 +106,6 @@ async function DeleteBookData(e) {
negativeText: '取消',
onPositiveClick: async () => {
da?.destroy()
softwareStore.spin.spinning = true
softwareStore.spin.tip = '正在删除小说数据。。。'
let res = await window.book.DeleteBookData(book.value.id)
@@ -116,7 +114,6 @@ async function DeleteBookData(e) {
message.error(res.message)
return
}
// 这边直接把数据删除了就行
reverseManageStore.bookData = reverseManageStore.bookData.filter(
(item) => item.id != book.value.id
@@ -940,7 +940,9 @@ async function ImportWordAndSrtFunc() {
subValue: element.subValue ? element.subValue : [],
name: element.name + '.png',
prompt: element.prompt,
timeLimit: element.timeLimit
timeLimit: element.timeLimit,
start_time: element.startTime,
end_time: element.endTime
})
}
@@ -134,7 +134,9 @@ async function ImportWord() {
subValue: element.subValue ? element.subValue : [],
name: element.name + '.png',
prompt: element.prompt,
timeLimit: element.timeLimit
timeLimit: element.timeLimit,
start_time: element.startTime,
end_time: element.endTime
})
}
@@ -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>
@@ -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>
@@ -0,0 +1,106 @@
<template>
<div class="copy-writing-home">
<CopyWritingSimpleSetting />
<CopyWritingContent />
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useSoftwareStore } from '@/stores/software'
import { TimeDelay } from '@/define/Tools/time'
import { useMessage } from 'naive-ui'
import { useOptionStore } from '@/stores/option'
import CopyWritingSimpleSetting from './CopyWritingSimpleSetting.vue'
import CopyWritingContent from './CopyWritingContent.vue'
import { OptionKeyName, OptionType } from '@/define/enum/option'
let softwareStore = useSoftwareStore()
let message = useMessage()
let optionStore = useOptionStore()
onMounted(async () => {
try {
softwareStore.spin.spinning = true
softwareStore.spin.tip = '数据加载中'
// 这边同步数据和加载文案处理AI设置数据
await InitCopyWritingAISetting()
// 初始化界面数据
await InitCopyWritingData()
await TimeDelay(1000)
} catch (error) {
window.api.showGlobalMessageDialog({
code: 0,
message: '数据加载失败,请切换左侧菜单重试,错误信息:' + error.message
})
} finally {
softwareStore.spin.spinning = false
}
})
/**
* 初始化文案处理AI设置
*/
async function InitCopyWritingAISetting() {
try {
let initRes = await window.options.InitCopyWritingAISetting()
if (initRes.code == 0) {
window.api.showGlobalMessageDialog(initRes)
return
}
optionStore.CW_AISetting = initRes.data
message.success('同步/初始化/加载 文案处理AI设置成功')
} catch (error) {
throw new Error('初始化文案处理AI设置失败,错误信息:' + error.message)
}
}
/**
* 初始化文案处理界面数据
*/
async function InitCopyWritingData() {
try {
let initRes = await window.options.GetOptionByKey(OptionKeyName.CW_AISimpleSetting)
if (initRes.code == 0) {
window.api.showGlobalMessageDialog(initRes)
return
}
console.log(initRes)
if (initRes.data == null) {
// 初始化数据
optionStore.CW_AISimpleSetting = {
gptType: undefined,
gptData: undefined,
gptAI: 'laiapi',
isStream: false,
isSplit: false,
splitNumber: 500,
oldWord: '',
newWord: '',
oldWordCount: 0,
newWordCount: 0,
wordStruct: []
}
// 保存到数据库
let saveRes = await window.options.ModifyOptionByKey(
OptionKeyName.CW_AISimpleSetting,
JSON.stringify(optionStore.CW_AISimpleSetting),
OptionType.JOSN
)
if (saveRes.code == 0) {
throw new Error('初始化文案处理界面数据失败,错误信息:' + saveRes.message)
} else {
message.success('未找到文案处理界面数据,已初始化数据')
return
}
} else {
optionStore.CW_AISimpleSetting = JSON.parse(initRes.data.value)
}
message.success('同步/初始化/加载 文案处理界面数据成功')
} catch (error) {
throw new Error('初始化文案处理界面数据失败,错误信息:' + error.message)
}
}
</script>
@@ -0,0 +1,88 @@
<template>
<div class="copy-writing-show">
<div>
<div style="display: flex; align-items: center; flex-direction: row; gap: 10px">
<n-button type="info" size="small" @click="CopyNewData"> 复制 </n-button>
<n-button type="info" size="small" @click="FormatOutput"> 一键格式化 </n-button>
<span style="font-size: 16px; color: red">
注意这边的格式化不一定会完全格式化需要自己手动检查
</span>
</div>
<n-input
type="textarea"
:autosize="{
minRows: 18,
maxRows: 18
}"
style="margin-top: 10px"
v-model:value="word"
placeholder="请输入内容"
:rows="4"
/>
</div>
<div style="font-size: 20px; color: red">
注意当前弹窗的修改和格式化只在改界面有效关闭或重新打开会重新加载同步外部表格数据之前修改的数据会试下
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { NButton, NInput, useMessage } from 'naive-ui'
import { useOptionStore } from '@/stores/option'
import { isEmpty } from 'lodash'
let optionStore = useOptionStore()
let word = ref('')
let message = useMessage()
onMounted(() => {
word.value = optionStore.CW_AISimpleSetting.wordStruct.map((item) => item.newWord).join('\n')
})
/**
* 复制新数据
*/
async function CopyNewData() {
try {
let copyData = word.value
await navigator.clipboard.writeText(copyData)
message.success('复制成功')
} catch (error) {
message.error('复制失败,错误信息:' + error.message)
}
}
/**
* 格式化输出
*/
async function FormatOutput() {
let splitData = word.value.split('\n').filter((item) => {
return !isEmpty(item)
})
let isNumberedFormat = (str) => {
return /^\d+\./.test(str)
}
let isTextFormat = (str) => {
return /^【文本】/.test(str)
}
let type = undefined
splitData = splitData.map((item) => {
if (isNumberedFormat(item)) {
type = 'startNumber'
return item.replace(/^\d+\./, '')
} else if (isTextFormat(item)) {
type = 'startText'
return item.replace('&【', '\n【')
} else {
return item
}
})
if (type == 'startNumber') {
word.value = splitData.join('\n')
} else {
word.value = splitData.join('\n\n')
}
}
</script>
@@ -1,11 +1,11 @@
<template>
<div style="min-width: 800px; overflow: auto">
<div style="width: 100%">
<n-form ref="formRef" :model="formValue" inline label-placement="left">
<n-form ref="formRef" :model="optionStore.CW_AISimpleSetting" inline label-placement="left">
<n-form-item label="选择类型" path="gptType">
<n-select
style="width: 160px"
v-model:value="formValue.gptType"
v-model:value="optionStore.CW_AISimpleSetting.gptType"
@update:value="UpdateSelectPromptType"
:options="gptTypeOptions"
placeholder="请选择提示词类型"
@@ -15,7 +15,7 @@
<n-form-item label="选择预设" path="gptData">
<n-select
style="width: 200px"
v-model:value="formValue.gptData"
v-model:value="optionStore.CW_AISimpleSetting.gptData"
:options="gptDataOptions"
placeholder="请选择提示词数据"
>
@@ -24,7 +24,7 @@
<n-form-item label="选择请求AI" path="gptAI">
<n-select
style="width: 160px"
v-model:value="formValue.gptAI"
v-model:value="optionStore.CW_AISimpleSetting.gptAI"
:options="gptOptions"
placeholder="请选择AI"
>
@@ -34,100 +34,80 @@
></n-button>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="ActionStart">开始生成</n-button>
<n-button type="primary" @click="SaveData">保存数据</n-button>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="ActionStart">开始 AI 生成</n-button>
</n-form-item>
</n-form>
<n-form :model="formValue" inline label-placement="left">
<n-form :model="optionStore.CW_AISimpleSetting" inline label-placement="left">
<n-form-item path="isStream">
<n-checkbox label="是否流式发送" v-model:checked="formValue.isStream" />
<n-checkbox
label="是否流式发送"
v-model:checked="optionStore.CW_AISimpleSetting.isStream"
/>
</n-form-item>
<n-form-item path="isSplit">
<n-checkbox label="是否拆分发送" v-model:checked="formValue.isSplit" />
<n-checkbox
label="是否拆分发送"
v-model:checked="optionStore.CW_AISimpleSetting.isSplit"
@update:checked="handleCheckedChange"
/>
</n-form-item>
<n-form-item label="每次发送字符" path="splitNumber">
<n-form-item label="单次最大字符" path="splitNumber">
<n-input-number
v-model:value="formValue.splitNumber"
v-model:value="optionStore.CW_AISimpleSetting.splitNumber"
:min="1"
:show-button="false"
:max="99999"
></n-input-number>
</n-form-item>
<n-form-item path="splitNumber">
<div style="color: red">注意爆款开头不要分发送</div>
<div style="color: red; font-size: 20px">注意爆款开头不要分发送</div>
</n-form-item>
</n-form>
</div>
<div style="display: flex; width: 100%">
<div style="flex: 1; margin-right: 10px">
<n-input
type="textarea"
:autosize="{ minRows: 30, maxRows: 30 }"
v-model:value="oldWord"
placeholder="请输入内容"
/>
</div>
<div style="flex: 1">
<n-input
type="textarea"
:autosize="{ minRows: 30, maxRows: 30 }"
v-model:value="newWord"
placeholder="请输入内容"
/>
</div>
</div>
<div style="display: flex; justify-content: flex-end; align-items: center; margin-right: 20px">
<n-button type="primary" @click="FormatOutput()"> 格式化 </n-button>
<div style="color: red; margin-left: 5px">
注意由于GPT输出的格式化有太多的不确定不一定可以完全格式需要手动检查
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch, h } from 'vue'
import { ref, onMounted, onUnmounted, toRaw, h } from 'vue'
import {
useMessage,
useDialog,
NForm,
NFormItem,
NInput,
NSelect,
NButton,
NIcon,
NCheckbox,
NInputNumber
} from 'naive-ui'
import { SettingsOutline } from '@vicons/ionicons5'
import { Copy, SettingsOutline } from '@vicons/ionicons5'
import ManageAISetting from './ManageAISetting.vue'
import { useSoftwareStore } from '../../../../stores/software'
import { isEmpty } from 'lodash'
import { DEFINE_STRING } from '../../../../define/define_string'
import { useOptionStore } from '@/stores/option'
import { OptionKeyName, OptionType } from '@/define/enum/option'
import CopyWriting from '../../common/copyWriting'
import { TimeDelay } from '@/define/Tools/time'
let optionStore = useOptionStore()
let softwareStore = useSoftwareStore()
let message = useMessage()
let dialog = useDialog()
let formValue = ref({
gptType: undefined,
gptData: undefined,
gptAI: undefined,
isStream: true,
isSplit: false,
splitNumber: 1000
})
let oldWord = ref('')
let newWord = ref('')
let gptTypeOptions = ref([])
let gptDataOptions = ref([])
let gptAllData = undefined
let formRef = ref(null)
let gptOptions = ref([
{ label: 'LAI API', value: 'laiapi' },
{ label: 'KIMI', value: 'kimi' }
])
let softwareStore = useSoftwareStore()
let gptOptions = ref([{ label: 'LAI API', value: 'laiapi' }])
let allPromptDataOptions = ref([])
//
/**
* 加载远程提示词数据包括提示词预设等
*/
async function InitServerGptOptions() {
let gptRes = await window.gpt.InitServerGptOptions()
if (gptRes.code == 0) {
@@ -168,7 +148,13 @@ function debounce(func, wait) {
}
let UpdateWord = debounce((value) => {
newWord.value = value
debugger
let index = optionStore.CW_AISimpleSetting.wordStruct.findIndex((item) => item.id == value.id)
if (index == -1) {
return
}
optionStore.CW_AISimpleSetting.wordStruct[index].newWord = value.newWord
// newWord.value += value
}, 300)
onMounted(async () => {
@@ -183,46 +169,38 @@ onUnmounted(() => {
window.api.removeEventListen(DEFINE_STRING.GPT.GPT_STREAM_RETURN)
})
let ruleObj = (errorMessage) => {
return [
{
required: true,
validator(rule, value) {
if (value == null || value == '') return new Error(errorMessage)
return true
},
trigger: ['input', 'blur', 'change']
async function handleCheckedChange(checked) {
softwareStore.spin.spinning = true
softwareStore.spin.tip = '数据处理中......'
try {
if (checked) {
await CopyWriting.SplitOrMergeOldText(true)
} else {
await CopyWriting.SplitOrMergeOldText(false)
}
]
await TimeDelay(500)
} catch (error) {
message.error(error.message)
} finally {
softwareStore.spin.spinning = false
}
}
async function FormatOutput() {
let splitData = newWord.value.split('\n').filter((item) => {
return !isEmpty(item)
})
let isNumberedFormat = (str) => {
return /^\d+\./.test(str)
}
let isTextFormat = (str) => {
return /^【文本】/.test(str)
}
let type = undefined
splitData = splitData.map((item) => {
if (isNumberedFormat(item)) {
type = 'startNumber'
return item.replace(/^\d+\./, '')
} else if (isTextFormat(item)) {
type = 'startText'
return item.replace('&【', '\n【')
} else {
return item
}
})
if (type == 'startNumber') {
newWord.value = splitData.join('\n')
/**
* 保存数据
*/
async function SaveData() {
let res = await window.options.ModifyOptionByKey(
OptionKeyName.CW_AISimpleSetting,
JSON.stringify(optionStore.CW_AISimpleSetting),
OptionType.JOSN
)
if (res.code == 0) {
window.api.showGlobalMessageDialog(res)
return false
} else {
newWord.value = splitData.join('\n\n')
message.success('数据保存成功')
return true
}
}
@@ -233,13 +211,12 @@ async function AISetting() {
//
//
let dialogWidth = 800
let dialogHeight = 600
dialog.create({
title: 'AI设置',
showIcon: false,
closeOnEsc: false,
content: () => h(ManageAISetting, { type: formValue.value.gptAI }),
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
content: () => h(ManageAISetting, { type: optionStore.CW_AISimpleSetting.gptAI }),
style: `width : ${dialogWidth}px`,
maskClosable: false,
onClose: async () => {}
})
@@ -249,25 +226,30 @@ async function AISetting() {
* 开始执行GPT
*/
async function ActionStart() {
//
if (
isEmpty(formValue.value.gptType) ||
isEmpty(formValue.value.gptData) ||
isEmpty(formValue.value.gptAI)
) {
message.error('请选择完整的数据')
return
}
softwareStore.spin.spinning = true
softwareStore.spin.tip = '生成中......'
let res = await window.write.ActionStart(toRaw(formValue.value), oldWord.value)
softwareStore.spin.spinning = false
if (res.code == 0) {
message.error(res.message)
try {
softwareStore.spin.spinning = true
softwareStore.spin.tip = '生成中......'
let ids = []
optionStore.CW_AISimpleSetting.wordStruct.forEach((item) => {
ids.push(item.id)
})
let res = await window.write.CopyWritingAIGeneration(ids)
if (res.code == 0) {
message.error(res.message)
window.api.showGlobalMessageDialog(res)
softwareStore.spin.spinning = false
return
}
//
await CopyWriting.SaveCWAISimpleSetting()
window.api.showGlobalMessageDialog(res)
await TimeDelay(200)
} catch (error) {
message.error('生成失败:' + error.message)
} finally {
softwareStore.spin.spinning = false
return
}
newWord.value = res.data
}
/**
@@ -3,56 +3,22 @@
<n-card title="LAI API 设置">
<div style="display: flex">
<n-input
v-model:value="aiSetting.laiapi.gpt_url"
v-model:value="optionStore.CW_AISetting.laiapi.gpt_url"
style="margin-right: 10px"
type="text"
placeholder="请输入GPT URL"
/>
<n-input
v-model:value="aiSetting.laiapi.api_key"
v-model:value="optionStore.CW_AISetting.laiapi.api_key"
style="margin-right: 10px"
type="text"
placeholder="请输入API KEY"
/>
<n-input v-model:value="aiSetting.laiapi.model" type="text" placeholder="请输入Model" />
</div>
</n-card>
<!-- <n-card title="豆包设置">
<div style="display: flex">
<n-input
v-model:value="aiSetting.doubao.gpt_url"
v-model:value="optionStore.CW_AISetting.laiapi.model"
type="text"
style="margin-right: 10px"
placeholder="请输入豆包的调用网址"
placeholder="请输入Model"
/>
<n-input
v-model:value="aiSetting.doubao.api_key"
type="text"
style="margin-right: 10px"
placeholder="请输入豆包的APIKEY"
/>
<n-input
v-model:value="aiSetting.doubao.model"
type="text"
placeholder="请输入豆包的模型"
/>
</div>
</n-card> -->
<n-card title="KIMI设置">
<div style="display: flex">
<n-input
v-model:value="aiSetting.kimi.gpt_url"
type="text"
style="margin-right: 10px"
placeholder="请输入KIMI的网址"
/>
<n-input
v-model:value="aiSetting.kimi.api_key"
type="text"
style="margin-right: 10px"
placeholder="请输入KIMI的APIKEY"
/>
<n-input v-model:value="aiSetting.kimi.model" type="text" placeholder="请输入KIMI的模型" />
</div>
</n-card>
<div style="display: flex; justify-content: flex-end; margin: 20px">
@@ -61,75 +27,46 @@
</n-space>
</template>
<script>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch } from 'vue'
import { useMessage, NCard, NSpace, NInput, NSelect, NButton } from 'naive-ui'
<script setup>
import { useMessage, useDialog, NCard, NSpace, NInput, NSelect, NButton } from 'naive-ui'
import { isEmpty } from 'lodash'
import { useOptionStore } from '@/stores/option'
import { OptionKeyName, OptionType } from '@/define/enum/option'
let optionStore = useOptionStore()
export default defineComponent({
components: { NCard, NSpace, NInput, NSelect, NButton },
props: ['type'],
setup(props) {
let message = useMessage()
let aiSetting = ref({
laiapi: {
gpt_url: undefined,
api_key: undefined,
model: undefined
},
kimi: {
gpt_url: undefined,
api_key: undefined,
model: undefined
},
doubao: { gpt_url: undefined, api_key: undefined, model: undefined }
})
let message = useMessage()
let dialog = useDialog()
onMounted(async () => {
// 获取AIsetting
let res = await window.gpt.GetAISetting()
if (res.code == 0) {
message.error(res.message)
return
}
aiSetting.value = res.data
})
function checkValue(obj) {
for (const d in obj) {
if (isEmpty(d)) {
return false
}
}
async function SaveAISetting() {
let da = dialog.warning({
title: '提示',
content: '确认保存AI设置?这边不会检测数据的可用性,请确保数据填写正确!!!',
positiveText: '确认',
negativeText: '取消',
onNegativeClick: () => {
message.info('用户取消操作')
return true
}
async function SaveAISetting() {
// 判断当前是选择是哪个AI,判断是不是有填写
let checkRes = true
if (props.type == 'laiapi') {
checkRes = checkValue(Object.values(aiSetting.value.laiapi))
} else if (props.type == 'kimi') {
checkRes = checkValue(Object.values(aiSetting.value.kimi))
} else if (props.type == 'doubao') {
checkRes = checkValue(Object.values(aiSetting.value.doubao))
}
if (!checkRes) {
},
onPositiveClick: async () => {
da.destroy()
// 保存数据
let aiSetting = optionStore.CW_AISetting.laiapi
if (isEmpty(aiSetting.gpt_url) || isEmpty(aiSetting.api_key) || isEmpty(aiSetting.model)) {
message.error('请填写完整选择的AI相关的设置')
return
}
let res = await window.gpt.SaveAISetting(toRaw(aiSetting.value))
// 直接保存
let res = await window.options.ModifyOptionByKey(
OptionKeyName.CW_AISetting,
JSON.stringify(optionStore.CW_AISetting),
OptionType.JSON
)
if (res.code == 0) {
message.error(res.message)
return
window.api.showGlobalMessageDialog(res)
} else {
message.success('保存AI设置成功')
}
message.success('保存成功')
}
return { aiSetting, SaveAISetting }
}
})
})
}
</script>
@@ -112,15 +112,16 @@
v-if="formValue.gpt_business == 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65'"
style="width: 120px; margin-left: 30px"
path="gpt_key"
label="LAI 站点选择"
label="是否国内转发"
>
<n-select
v-model:value="formValue.laiApiSelect"
placeholder="选择LAIAPI的请求站点"
style="width: 200px"
:options="laiApiOptions"
>
</n-select>
v-model:value="formValue.useTransfer"
:options="[
{ label: '是', value: true },
{ label: '否', value: false }
]"
style="width: 100px"
/>
</n-form-item>
<n-form-item style="margin-left: 30px" path="gpt_model" label="GPT模型">
<n-select
@@ -225,6 +226,7 @@ let formValue = ref({
character_select_model: window.config.character_select_model,
window_wh_bm_remember: window.config.window_wh_bm_remember,
laiApiSelect: window.config.laiApiSelect ? window.config.laiApiSelect : LaiAPIType.MAIN,
useTransfer: window.config.useTransfer ?? false,
hdScale: window.config.hdScale ?? 2,
defaultImageMode: window.config.defaultImageMode ?? 'mj',
defaultVideoMode: window.config.defaultVideoMode ?? ImageToVideoModels.RUNWAY
+30 -27
View File
@@ -2,53 +2,56 @@
<div>
<n-form
label-placement="left"
label-width="auto"
label-width="100"
require-mark-placement="right-hanging"
:model="settingStore.ttsSetting.edgeTTS"
:model="optionStore.TTS_GlobalSetting.selectModel"
>
<n-form-item label="合成角色">
<n-select
@update:value="ChangeCharacter"
v-model:value="settingStore.ttsSetting.edgeTTS.value"
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.value"
:options="roleOptions"
/>
</n-form-item>
<n-form-item label="音量">
<n-input-number
v-model:value="settingStore.ttsSetting.edgeTTS.volumn"
:min="0"
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.volumn"
:min="-100"
:max="100"
/>
</n-form-item>
<n-form-item label="语速">
<n-input-number v-model:value="settingStore.ttsSetting.edgeTTS.rate" :min="0" :max="100" />
<n-input-number
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.rate"
:min="-100"
:max="100"
/>
</n-form-item>
<n-form-item label="语调">
<n-input-number v-model:value="settingStore.ttsSetting.edgeTTS.pitch" :min="0" :max="100" />
<n-input-number
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.pitch"
:min="-100"
:max="100"
/>
</n-form-item>
<n-form-item label="超时时间(ms)">
<n-input-number
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.timeOut"
:min="0"
:max="10000000"
/>
</n-form-item>
</n-form>
</div>
</template>
<script setup>
import { ref, onMounted, defineComponent, watch, inject } from 'vue'
import {
useMessage,
NForm,
NFormItem,
NInputNumber,
NSelect,
NButton,
NIcon,
NPopover,
NCheckbox
} from 'naive-ui'
import { GetEdgeTTSRole } from '../../../../define/tts/ttsDefine'
import { ReaderOutline } from '@vicons/ionicons5'
import { useSettingStore } from '../../../../stores/setting'
import { ref, onMounted } from 'vue'
import { useMessage, NForm, NFormItem, NInputNumber, NSelect } from 'naive-ui'
import { useOptionStore } from '@/stores/option'
let message = useMessage()
let settingStore = useSettingStore()
let optionStore = useOptionStore()
async function SwitchTTSOptions(key) {
console.log('SwitchTTSOptions', key)
@@ -73,9 +76,9 @@ onMounted(async () => {
*/
function ChangeCharacter(value) {
let role = roleOptions.value.find((item) => item.value === value)
settingStore.ttsSetting.edgeTTS.value = role.value
settingStore.ttsSetting.edgeTTS.label = role.label
settingStore.ttsSetting.edgeTTS.lang = role.lang
settingStore.ttsSetting.edgeTTS.gender = role.gender
optionStore.TTS_GlobalSetting.edgeTTS.value = role.value
optionStore.TTS_GlobalSetting.edgeTTS.label = role.label
optionStore.TTS_GlobalSetting.edgeTTS.lang = role.lang
optionStore.TTS_GlobalSetting.edgeTTS.gender = role.gender
}
</script>
+62 -170
View File
@@ -1,66 +1,28 @@
<template>
<div style="display: flex; min-width: 900px; overflow: auto">
<div class="text-input">
<n-input
v-model:value="text"
type="textarea"
placeholder="请输入配音的文本内容"
:autosize="{
minRows: 30,
maxRows: 30
}"
show-count
></n-input>
<div class="tts-options">
<n-button
:color="softwareStore.SoftColor.BROWN_YELLOW"
size="small"
@click="FormatWordString"
>
格式化文档
</n-button>
<n-popover trigger="hover">
<template #trigger>
<n-button quaternary circle color="#b6a014" @click="ModifySplitChar">
<template #icon>
<n-icon size="25"> <AddCircleOutline /> </n-icon>
</template>
</n-button>
</template>
<span>添加分割标识符</span>
</n-popover>
<n-button
style="margin-right: 10px"
:color="softwareStore.SoftColor.BROWN_YELLOW"
size="small"
@click="ClearText"
>
清空内容
</n-button>
</div>
</div>
<TTSTextInput />
<div class="audio-setting">
<div class="param-setting">
<n-form label-placement="left">
<n-form-item label="选择配音渠道">
<n-select
placeholder="请选择配音渠道"
v-model:value="settingStore.ttsSetting.selectModel"
v-model:value="optionStore.TTS_GlobalSetting.selectModel"
:options="ttsOptions"
></n-select>
</n-form-item>
</n-form>
<EdgeTTS v-if="settingStore.ttsSetting.selectModel == 'edge-tts'" />
<AzureTTS
<EdgeTTS v-if="optionStore.TTS_GlobalSetting.selectModel == 'edge-tts'" />
<!-- <AzureTTS
:azureTTS="settingStore.ttsSetting.azureTTS"
v-else-if="settingStore.ttsSetting.selectModel == 'azure-tts'"
/>
v-else-if="optionStore.TTS_GlobalSetting.selectModel == 'azure-tts'"
/> -->
</div>
<div class="autio-button">
<n-button
:color="softwareStore.SoftColor.BROWN_YELLOW"
style="margin-right: 10px"
@click="SaveTTSConfig"
@click="SaveTTSGlobalSetting"
>
保存配置信息
</n-button>
@@ -80,17 +42,21 @@
</n-button>
</div>
<div style="display: flex; align-items: center">
<audio ref="audio" :src="audioUrl" controls style="width: 100%"></audio>
<audio
ref="audio"
:src="optionStore.TTS_GlobalSetting.saveAudioPath"
controls
style="width: 100%"
></audio>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, reactive, h } from 'vue'
import { ref, onMounted, toRaw, h } from 'vue'
import {
useMessage,
NInput,
NSelect,
NFormItem,
NForm,
@@ -101,155 +67,92 @@ import {
} from 'naive-ui'
import EdgeTTS from './EdgeTTS.vue'
import AzureTTS from './AzureTTS.vue'
import { GetTTSSelect } from '../../../../define/tts/ttsDefine'
import { useSoftwareStore } from '../../../../stores/software'
import { AddCircleOutline } from '@vicons/ionicons5'
import InputDialogContent from '../Original/Components/InputDialogContent.vue'
import { useSettingStore } from '../../../../stores/setting'
import TTSHistory from './TTSHistory.vue'
import { FormatWord } from '../../../../define/Tools/write'
import TTSTextInput from './TTSTextInput.vue'
import { useOptionStore } from '@/stores/option'
import { TimeDelay } from '@/define/Tools/time'
import InitCommon from '../../common/initCommon'
import TTSCommon from '../../common/ttsCommon'
let optionStore = useOptionStore()
let message = useMessage()
let dialog = useDialog()
let softwareStore = useSoftwareStore()
let text = ref('你好,我是你的智能语音助手')
let ttsOptions = ref([
{
label: 'EdgeTTS(免费)',
value: 'edge-tts'
}
])
let splitRef = ref(null)
let settingStore = useSettingStore()
let writeSetting = ref({
split_char: '。,“”‘’!?【】《》()…—:;.,\'\'""!?[]<>()...-:;',
merge_count: 3,
merge_char: '',
end_char: '。'
})
let audioUrl = ref(null)
onMounted(async () => {
softwareStore.spin.spinning = true
softwareStore.spin.tip = '正在加载,请稍等...'
try {
// 加载服务端的TTS配置(目前的TTS配置是全局的)
let res = await window.tts.GetTTSCOnfig()
if (res.code == 0) {
message.error(res.message)
} else {
settingStore.ttsSetting = res.data
}
await InitTTSGlobalSetting()
})
// 加载文字设置
let writeSettingRes = await window.write.GetWriteCOnfig()
if (writeSettingRes.code == 0) {
message.error(writeSettingRes.message)
} else {
writeSetting.value = writeSettingRes.data
}
/**
* 初始化TTS配置信息
*/
async function InitTTSGlobalSetting() {
try {
softwareStore.spin.spinning = true
softwareStore.spin.tip = '正在加载数据,请稍等...'
await InitCommon.InitTTSGlobalSetting()
await TimeDelay(300)
} catch (error) {
message.error('加载失败,失败原因:' + error.toString())
window.api.showGlobalMessageDialog({
code: 0,
message: '加载或者是初始化TTS信息失败,失败原因:' + error.toString()
})
} finally {
softwareStore.spin.spinning = false
}
})
/**
* 修改分割符
*/
async function ModifySplitChar() {
// 判断当前数据是不是存在
// 处理数据。获取当前的所有的数据
let dialogWidth = 400
let dialogHeight = 150
dialog.create({
title: '添加分割符',
showIcon: false,
closeOnEsc: false,
content: () =>
h(InputDialogContent, {
ref: splitRef,
initData: writeSetting.value.split_char,
placeholder: '请输入分割符'
}),
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
maskClosable: false,
onClose: async () => {
writeSetting.value.split_char = splitRef.value.data
// 保存数据
let saveRes = await window.write.SaveWriteConfig(toRaw(writeSetting.value))
if (saveRes.code == 0) {
message.error(saveRes.message)
return
}
message.success('分隔符保存成功')
}
})
}
/**
* 解析/格式化文档
* 调用方法保存全部配置信息
*/
async function FormatWordString() {
async function SaveTTSGlobalSetting() {
try {
let wordSrr = FormatWord(text.value)
text.value = wordSrr.join('\n')
message.success('文本格式化成功')
await TTSCommon.SaveTTSGlobalSetting()
message.success('保存配置信息成功')
} catch (error) {
message.error('文本格式化失败,失败原因:' + error.toString())
message.error('保存配置信息失败,失败原因:' + error.toString())
}
}
/**
* 删除文本内容
*/
async function ClearText() {
text.value = ''
}
/**
* 保存TTS配置信息
*/
async function SaveTTSConfig() {
let saveRes = await window.tts.SaveTTSConfig(toRaw(settingStore.ttsSetting))
if (saveRes.code == 0) {
message.error(saveRes.message)
return
}
message.success('TTS配置保存成功')
}
/**
* 开始合成音频
*/
async function GenerateAudio() {
if (text.value == '') {
message.error('文本内容不能为空')
return
}
try {
if (optionStore.TTS_GlobalSetting.ttsText == '') {
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))
if (saveRes.code == 0) {
// 开始真正的合成音频
let generateRes = await window.tts.GenerateAudio()
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
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;
display: flex;
}
.text-input {
flex: 4;
height: 100%;
margin-right: 10px;
}
.audio-setting {
flex: 2;
margin: 0 20px;
}
.tts-options {
margin-top: 10px;
margin-right: 0;
display: flex;
align-items: center;
}
</style>
@@ -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>
+1 -1
View File
@@ -16,7 +16,7 @@ const routes = [
{
path: '/gptCopywriting',
name: 'gptCopywriting',
component: () => import('./components/CopyWriting/CopyWriting.vue')
component: () => import('./components/CopyWriting/CopyWritingHome.vue')
},
{
path: '/global_setting',