添加文案处理的功能

This commit is contained in:
2024-07-13 15:44:13 +08:00
parent 669e57824d
commit c8a46d59fb
80 changed files with 7383 additions and 2613 deletions
+10 -3
View File
@@ -6,7 +6,10 @@
<n-message-provider>
<n-dialog-provider>
<n-notification-provider>
<RouterView></RouterView>
<n-spin :show="softwareStore.spin.spinning">
<RouterView></RouterView>
<template #description> {{ softwareStore.spin.tip }} </template>
</n-spin>
</n-notification-provider>
</n-dialog-provider>
</n-message-provider>
@@ -22,9 +25,11 @@ import {
NDialogProvider,
NConfigProvider,
darkTheme,
NNotificationProvider
NNotificationProvider,
NSpin
} from 'naive-ui'
import { useSoftwareStore } from '../../stores/software'
import { SoftColor } from '../../define/enum/softwareEnum'
hljs.registerLanguage('javascript', javascript)
export default defineComponent({
@@ -32,12 +37,14 @@ export default defineComponent({
NConfigProvider,
NDialogProvider,
NMessageProvider,
NNotificationProvider
NNotificationProvider,
NSpin
},
setup() {
let softwareStore = useSoftwareStore()
onMounted(async () => {
softwareStore.SoftColor = SoftColor
window.api.getSettingDafultData(async (value) => {
await window.darkMode.toggle(value.theme)
})
@@ -0,0 +1,194 @@
<template>
<div style="min-width: 800px; overflow: auto">
<div style="width: 100%">
<n-form ref="formRef" :model="formValue" inline label-placement="left">
<n-form-item label="选择类型" path="gptType">
<n-select
style="width: 160px"
v-model:value="formValue.gptType"
:options="gptTypeOptions"
placeholder="请选择提示词类型"
>
</n-select>
</n-form-item>
<n-form-item label="选择预设" path="gptData">
<n-select
style="width: 160px"
v-model:value="formValue.gptData"
:options="gptDataOptions"
placeholder="请选择提示词数据"
>
</n-select>
</n-form-item>
<n-form-item label="选择请求AI" path="gptAI">
<n-select
style="width: 160px"
v-model:value="formValue.gptAI"
:options="gptOptions"
placeholder="请选择AI"
>
</n-select>
<n-button type="text" style="font-size: 20px" @click="AISetting">
<n-icon> <SettingsOutline /> </n-icon
></n-button>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="ActionStart">开始生成</n-button>
</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>
</template>
<script>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch, h } from 'vue'
import { useMessage, useDialog, NForm, NFormItem, NInput, NSelect, NButton, NIcon } from 'naive-ui'
import { SettingsOutline } from '@vicons/ionicons5'
import ManageAISetting from './ManageAISetting.vue'
import { useSoftwareStore } from '../../../../stores/software'
import { isEmpty } from 'lodash'
export default defineComponent({
components: { NForm, NFormItem, NInput, NSelect, NButton, NIcon, SettingsOutline },
setup() {
let message = useMessage()
let dialog = useDialog()
let formValue = ref({
gptType: undefined,
gptData: undefined,
gptAI: undefined
})
let oldWord = ref(undefined)
let newWord = ref(undefined)
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' },
{ label: 'DouBao', value: 'doubao' }
])
let softwareStore = useSoftwareStore()
// 加载服务端数据
async function InitServerGptOptions() {
let gptRes = await window.gpt.InitServerGptOptions()
if (gptRes.code == 0) {
message.error(gptRes.message)
return
}
gptAllData = gptRes.data
// 处理数据
gptTypeOptions.value = gptRes.data.promptType.map((item) => {
return { label: item.name, value: item.id }
})
gptDataOptions.value = gptRes.data.promptData.map((item) => {
return { label: item.name, value: item.id }
})
}
onMounted(async () => {
await InitServerGptOptions()
})
let ruleObj = (errorMessage) => {
return [
{
required: true,
validator(rule, value) {
if (value == null || value == '') return new Error(errorMessage)
return true
},
trigger: ['input', 'blur', 'change']
}
]
}
let rules = {
gptAI: ruleObj('必选提示词类型'),
gptData: ruleObj('必选提示词预设'),
gptType: ruleObj('必填提示词AI')
}
/**
* AI设置相关
*/
async function AISetting() {
debugger
// 判断当前数据是不是存在
// 处理数据。获取当前的所有的数据
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`,
maskClosable: false,
onClose: async () => {}
})
}
/**
* 开始执行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)
softwareStore.spin.spinning = false
return
}
newWord.value = res.data
}
return {
formValue,
gptTypeOptions,
gptDataOptions,
oldWord,
newWord,
rules,
formRef,
AISetting,
gptOptions,
softwareStore,
ActionStart
}
}
})
</script>
@@ -0,0 +1,135 @@
<template>
<n-space vertical>
<n-card title="LAI API 设置">
<div style="display: flex">
<n-input
v-model:value="aiSetting.laiapi.gpt_url"
style="margin-right: 10px"
type="text"
placeholder="请输入GPT URL"
/>
<n-input
v-model:value="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"
type="text"
style="margin-right: 10px"
placeholder="请输入豆包的调用网址"
/>
<n-input
v-model:value="aiSetting.doubao.api_key"
type="text"
style="margin-right: 10px"
placeholder="请输入豆包的APIKEY"
/>
<n-input
v-model:value="aiSetting.doubao.model"
type="text"
placeholder="请输入豆包的模型"
/>
</div>
</n-card>
<n-card title="KIMI设置">
<div style="display: flex">
<n-input
v-model:value="aiSetting.kimi.gpt_url"
type="text"
style="margin-right: 10px"
placeholder="请输入KIMI的网址"
/>
<n-input
v-model:value="aiSetting.kimi.api_key"
type="text"
style="margin-right: 10px"
placeholder="请输入KIMI的APIKEY"
/>
<n-input v-model:value="aiSetting.kimi.model" type="text" placeholder="请输入KIMI的模型" />
</div>
</n-card>
<div style="display: flex; justify-content: flex-end; margin: 20px">
<n-button type="primary" @click="SaveAISetting">保存</n-button>
</div>
</n-space>
</template>
<script>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch } from 'vue'
import { useMessage, NCard, NSpace, NInput, NSelect, NButton } from 'naive-ui'
import { isEmpty } from 'lodash'
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 }
})
onMounted(async () => {
debugger
// 获取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
}
}
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) {
message.error('请填写完整选择的AI相关的设置')
return
}
let res = await window.gpt.SaveAISetting(toRaw(aiSetting.value))
if (res.code == 0) {
message.error(res.message)
return
}
message.success('保存成功')
}
return { aiSetting, SaveAISetting }
}
})
</script>
+50 -1
View File
@@ -50,7 +50,9 @@ import {
PaperPlaneOutline,
SettingsOutline,
DuplicateOutline,
GridOutline
GridOutline,
RadioOutline,
BookOutline
} from '@vicons/ionicons5'
import CheckMachineId from '../Components/CheckMachineId.vue'
import axios from 'axios'
@@ -85,10 +87,12 @@ export default defineComponent({
if (option.key === 'food') return null
if (option.key == 'sdoriginal') return h(NIcon, null, { default: () => h(PaperPlaneOutline) })
if (option.key == 'setting') return h(NIcon, null, { default: () => h(SettingsOutline) })
if (option.key == 'gptCopywriting') return h(NIcon, null, { default: () => h(BookOutline) })
if (option.key == 'reverse_management')
return h(NIcon, null, { default: () => h(GridOutline) })
if (option.key == 'backward_matrix')
return h(NIcon, null, { default: () => h(DuplicateOutline) })
if (option.key == 'TTS_Services') return h(NIcon, null, { default: () => h(RadioOutline) })
}
onMounted(async () => {
@@ -230,6 +234,21 @@ export default defineComponent({
}
const menuOptions = [
{
label: () =>
h(
RouterLink,
{
to: {
name: 'gptCopywriting'
}
},
{
default: () => '文案处理'
}
),
key: 'gptCopywriting'
},
{
label: () =>
h(
@@ -434,6 +453,36 @@ export default defineComponent({
}
]
}
// {
// label: () =>
// h(
// RouterLink,
// {
// to: {
// name: 'test_options'
// }
// },
// {
// default: () => '测试操作'
// }
// ),
// key: 'test_options'
// },
// {
// label: () =>
// h(
// RouterLink,
// {
// to: {
// name: 'TTS_Services'
// }
// },
// {
// default: () => '语音服务'
// }
// ),
// key: 'TTS_Services'
// }
]
return {
@@ -4,6 +4,9 @@
<n-button color="#ee7959" style="margin-left: 10px" @click="GetCharacter" size="small"
>角色分析标签集</n-button
>
<n-button color="#ee7959" style="margin-left: 10px" @click="ImportPrompt" size="small"
>导入提示词</n-button
>
<n-dropdown trigger="hover" :options="GptButtonOptions" @select="ButtonSelect">
<n-button color="#ee7959" style="margin-left: 10px" @click="GPTPrompt" size="small"
>一键推理提示词</n-button
@@ -338,11 +341,60 @@ export default defineComponent({
await ResetImage()
}
// 导入提示词
async function ImportPrompt() {
// 弹窗选择文件
window.api.SelectFile(['txt'], async (value) => {
if (value.code == 0) {
message.error(value.message)
return
}
debugger
console.log(value.value)
// 拿到提示词文件地址,然后还是获取
await window.pmpt.OpenPromptFileTxt(value.value, (res) => {
debugger
console.log(res)
if (res.code == 0) {
message.error(res.message)
return
}
// 修改数据
// 判断获取的数据是不是小于当前的data的行
if (res.data.length > data.value.length) {
dialog.warning({
title: '提示',
content: '导入的数据行数大于当前的数据行数,多余的数据会被删除,是否继续导入?',
positiveText: '继续',
negativeText: '取消',
onPositiveClick: async () => {
debugger
// 开始导入
for (let i = 0; i < data.value.length && i < res.data.length; i++) {
const element = res.data[i]
// 开始修改
data.value[i].gpt_prompt = element
}
}
})
} else {
// 开始导入
for (let i = 0; i < data.value.length && i < res.data.length; i++) {
const element = res.data[i]
// 开始修改
data.value[i].gpt_prompt = element
}
}
})
})
}
return {
ImportWord,
data,
GetCharacter,
tagTreeData,
ImportPrompt,
GPTPrompt,
OpenPromptSetting,
GenerateImageAll,
@@ -34,7 +34,7 @@ export default defineComponent({
// 自动开始
async function AutoAction() {
// 开始自动执行。先分镜,后面在添加任务中
let res_frame = await window.book.GetFrameData(reverseManageStore.selectBook.id)
let res_frame = await window.book.AutoAction(reverseManageStore.selectBook.id)
}
// 选择自动开始的操作(停止或者是继续)
@@ -0,0 +1,18 @@
<template>
<div>AzureTTS</div>
</template>
<script>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch } from 'vue'
import { useMessage } from 'naive-ui'
export default defineComponent({
components: {},
setup() {
onMounted(async () => {})
return {}
}
})
</script>
@@ -0,0 +1,79 @@
<template>
<div>
<n-form
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
:model="edgeTTs"
>
<n-form-item label="合成角色">
<n-select v-model:value="edgeTTs.value" :options="roleOptions" />
</n-form-item>
<n-form-item label="音量">
<n-input-number v-model:value="edgeTTs.volumn" :min="0" :max="100" />
</n-form-item>
<n-form-item label="语速">
<n-input-number v-model:value="edgeTTs.rate" :min="0" :max="100" />
</n-form-item>
<n-form-item label="语调">
<n-input-number v-model:value="edgeTTs.pitch" :min="0" :max="100" />
</n-form-item>
<n-form-item label="生成SRT">
<n-checkbox v-model:checked="edgeTTs.saveSubtitles"> 复选框 </n-checkbox>
</n-form-item>
</n-form>
</div>
</template>
<script>
import { ref, onMounted, defineComponent, watch } 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 { useSoftwareStore } from '../../../../stores/software'
export default defineComponent({
components: {
NForm,
NCheckbox,
NFormItem,
NInputNumber,
NSelect,
NButton,
NIcon,
ReaderOutline,
NPopover
},
props: ['edgeTTs'],
setup(props) {
let edgeTTs = ref(props.edgeTTs)
let message = useMessage()
let softwareStore = useSoftwareStore()
watch(
() => props.edgeTTs,
(val) => {
edgeTTs.value = val
}
)
let roleOptions = ref([])
onMounted(async () => {
// 获取配音角色列表
roleOptions.value = GetEdgeTTSRole()
})
return { edgeTTs, roleOptions, softwareStore }
}
})
</script>
+320
View File
@@ -0,0 +1,320 @@
<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="FormatWord">
格式化文档
</n-button>
<n-popover trigger="hover">
<template #trigger>
<n-button quaternary circle color="#b6a014" @click="ModifySplitChar">
<template #icon>
<n-icon size="25"> <AddCircleOutline /> </n-icon>
</template>
</n-button>
</template>
<span>添加分割标识符</span>
</n-popover>
<n-button
style="margin-right: 10px"
:color="softwareStore.SoftColor.BROWN_YELLOW"
size="small"
@click="ClearText"
>
清空内容
</n-button>
</div>
</div>
<div class="audio-setting">
<div class="param-setting">
<n-form label-placement="left">
<n-form-item label="选择配音渠道">
<n-select
placeholder="请选择配音渠道"
v-model:value="ttsConfig.selectModel"
:options="ttsOptions"
></n-select>
</n-form-item>
</n-form>
<EdgeTTS
:edgeTTs="ttsConfig.edgeTTS"
ref="edgettsRef"
v-if="ttsConfig.selectModel == 'edge-tts'"
/>
<AzureTTS
:azureTTS="ttsConfig.azureTTS"
ref="azurettsRef"
v-else-if="ttsConfig.selectModel == 'azure-tts'"
/>
</div>
<div class="autio-button">
<n-button
:color="softwareStore.SoftColor.BROWN_YELLOW"
style="margin-right: 10px"
@click="SaveTTSConfig"
>
保存配置信息
</n-button>
<n-button
:color="softwareStore.SoftColor.BROWN_YELLOW"
@click="GenerateAudio"
style="margin-right: 10px"
>
开始合成
</n-button>
<n-button
style="margin-right: 10px"
:color="softwareStore.SoftColor.BROWN_YELLOW"
@click="ShowHistory"
>
查看配音历史
</n-button>
</div>
<div style="display: flex; align-items: center">
<audio
ref="audio"
src="D:\\3.大力\\1\\1719165587206_6cd74afd-ff56-4ba7-abc9-9021a70ac9c7.wav"
controls
style="width: 100%"
></audio>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch, h } from 'vue'
import {
useMessage,
NInput,
NSelect,
NFormItem,
NForm,
NButton,
NPopover,
NIcon,
useDialog
} 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'
export default defineComponent({
components: {
NInput,
NSelect,
NFormItem,
NForm,
EdgeTTS,
AzureTTS,
NButton,
NPopover,
AddCircleOutline,
NIcon,
InputDialogContent
},
setup() {
let message = useMessage()
let dialog = useDialog()
let softwareStore = useSoftwareStore()
let text = ref('你好,我是你的智能语音助手')
let ttsOptions = ref([])
let edgettsRef = ref(null)
let azurettsRef = ref(null)
let splitRef = ref(null)
let ttsConfig = ref({
selectModel: 'edge-tts',
edgeTTS: {
name: 'zh-CN-XiaoxiaoNeural',
gender: 'Female',
label: '晓晓',
lang: 'zh-CN',
pitch: '0', // 语调
rate: '10', // 倍速
volumn: '0' // 音量
}
})
let writeSetting = ref({
split_char: '。,“”‘’!?【】《》()…—:;.,\'\'""!?[]<>()...-:;',
merge_count: 3,
merge_char: '',
end_char: '。'
})
onMounted(async () => {
ttsOptions.value = GetTTSSelect()
// 加载服务端的TTS配置(目前的TTS配置是全局的)
let res = await window.tts.GetTTSCOnfig()
if (res.code == 0) {
message.error(res.message)
} else {
ttsConfig.value = res.data
}
// 加载文字设置
let writeSettingRes = await window.write.GetWriteCOnfig()
if (writeSettingRes.code == 0) {
message.error(writeSettingRes.message)
} else {
writeSetting.value = writeSettingRes.data
}
})
/**
* 修改分割符
*/
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 FormatWord() {
let split_arr = Array.from(writeSetting.value.split_char)
split_arr.forEach((item) => {
let specialCharacters = [
'.',
'*',
'?',
'+',
'^',
'$',
'[',
']',
'(',
')',
'{',
'}',
'|',
'\\'
]
let regex
if (specialCharacters.includes(item)) {
regex = new RegExp('\\' + item, 'g')
} else {
regex = new RegExp(item, 'g')
}
text.value = text.value.replace(regex, '\n')
})
// 删除空行
let word_arr = text.value.split('\n')
word_arr = word_arr.filter((item) => item != '' && item != null)
text.value = word_arr.join('\n')
}
/**
* 删除文本内容
*/
async function ClearText() {
text.value = ''
}
/**
* 保存TTS配置信息
*/
async function SaveTTSConfig() {}
/**
* 开始合成音频
*/
async function GenerateAudio() {
if (text.value == '') {
message.error('文本内容不能为空')
return
}
if (ttsConfig.value.selectModel == 'edge-tts') {
// let res =
} else if (ttsConfig.value.selectModel == 'azure-tts') {
}
}
/**
* 保存配置信息
*/
async function SaveTTSConfig() {}
return {
text,
azurettsRef,
edgettsRef,
ttsConfig,
writeSetting,
softwareStore,
ModifySplitChar,
SaveTTSConfig,
FormatWord,
ClearText,
splitRef,
GenerateAudio,
ttsOptions: [{ label: 'Edge TTS(免费)', value: 'edge-tts' }]
}
}
})
</script>
<style scoped>
.autio-button {
margin-top: 10px;
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,423 @@
<template>
<div id="video-canvas" style="position: relative">
<video ref="videoRef" muted style="width: 100%" :src="videoSrc" id="video" autoplay></video>
<canvas
ref="canvasRef"
@mousemove="handleMouseMove"
@mousedown="handleMouseDown"
@mouseup="handleMouseUp"
style="position: absolute; top: 0; left: 0; pointer-events: auto"
/>
<n-divider />
<div class="option-button">
<n-button @click="SetVideoMuted" :color="buttonColor">{{
isMuted ? '取消静音' : '开启静音'
}}</n-button>
<n-button
@click="SaveSelectPosition"
style="margin-left: 5px"
:loading="SaveSelectPositionLodding"
:color="softwareStore.SoftColor.BROWN_YELLOW"
>保存位置</n-button
>
<n-button
@click="OpenBookSubtitlePositionScreenshot"
style="margin-left: 5px"
:color="softwareStore.SoftColor.BROWN_YELLOW"
>查看截图</n-button
>
<n-button
@click="GetCurrentFrameText"
style="margin-left: 5px; width: 150px"
:color="softwareStore.SoftColor.BROWN_YELLOW"
:loading="GetCurrentFrameTextLodding"
>
{{ GetCurrentFrameTextLodding ? '提取文本中' : '提取保存帧' }}</n-button
>
<n-button
@click="GetVideoFrameText"
style="margin-left: 5px; width: 150px"
:color="softwareStore.SoftColor.BROWN_YELLOW"
:loading="GetVideoFrameTextLodding"
>
{{ GetVideoFrameTextLodding ? '视频文案提取中' : '提示视频文案' }}</n-button
>
<n-button
v-if="type == 'storyboard_video'"
@click="GetAllImageText"
style="margin-left: 5px"
:color="softwareStore.SoftColor.BROWN_YELLOW"
>提取所有</n-button
>
</div>
<div class="output-text" style="margin-top: 10px">
<n-input
type="textarea"
placeholder="识别到的字幕文本(可能有错,需要手动看一下)"
:autosize="{
minRows: 5,
maxRows: 7
}"
v-model:value="frameText"
>
</n-input>
</div>
</div>
</template>
<script>
import { ref, onMounted, defineComponent, onUnmounted, computed, toRaw } from 'vue'
import { NDivider, NButton, useMessage, NInput, useDialog } from 'naive-ui'
import { useSoftwareStore } from '../../../../stores/software'
import { useReverseManageStore } from '../../../../stores/reverseManage'
import { SubtitleSavePositionType } from '../../../../define/enum/waterMarkAndSubtitle'
export default defineComponent({
components: {
NDivider,
NButton,
NInput
},
props: ['videoSrc', 'mark', 'videoWidth', 'type', 'height'],
setup(props) {
let message = useMessage()
let dialog = useDialog()
let softwareStore = useSoftwareStore()
let reverseManageStore = useReverseManageStore()
let videoRef = ref(null)
let canvasRef = ref(null)
let videoSrc = ref(
props.videoSrc ||
'D:\\文\\物价暴跌百万倍,我成了神豪-七猫\\1\\价暴跌百万倍,我成了神豪1_output_crop_00001.mp4'
)
let type = ref(props.type ? props.type : SubtitleSavePositionType.MAIN_VIDEO)
let mark = ref(props.mark)
let videoWidth = ref(props.videoWidth == null ? 800 : props.videoWidth)
let drawing = ref(false)
let moving = ref(false)
let startPos = ref(null)
let rectPos = ref(null)
let offset = ref({ x: 0, y: 0 })
let isPlaying = ref(false)
let buttonColor = ref(softwareStore.SoftColor.BROWN_YELLOW)
let isMuted = ref(false)
let frameText = ref('')
let GetCurrentFrameTextLodding = ref(false)
let SaveSelectPositionLodding = ref(false)
let GetVideoFrameTextLodding = ref(false)
onMounted(() => {
window.addEventListener('resize', updateVideoPosition)
updateVideoPosition() // 初始更新
// resizeObserver.observe(videoRef.value)
const video = videoRef.value
video.onloadedmetadata = () => {
let { width, height } = video.getBoundingClientRect()
canvasRef.value.width = width
canvasRef.value.height = height
canvasRef.value.style.width = `${width}px`
canvasRef.value.style.height = `${height}px`
}
let videoCanvas = document.getElementById('video-canvas')
videoCanvas.style.width = `${videoWidth.value}px`
videoCanvas.style.height = `${props.height}px`
videoCanvas.style.overflow = 'scroll'
buttonColor.value = videoRef.value.muted
? softwareStore.SoftColor.ERROR_RED
: softwareStore.SoftColor.ERROR_RED
isMuted.value = videoRef.value.muted
})
onUnmounted(() => {
window.removeEventListener('resize', updateVideoPosition)
})
/**
* 获取当前鼠标在canvas中的相对位置
* @param e
*/
function getCanvasRelativePosition(e) {
const rect = canvasRef.value.getBoundingClientRect()
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
/**
* canvas绘制矩形
* @param rect
*/
const drawRectangle = (rect) => {
const ctx = canvasRef.value.getContext('2d')
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height) // 清除之前的绘制
ctx.strokeStyle = 'red' // 设置矩形颜色
ctx.lineWidth = 2 // 设置线宽
ctx.strokeRect(rect.startX, rect.startY, rect.width, rect.height)
}
/**
* 鼠标再canvas中移动事件
* @param e
*/
function handleMouseMove(e) {
if (drawing.value && startPos.value) {
videoRef.value.pause()
isPlaying.value = false
const currentPos = getCanvasRelativePosition(e)
const rect = {
startX: Math.min(startPos.value.x, currentPos.x),
startY: Math.min(startPos.value.y, currentPos.y),
width: Math.abs(startPos.value.x - currentPos.x),
height: Math.abs(startPos.value.y - currentPos.y)
}
drawRectangle(rect)
} else if (moving.value && rectPos.value) {
videoRef.value.pause()
isPlaying.value = false
const currentPos = getCanvasRelativePosition(e)
const newRectPos = {
startX: currentPos.x - offset.value.x,
startY: currentPos.y - offset.value.y,
width: rectPos.value.width,
height: rectPos.value.height,
videoWidth: canvasRef.value.width,
videoHeight: canvasRef.value.height
}
rectPos.value = newRectPos
drawRectangle(newRectPos)
}
}
/**
* 鼠标再canvas中按下事件
* @param e
*/
function handleMouseDown(e) {
const pos = getCanvasRelativePosition(e)
if (
rectPos.value &&
pos.x >= rectPos.value.startX &&
pos.x <= rectPos.value.startX + rectPos.value.width &&
pos.y >= rectPos.value.startY &&
pos.y <= rectPos.value.startY + rectPos.value.height
) {
moving.value = true
offset.value = { x: pos.x - rectPos.value.startX, y: pos.y - rectPos.value.startY }
} else {
startPos.value = pos
drawing.value = true
}
if (videoRef.value.paused) {
videoRef.value.play()
isPlaying.value = true
} else {
videoRef.value.pause()
isPlaying.value = false
}
}
/**
* 鼠标再canvas中抬起事件
* @param e
*/
function handleMouseUp(e) {
if (drawing.value) {
const pos = getCanvasRelativePosition(e)
rectPos.value = {
startX: Math.min(startPos.value.x, pos.x),
startY: Math.min(startPos.value.y, pos.y),
width: Math.abs(startPos.value.x - pos.x),
height: Math.abs(startPos.value.y - pos.y),
videoWidth: canvasRef.value.width,
videoHeight: canvasRef.value.height
}
drawRectangle(rectPos.value)
startPos.value = null
drawing.value = false
}
if (moving.value) {
moving.value = false
}
}
/**
* 监听video的resize事件
*/
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect
if (canvasRef.value) {
canvasRef.value.width = width
canvasRef.value.height = height
canvasRef.value.style.width = `${width}px`
canvasRef.value.style.height = `${height}px`
}
}
})
/**
* 更新canvas的位置
*/
const updateVideoPosition = () => {
if (videoRef.value && videoRef.value.parentElement) {
const videoRect = videoRef.value.getBoundingClientRect()
const parentRect = videoRef.value.parentElement.getBoundingClientRect()
const relativeTop = videoRect.top - parentRect.top
const relativeLeft = videoRect.left - parentRect.left
const { width, height } = videoRect
if (canvasRef.value) {
canvasRef.value.style.top = `${relativeTop}px`
canvasRef.value.style.left = `${relativeLeft}px`
canvasRef.value.style.width = `${width}px`
canvasRef.value.style.height = `${height}px`
}
}
}
/**
* 设置静音
*/
function SetVideoMuted() {
videoRef.value.muted = !videoRef.value.muted
isMuted.value = videoRef.value.muted
if (videoRef.value.muted) {
buttonColor.value = softwareStore.SoftColor.ERROR_RED
} else {
buttonColor.value = softwareStore.SoftColor.BROWN_YELLOW
}
}
/**
* 保存当前选中的所有的位置(目前只支持单个)
*/
async function SaveSelectPosition() {
if (rectPos.value == null) {
message.error('请先选择一个区域')
return
}
SaveSelectPositionLodding.value = true
message.success(JSON.stringify(rectPos.value))
// 调用保存
let saveRes = await window.book.SaveBookSubtitlePosition({
id: reverseManageStore.selectBook.id
? reverseManageStore.selectBook.id
: '58053f68-da54-4a48-92fc-8a5c3cb75043',
bookSubtitlePosition: [toRaw(rectPos.value)],
currentTime: videoRef.value.currentTime,
type: type.value
})
if (saveRes.code == 0) {
message.error('保存字幕位置并截取示例图片失败')
SaveSelectPositionLodding.value = false
return
}
message.success('保存字幕位置并截取示例图片成功')
SaveSelectPositionLodding.value = false
}
/**
* 查看字幕位置示例截图
*/
async function OpenBookSubtitlePositionScreenshot() {
if (rectPos.value == null) {
message.error('请先选择一个区域')
return
}
let res = await window.book.OpenBookSubtitlePositionScreenshot({
id: reverseManageStore.selectBook.id
? reverseManageStore.selectBook.id
: '58053f68-da54-4a48-92fc-8a5c3cb75043',
type: type.value
})
if (res.code == 0) {
message.error(res.message)
return
}
message.success(res.message)
}
/**
* 获取当前的字幕
*/
async function GetCurrentFrameText() {
GetCurrentFrameTextLodding.value = true
if (type.value == SubtitleSavePositionType.MAIN_VIDEO) {
let res = await window.book.GetCurrentFrameText({
id: reverseManageStore.selectBook.id
? reverseManageStore.selectBook.id
: '58053f68-da54-4a48-92fc-8a5c3cb75043',
currentTime: videoRef.value.currentTime,
type: type.value
})
if (res.code == 0) {
message.error(res.message)
GetCurrentFrameTextLodding.value = false
return
}
message.success(res.data)
frameText.value = res.data
}
GetCurrentFrameTextLodding.value = false
}
/**
* 获取整个视频的文案信息,保存到和视频的同级目录中
*/
async function GetVideoFrameText() {
GetVideoFrameTextLodding.value = true
if (type.value == SubtitleSavePositionType.MAIN_VIDEO) {
let res = await window.book.GetVideoFrameText({
id: reverseManageStore.selectBook.id
? reverseManageStore.selectBook.id
: '58053f68-da54-4a48-92fc-8a5c3cb75043',
type: type.value
})
if (res.code == 0) {
message.error(res.message)
GetVideoFrameTextLodding.value = false
return
}
message.success(res.message)
frameText.value = res.data
GetVideoFrameTextLodding.value = false
}
}
return {
videoRef,
canvasRef,
handleMouseMove,
handleMouseDown,
handleMouseUp,
GetCurrentFrameTextLodding,
SaveSelectPositionLodding,
GetVideoFrameTextLodding,
videoSrc,
mark,
videoWidth,
drawing,
moving,
startPos,
rectPos,
offset,
isPlaying,
SetVideoMuted,
buttonColor,
isMuted,
softwareStore,
SaveSelectPosition,
reverseManageStore,
type,
OpenBookSubtitlePositionScreenshot,
GetCurrentFrameText,
frameText,
GetVideoFrameText
}
}
})
</script>
+108 -30
View File
@@ -1,42 +1,120 @@
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router';
import { createRouter, createWebHashHistory } from 'vue-router'
import App from './App.vue'
import { Home } from '@vicons/ionicons5';
const app = createApp(App);
import { Home } from '@vicons/ionicons5'
const app = createApp(App)
import { createPinia } from 'pinia'
const pinia = createPinia()
const routes = [
{
path: "/",
component: () => import('./components/Home/Home.vue'),
children: [
{ path: "/global_setting", name: "global_setting", component: () => import('./components/Setting/Setting.vue') },
{ path: "/clip_setting", name: "clip_setting", component: () => import('./components/Setting/ClipSetting.vue') },
{ path: "/getframe", name: "getframe", component: () => import('./components/Backstep/GetFrame.vue') },
{ path: "/pushBackPrompt", name: "pushBackPrompt", component: () => import('./components/Backstep/PushBackPrompt.vue') },
{ path: "/regenerate", name: "regenerate", component: () => import('./components/Backstep/ReGenerate.vue') },
{ path: "/align_draft", name: "align_draft", component: () => import('./components/Clip/AlignDraft.vue') },
{ path: "/add_draft", name: "add_draft", component: () => import('./components/Clip/AddDraft.vue') },
{ path: "/VideoGenerate", name: "VideoGenerate", component: () => import('./components/Backstep/VideoGenerate.vue') },
{ path: "/sd_setting", name: "sd_setting", component: () => import('./components/Setting/SDSetting.vue') },
{ path: "/copywriting", name: "copywriting", component: () => import('./components/Backstep/CopyWriting.vue') },
{ path: "/videogeneratesetting", name: "videogeneratesetting", component: () => import('./components/Setting/VideoGenerateSetting.vue') },
{ path: "/ShowMessage", name: "ShowMessage", component: () => import('./components/Home/ShowMessage.vue') },
{ path: "/sdoriginal", name: "sdoriginal", component: () => import('./components/Original/MainPage.vue') },
{ path: "/mj_setting", name: "mj_setting", component: () => import('./components/Setting/MJSetting.vue') },
{ path: '/reverse_management', name: "reverse_management", component: () => import('./components/ReverseManage/ReverseManage.vue') },
{ path: '/manage_book/:id', name: "manage_book", component: () => import('./components/ReverseManage/ManageBookDetail.vue') },
]
},
{ path: "/ReDrawImage", component: () => import('./components/Home/ReDrawImageWD.vue'), },
{
path: '/',
component: () => import('./components/Home/Home.vue'),
children: [
{
path: '/gptCopywriting',
name: 'gptCopywriting',
component: () => import('./components/CopyWriting/CopyWriting.vue')
},
{
path: '/global_setting',
name: 'global_setting',
component: () => import('./components/Setting/Setting.vue')
},
{
path: '/clip_setting',
name: 'clip_setting',
component: () => import('./components/Setting/ClipSetting.vue')
},
{
path: '/getframe',
name: 'getframe',
component: () => import('./components/Backstep/GetFrame.vue')
},
{
path: '/pushBackPrompt',
name: 'pushBackPrompt',
component: () => import('./components/Backstep/PushBackPrompt.vue')
},
{
path: '/regenerate',
name: 'regenerate',
component: () => import('./components/Backstep/ReGenerate.vue')
},
{
path: '/align_draft',
name: 'align_draft',
component: () => import('./components/Clip/AlignDraft.vue')
},
{
path: '/add_draft',
name: 'add_draft',
component: () => import('./components/Clip/AddDraft.vue')
},
{
path: '/VideoGenerate',
name: 'VideoGenerate',
component: () => import('./components/Backstep/VideoGenerate.vue')
},
{
path: '/sd_setting',
name: 'sd_setting',
component: () => import('./components/Setting/SDSetting.vue')
},
{
path: '/copywriting',
name: 'copywriting',
component: () => import('./components/Backstep/CopyWriting.vue')
},
{
path: '/videogeneratesetting',
name: 'videogeneratesetting',
component: () => import('./components/Setting/VideoGenerateSetting.vue')
},
{
path: '/ShowMessage',
name: 'ShowMessage',
component: () => import('./components/Home/ShowMessage.vue')
},
{
path: '/sdoriginal',
name: 'sdoriginal',
component: () => import('./components/Original/MainPage.vue')
},
{
path: '/mj_setting',
name: 'mj_setting',
component: () => import('./components/Setting/MJSetting.vue')
},
{
path: '/reverse_management',
name: 'reverse_management',
component: () => import('./components/ReverseManage/ReverseManage.vue')
},
{
path: '/manage_book/:id',
name: 'manage_book',
component: () => import('./components/ReverseManage/ManageBookDetail.vue')
},
{
path: '/test_options',
name: 'test_options',
component: () => import('./components/VideoSubtitle/VideoCanvas.vue')
},
{
path : "/TTS_Services",
name : "TTS_Services",
component : () => import('./components/TTS/TTSHome.vue')
}
]
},
{ path: '/ReDrawImage', component: () => import('./components/Home/ReDrawImageWD.vue') }
]
const router = createRouter({
history: createWebHashHistory(),
routes
history: createWebHashHistory(),
routes
})
app.use(router);
app.use(router)
app.use(pinia)
app.mount('#app')