V 3.2.3
1.优化文案处理逻辑,重构界面 2.修复批量导出草稿只能导出一个的bug 3.添加自动 推理人物 场景 方便快速生成标签 4.(聚合推文) 修复删除数据bug 5.新增推理国内转发接口(包括翻译) 6.新增文案导入时导入SRT后可手动校验一遍时间数据,简化简单过程 7.语音服务那边添加字符不生效,格式化不生效 8.优化语音服务(数据结构优化,可设置合成超时时间)
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
+93
-111
@@ -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
|
||||
|
||||
@@ -2,53 +2,56 @@
|
||||
<div>
|
||||
<n-form
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
label-width="100"
|
||||
require-mark-placement="right-hanging"
|
||||
:model="settingStore.ttsSetting.edgeTTS"
|
||||
:model="optionStore.TTS_GlobalSetting.selectModel"
|
||||
>
|
||||
<n-form-item label="合成角色">
|
||||
<n-select
|
||||
@update:value="ChangeCharacter"
|
||||
v-model:value="settingStore.ttsSetting.edgeTTS.value"
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.value"
|
||||
:options="roleOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="音量">
|
||||
<n-input-number
|
||||
v-model:value="settingStore.ttsSetting.edgeTTS.volumn"
|
||||
:min="0"
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.volumn"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="语速">
|
||||
<n-input-number v-model:value="settingStore.ttsSetting.edgeTTS.rate" :min="0" :max="100" />
|
||||
<n-input-number
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.rate"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="语调">
|
||||
<n-input-number v-model:value="settingStore.ttsSetting.edgeTTS.pitch" :min="0" :max="100" />
|
||||
<n-input-number
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.pitch"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="超时时间(ms)">
|
||||
<n-input-number
|
||||
v-model:value="optionStore.TTS_GlobalSetting.edgeTTS.timeOut"
|
||||
:min="0"
|
||||
:max="10000000"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, defineComponent, watch, inject } from 'vue'
|
||||
import {
|
||||
useMessage,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInputNumber,
|
||||
NSelect,
|
||||
NButton,
|
||||
NIcon,
|
||||
NPopover,
|
||||
NCheckbox
|
||||
} from 'naive-ui'
|
||||
import { GetEdgeTTSRole } from '../../../../define/tts/ttsDefine'
|
||||
import { ReaderOutline } from '@vicons/ionicons5'
|
||||
import { useSettingStore } from '../../../../stores/setting'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useMessage, NForm, NFormItem, NInputNumber, NSelect } from 'naive-ui'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
|
||||
let message = useMessage()
|
||||
let settingStore = useSettingStore()
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
async function SwitchTTSOptions(key) {
|
||||
console.log('SwitchTTSOptions', key)
|
||||
@@ -73,9 +76,9 @@ onMounted(async () => {
|
||||
*/
|
||||
function ChangeCharacter(value) {
|
||||
let role = roleOptions.value.find((item) => item.value === value)
|
||||
settingStore.ttsSetting.edgeTTS.value = role.value
|
||||
settingStore.ttsSetting.edgeTTS.label = role.label
|
||||
settingStore.ttsSetting.edgeTTS.lang = role.lang
|
||||
settingStore.ttsSetting.edgeTTS.gender = role.gender
|
||||
optionStore.TTS_GlobalSetting.edgeTTS.value = role.value
|
||||
optionStore.TTS_GlobalSetting.edgeTTS.label = role.label
|
||||
optionStore.TTS_GlobalSetting.edgeTTS.lang = role.lang
|
||||
optionStore.TTS_GlobalSetting.edgeTTS.gender = role.gender
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,66 +1,28 @@
|
||||
<template>
|
||||
<div style="display: flex; min-width: 900px; overflow: auto">
|
||||
<div class="text-input">
|
||||
<n-input
|
||||
v-model:value="text"
|
||||
type="textarea"
|
||||
placeholder="请输入配音的文本内容"
|
||||
:autosize="{
|
||||
minRows: 30,
|
||||
maxRows: 30
|
||||
}"
|
||||
show-count
|
||||
></n-input>
|
||||
<div class="tts-options">
|
||||
<n-button
|
||||
:color="softwareStore.SoftColor.BROWN_YELLOW"
|
||||
size="small"
|
||||
@click="FormatWordString"
|
||||
>
|
||||
格式化文档
|
||||
</n-button>
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button quaternary circle color="#b6a014" @click="ModifySplitChar">
|
||||
<template #icon>
|
||||
<n-icon size="25"> <AddCircleOutline /> </n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<span>添加分割标识符</span>
|
||||
</n-popover>
|
||||
<n-button
|
||||
style="margin-right: 10px"
|
||||
:color="softwareStore.SoftColor.BROWN_YELLOW"
|
||||
size="small"
|
||||
@click="ClearText"
|
||||
>
|
||||
清空内容
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<TTSTextInput />
|
||||
<div class="audio-setting">
|
||||
<div class="param-setting">
|
||||
<n-form label-placement="left">
|
||||
<n-form-item label="选择配音渠道">
|
||||
<n-select
|
||||
placeholder="请选择配音渠道"
|
||||
v-model:value="settingStore.ttsSetting.selectModel"
|
||||
v-model:value="optionStore.TTS_GlobalSetting.selectModel"
|
||||
:options="ttsOptions"
|
||||
></n-select>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<EdgeTTS v-if="settingStore.ttsSetting.selectModel == 'edge-tts'" />
|
||||
<AzureTTS
|
||||
<EdgeTTS v-if="optionStore.TTS_GlobalSetting.selectModel == 'edge-tts'" />
|
||||
<!-- <AzureTTS
|
||||
:azureTTS="settingStore.ttsSetting.azureTTS"
|
||||
v-else-if="settingStore.ttsSetting.selectModel == 'azure-tts'"
|
||||
/>
|
||||
v-else-if="optionStore.TTS_GlobalSetting.selectModel == 'azure-tts'"
|
||||
/> -->
|
||||
</div>
|
||||
<div class="autio-button">
|
||||
<n-button
|
||||
:color="softwareStore.SoftColor.BROWN_YELLOW"
|
||||
style="margin-right: 10px"
|
||||
@click="SaveTTSConfig"
|
||||
@click="SaveTTSGlobalSetting"
|
||||
>
|
||||
保存配置信息
|
||||
</n-button>
|
||||
@@ -80,17 +42,21 @@
|
||||
</n-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<audio ref="audio" :src="audioUrl" controls style="width: 100%"></audio>
|
||||
<audio
|
||||
ref="audio"
|
||||
:src="optionStore.TTS_GlobalSetting.saveAudioPath"
|
||||
controls
|
||||
style="width: 100%"
|
||||
></audio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, defineComponent, onUnmounted, toRaw, reactive, h } from 'vue'
|
||||
import { ref, onMounted, toRaw, h } from 'vue'
|
||||
import {
|
||||
useMessage,
|
||||
NInput,
|
||||
NSelect,
|
||||
NFormItem,
|
||||
NForm,
|
||||
@@ -101,155 +67,92 @@ import {
|
||||
} from 'naive-ui'
|
||||
import EdgeTTS from './EdgeTTS.vue'
|
||||
import AzureTTS from './AzureTTS.vue'
|
||||
import { GetTTSSelect } from '../../../../define/tts/ttsDefine'
|
||||
import { useSoftwareStore } from '../../../../stores/software'
|
||||
import { AddCircleOutline } from '@vicons/ionicons5'
|
||||
import InputDialogContent from '../Original/Components/InputDialogContent.vue'
|
||||
import { useSettingStore } from '../../../../stores/setting'
|
||||
import TTSHistory from './TTSHistory.vue'
|
||||
import { FormatWord } from '../../../../define/Tools/write'
|
||||
import TTSTextInput from './TTSTextInput.vue'
|
||||
import { useOptionStore } from '@/stores/option'
|
||||
import { TimeDelay } from '@/define/Tools/time'
|
||||
import InitCommon from '../../common/initCommon'
|
||||
import TTSCommon from '../../common/ttsCommon'
|
||||
let optionStore = useOptionStore()
|
||||
|
||||
let message = useMessage()
|
||||
let dialog = useDialog()
|
||||
|
||||
let softwareStore = useSoftwareStore()
|
||||
let text = ref('你好,我是你的智能语音助手')
|
||||
let ttsOptions = ref([
|
||||
{
|
||||
label: 'EdgeTTS(免费)',
|
||||
value: 'edge-tts'
|
||||
}
|
||||
])
|
||||
let splitRef = ref(null)
|
||||
let settingStore = useSettingStore()
|
||||
let writeSetting = ref({
|
||||
split_char: '。,“”‘’!?【】《》()…—:;.,\'\'""!?[]<>()...-:;',
|
||||
merge_count: 3,
|
||||
merge_char: ',',
|
||||
end_char: '。'
|
||||
})
|
||||
let audioUrl = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '正在加载,请稍等...'
|
||||
try {
|
||||
// 加载服务端的TTS配置(目前的TTS配置是全局的)
|
||||
let res = await window.tts.GetTTSCOnfig()
|
||||
if (res.code == 0) {
|
||||
message.error(res.message)
|
||||
} else {
|
||||
settingStore.ttsSetting = res.data
|
||||
}
|
||||
await InitTTSGlobalSetting()
|
||||
})
|
||||
|
||||
// 加载文字设置
|
||||
let writeSettingRes = await window.write.GetWriteCOnfig()
|
||||
if (writeSettingRes.code == 0) {
|
||||
message.error(writeSettingRes.message)
|
||||
} else {
|
||||
writeSetting.value = writeSettingRes.data
|
||||
}
|
||||
/**
|
||||
* 初始化TTS配置信息
|
||||
*/
|
||||
async function InitTTSGlobalSetting() {
|
||||
try {
|
||||
softwareStore.spin.spinning = true
|
||||
softwareStore.spin.tip = '正在加载数据,请稍等...'
|
||||
await InitCommon.InitTTSGlobalSetting()
|
||||
|
||||
await TimeDelay(300)
|
||||
} catch (error) {
|
||||
message.error('加载失败,失败原因:' + error.toString())
|
||||
window.api.showGlobalMessageDialog({
|
||||
code: 0,
|
||||
message: '加载或者是初始化TTS信息失败,失败原因:' + error.toString()
|
||||
})
|
||||
} finally {
|
||||
softwareStore.spin.spinning = false
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 修改分割符
|
||||
*/
|
||||
async function ModifySplitChar() {
|
||||
// 判断当前数据是不是存在
|
||||
// 处理数据。获取当前的所有的数据
|
||||
let dialogWidth = 400
|
||||
let dialogHeight = 150
|
||||
dialog.create({
|
||||
title: '添加分割符',
|
||||
showIcon: false,
|
||||
closeOnEsc: false,
|
||||
content: () =>
|
||||
h(InputDialogContent, {
|
||||
ref: splitRef,
|
||||
initData: writeSetting.value.split_char,
|
||||
placeholder: '请输入分割符'
|
||||
}),
|
||||
style: `width : ${dialogWidth}px; min-height : ${dialogHeight}px`,
|
||||
maskClosable: false,
|
||||
onClose: async () => {
|
||||
writeSetting.value.split_char = splitRef.value.data
|
||||
// 保存数据
|
||||
let saveRes = await window.write.SaveWriteConfig(toRaw(writeSetting.value))
|
||||
if (saveRes.code == 0) {
|
||||
message.error(saveRes.message)
|
||||
return
|
||||
}
|
||||
message.success('分隔符保存成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析/格式化文档
|
||||
* 调用方法保存全部配置信息
|
||||
*/
|
||||
async function FormatWordString() {
|
||||
async function SaveTTSGlobalSetting() {
|
||||
try {
|
||||
let wordSrr = FormatWord(text.value)
|
||||
text.value = wordSrr.join('\n')
|
||||
message.success('文本格式化成功')
|
||||
await TTSCommon.SaveTTSGlobalSetting()
|
||||
message.success('保存配置信息成功')
|
||||
} catch (error) {
|
||||
message.error('文本格式化失败,失败原因:' + error.toString())
|
||||
message.error('保存配置信息失败,失败原因:' + error.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文本内容
|
||||
*/
|
||||
async function ClearText() {
|
||||
text.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存TTS配置信息
|
||||
*/
|
||||
async function SaveTTSConfig() {
|
||||
let saveRes = await window.tts.SaveTTSConfig(toRaw(settingStore.ttsSetting))
|
||||
if (saveRes.code == 0) {
|
||||
message.error(saveRes.message)
|
||||
return
|
||||
}
|
||||
message.success('TTS配置保存成功')
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始合成音频
|
||||
*/
|
||||
async function GenerateAudio() {
|
||||
if (text.value == '') {
|
||||
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>
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user